一、JVM記憶體模型
1.1、與執行時資料區
前面講過了執行時資料區那接下來我們聊下記憶體模型,JVM的記憶體模型指的是方法區和堆;在很多情況下網上講解會把記憶體模型和執行時資料區認為是一個東西,這是錯誤的想法,如果不信可以自己去官網求證
記憶體模型我們可以分為非堆區(元空間,用的是本地記憶體)和堆區,在堆區分為兩大塊,一個是Old區,一個是Young區。Young區分為兩大塊,一個是Survivor區(S0+S1),一塊是Eden區。 Eden:S0:S1=8:1:1;S0和S1一樣大,也可以叫From和To。
1.2、圖形展示
一塊是非堆區,一塊是堆區,堆區分為兩大塊,一個是Old區,一個是Young區,Young區分為兩大塊,一個是Survivor區(S0+S1),一塊是Eden區,S0和S1一樣大,也可以叫From和To
1.3、物件建立過程
一般情況下,新建立的物件都會被分配到Eden區,一些特殊的大的物件會直接分配到Old區(新生代空間不夠時,借老年代空間用的情況)比如有物件A,B,C等建立在Eden區,但是Eden區的記憶體空間肯定有限,比如有100M,假如已經使用了100M或者達到一個設定的臨界值,這時候就需要對Eden記憶體空間進行清理,即垃圾收集(Garbage Collect),這樣的GC我們稱之為Minor GC,Minor GC指得是Young區的GC。經過GC之後,有些物件就會被清理掉,有些物件可能還存活著,對於存活著的物件需要將其複製到Survivor區,然後再清空Eden區中的這些物件。Survivor區分為兩塊S0和S1。在同一個時間點上,S0和S1只能有一個區有資料,另外一個是空的。
b.survivor區工作過程
比如一開始只有Eden區和From中有物件,To中是空的。此時進行一次GC操作,From區中物件的年齡就會+1,我們知道Eden區中所有存活的物件會被複制到To區,From區中還能存活的物件會有兩個去處。若物件年齡達到之前設定好的年齡閾值,此時物件 會被移動到Old區,沒有達到閾值的物件會被複制到To區。此時Eden區和From區已經被清空。這時候From和To交換角色,之前的From變成了To,之前的To變成了From。也就是說無論如何都要保證名為To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到To區被填滿,然後會將所有物件複製到老年代中。
1.4、OId區
一般Old區都是年齡比較大的物件,或者相對超過了某個閾值的物件。在Old區也會有GC的操作,Old區的GC我們稱作為Major GC,每次GC之後還能存活的物件年齡也會+1,如果年齡超過了某個閾值,就會被回收。
二、常見問題
2.1、如何理解各種GC
- Partial GC:Partial其實也就是部分的意思.那麼翻譯過來也就是回收部分GC堆的模式,他並不會回收我們整個堆.而我們的young GC以及我們的Old GC都屬於這種模式
- young GC:只回收young區
- old GC:只回收Old區
- full GC:實際上就是對於整體回收
2.2、為什麼需要Survivor區
如果沒有Survivor,Eden區每進行一次Minor GC,存活的物件就會被送到老年代。這樣一來,老年代很快被填滿,觸發Major GC(因為Major GC一般伴隨著Minor GC,也可以看做觸發了Full GC)。老年代的記憶體空間遠大於新生代,進行一次Full GC消耗的時間比Minor GC長得多。執行時間長有什麼壞處?頻發的Full GC消耗的時間很長,會影響大型程式的執行和響應速度。
這時有人會想到對老年代的空間進行增加。假如增加老年代空間,更多存活物件才能填滿老年代。雖然降低Full GC頻率,但是隨著老年代空間加大,一旦發生Full GC,執行所需要的時間更長。假如減少老年代空間,雖然Full GC所需時間減少,但是老年代很快被存活物件填滿,Full GC頻率增加。所以Survivor的存在意義,就是減少被送到老年代的物件,進而減少Full GC的發生,Survivor的預篩選保證,只有經歷16次Minor GC還能在新生代中存活的物件,才會被送到老年代。
2.3、為什麼需要兩個Survivor區
最大的好處就是解決了碎片化。也就是說為什麼一個Survivor區不行?第一部分中,我們知道了必須設定Survivor區。假設現在只有一個Survivor區,我們來模擬一下流程:剛剛新建的物件在Eden中,一旦Eden滿了,觸發一次Minor GC,Eden中的存活物件就會被移動到Survivor區。這樣繼續迴圈下去,下一次Eden滿了的時候,問題來了,此時進行Minor GC,Eden和Survivor各有一些存活物件,如果此時把Eden區的存活物件硬放到Survivor區,很明顯這兩部分物件所佔有的記憶體是不連續的,也就導致了記憶體碎片化。所以要有兩個survivor,並且永遠有一個Survivor space是空的,另一個非空的Survivor space無碎片。
2.4、新生代中Eden:S1:S2為什麼是8:1:1
新生代中的可用記憶體:複製演算法用來擔保的記憶體為9:1;可用記憶體中Eden:S1區為8:1;即新生代中Eden:S1:S2 = 8:1:1現代的商業虛擬機器都採用這種收集演算法來回收新生代,IBM公司的專門研究表明,新生代中的物件大概98%是“朝生夕死”的
2.5、堆記憶體中都是執行緒共享的區域嗎
JVM預設為每個執行緒在Eden上開闢一個buffer區域,用來加速物件的分配,稱之為TLAB,全稱:Thread Local Allocation Buffer。物件優先會在TLAB上分配,但是TLAB空間通常會比較小,如果物件比較大,那麼還是在共享區域分配。
三、體驗驗證
如果我們想自己驗證下JVM的執行過程我們也可以用在cmd視窗寫命令調出檢視工具jvisualgc外掛下載連結 :https://visualvm.github.io/pluginscenters.html --->選擇對應版本連結--->Tools--->Visual GC
3.1、堆記憶體溢位
@RestController public class HeapController { List<String > list=new ArrayList<String> (); @GetMapping("/heap") public String heap(){ while(true){ list.add(" 堆記憶體溢位"); } } }
記得設定引數比如-Xmx20M -Xms20M ;啟動專案後我們用監聽工具訪問可以在本地看到如下圖解
3.2、方法區記憶體溢位
比如向方法區中新增Class的資訊,加入依賴
<dependency> <groupId>asm</groupId> <artifactId>asm</artifactId> <version>3.3.1</version> </dependency>
public class MyMetaspace extends ClassLoader { public static List<Class<?>> createClasses() { List<Class<?>> classes = new ArrayList<Class<?>> (); for (int i = 0; i < 10000000; ++i) { ClassWriter cw = new ClassWriter(0); cw.visit( Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null); mw.visitVarInsn(Opcodes.ALOAD, 0); mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V"); mw.visitInsn(Opcodes.RETURN); mw.visitMaxs(1, 1); mw.visitEnd(); MyMetaspace test = new MyMetaspace(); byte[] code = cw.toByteArray(); Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length); classes.add(exampleClass); } return classes; } }
@RestController public class NonHeapController { List<Class<?>> list=new ArrayList<Class<?>> (); @GetMapping("/nonheap") public String nonheap(){ while(true){ list.addAll(MyMetaspace.createClasses()); } } }
設定Metaspace的大小,比如-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M ,然後執行程式碼
3.3、虛擬機器棧
public class Demo { public static long count=0; public static void method(long i){ System.out.println(count++); method(i); } public static void main(String[] args) { method(1); } }
Stack Space用來做方法的遞迴呼叫時壓入Stack Frame(棧幀)。所以當遞迴呼叫太深的時候,就有可能耗盡Stack Space,爆出StackOverflow的錯誤。-Xss128k:設定每個執行緒的堆疊大小。JDK 5以後每個執行緒堆疊大小為1M,以前每個執行緒堆疊大小為256K。根據應用的執行緒所需記憶體大小進行調整。在相同實體記憶體下,減小這個值能生成更多的執行緒。但是作業系統對一個程式內的執行緒數還是有限制的,不能無限生成,經驗值在3000~5000左右。
執行緒棧的大小是個雙刃劍,如果設定過小,可能會出現棧溢位,特別是在該執行緒內有遞迴、大的迴圈時出現溢位的可能性更大,如果該值設定過大,就有影響到建立棧的數量,如果是多執行緒的應用,就會出現記憶體溢位的錯誤。