JVM記憶體模型(五)

一、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插件下載鏈接 ://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左右。
執行緒棧的大小是個雙刃劍,如果設置過小,可能會出現棧溢出,特別是在該執行緒內有遞歸、大的循環時出現溢出的可能性更大,如果該值設置過大,就有影響到創建棧的數量,如果是多執行緒的應用,就會出現記憶體溢出的錯誤。
Tags: