1. JVM 內(nèi)存模型
.java文件會(huì)被編譯器編譯為.class文件,然后由JVM中的類加載器加載各個(gè)類的字節(jié)碼文件,加載完畢后,交由JVM執(zhí)行。JVM會(huì)用一段空間來存儲(chǔ)程序執(zhí)行期間需要的數(shù)據(jù)和相關(guān)信息,這段空間一般稱為Runtime Data Area運(yùn)行時(shí)數(shù)據(jù)區(qū),也就是JVM內(nèi)存。

1.1. 程序計(jì)數(shù)器
程序計(jì)數(shù)器是一個(gè)記錄著當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
JVM 采用 CPU 時(shí)間片輪轉(zhuǎn)算法來調(diào)度多線程。當(dāng)被掛起的線程重新獲取到時(shí)間片時(shí),它必須知道上次執(zhí)行到哪里才能繼續(xù)執(zhí)行,因此程序計(jì)數(shù)器就是記錄某個(gè)線程的字節(jié)碼執(zhí)行位置。
- 占用的內(nèi)存空間較小。
- 線程私有,每個(gè)線程都有獨(dú)立程序計(jì)數(shù)器。
- JVM 規(guī)范中唯一沒有規(guī)定
OutOfMemoryError情況的區(qū)域。 - 執(zhí)行 java 方法時(shí),程序計(jì)數(shù)器是有值的,且記錄的是正在執(zhí)行的字節(jié)碼指令的地址。
- 執(zhí)行 native 本地方法時(shí),程序計(jì)數(shù)器的值為空(Undefined)。因?yàn)?native 方法是 java 通過 JNI 直接調(diào)用本地 C/C++ 庫,由于該方法是通過 C/C++ 實(shí)現(xiàn),無法產(chǎn)生相應(yīng)的字節(jié)碼,并且 C/C++ 執(zhí)行時(shí)的內(nèi)存分配是由自己語言決定的,而不是由 JVM 決定的。
1.2. 虛擬機(jī)棧
描述 Java 方法執(zhí)行的內(nèi)存模型,用于存儲(chǔ)棧幀。
- 線程隔離性,每個(gè)線程都有獨(dú)立虛擬機(jī)棧。
- 使用的內(nèi)存不需要保證是連續(xù)的。
- JVM 規(guī)范既允許虛擬機(jī)棧被實(shí)現(xiàn)成固定大?。H萘吭诰€程創(chuàng)建時(shí)確定),也允許通過動(dòng)態(tài)擴(kuò)容和收縮來調(diào)整大小。
1.2.1. 棧幀
每個(gè)線程中調(diào)用一個(gè)相同或不同的方法,都會(huì)創(chuàng)建一個(gè)新的棧幀。調(diào)用的方法鏈越多,創(chuàng)建的棧幀越多(遞歸)。每個(gè)方法從調(diào)用到執(zhí)行完成的過程,就對(duì)應(yīng)入棧到出棧的過程。在 Running 線程中,所有的指令都只能針對(duì)當(dāng)前幀(位于棧頂?shù)膸┻M(jìn)行操作。
存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法返回地址、附加信息等信息。
- 局部變量表:用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。
- 操作數(shù)棧:方法的執(zhí)行操作都在此完成,每一個(gè)字節(jié)碼指令往操作數(shù)棧進(jìn)行寫入和提取的過程,就是入棧和出棧的過程。JVM 的 JIT 引擎稱為 “基于棧的執(zhí)行引擎”,這里的 “?!?就是操作數(shù)棧。
- 動(dòng)態(tài)連接:每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬性方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過程中的動(dòng)態(tài)連接。
- 方法返回地址:方法退出后,需要返回到被調(diào)用的位置程序才能繼續(xù)執(zhí)行。一般地,方法正常退出時(shí),返回地址可以是調(diào)用者的程序計(jì)數(shù)器的值,棧幀中很可能會(huì)保存這個(gè)計(jì)數(shù)器值。方法異常退出時(shí),返回地址要通過異常處理器表來確定,棧幀中一般不會(huì)保存這部分信息。
- 附加信息:JVM 規(guī)范允許具體的虛擬機(jī)實(shí)現(xiàn)增加一些規(guī)范里沒有描述的信息到棧幀中,例如與調(diào)試相關(guān)的信息,這部分信息完全取決于具體的虛擬機(jī)實(shí)現(xiàn)。
1.2.2. 棧內(nèi)存溢出
導(dǎo)致棧內(nèi)存溢出的情況:
- 壓入的棧幀過多(遞歸調(diào)用)。
- 壓入的棧幀過大(不容易出現(xiàn))。
1.3. 本地方法棧
與虛擬機(jī)棧幾乎相同,對(duì)象是Native方法。為虛擬機(jī)使用到的 Native 方法服務(wù)。JVM 規(guī)范中對(duì)本地方法棧沒有強(qiáng)制規(guī)定,不同虛擬機(jī)可以自由實(shí)現(xiàn)。

- 本地方法棧是一個(gè)后入先出棧。
- 由于是線程私有的,生命周期隨著線程,線程啟動(dòng)而產(chǎn)生,線程結(jié)束而消亡。
- 本地方法棧會(huì)拋出
StackOverflowError和OutOfMemoryError錯(cuò)誤。
1.4. 堆
最大的內(nèi)存空間,被所有線程共享,用來存儲(chǔ)對(duì)象實(shí)例及數(shù)組內(nèi)容。幾乎所有的對(duì)象實(shí)例都會(huì)存儲(chǔ)在堆中分配。
- 從內(nèi)存分配的角度看,線程共享的 Java 堆中可能劃分出多個(gè)線程私有的線程本地分配緩存區(qū)(Thread Local Allocation Buffer,TLAB)。
- JVM 規(guī)范規(guī)定,Java 堆可以物理不連續(xù),只要邏輯連續(xù)即可。既可以是固定大小,也可以是可擴(kuò)展大小,主流虛擬機(jī)都是按照可擴(kuò)展實(shí)現(xiàn)。
- 如果是可擴(kuò)展大小,如果嘗試擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存,那 JVM 將拋出
OutOfMemoryError異常。
1.5. 方法區(qū)
JVM 規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但它有一個(gè)別名 Non-Heap(非堆),目的是與 Java 堆區(qū)分開來。
- 方法區(qū)與 Java 堆一樣,是所有線程共享的內(nèi)存區(qū)域。
- JDK7 之前(永久代)用于存放已經(jīng)被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
- 運(yùn)行時(shí)常量池是方法區(qū)的一部分。Class 文件中除了有類的版本 / 字段 / 方法 / 接口等描述信息外,還有一項(xiàng)信息是常量池,用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將類在加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。運(yùn)行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的是
String.intern()方法。受方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出OutOfMemoryError異常。 - Java 中包裝類
Byte、Short、Integer、Long、Character、Boolean都實(shí)現(xiàn)了常量池技術(shù),Float和Double則沒有實(shí)現(xiàn)。Byte、Short、Integer、Long、Character這 5 種整型的包裝類也只是在對(duì)應(yīng)值在-128~127之間時(shí)才可使用對(duì)象池。
1.5.1. 組成結(jié)構(gòu)
【JDK 6】
永久代物理上是堆的一部分,和新生代,老年代地址是連續(xù)的。

【JDK 8】
元空間屬于本地內(nèi)存。

1.5.2. 方法區(qū)內(nèi)存溢出
- JDK 8之前會(huì)導(dǎo)致永久代內(nèi)存溢出
java.lang.OutOfMemoryError: PermGen space。 - JDK 8之后會(huì)導(dǎo)致永久代內(nèi)存溢出
java.lang.OutOfMemoryError: Metaspace。
2. 逃逸分析
逃逸分析是 Java 虛擬機(jī)中的一種優(yōu)化技術(shù),但它并不是直接優(yōu)化代碼,而是為其他優(yōu)化手段提供優(yōu)化依據(jù)的分析技術(shù)。JDK8 默認(rèn)開啟。
逃逸分析的基本行為就是分析對(duì)象動(dòng)態(tài)作用域,當(dāng)一個(gè)對(duì)象在方法中被定義后,它可能被外部方法所引用,稱為方法逃逸;也可能被外部線程訪問到,稱為線程逃逸。
對(duì)象的三種逃逸狀態(tài):
-
全局逃逸 GlobalEscape: 一個(gè)對(duì)象的引用逃出了方法或者線程:
- 對(duì)象的引用賦值給一個(gè)類變量(成員變量或靜態(tài)成員變量)。
- 對(duì)象的引用存儲(chǔ)在已逃逸的對(duì)象中。
- 對(duì)象的引用作為方法的返回值返回。
- 參數(shù)逃逸 ArgEscape: 在方法調(diào)用過程中傳遞對(duì)象的引用給調(diào)用方法。
- 沒有逃逸 NoEscape: 一個(gè)可以進(jìn)行標(biāo)量替換的對(duì)象,可以不將這種對(duì)象分配在堆上。
private Object o;
private HashMap<Integer, Object> map = new HashMap<>();
// 給成員變量賦值,發(fā)生全局逃逸
public void test1() {
o = new Object();
}
// 存儲(chǔ)在已逃逸對(duì)象中,發(fā)生全局逃逸
public void test2() {
Object o = new Object();
map.put(0, o);
}
// 作為方法返回值,發(fā)生全局逃逸
public Object test3() {
return new Object();
}
// 實(shí)例引用傳遞,發(fā)生參數(shù)逃逸
public void test4() {
Object o = methodPointerEscape();
}
// 純粹的局部作用域,沒有逃逸
public void test5() {
Object o = new Object();
}
2.1. 標(biāo)量替換
把一個(gè) Java 對(duì)象拆散,根據(jù)程序訪問的情況,將其使用到的成員變量恢復(fù)到基本數(shù)據(jù)類型來訪問,就叫標(biāo)量替換。
【標(biāo)量】
一個(gè)數(shù)據(jù)無法再分解為更小的數(shù)據(jù)來表示了,Java 虛擬機(jī)中的基本數(shù)據(jù)類型 byte、short、int、long、boolean、char、float、double 以及 reference 類型等,都不能再進(jìn)一步分解了,這些就可以稱為標(biāo)量。
【聚合量】
一個(gè)數(shù)據(jù)可以繼續(xù)分解,就稱為聚合量。對(duì)象就是最典型的聚合量。
【替換過程】
如果一個(gè)對(duì)象沒有逃逸,則運(yùn)行時(shí)可能不創(chuàng)建這個(gè)對(duì)象,而改為直接創(chuàng)建它的若干個(gè)被這個(gè)方法使用到的成員變量來替代。
將對(duì)象拆分后,除了可以讓對(duì)象的成員變量在棧上分配和讀寫外(棧上存儲(chǔ)的數(shù)據(jù),有很大概率會(huì)被虛擬機(jī)分配至物理機(jī)器的高速寄存器中存儲(chǔ)),還可以為后續(xù)的進(jìn)一步優(yōu)化手段創(chuàng)造條件。
class User {
int age;
int id;
}
public void test() {
// 由于User對(duì)象沒有逃逸,且User對(duì)象可以被拆分為兩個(gè)標(biāo)量
// 因此這個(gè)User對(duì)象可以被分配在棧中
User user = new User();
// user.id = 1;
}
2.2. 棧上分配
基于逃逸分析和標(biāo)量替換。JDK8 默認(rèn)開啟。
【原理】
方法內(nèi)局部變量對(duì)象未發(fā)生逃逸,則使用標(biāo)量替換將該對(duì)象分解,并在棧上分配內(nèi)存,不在堆中分配,分配完成后,繼續(xù)在調(diào)用棧內(nèi)執(zhí)行。
方法執(zhí)行完后自動(dòng)銷毀,線程結(jié)束后??臻g被回收,局部變量對(duì)象也被回收,不需要 GC ,提高系統(tǒng)性能。
public static void alloc() {
byte[] b = new byte[2];
b[0] = 1;
}
public static void main(String[] args) {
// 短時(shí)間內(nèi)在堆內(nèi)存中大量創(chuàng)建和銷毀對(duì)象,會(huì)頻繁GC,引發(fā)內(nèi)存抖動(dòng),最終的執(zhí)行時(shí)間約900ms左右
// 使用棧上分配可以完全避免堆內(nèi)存的內(nèi)存抖動(dòng),最終的執(zhí)行時(shí)間約6ms左右
for (int i = 0; i < 100000000; i++) {
alloc();
}
}
【使用場(chǎng)景】
對(duì)于大量的零散小對(duì)象,棧上分配的速度快,可以避免 GC 帶來的 Stop The World。但??臻g比較小,因此大對(duì)象不適合進(jìn)行棧上分配。
2.3. 同步消除
如果一個(gè)對(duì)象沒有逃逸,對(duì)這個(gè)變量的同步措施就可以消除掉。單線程中是沒有鎖競(jìng)爭(zhēng)。(即鎖和鎖塊內(nèi)的對(duì)象不會(huì)逃逸出線程,就可以把這個(gè)同步塊取消)
public static void alloc() {
byte[] b = new byte[2];
// 不會(huì)線程逃逸,所以該同步鎖可以去掉
// 開啟使用同步消除執(zhí)行時(shí)間 10 ms左右
// 關(guān)閉使用同步消除執(zhí)行時(shí)間 3870 ms左右
synchronized (b) {
b[0] = 1;
}
}
public static void main(String[] args) {
for (int i = 0; i < 100000000; i++) {
alloc();
}
}