App下載

Java虛擬機棧和內(nèi)存模型深層次的剖析

猿友 2021-07-22 14:27:53 瀏覽數(shù) (1630)
反饋

1、結(jié)合字節(jié)碼指令理解Java虛擬機棧和棧幀

棧幀:每個棧幀對應(yīng)一個被調(diào)用的方法,可以理解為一個方法的運行空間。

每個棧幀中包括局部變量表(Local Variables)、操作數(shù)棧(Operand Stack)、指向運行時常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。

局部變量表:方法中定義的局部變量以及方法的參數(shù)存放在這張表中,局部變量表中的變量不可直接使用,如需要使用的話,必須通過相關(guān)指令將其加載至操作數(shù)棧中作為操作數(shù)使用。

操作數(shù)棧:以壓棧和出棧的方式存儲操作數(shù)的。

動態(tài)鏈接:每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接(Dynamic Linking)。

方法返回地址:當(dāng)一個方法開始執(zhí)行后,只有兩種方式可以退出,一種是遇到方法返回的字節(jié)碼指令;一種是遇見異常,并且這個異常沒有在方法體內(nèi)得到處理。

2021414111416634

class Person{
    private String name="Jack";
    private int age;
    private final double salary=100;
    private static String address;
    private final static String hobby="Programming";
   
    public void say(){
        System.out.println("person say..."); 
    }
    
    public static int calc(int op1,int op2){
        op1=3;
        int result=op1+op2;
        return result;
    }
    
    public static void order(){
    }
    
    public static void main(String[] args){
        calc(1,2);
        order(); 
    }
}
Compiled from "Person.java" class Person {
...
  public static int calc(int, int);
    Code:
        0: iconst_3 //將int類型常量3壓入[操作數(shù)棧]
        1: istore_0 //將int類型值存入[局部變量0]
        2: iload_0  //從[局部變量0]中裝載int類型值入棧
        3: iload_1  //從[局部變量1]中裝載int類型值入棧
        4: iadd //將棧頂元素彈出棧,執(zhí)行int類型的加法,結(jié)果入棧
        【For example, the iadd instruction (§iadd) adds two int values together. It requires that the int values to be added be the top two values of the operand stack, pushed there by previous instructions. Both of the int values are popped from the operand stack. They are added, and their sum is pushed back onto the operand stack. Subcomputations may be nested on the operand stack, resulting in values that can be used by the encompassing computation.】
        5: istore_2 //將棧頂int類型值保存到[局部變量2]中
        6: iload_2  //從[局部變量2]中裝載int類型值入棧
        7: ireturn  //從方法中返回int類型的數(shù)據(jù)
...
}

2021041409365410

2、深入分析

2.1 棧指向堆

如果在棧幀中有一個變量,類型為引用類型,比如 Object obj=new Object(),這時候就是典型的棧中元素指向堆中的對象。

2021041409365411

2.2 方法區(qū)指向堆

方法區(qū)中會存放靜態(tài)變量,常量等數(shù)據(jù)。如果是下面這種情況,就是典型的方法區(qū)中元素指向堆中的對象。

private static Object obj=new Object();

2021041409365412

2.3 堆指向方法區(qū)

方法區(qū)中會包含類的信息,堆中會有對象,那怎么知道對象是哪個類創(chuàng)建的呢?

2021041409365513

思考:一個對象怎么知道它是由哪個類創(chuàng)建出來的?怎么記錄?這就需要了解一個Java對象的具體信息咯。

2.4 Java對象內(nèi)存布局

一個Java對象在內(nèi)存中包括3個部分:對象頭、實例數(shù)據(jù)和對齊填充。

2021041409365514

3、內(nèi)存模型

 3.1 圖解

一塊是非堆區(qū),一塊是堆區(qū)。
堆區(qū)分為兩大塊,一個是Old區(qū),一個是Young區(qū)。 Young區(qū)分為兩大塊,一個是Survivor區(qū)(S0+S1),一塊是Eden區(qū)。 Eden:S0:S1=8:1:1 S0和S1一樣大,也可以叫From和To。

2021041409365515

根據(jù)之前對于Heap的介紹可以知道,一般對象和數(shù)組的創(chuàng)建會在堆中分配內(nèi)存空間,關(guān)鍵是堆中有這么多區(qū)域,那一個對象的創(chuàng)建到底在哪個區(qū)域呢?

3.2 對象創(chuàng)建所在區(qū)域

一般情況下,新創(chuàng)建的對象都會被分配到Eden區(qū),一些特殊的大的對象會直接分配到Old區(qū)。
比如有對象A,B,C等創(chuàng)建在Eden區(qū),但是Eden區(qū)的內(nèi)存空間肯定有限,比如有100M,假如已經(jīng)使用了 100M 或者達到一個設(shè)定的臨界值,這時候就需要對Eden內(nèi)存空間進行清理,即垃圾收集(Garbage Collect), 這樣的GC我們稱之為Minor GC,Minor GC指的是Young區(qū)的GC。

經(jīng)過GC之后,有些對象就會被清理掉,有些對象可能還存活著,對于存活著的對象需要將其復(fù)制到Survivor 區(qū),然后再清空Eden區(qū)中的這些對象。

3.3 Survivor區(qū)詳解

由圖解可以看出,Survivor區(qū)分為兩塊S0和S1,也可以叫做From和To。 在同一個時間點上,S0和S1只能有一個區(qū)有數(shù)據(jù),另外一個是空的。

接著上面的GC來說,比如一開始只有Eden區(qū)和From中有對象,To中是空的。 此時進行一次GC操作,F(xiàn)rom區(qū)中對象的年齡就會+1,我們知道Eden區(qū)中所有存活的對象會被復(fù)制到To區(qū),
From區(qū)中還能存活的對象會有兩個去處。

若對象年齡達到之前設(shè)置好的年齡閾值,此時對象會被移動到Old區(qū),????Eden????From??沒有達到閾值的 對象會被復(fù)制到To區(qū)。 此時Eden區(qū)和From區(qū)已經(jīng)被清空(被GC的對象肯定沒了,沒有被GC的對象都有了各自的去處)。
這時候From和To交換角色,之前的From變成了To,之前的To變成了From。 也就是說無論如何都要保證名為To的Survivor區(qū)域是空的。
Minor GC會一直重復(fù)這樣的過程,直到To區(qū)被填滿,然后會將所有對象復(fù)制到老年代中。

3.4 Old區(qū)詳解

從上面的分析可以看出,一般Old區(qū)都是年齡比較大的對象,或者相對超過了某個閾值的對象。

在Old區(qū)也會有GC的操作,Old區(qū)的GC我們稱作為Major GC。

3.5 對象的一輩子理解

我是一個普通的Java對象,我出生在Eden區(qū),在Eden區(qū)我還看到和我長的很像的小兄弟,我們在Eden區(qū)中玩了挺長時間。有 一天Eden區(qū)中的人實在是太多了,我就被迫去了Survivor區(qū)的“From”區(qū),自從去了Survivor區(qū),我就開始漂了,有時候在 Survivor的“From”區(qū),有時候在Survivor的“To”區(qū),居無定所。直到我18歲的時候,爸爸說我成人了,該去社會上闖闖了。于是我就去了年老代那邊,年老代里,人很多,并且年齡都挺大的,我在這里也認識了很多人。在年老代里,我生活了20年(每次 GC加一歲),然后被回收。

2021041409365616

3.6 常見問題

  •  如何理解Minor/Major/Full GC

Minor GC:新生代

Major GC:老年代

Full GC:新生代+老年代

  • 為什么需要Survivor區(qū)?只有Eden不行嗎?

如果沒有Survivor,Eden區(qū)每進行一次Minor GC,存活的對象就會被送到老年代。 這樣一來,老年代很快被填滿,觸發(fā)Major GC(因為Major GC一般伴隨著Minor GC,也可以看做觸發(fā)了Full GC)。 老年代的內(nèi)存空間遠大于新生代,進行一次Full GC消耗的時間比Minor GC長得多。執(zhí)行時間長有什么壞處?頻發(fā)的Full GC消耗的時間很長,會影響大型程序的執(zhí)行和響應(yīng)速度。

可能你會說,那就對老年代的空間進行增加或者較少咯。假如增加老年代空間,更多存活對象才能填滿老年代。雖然降低Full GC頻率,但是隨著老年代空間加大,一旦發(fā)生Full GC,執(zhí)行所需要的時間更長。

假如減少老年代空間,雖然Full GC所需時間減少,但是老年代很快被存活對象填滿,F(xiàn)ull GC頻率增加。

所以Survivor的存在意義,就是減少被送到老年代的對象,進而減少Full GC的發(fā)生,Survivor的預(yù)篩選保證,只有經(jīng)歷16 次Minor GC還能在新生代中存活的對象,才會被送到老年代。

  • 為什么需要兩個Survivor區(qū)?

最大的好處就是解決了碎片化。也就是說為什么一個Survivor區(qū)不行?第一部分中,我們知道了必須設(shè)置Survivor區(qū)。假設(shè),現(xiàn)在只有一個Survivor區(qū),我們來模擬一下流程:

剛剛新建的對象在Eden中,一旦Eden滿了,觸發(fā)一次Minor GC,Eden中的存活對象就會被移動到Survivor區(qū)。這樣繼續(xù)循環(huán)下去,下一次Eden滿了的時候,問題來了,此時進行Minor GC,Eden和Survivor各有一些存活對象,如果此時把Eden區(qū)的,存活對象硬放到Survivor區(qū),很明顯這兩部分對象所占有的內(nèi)存是不連續(xù)的,也就導(dǎo)致了內(nèi)存碎片化。
永遠有一個Survivor space是空的,另一個非空的Survivor space無碎片。

  • 新生代中Eden:S1:S2為什么是8:1:1?

新生代中的可用內(nèi)存:復(fù)制算法用來擔(dān)保的內(nèi)存為9:1

可用內(nèi)存中Eden:S1區(qū)為8:1

即新生代中Eden:S1:S2 = 8:1:1

4、驗證

4.1 堆內(nèi)存溢出

程序:

@RestController
public class HeapController {
    List<Person> list=new ArrayList<Person>();
    @GetMapping("/heap")
    public String heap() throws Exception{
        while(true){
            list.add(new Person()); 
            Thread.sleep(1);
        } 
    }
}

記得設(shè)置參數(shù)比如-Xmx20M -Xms20M

運行結(jié)果:

Exception in thread "http-nio-8080-exec-2" java.lang.OutOfMemoryError: GC overhead limit exceeded

4.2 方法區(qū)內(nèi)存溢出

比如向方法區(qū)中添加Class的信息

4.2.1 asm依賴和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();
            Metaspace test = new Metaspace();
            byte[] code = cw.toByteArray();
            Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length); 
            classes.add(exampleClass);
        }
        return classes;
    }
}

4.2.2 代碼

@RestController
public class NonHeapController {
    List<Class<?>> list=new ArrayList<Class<?>>();
    @GetMapping("/nonheap")
    public String nonheap() throws Exception{
        while(true){ 
        list.addAll(MyMetaspace.createClasses()); 
        Thread.sleep(5);
        } 
    }
}

設(shè)置Metaspace的大小,比如-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M

運行結(jié)果:

java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_191]
at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_191]

4.3 虛擬機棧

 4.3.1 代碼演示StackOverFlow

public class StackDemo {
    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);
    }
}

運行結(jié)果:

2021041409365617

4.3.2 理解和說明

Stack Space用來做方法的遞歸調(diào)用時壓入Stack Frame(棧幀)。所以當(dāng)遞歸調(diào)用太深的時候,就有可能耗盡Stack Space,爆出StackOverflow的錯誤。

-Xss128k:設(shè)置每個線程的堆棧大小。JDK 5以后每個線程堆棧大小為1M,以前每個線程堆棧大小為256K。根據(jù)應(yīng)用的線程所需內(nèi)存大小進行調(diào)整。在相同物理內(nèi)存下,減小這個值能生成更多的線程。但是操作系統(tǒng)對一個進程內(nèi)的線程數(shù)還是有限制的,不能無限生成,經(jīng)驗值在3000~5000左右。

線程棧的大小是個雙刃劍,如果設(shè)置過小,可能會出現(xiàn)棧溢出,特別是在該線程內(nèi)有遞歸、大的循環(huán)時出現(xiàn)溢出的可能性更大,如果該值設(shè)置過大,就有影響到創(chuàng)建棧的數(shù)量,如果是多線程的應(yīng)用,就會出現(xiàn)內(nèi)存溢出的錯誤。

以上就是關(guān)于 Java 虛擬機棧和內(nèi)存模型深層次剖析的全部內(nèi)容,想要了解更多相關(guān) Java 虛擬機棧和內(nèi)存模型的其他內(nèi)容請搜索W3Cschool以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持我們!


0 人點贊