JVM記憶體模型總結,有各版本JDK對比、有元空間OOM監控案例、有Java版虛擬機,綜合實踐學習!


作者:小傅哥
部落格://bugstack.cn
Github://github.com/fuzhengwei/CodeGuide/wiki

沉澱、分享、成長,讓自己和他人都能有所收穫!😄

一、前言

看了一篇文章30歲有多難!

每篇文章的開篇總喜歡寫一些,從個人視角看這個世界的感悟。

最近看到一篇文章,30歲有多難。文中的一些主人公好像在學業、工作、生活、愛情等方面都過的都不如意。要不是錯過這,要不是走錯那。總結來看,就像是很倒霉的一群倒霉蛋兒在跟生活對干!

但其實每個人可能都遇到過生活中最難的時候,或早或晚。就像我剛畢業不久時一連串遇到;冬天裡丟過第一部手機修一個進了水的電腦租的房子第一次被騙,一連串下來頭一次要趕在工資沒發的時候,選擇少吃早飯還是午飯,看看能扛過去那頓。

哈哈哈哈哈,現在想想還挺有意思的,不過這些亂遭的事很多是自己的意識和能力不足時做出的錯誤選擇而導致的。

人那,想開車就要考駕照,想走遠就要有能力。多提升認知,多拓寬眼界!生活的意義就是不斷的更新自己!

二、面試題

謝飛機,小記!,冬風吹、戰鼓擂。被窩裡,誰怕誰。

謝飛機:歪?大哥,你在嗎?

面試官:咋了,大周末的,這麼早打電話!?

謝飛機:我夢見,我去Google寫JVM了,給你們公司用,之後蹦了,讓我起來改bug!

面試官:啊!?啊,那我問你,JDK 1.8 與 JDK 1.7 在運行時數據區的設計上,你都怎麼做的優化策略的?

謝飛機:我沒寫這,我不知道!

面試官:擦。。。

三、 JDK1.6、JDK1.7、JDK1.8 記憶體模型演變

圖 25-1  JDK1.6、JDK1.7、JDK1.8,記憶體模型演變

如圖 25-1 是 JDK 1.6、1.7、1.8 的記憶體模型演變過程,其實這個記憶體模型就是 JVM 運行時數據區依照JVM虛擬機規範的具體實現過程。

在圖 25-1 中各個版本的迭代都是為了更好的適應CPU性能提升,最大限度提升的JVM運行效率。這些版本的JVM記憶體模型主要有以下差異:

  • JDK 1.6:有永久代,靜態變數存放在永久代上。
  • JDK 1.7:有永久代,但已經把字元串常量池、靜態變數,存放在堆上。逐漸的減少永久代的使用。
  • JDK 1.8:無永久代,運行時常量池、類常量池,都保存在元數據區,也就是常說的元空間。但字元串常量池仍然存放在堆上。

四、記憶體模型各區域介紹

1. 程式計數器

  • 較小的記憶體空間、執行緒私有,記錄當前執行緒所執行的位元組碼行號。
  • 如果執行 Java 方法,計數器記錄虛擬機位元組碼當前指令的地址,本地方法則為空。
  • 這一塊區域沒有任何 OutOfMemoryError 定義。

以上,就是關於程式計數器的定義,如果這樣看沒有感覺,我們舉一個例子。

定義一段 Java 方法的程式碼,這段程式碼是計算圓形的周長。

public static float circumference(float r){
        float pi = 3.14f;
        float area = 2 * pi * r;
        return area;
}

接下來,如圖 25-2 是這段程式碼的在虛擬機中的執行過程,左側是它的程式計數器對應的行號。

圖 25-2 程式計數器

  • 這些行號每一個都會對應一條需要執行的位元組碼指令,是壓棧還是彈出或是執行計算。
  • 之所以說是執行緒私有的,因為如果不是私有的,那麼整個計算過程最終的結果也將錯誤。

2. Java虛擬機棧

  • 每一個方法在執行的同時,都會創建出一個棧幀,用於存放局部變數表、操作數棧、動態鏈接、方法出口、執行緒等資訊。
  • 方法從調用到執行完成,都對應著棧幀從虛擬機中入棧和出棧的過程。
  • 最終,棧幀會隨著方法的創建到結束而銷毀。

可能這麼只從定義看上去仍然沒有什麼感覺,我們再找一個例子。

這是一個關於斐波那契數列(Fibonacci sequence)求值的例子,我們通過斐波那契數列在虛擬機中的執行過程,來體會Java虛擬機棧的用途。

斐波那契數列(Fibonacci sequence),又稱黃金分割數列、因數學家列昂納多·斐波那契(Leonardoda Fibonacci)以兔子繁殖為例子而引入,故又稱為「兔子數列」,指的是這樣一個數列:1、1、2、3、5、8、13、21、34、……在數學上,斐波納契數列以如下被以遞推的方法定義:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)在現代物理、准晶體結構、化學等領域,斐波納契數列都有直接的應用,為此,美國數學會從1963年起出版了以《斐波納契數列季刊》為名的一份數學雜誌,用於專門刊載這方面的研究成果。

圖 25-3 斐波那契數列在虛擬機棧中的執行過程

  • 整個這段流程,就是方法的調用和返回。在調用過程申請了操作數棧的深度和局部變數的大小。
  • 以及相應的資訊從各個區域獲取並操作,其實也就是入棧和出棧的過程。

3. 本地方法棧

  • 本地方法棧與Java虛擬機棧作用類似,唯一不同的就是本地方法棧執行的是Native方法,而虛擬機棧是為JVM執行Java方法服務的。
  • 另外,與 Java 虛擬機棧一樣,本地方法棧也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
  • JDK1.8 HotSpot虛擬機直接就把本地方法棧和虛擬機棧合二為一。

關於本地方法棧在以上的例子已經涉及了這部分內容,這裡就不在贅述了。

4. 堆和元空間

圖 25-4 Java 堆區域劃分

  • JDK 1.8 JVM 的記憶體結構主要由三大塊組成:堆記憶體、元空間和棧,Java 堆是記憶體空間佔據最大的一塊區域。
  • Java 堆,由年輕代和年老代組成,分別佔據1/3和2/3。
  • 而年輕代又分為三部分,EdenFrom SurvivorTo Survivor,佔據比例為8:1:1,可調。
  • 另外這裡我們特意畫出了元空間,也就是直接記憶體區域。在 JDK 1.8 之後就不在堆上分配方法區了。
  • 元空間從虛擬機Java堆中轉移到本地記憶體,默認情況下,元空間的大小僅受本地記憶體的限制,說白了也就是以後不會因為永久代空間不夠而拋出OOM異常出現了。jdk1.8以前版本的 class和JAR包數據存儲在 PermGen下面 ,PermGen 大小是固定的,而且項目之間無法共用,公有的 class,所以比較容易出現OOM異常。
  • 升級 JDK 1.8後,元空間配置參數,-XX:MetaspaceSize=512M XX:MaxMetaspaceSize=1024M。教你個小技巧通過jps、jinfo查看元空間,如下:
    • 通過命令查看元空間
    • 通過jinfo查看默認MetaspaceSize大小(約20M),MaxMetaspaceSize比較大。

其他:關於 JDK1.8 元空間的介紹: Move part of the contents of the permanent generation in Hotspot to the Java heap and the remainder to native memory. //openjdk.java.net/jeps/122

5. 常量池

  • 從 JDK 1.7開始把常量池從永久代中剝離,直到 JDK1.8 去掉了永久代。而字元串常量池一直放在堆空間,用於存儲字元串對象,或是字元串對象的引用。

五、手擼虛擬機(記憶體模型)

其實以上的內容,已經完整的介紹了JVM虛擬機的記憶體模型,也就是運行時數據區的結構。但是這東西看完可能就忘記了,因為缺少一個可親手操作的程式碼。

所以,這裡我給大家用Java程式碼寫一段關於數據槽、棧幀、局部變數、虛擬機棧以及堆的程式碼結構,讓大家更好的加深對虛擬機記憶體模型的印象。

1. 工程結構

運行時數據區
├── heap
│   ├── constantpool
│   ├── methodarea
│   │   ├── Class.java
│   │   ├── ClassMember.java
│   │   ├── Field.java
│   │   ├── Method.java
│   │   ├── MethodDescriptor.java
│   │   ├── MethodDescriptorParser.java
│   │   ├── MethodLookup.java
│   │   ├── Object.java
│   │   ├── Slots.java
│   │   └── StringPool.java
│   └── ClassLoader.java
├── Frame.java
├── JvmStack.java
├── LocalVars.java
├── OperandStack.java
├── Slot.java
└── Thread.java

以上這部分就是使用Java實現的部分JVM虛擬機功能,這部分主要包括如下內容:

  • Frame,棧幀
  • JvmStack,虛擬機棧
  • LocalVars,局部變數
  • OperandStack,操作數棧
  • Slot,數據槽
  • Thread,執行緒
  • heap,堆,裡面包括常量池和方法區

2. 重點程式碼

操作數棧 OperandStack

public class OperandStack {

    private int size = 0;
    private Slot[] slots;

    public OperandStack(int maxStack) {
        if (maxStack > 0) {
            slots = new Slot[maxStack];
            for (int i = 0; i < maxStack; i++) {
                slots[i] = new Slot();
            }
        }
    }
    //...
}

虛擬機棧 OperandStack

public class JvmStack {

    private int maxSize;
    private int size;
    private Frame _top;
    
    //...
}

棧幀 Frame

public class Frame {

    //stack is implemented as linked list
    Frame lower;

    //局部變數表
    private LocalVars localVars;

    //操作數棧
    private OperandStack operandStack;

    private Thread thread;

    private Method method;

    private int nextPC;
 
    //...
}
  • 關於程式碼結構看到這有點感覺了嗎?
  • Slot數據槽,就是一個數組結構,用於存放數據的。
  • 操作數棧、局部變數表,都是使用數據槽進行入棧入棧操作。
  • 在棧幀里,可以看到連接、局部變數表、操作數棧、方法、執行緒等,那麼文中說到的當有一個新的每一個方法在執行的同時,都會創建出一個棧幀,是不就對了上,可以真的理解了。
  • 如果你對JVM的實現感興趣,可以閱讀用Java實現JVM源碼//github.com/fuzhengwei/itstack-demo-jvm

六、jconsole監測元空間溢出

不是說 JDK 1.8 的記憶體模型把永久代下掉,換上元空間了嗎?但不測試下,就感受不到呀,沒有證據!

所有關於程式碼邏輯的學習,都需要有數據基礎和證明過程,這樣才能有深刻的印象。走著,帶你把元空間干滿,讓它OOM!

1. 找段持續創建大對象的程式碼

public static void main(String[] args) throws InterruptedException {
    
    Thread.sleep(5000);
    
    ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
    while (true) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(MetaSpaceOomMock.class);
        enhancer.setCallbackTypes(new Class[]{Dispatcher.class, MethodInterceptor.class});
        enhancer.setCallbackFilter(new CallbackFilter() {
            @Override
            public int accept(Method method) {
                return 1;
            }
            @Override
            public boolean equals(Object obj) {
                return super.equals(obj);
            }
        });
        System.out.println(enhancer.createClass().getName() + loadingBean.getTotalLoadedClassCount() + loadingBean.getLoadedClassCount() + loadingBean.getUnloadedClassCount());
    }
}
  • 網上找了一段基於CGLIB的,你可以寫一些其他的。
  • Thread.sleep(5000);,睡一會,方便我們點檢測,要不程式太快就異常了。

2. 調整元空間大小

默認情況下元空間太大了,不方便測試出結果,所以我們把它調的小一點。

-XX:MetaspaceSize=8m
-XX:MaxMetaspaceSize=80m

3. 設置監控參數

基於 jconsole 監控,我們需要設置下參數。

-Djava.rmi.server.hostname=127.0.0.1
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=7397
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false

4. 測試運行

4.1 配置參數

以上的測試參數,配置到IDEA中運行程式里就可以,如下:

圖 25-5 設置程式運行參數,監控OOM

另外,jconsole 可以通過 IDEA 提供的 Terminal 啟動,直接輸入 jconsole,回車即可。

4.2 測試結果

org.itstack.interview.MetaSpaceOomMock$$EnhancerByCGLIB$$bd2bb16e999099900
org.itstack.interview.MetaSpaceOomMock$$EnhancerByCGLIB$$9c774e64999199910
org.itstack.interview.MetaSpaceOomMock$$EnhancerByCGLIB$$cac97732999299920
org.itstack.interview.MetaSpaceOomMock$$EnhancerByCGLIB$$91c6a15a999399930
Exception in thread "main" java.lang.IllegalStateException: Unable to load cache item
	at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:79)
	at net.sf.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
	at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:119)
	at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
	at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
	at net.sf.cglib.proxy.Enhancer.createClass(Enhancer.java:337)
	at org.itstack.interview.MetaSpaceOomMock.main(MetaSpaceOomMock.java:34)
Caused by: java.lang.OutOfMemoryError: Metaspace
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:467)
	at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:339)
	at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
	at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:96)
	at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:94)
	at net.sf.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54)
	at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
	at java.util.concurrent.FutureTask.run(FutureTask.java)
	at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61)
	... 6 more
  • 要的就是這句,java.lang.OutOfMemoryError: Metaspace,元空間OOM,證明 JDK1.8 已經去掉永久代,換位元空間。

4.3 監控截圖

圖 25-6 jconsole監測元空間溢出

  • 圖 25-6,就是監測程式OOM時的元空間表現。這回對這個元空間就有感覺了吧!

七、總結

  • 本文從 JDK 各個版本關於記憶體模型結構的演變,來了解各個區域,包括:程式計數器、Java 虛擬機棧、本地方法棧、堆和元空間。並了解從 JDK 1.8 開始去掉方法區引入元空間的核心目的和作用。
  • 在通過手擼JVM程式碼的方式讓大家對運行時數據區有一個整體的認知,也通過這樣的方式讓大家對學習這部分知識有一個抓手。
  • 最後我們通過 jconsole 檢測元空間溢出的整個過程,來學以致用,看看元空間到底在解決什麼問題以及怎麼測試。

八、系列推薦