Java虛擬機(jī)是JVM類語(yǔ)言的根基屯掖,其中動(dòng)態(tài)內(nèi)存管理和垃圾收集技術(shù)是JVM中最重要的特性性湿。本節(jié)主要講述其中的內(nèi)存管理相關(guān)概念纬傲。
一 Java虛擬機(jī)的基本結(jié)構(gòu)
如圖所示為Java虛擬機(jī)的基本結(jié)構(gòu),每個(gè)模塊介紹如下:
- 類加載子系統(tǒng)
類加載子系統(tǒng)負(fù)責(zé)從文件或網(wǎng)絡(luò)中加載Class字節(jié)碼信息肤频,然后存放于方法區(qū)叹括。 - 方法區(qū)
方法區(qū)是各個(gè)線程共享的內(nèi)存區(qū)域,用于存儲(chǔ)虛擬機(jī)加載的類變量宵荒、常量汁雷、靜態(tài)變量以及即時(shí)編譯后的代碼等數(shù)據(jù)。 - Java堆
在虛擬機(jī)啟動(dòng)的時(shí)候建立报咳,是Java程序最主要的內(nèi)存工作區(qū)域侠讯,幾乎所有的對(duì)象實(shí)例和數(shù)組都在Java堆上分配。和方法區(qū)一樣暑刃,是各個(gè)線程共享的內(nèi)存區(qū)域厢漩。可通過-Xmx和Xms虛擬機(jī)參數(shù)控制Java堆大小岩臣。 - 垃圾回收系統(tǒng)
垃圾回收是Java虛擬機(jī)的重要組成部分袁翁,其中的垃圾回收器可以對(duì)Java堆、方法區(qū)和直接內(nèi)存進(jìn)行回收婿脸。同時(shí)Java堆是回收器的工作重點(diǎn)粱胜。 - 直接內(nèi)存
在Java的NIO庫(kù)中,允許Java程序使用直接內(nèi)存狐树,它是Java堆外直接向系統(tǒng)申請(qǐng)的內(nèi)存區(qū)域焙压。通常情況下該區(qū)域的內(nèi)存訪問速度優(yōu)于Java堆。 - Java棧
Java棧是線程私有的,它的生命周期和線程相同涯曲。它在線程創(chuàng)建的時(shí)候被創(chuàng)建野哭。Java棧中保存幀信息,每個(gè)方法創(chuàng)建的時(shí)候都會(huì)創(chuàng)建一個(gè)棧幀幻件,用于存儲(chǔ)局部變量拨黔、方法參數(shù)、操作數(shù)棧绰沥、方法出口燈信息篱蝇,和方法的調(diào)用返回密切相關(guān)。 - 本地方法棧
和Java棧類似徽曲,但其中最大的不同是Java棧用于方法調(diào)用零截,而本地方法棧用于本地方法調(diào)用。 - PC寄存器
該區(qū)域也是每個(gè)線層私有的空間秃臣。Java虛擬會(huì)為每個(gè)Java線程創(chuàng)建PC寄存器涧衙。當(dāng)當(dāng)前執(zhí)行的方法不是本地方法時(shí),PC寄存器就會(huì)指向當(dāng)前正在被執(zhí)行的指令奥此。若當(dāng)前執(zhí)行的方式是本地方法弧哎,則PC寄存器的值為undefined。 - 執(zhí)行引擎
執(zhí)行引擎是虛擬機(jī)最核心的組件之一稚虎,它負(fù)責(zé)執(zhí)行虛擬機(jī)的字節(jié)碼撤嫩。
二 Java堆
Java堆是和Java應(yīng)用程序關(guān)系最為密切的內(nèi)存空間。Java堆內(nèi)存通過垃圾回收機(jī)制祥绞,垃圾對(duì)象會(huì)被自動(dòng)清理非洲,而不需要顯示的釋放鸭限。Java堆分為新生代和老年代蜕径。其中新生代存放新生對(duì)象或年齡不大的對(duì)象,而老年代則存放老年對(duì)象败京。新生代和老年代結(jié)構(gòu)如下圖所示:
在大多數(shù)情況下兜喻,對(duì)象首先在Eden區(qū)分配,在一次新生代回收后赡麦,若對(duì)象還存活著則進(jìn)入S0或S1朴皆,在這之后,每經(jīng)一次新生代回收泛粹,若對(duì)象還存活著則它的年齡會(huì)加1遂铡,達(dá)到一定年齡后該對(duì)象就被認(rèn)為是老年對(duì)象,從而進(jìn)入老年代晶姊。當(dāng)然這里只是其中一種方式進(jìn)入老年代扒接,后續(xù)文章會(huì)有詳細(xì)敘述。
三 Java棧
Java棧是一塊線程私有內(nèi)存空間。Java棧用于傳遞每次函數(shù)調(diào)用的數(shù)據(jù)钾怔。它是一塊后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)碱呼,在其中保存的主要內(nèi)容是棧幀。每一次函數(shù)調(diào)用都會(huì)有對(duì)應(yīng)的棧幀壓如Java棧宗侦,同時(shí)函數(shù)結(jié)束時(shí)棧幀被彈出愚臀。
上圖可用下面的代碼表示:
public void function1() {
public void function2();
}
public void function2() {
public void function3();
}
public void function3() {
....
}
...
在一個(gè)棧幀中至少包含局部變量表、操作數(shù)棧和幀數(shù)據(jù)區(qū)幾個(gè)部分矾利。
但是Java椆昧眩空間也不能無(wú)限使用下去,它受-Xss參數(shù)限制梦皮,該參數(shù)也決定了函數(shù)調(diào)用的最大深度炭分。
示例:
public class TestStackDeep {
private static int count = 0;
public static void recursion() {
count++;
recursion();
}
public static void main(String[] args) {
try {
recursion();
} catch (Throwable e) {
System.out.println("deep of calling = " + count);
e.printStackTrace();
}
}
}
上面的代碼計(jì)算最大棧深度,設(shè)置虛擬機(jī)參數(shù)-Xss256K剑肯,其結(jié)果為:
deep of calling = 2374
當(dāng)設(shè)置-Xss512K時(shí)捧毛,結(jié)果為:
deep of calling = 9245
棧溢出則會(huì)拋出java.lang.StackOverflowError異常。
1)局部變量表
局部變量表用于保存函數(shù)的參數(shù)以及局部變量让网,它同樣也隨函數(shù)的調(diào)用而生滅呀忧,函數(shù)變量表可通過jclasslib工具查看,在Idea中溃睹,jclasslib可作為插件方式安裝而账。
示例代碼:
public class TestStack {
public void test1() {
int m, n, i, j, k;
System.out.println("hello world");
}
public void test2(int param1, int param2) {
long m, n, i, j, k;
System.out.println("hello world");
}
public static void main(String[] args) {
TestStack testStack = new TestStack();
}
}
通過查看jclasslib可看到以下內(nèi)容:
在jclasslib中,可看到當(dāng)前類中的靜態(tài)池因篇,接口字段以及方法等信息泞辐。查看方法的Code部分可看到局部變量表統(tǒng)計(jì)信息:
從上圖看出,test2()最大局部變量表占用大小為13字竞滓,因?yàn)閠est2()參數(shù)為兩個(gè)int咐吼,加上this字段以及5個(gè)long變量正好是13字(int占用一個(gè)字,this占用一個(gè)字商佑,long占用兩個(gè)字)锯茄。查看局部變量表信息可通過LocalVariableTable:
上圖中分別對(duì)應(yīng)了局部變量的作用域范圍,所在槽位(index)茶没,變量名(name)以及數(shù)據(jù)類型(Descriptor)
棧幀中局部變量表的槽位是可以復(fù)用的肌幽,如果一個(gè)局部變量過了其作用域,那么在其后申明的新局部變量就有可能復(fù)用過期局部變量的槽位抓半,從而達(dá)到節(jié)省資源的目的喂急。
2)操作數(shù)棧
操作數(shù)棧主要用于保存計(jì)算過程中間結(jié)果。也作為計(jì)算過程中變量的臨時(shí)存儲(chǔ)空間笛求。
3)幀數(shù)據(jù)區(qū)
4)棧上分配
棧上分配是Java虛擬機(jī)提供的一項(xiàng)優(yōu)化技術(shù)廊移,基本思想是對(duì)于線程私有對(duì)象(即不會(huì)被其它線程訪問到的對(duì)象實(shí)例)讥蔽,可以將它們分配在棧上,而不必從堆中分配画机,這樣的好處是該對(duì)象在函數(shù)調(diào)用完畢后可以自行銷毀而不必接入垃圾回收器冶伞,從而提高系統(tǒng)的整體性能。棧上分配的一個(gè)基礎(chǔ)技術(shù)是逃逸分析步氏,逃逸分析的目的是判斷對(duì)象作用域是否逃逸出函數(shù)體响禽。
在編譯程序優(yōu)化理論中,逃逸分析是一種確定指針動(dòng)態(tài)范圍的方法——分析在程序的哪些地方可以訪問到指針荚醒。它涉及到指針分析和形狀分析芋类。
當(dāng)一個(gè)變量(或?qū)ο?在子程序中被分配時(shí),一個(gè)指向變量的指針可能逃逸到其它執(zhí)行線程中界阁,或是返回到調(diào)用者子程序侯繁。如果使用尾遞歸優(yōu)化(通常在函數(shù)編程語(yǔ)言中是需要的),對(duì)象也可以看作逃逸到被調(diào)用的子程序中泡躯。如果一種語(yǔ)言支持第一類型的延續(xù)性在Scheme和Standard ML of New Jersey中同樣如此)贮竟,部分調(diào)用棧也可能發(fā)生逃逸。
編譯器可以使用逃逸分析的結(jié)果作為優(yōu)化的基礎(chǔ):[1]
- 將堆分配轉(zhuǎn)化為棧分配较剃。如果某個(gè)對(duì)象在子程序中被分配咕别,并且指向該對(duì)象的指針永遠(yuǎn)不會(huì)逃逸,該對(duì)象就可以在分配在棧上写穴,而不是在堆上惰拱。在有垃圾收集的語(yǔ)言中,這種優(yōu)化可以降低垃圾收集器運(yùn)行的頻率啊送。
- 同步消除偿短。如果發(fā)現(xiàn)某個(gè)對(duì)象只能從一個(gè)線程可訪問,那么在這個(gè)對(duì)象上的操作可以不需要同步馋没。
- 分離對(duì)象或標(biāo)量替換昔逗。如果某個(gè)對(duì)象的訪問方式不要求該對(duì)象是一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu),那么對(duì)象的部分(或全部)可以不存儲(chǔ)在內(nèi)存披泪,而是存儲(chǔ)在CPU寄存器中纤子。
例如下面的代碼便是一個(gè)逃逸對(duì)象:
private static Bean bean;
public static void alloc() {
bean = new Bean();
bean.setParam(23);
....
}
其中bean字段可能被其它線程訪問到搬瑰,故屬于逃逸對(duì)象款票。
下面的代碼顯示了非逃逸對(duì)象:
public static void alloc() {
bean = new Bean();
bean.setParam(23);
....
}
啟用逃逸分析需要設(shè)置-server執(zhí)行程序,JVM參數(shù)如下:
-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations
- -Xmx256m -Mms256m 分別制定了堆最大空間和堆最小空間為10M;
- -XX:+DoEscapeAnalysis 啟用逃逸分析泽论;
- -XX:+PrintGC 打印GC信息艾少;
- -XX:-UseTLAB 關(guān)閉TLAB;
- -XX:+EliminateAllocations 開啟標(biāo)量替換翼悴,允許將對(duì)象打散分配到棧上缚够。
以上參數(shù)都是默認(rèn)啟用的幔妨。
示例:
public class OnStackTest {
public static class User {
public int id = 0;
public String name = "";
}
public static void alloc() {
User user = new User();
user.id = 10;
user.name = "vincent";
}
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
alloc();
}
System.out.println(System.currentTimeMillis() - start);
}
}
上面的代碼進(jìn)行了1000000000次調(diào)用,但是產(chǎn)生的GC日志很少:
[GC (Allocation Failure) 2047K->536K(9728K), 0.0008531 secs]
7
但如果關(guān)閉了逃逸分析谍椅,則會(huì)產(chǎn)生大量的GC日志误堡。例如將-XX:+DoEscapeAnalysis 替換成-XX:-DoEscapeAnalysis
四 方法區(qū)
方法區(qū)也是所有線程共享的內(nèi)存區(qū)域,用于保存系統(tǒng)類信息雏吭,例如字段锁施,方法常量池等。該區(qū)域大小決定了系統(tǒng)可以保存多少類杖们。但若定義了太多類同樣也會(huì)導(dǎo)致方法區(qū)溢出悉抵。
在JDK6和JDK7中,方法區(qū)可鏈接為永久區(qū)摘完,通過參數(shù)-XX:PermSize和-XX:MaxPermSize指定姥饰。但在JDK8中,永久區(qū)已經(jīng)被移除孝治,替代為元數(shù)據(jù)區(qū)列粪,可使用-XX:MaxMetaspaceSize參數(shù)指定,若不指定該參數(shù)谈飒,默認(rèn)情況下虛擬機(jī)會(huì)耗盡所有可用系統(tǒng)內(nèi)存篱竭,在VisualVM中可觀察永久區(qū):
元數(shù)據(jù)區(qū)溢出虛擬機(jī)會(huì)拋出java.lang.OutOfMemoryError: Metaspace異常。
參考
- 《實(shí)戰(zhàn)Java虛擬機(jī): JVM故障與性能優(yōu)化》
- 《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐》
- Java中的逃逸分析和TLAB以及Java對(duì)象分配
- 逃逸分析
- 深入分析JVM逃逸分析對(duì)性能的影響