JVM內(nèi)存模型總結和上手實踐,親測學完沒脫發(fā)!
本文轉載自微信公眾號「bugstack蟲洞棧」,作者小傅哥。轉載本文請聯(lián)系bugstack蟲洞棧公眾號。
目錄
- 一、前言
- 二、面試題
- 三、 JDK1.6、JDK1.7、JDK1.8 內(nèi)存模型演變
- 四、內(nèi)存模型各區(qū)域介紹
- 1. 程序計數(shù)器
- 2. Java虛擬機棧
- 3. 本地方法棧
- 4. 堆和元空間
- 5. 常量池
- 五、手擼虛擬機(內(nèi)存模型)
- 1. 工程結構
- 2. 重點代碼
- 六、jconsole監(jiān)測元空間溢出
- 1. 找段持續(xù)創(chuàng)建大對象的代碼
- 2. 調(diào)整元空間大小
- 3. 設置監(jiān)控參數(shù)
- 4. 測試運行
- 七、總結
- 八、系列推薦
一、前言
看了一篇文章30歲有多難!
每篇文章的開篇總喜歡寫一些,從個人視角看這個世界的感悟。
最近看到一篇文章,30歲有多難。文中的一些主人公好像在學業(yè)、工作、生活、愛情等方面都過的都不如意。要不是錯過這,要不是走錯那??偨Y來看,就像是很倒霉的一群倒霉蛋兒在跟生活對干!
但其實每個人可能都遇到過生活中最難的時候,或早或晚。就像我剛畢業(yè)不久時一連串遇到;冬天里丟過第一部手機、修一個進了水的電腦、租的房子第一次被騙,一連串下來頭一次要趕在工資沒發(fā)的時候,選擇少吃早飯還是午飯,看看能扛過去那頓。
哈哈哈哈哈,現(xiàn)在想想還挺有意思的,不過這些亂遭的事很多是自己的意識和能力不足時做出的錯誤選擇而導致的。
人那,想開車就要考駕照,想走遠就要有能力。多提升認知,多拓寬眼界!生活的意義就是不斷的更新自己!
二、面試題
謝飛機,小記!,冬風吹、戰(zhàn)鼓擂。被窩里,誰怕誰。
「謝飛機」:歪?大哥,你在嗎?
「面試官」:咋了,大周末的,這么早打電話!?
「謝飛機」:我夢見,我去谷歌寫JVM了,給你們公司用,之后蹦了,讓我起來改bug!
「面試官」:啊!?啊,那我問你,JDK 1.8 與 JDK 1.7 在運行時數(shù)據(jù)區(qū)的設計上,你都怎么做的優(yōu)化策略的?
「謝飛機」:我沒寫這,我不知道!
「面試官」:擦。。。
三、 JDK1.6、JDK1.7、JDK1.8 內(nèi)存模型演變
圖 25-1 JDK1.6、JDK1.7、JDK1.8,內(nèi)存模型演變
如圖 25-1 是 JDK 1.6、1.7、1.8 的內(nèi)存模型演變過程,其實這個內(nèi)存模型就是 JVM 運行時數(shù)據(jù)區(qū)依照JVM虛擬機規(guī)范的具體實現(xiàn)過程。
在圖 25-1 中各個版本的迭代都是為了更好的適應CPU性能提升,最大限度提升的JVM運行效率。這些版本的JVM內(nèi)存模型主要有以下差異:
- JDK 1.6:有永久代,靜態(tài)變量存放在永久代上。
- JDK 1.7:有永久代,但已經(jīng)把字符串常量池、靜態(tài)變量,存放在堆上。逐漸的減少永久代的使用。
- JDK 1.8:無永久代,運行時常量池、類常量池,都保存在元數(shù)據(jù)區(qū),也就是常說的元空間。但字符串常量池仍然存放在堆上。
四、內(nèi)存模型各區(qū)域介紹
1. 程序計數(shù)器
較小的內(nèi)存空間、線程私有,記錄當前線程所執(zhí)行的字節(jié)碼行號。
如果執(zhí)行 Java 方法,計數(shù)器記錄虛擬機字節(jié)碼當前指令的地址,本地方法則為空。
這一塊區(qū)域沒有任何 OutOfMemoryError 定義。
「以上」,就是關于程序計數(shù)器的定義,如果這樣看沒有感覺,我們舉一個例子。
定義一段 Java 方法的代碼,這段代碼是計算圓形的周長。
- public static float circumference(float r){
- float pi = 3.14f;
- float area = 2 * pi * r;
- return area;
- }
接下來,如圖 25-2 是這段代碼的在虛擬機中的執(zhí)行過程,左側是它的程序計數(shù)器對應的行號。
圖 25-2 程序計數(shù)器
這些行號每一個都會對應一條需要執(zhí)行的字節(jié)碼指令,是壓棧還是彈出或是執(zhí)行計算。
之所以說是線程私有的,因為如果不是私有的,那么整個計算過程最終的結果也將錯誤。
2. Java虛擬機棧
- 每一個方法在執(zhí)行的同時,都會創(chuàng)建出一個棧幀,用于存放局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口、線程等信息。
- 方法從調(diào)用到執(zhí)行完成,都對應著棧幀從虛擬機中入棧和出棧的過程。
- 最終,棧幀會隨著方法的創(chuàng)建到結束而銷毀。
可能這么只從定義看上去仍然沒有什么感覺,我們再找一個例子。
這是一個關于斐波那契數(shù)列(Fibonacci sequence)求值的例子,我們通過斐波那契數(shù)列在虛擬機中的執(zhí)行過程,來體會Java虛擬機棧的用途。
斐波那契數(shù)列(Fibonacci sequence),又稱黃金分割數(shù)列、因數(shù)學家列昂納多·斐波那契(Leonardoda Fibonacci)以兔子繁殖為例子而引入,故又稱為“兔子數(shù)列”,指的是這樣一個數(shù)列:1、1、2、3、5、8、13、21、34、……在數(shù)學上,斐波納契數(shù)列以如下被以遞推的方法定義:F(1)=1,F(xiàn)(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)在現(xiàn)代物理、準晶體結構、化學等領域,斐波納契數(shù)列都有直接的應用,為此,美國數(shù)學會從1963年起出版了以《斐波納契數(shù)列季刊》為名的一份數(shù)學雜志,用于專門刊載這方面的研究成果。
圖 25-3 斐波那契數(shù)列在虛擬機棧中的執(zhí)行過程
整個這段流程,就是方法的調(diào)用和返回。在調(diào)用過程申請了操作數(shù)棧的深度和局部變量的大小。
以及相應的信息從各個區(qū)域獲取并操作,其實也就是入棧和出棧的過程。
3. 本地方法棧
本地方法棧與Java虛擬機棧作用類似,唯一不同的就是本地方法棧執(zhí)行的是Native方法,而虛擬機棧是為JVM執(zhí)行Java方法服務的。
另外,與 Java 虛擬機棧一樣,本地方法棧也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
JDK1.8 HotSpot虛擬機直接就把本地方法棧和虛擬機棧合二為一。
關于本地方法棧在以上的例子已經(jīng)涉及了這部分內(nèi)容,這里就不在贅述了。
4. 堆和元空間
圖 25-4 Java 堆區(qū)域劃分
- JDK 1.8 JVM 的內(nèi)存結構主要由三大塊組成:堆內(nèi)存、元空間和棧,Java 堆是內(nèi)存空間占據(jù)最大的一塊區(qū)域。
- Java 堆,由年輕代和年老代組成,分別占據(jù)1/3和2/3。
- 而年輕代又分為三部分,「Eden」、「From Survivor」、「To Survivor」,占據(jù)比例為8:1:1,可調(diào)。
- 另外這里我們特意畫出了元空間,也就是直接內(nèi)存區(qū)域。在 JDK 1.8 之后就不在堆上分配方法區(qū)了。
- 「元空間」從虛擬機Java堆中轉移到本地內(nèi)存,默認情況下,元空間的大小僅受本地內(nèi)存的限制,說白了也就是以后不會因為永久代空間不夠而拋出OOM異常出現(xiàn)了。jdk1.8以前版本的 class和JAR包數(shù)據(jù)存儲在 PermGen下面 ,PermGen 大小是固定的,而且項目之間無法共用,公有的 class,所以比較容易出現(xiàn)OOM異常。
- 升級 JDK 1.8后,元空間配置參數(shù),-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. http://openjdk.java.net/jeps/122
5. 常量池
從 JDK 1.7開始把常量池從永久代中剝離,直到 JDK1.8 去掉了永久代。而字符串常量池一直放在堆空間,用于存儲字符串對象,或是字符串對象的引用。
五、手擼虛擬機(內(nèi)存模型)
其實以上的內(nèi)容,已經(jīng)完整的介紹了JVM虛擬機的內(nèi)存模型,也就是運行時數(shù)據(jù)區(qū)的結構。但是這東西看完可能就忘記了,因為缺少一個可親手操作的代碼。
「所以」,這里我給大家用Java代碼寫一段關于數(shù)據(jù)槽、棧幀、局部變量、虛擬機棧以及堆的代碼結構,讓大家更好的加深對虛擬機內(nèi)存模型的印象。
1. 工程結構
運行時數(shù)據(jù)區(qū)
- 運行時數(shù)據(jù)區(qū)
- ├── 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實現(xiàn)的部分JVM虛擬機功能,這部分主要包括如下內(nèi)容:
- Frame,棧幀
- JvmStack,虛擬機棧
- LocalVars,局部變量
- OperandStack,操作數(shù)棧
- Slot,數(shù)據(jù)槽
- Thread,線程
- heap,堆,里面包括常量池和方法區(qū)
2. 重點代碼
「操作數(shù)棧 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;
- //操作數(shù)棧
- private OperandStack operandStack;
- private Thread thread;
- private Method method;
- private int nextPC;
- //...
- }
- 關于代碼結構看到這有點感覺了嗎?
- Slot數(shù)據(jù)槽,就是一個數(shù)組結構,用于存放數(shù)據(jù)的。
- 操作數(shù)棧、局部變量表,都是使用數(shù)據(jù)槽進行入棧入棧操作。
- 在棧幀里,可以看到連接、局部變量表、操作數(shù)棧、方法、線程等,那么文中說到的當有一個新的每一個方法在執(zhí)行的同時,都會創(chuàng)建出一個棧幀,是不就對了上,可以真的理解了。
- 如果你對JVM的實現(xiàn)感興趣,可以閱讀用Java實現(xiàn)JVM源碼:https://github.com/fuzhengwei/itstack-demo-jvm
六、jconsole監(jiān)測元空間溢出
不是說 JDK 1.8 的內(nèi)存模型把永久代下掉,換上元空間了嗎?但不測試下,就感受不到呀,沒有證據(jù)!
所有關于代碼邏輯的學習,都需要有數(shù)據(jù)基礎和證明過程,這樣才能有深刻的印象。走著,帶你把元空間干滿,讓它OOM!
1. 找段持續(xù)創(chuàng)建大對象的代碼
- 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());
- }
- }
網(wǎng)上找了一段基于CGLIB的,你可以寫一些其他的。
Thread.sleep(5000);,睡一會,方便我們點檢測,要不程序太快就異常了。
2. 調(diào)整元空間大小
默認情況下元空間太大了,不方便測試出結果,所以我們把它調(diào)的小一點。
- -XX:MetaspaceSize=8m
- -XX:MaxMetaspaceSize=80m
3. 設置監(jiān)控參數(shù)
基于 jconsole 監(jiān)控,我們需要設置下參數(shù)。
- -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 配置參數(shù)
「以上的測試參數(shù)」,配置到IDEA中運行程序里就可以,如下:
圖 25-5 設置程序運行參數(shù),監(jiān)控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 已經(jīng)去掉永久代,換位元空間。
4.3 監(jiān)控截圖
圖 25-6 jconsole監(jiān)測元空間溢出
圖 25-6,就是監(jiān)測程序OOM時的元空間表現(xiàn)。這回對這個元空間就有感覺了吧!
七、總結
本文從 JDK 各個版本關于內(nèi)存模型結構的演變,來了解各個區(qū)域,包括:程序計數(shù)器、Java 虛擬機棧、本地方法棧、堆和元空間。并了解從 JDK 1.8 開始去掉方法區(qū)引入元空間的核心目的和作用。
在通過手擼JVM代碼的方式讓大家對運行時數(shù)據(jù)區(qū)有一個整體的認知,也通過這樣的方式讓大家對學習這部分知識有一個抓手。
最后我們通過 jconsole 檢測元空間溢出的整個過程,來學以致用,看看元空間到底在解決什么問題以及怎么測試。