面試官:說說虛擬機的內(nèi)存結構有哪些?
一、摘要
熟悉 Java 語言特性的同學都知道,相比 C、C++ 等編程語言,Java 無需通過手動方式回收內(nèi)存,內(nèi)存中所有的對象都可以交給 Java 虛擬機來幫助自動回收;而像 C、C++ 等編程語言,需要開發(fā)者通過代碼手動釋放內(nèi)存資源,否則會導致內(nèi)存溢出。
盡管如此,如果編程不當,Java 應用程序也可能會出現(xiàn)內(nèi)存溢出的現(xiàn)象,例如下面這個異常!
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2760)
at java.util.Arrays.copyOf(Arrays.java:2734)
at java.util.ArrayList.ensureCapacity(ArrayList.java:167)
at java.util.ArrayList.add(ArrayList.java:351)
它表示當前服務已出現(xiàn)內(nèi)存溢出,簡單的說就是當服務出現(xiàn)了內(nèi)存不足時,就會拋OutOfMemoryError異常。
這種異常是怎么出現(xiàn)的呢?該如何解決呢?
熟悉 JVM 內(nèi)存結構的同學,可能會很快看得出以上錯誤信息表示虛擬機堆內(nèi)存空間不足,因此了解 JVM 內(nèi)存結構對快速定位問題并解決問題有著非常重要的意義。今天我們一起來了解一下 JVM 內(nèi)存結構。
本文以 JDK1.7 版本為例,不同的版本 JVM 內(nèi)存布局可能稍有不同,但是所涉及的知識點基本大同小異。
二、內(nèi)存結構介紹
Java 虛擬機在執(zhí)行程序的過程中,會把所管理的內(nèi)存劃分成若干不同的數(shù)據(jù)區(qū)域。這些區(qū)域各有各有的用途,有的區(qū)域會隨著虛擬機進程的啟動而一直存在;有的區(qū)域會伴隨著用戶線程的啟用和結束而創(chuàng)建和銷毀。
其次,JVM 內(nèi)存區(qū)域也稱為運行時數(shù)據(jù)區(qū)域,這些數(shù)據(jù)區(qū)域包括:程序計數(shù)器、虛擬機棧、本地方法棧、堆、方法區(qū)等,可以用如下圖來簡要概括。
圖片
其中,運行時數(shù)據(jù)區(qū)的程序計數(shù)器、虛擬機棧、本地方法棧屬于每個線程私有的區(qū)域;堆和方法區(qū)屬于所有線程間共享的區(qū)域。
運行時數(shù)據(jù)區(qū)的線程間內(nèi)存區(qū)域布局,可以用如下圖來簡要描述:
圖片
下面我們一起來看下每個區(qū)域的作用。
2.1、程序計數(shù)器
程序計數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它的作用可以看做是當前線程所執(zhí)行的字節(jié)碼的行號指示器。
在虛擬機的概念模型里,字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,比如分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復等基礎功能都需要依賴這個計數(shù)器來完成。
我們知道 Java 是支持多線程的,其中虛擬機的多線程就是通過輪流切換線程并分配處理器執(zhí)行時間的方式來實現(xiàn)的。在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內(nèi)核)只會執(zhí)行一條線程中的指令,為了線程切換后能恢復到正確的執(zhí)行位置,虛擬機為每個線程都設計了一個獨立的程序計數(shù)器,各條線程之間的程序計數(shù)器互不影響,獨立存儲,屬于線程私有的內(nèi)存區(qū)域,生命周期與線程相同。
在 JVM 規(guī)范中,如果線程執(zhí)行的是非native方法,則程序計數(shù)器中保存的是當前需要執(zhí)行的指令的地址;如果線程執(zhí)行的是native方法,則程序計數(shù)器中的值是Undefined,也就是空。
由于程序計數(shù)器中存儲的數(shù)據(jù)所占空間的大小不會隨程序的執(zhí)行而發(fā)生改變,因此,此內(nèi)存區(qū)域是唯一一個在 JVM 規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
2.2、虛擬機棧
虛擬機棧(Java Virtual Machine Stacks)與程序計數(shù)器一樣,也是線程私有的內(nèi)存區(qū)域,它的生命周期與線程相同。
虛擬機棧描述的是 Java 方法執(zhí)行時的內(nèi)存模型,每個方法執(zhí)行的時候都會創(chuàng)建一個棧幀(Stack Frame), 用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口和一些額外的附加信息。每一個方法從被調(diào)用直到執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的全過程。
虛擬機棧內(nèi)部結構,可以用如下圖來簡要描述。
圖片
下面簡單看看棧幀里的四種組成元素的作用。
2.2.1、局部變量表
局部變量表是一組變量值的存儲空間,用于存儲方法參數(shù)和局部變量,例如:
- 基本數(shù)據(jù)類型:比如 boolean、byte、char、short、int、float、long、double 等 8 種基本數(shù)據(jù)類型
- 對象引用類型:指向?qū)ο笃鹗嫉刂返囊弥羔?/li>
- 返回地址類型:指向一條字節(jié)碼指令的返回地址
通常,局部變量表的內(nèi)存空間在編譯器就會確定其大小,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是可以完全確定的,因此在程序執(zhí)行期間局部變量表的大小是不會改變的。
其次,局部變量表的最小單位為 32 位的字長,對于 64 位的 long 和 double 變量而言,虛擬機會為其分配兩個連續(xù)的局部變量空間。
2.2.2、操作數(shù)棧
操作數(shù)棧也常稱為操作棧,是一個后入先出的棧。虛擬機會利用操作棧的壓棧和出棧操作來執(zhí)行指令運算。
比如下面的兩個數(shù)據(jù)相加的計算示例。
begin
iload_0 // push the int in local variable 0 onto the stack
iload_1 // push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
end
在這個字節(jié)碼序列里,前兩個指令iload_0和iload_1將存儲在局部變量表中索引為0和1的整數(shù)壓入操作數(shù)棧中;接著iadd指令從操作數(shù)棧中彈出那兩個整數(shù)相加,再將結果壓入操作數(shù)棧;最后istore_2指令從操作數(shù)棧中彈出結果,并把它存儲到局部變量表索引為2的位置,完成數(shù)據(jù)的計算。
2.2.3、動態(tài)鏈接
每個棧幀都包含一個對當前方法類型的運行時常量池的引用,以支持方法調(diào)用過程中的動態(tài)鏈接。可以簡單的理解成,當前棧幀與運行時常量池的方法引用建立鏈接。
比如方法 a 入棧后,棧幀中的動態(tài)鏈接會持有對當前方法所屬類的常量池的引用,當方法 a 中調(diào)用了方法 b(符號引用),就可以通過運行時常量池查找到方法 b 具體的直接引用(方法地址),然后調(diào)用執(zhí)行。
2.2.4、方法出口
當一個方法執(zhí)行完畢之后,要返回之前調(diào)用它的地方,因此在棧幀中必須保存一個方法返回地址,也稱為方法出口。
在虛擬機棧中,只有兩種方式可以退出當前方法:
- 正常返回:當執(zhí)行遇到返回指令,會將返回值傳遞給上層的方法調(diào)用者,這種退出方式稱為正常返回,一般來說,調(diào)用者的程序計數(shù)器可以作為方法返回地址
- 異常返回:當執(zhí)行遇到異常,并且當前方法體內(nèi)沒有得到處理,就會導致方法退出,此時是沒有返回值的,這種退出方式稱為異常返回,返回地址要通過異常處理器表來確定
當一個方法返回時,可能依次進行以下 3 個操作:
- 1.恢復上層方法的局部變量表和操作數(shù)棧
- 2.把返回值壓入調(diào)用者棧幀的操作數(shù)棧
- 3.將程序計數(shù)器的值指向下一條方法指令位置
2.2.5、小結
在 JVM 規(guī)范中,對這個內(nèi)存區(qū)域規(guī)定了兩種異常狀況:
- 如果當前線程請求的棧深度大于虛擬機棧所允許的深度,將拋出StackOverFlowError異常(當前虛擬機棧不允許動態(tài)擴展的情況下)
- 如果虛擬機??梢詣討B(tài)擴展,當擴展到無法申請內(nèi)存到足夠的內(nèi)存,就會拋出OutOfMemoryError異常
2.3、本地方法棧
本地方法棧(Native Method Stacks)與虛擬機棧發(fā)揮的作用非常相似,主要區(qū)別在于:虛擬機棧為虛擬機執(zhí)行 Java 方法(也就是字節(jié)碼)服務;本地方法棧則是為虛擬機使用到的Native方法服務(通常采用 C 編寫)。
有些虛擬機發(fā)行版本,比如Sun HotSpot虛擬機,直接將本地方法棧和 Java 虛擬機棧合二為一。與虛擬機棧一樣,本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。
2.4、堆
Java 堆是被所有線程共享的最大的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例和數(shù)組都在這里分配內(nèi)存,也是出現(xiàn)OutOfMemoryError異常最常見的區(qū)域。
在虛擬機中,堆被劃分成兩個不同的區(qū)域:年輕代 (Young Generation) 和老年代 (Old Generation),默認情況下按照1 : 2的比例來分配空間。
其中年輕代又被劃分為三個不同的區(qū)域:Eden 區(qū)、From Survivor 區(qū)、To Survivor 區(qū),默認情況下按照8 : 1 : 1的比例來分配空間。
整個堆內(nèi)存的空間劃分,可以用如下圖來簡要描述。
圖片
這樣劃分的目的是為了使 JVM 能夠更好的管理堆內(nèi)存中的對象,包括內(nèi)存的分配以及回收。
新創(chuàng)建的對象分配會首先放在年輕代的 Eden 區(qū),此區(qū)的對象回收頻次會比較高,Survivor 區(qū)作為 Eden 區(qū)和 Old 區(qū)之間的緩沖區(qū),在 Survivor 區(qū)的對象經(jīng)歷若干次收集仍然存活的,就會被轉(zhuǎn)移到老年代 Old 區(qū)。
關于對象內(nèi)存回收的相關知識,我們在后續(xù)的文章會再次進行介紹。
2.5、方法區(qū)
方法區(qū)在 JVM 中也是一個非常重要的區(qū)域,和 Java 堆一樣,也是多個線程共享區(qū)域,它用于存儲類的信息(包括類的名稱、方法信息、字段信息)、靜態(tài)變量、常量以及即時編譯后的代碼等數(shù)據(jù)。
為了與 Java 堆區(qū)分,它還有一個別名 Non-Heap(非堆的意思)。相對而言,GC 對于這個區(qū)域的收集是很少出現(xiàn)的,但是也不意味著不會出現(xiàn)異常,當方法區(qū)無法滿足內(nèi)存分配需求時,也會拋出OutOfMemoryError異常。
在 Java 7 及之前版本,大家也習慣稱方法區(qū)它為“永久代”(Permanent Generation),更確切來說,應該是“HotSpot 使用永久代實現(xiàn)了方法區(qū)”!
2.6、運行時常量池
運行時常量池是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池 (Constant pool table),用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入運行時常量池中存放。
運行時常量池的功能類似于傳統(tǒng)編程語言的符號表,方便下游程序通過查表可找到對應的數(shù)據(jù)信息。
同時,運行時常量池相對于Class文件常量池的另外一個特性是具備動態(tài)性,Java 語言并不要求常量一定只有編譯器才產(chǎn)生,也就是說并非預置入Class文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池,運行期間也可能將新的常量放入池中,其中String.intern()方法就是這個特性的應用。
2.7、直接內(nèi)存
在之前的 Java NIO 文章中,我們提及到直接內(nèi)存。直接內(nèi)存(Direct Memory)并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是 JVM 規(guī)范中定義的內(nèi)存區(qū)域。
在 JDK1.4 中引入了 NIO 機制,它允許 Java 程序直接從操作系統(tǒng)中分配直接內(nèi)存,這部分內(nèi)存也被稱為堆外內(nèi)存,在某些場景下可以提高程序執(zhí)行性能,因為避免了在 Java 堆和 Native 堆中來回復制數(shù)據(jù)的耗時。
Java NIO 創(chuàng)建堆外內(nèi)存的簡單示例。
// 創(chuàng)建直接內(nèi)存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
這部分內(nèi)存如果出現(xiàn)資源不足,也可能導致OutOfMemoryError異常出現(xiàn)。
三、內(nèi)存設置相關的命令
所有內(nèi)存溢出的問題,除了代碼可能存在問題以外,更直觀的問題是內(nèi)存空間不足,如何通過參數(shù)來控制各區(qū)域的內(nèi)存大小呢?
我們先來看一張圖。
圖片
相關的常用控制參數(shù)介紹!
3.1、堆內(nèi)存大小相關參數(shù)設置
1)-Xms
設置堆的最小空間大小,此值必須是 1024 的倍數(shù)且大于 1 MB。附加字母 k 或 k 表示千字節(jié),m 或 m 表示兆字節(jié),g 或 g 表示千兆字節(jié),其它命令參數(shù)同理。比如-Xms1024m,表示堆的最小內(nèi)存為1024M,默認值為物理內(nèi)存的1/64。
2)-Xmx
設置堆的最大空間大小,此值必須是 1024 的倍數(shù)且大于 2 MB。比如-Xmx2048m,表示堆最大內(nèi)存為2G,默認值為物理內(nèi)存的1/4。
對于服務器部署,-Xms和-Xmx通常建議設置為相同的值,以避免堆的內(nèi)存空間頻繁擴縮。
3)-XX:+HeapDumpOnOutOfMemoryError
表示可以讓虛擬機在出現(xiàn)內(nèi)存溢出異常時 Dump 出當前的堆內(nèi)存轉(zhuǎn)儲快照
3.2、年輕代內(nèi)存大小相關參數(shù)設置
1)-XX:NewSize
設置年輕代的最小空間大小,比如-XX:NewSize=256m,表示年輕代的最小內(nèi)存為256M。
GC 在這個區(qū)域比在其他區(qū)域執(zhí)行的頻率更高,如果年輕一代的設置太小,那么將進行大量的小頻率 GCs。如果設置太大,那么會執(zhí)行完整的GCs,這可能需要很長時間才能完成。Oracle 建議將年輕一代的大小保持在堆總大小的一半到四分之一之間。同時,該值需要小于-Xms的值。
2)-XX:MaxNewSize
設置年輕代的最大空間大小,比如-XX:MaxNewSize=512m,表示年輕代的最大內(nèi)存為512M。
3)-Xmn
設置年輕代堆的初始大小和最大大小,比如-Xmn128m,表示年輕代的初始大小和最大大小為128M。
這個參數(shù)是對-XX:newSize、-XX:MaxnewSize兩個參數(shù)同時進行配置,雖然會很方便,但需要注意的是這個參數(shù)是在 JDK1.4 版本以后才加入的,低于此版本無法使用。
沒有直接設置老年代的參數(shù),但是可以設置堆空間大小和年輕代空間大小兩個參數(shù)來間接控制,公式如下:
老年代空間大小 = 堆空間大小 - 年輕代空間大小
3.3、比例方式相關參數(shù)設置
1)-XX:NewRatio
設置年輕代和老年代大小之間的比例,默認值是-XX:NewRatio=2,表示Young : Old = 1 : 2。
2)-XX:SurvivorRatio
設置 Eden 空間大小和 Survivor 空間大小之間的比例,默認值是-XX:SurvivorRatio=8,表示Eden : from : to = 8 : 1 : 1。
3)-XX:MinHeapFreeRatio
設置 GC 事件后允許的最小可用堆空間百分比(0到100),如果可用堆空間低于此值,則堆將被擴展。默認情況下,此參數(shù)為-XX:MinHeapFreeRatio=40,表示40%。
4)-XX:MaxHeapFreeRatio
設置 GC 事件后允許的最大可用堆空間百分比(0到100)。如果可用堆空間高于此值,則堆將被縮小。默認情況下,此參數(shù)為-XX:MaxHeapFreeRatio=70,表示70%。
3.4、非堆區(qū)相關參數(shù)設置
1)-XX:PermSize
設置永久代的最小空間大小,比如-XX:PermSize=256m,表示永久代的最小內(nèi)存為256M,默認值為物理內(nèi)存的1/64。
2)-XX:MaxPermSize
置永久代的最大空間大小,比如-XX:MaxPermSize=512m,表示永久代的最大內(nèi)存為512M,默認值為物理內(nèi)存的1/4。
值得注意的是,-XX:PermSize和-XX:MaxPermSize這兩個參數(shù),在 JDK1.7 及以前的版本中有效,在 JDK1.8 中已經(jīng)被棄用,被-XX:MetaspaceSize和-XX:MaxMetaspaceSize兩個參數(shù)取代。
3.5、棧內(nèi)存相關參數(shù)設置
1)-Xss
設置每個線程的棧大小,比如-Xss1024k,表示每個線程的堆??臻g大小為1024KB,通常不需要我們調(diào)整設置,默認值取決于平臺:
- Linux/ARM (32-bit):320 KB
- Linux/i386 (32-bit):320 KB
- Linux/x64 (64-bit):1024 KB
- OS X (64-bit):1024 KB
- Oracle Solaris/i386 (32-bit):320 KB
- Oracle Solaris/x64 (64-bit):1024 KB
2)-Xoss
設置每個線程中的本地方法棧大小,比如-Xoss128k,表示每個線程中的本地方法棧大小為128KB,不過 HotSpot 并不區(qū)分虛擬機棧和本地方法棧,因此對于 HotSpot 來說這個參數(shù)是無效的。
3.6、堆外內(nèi)存相關參數(shù)設置
1)-XX:MaxDirectMemorySize
此參數(shù)的含義是通過Direct ByteBuffer方式分配的最大堆外內(nèi)存大小。比如-XX:MaxDirectMemorySize=60m,表示堆外最大內(nèi)存不能超過60M,如果沒有設置,默認是 0,JVM 會自動申請內(nèi)存的大小,最大大小受限于-Xmx值。
四、內(nèi)存溢出的幾種場景
在上文中,我們介紹了 JVM 內(nèi)存結構以及可能會發(fā)生的異常狀況,下面我們一起來復現(xiàn)一下幾種常見的內(nèi)存溢出現(xiàn)象。
4.1、堆溢出
堆溢出測試類如下。
/**
* 虛擬機參數(shù): -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOMTest {
public static void main(String[] args) {
List<HeapOOMTest> list = new ArrayList<>();
while (true){
list.add(new HeapOOMTest());
}
}
}
在 IDEA 中設置 JVM 相關的參數(shù)。
圖片
運行后輸出結果如下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid21886.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at HeapOOMTest.main(HeapOOMTest.java:21)
Heap dump file created [12920047 bytes in 0.090 secs]
從報錯的日志上可以清晰的看到,出現(xiàn)內(nèi)存溢出的區(qū)域在Java heap space,問題代碼在HeapOOMTest.java:21。生成的快照文件在當前工程目錄下。
4.2、虛擬機棧和本地方法棧溢出
棧溢出測試類如下,JVM 相關的參數(shù)設置步驟同上。
/**
* 虛擬機參數(shù): -Xss256k
*/
public class StackOOMTest {
private int stackLength = 1;
public static void main(String[] args) {
StackOOMTest stackOOMTest = new StackOOMTest();
try {
stackOOMTest.stackLeak();
} catch (Throwable e){
System.out.println("stack length:" + stackOOMTest.stackLength);
throw e;
}
}
private void stackLeak() {
stackLength++;
stackLeak();
}
}
運行后輸出結果如下:
Exception in thread "main" java.lang.StackOverflowError
stack length:2326
at StackOOMTest.stackLeak(StackOOMTest.java:23)
at StackOOMTest.stackLeak(StackOOMTest.java:23)
at StackOOMTest.stackLeak(StackOOMTest.java:23)
at StackOOMTest.stackLeak(StackOOMTest.java:23)
at StackOOMTest.stackLeak(StackOOMTest.java:23)
......
在單個線程下,當棧幀的深度過大,也會超出虛擬機棧的最大容量,當無法分配內(nèi)存的時候,虛擬機就會拋出StackOverflowError異常。
我們在來看另一個例子。
/**
* 虛擬機參數(shù): -Xss256k
*/
public class StackOOMTest2 {
public static void main(String[] args) {
StackOOMTest2 stackOOMTest = new StackOOMTest2();
stackOOMTest.stackLeakByThread();
}
private void stackLeakByThread() {
while (true) {
new Thread(new Runnable() {
@Override
public void run() {
running();
}
}).start();
}
}
private void running() {
while (true) {
}
}
}
運行后輸出結果如下:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
在無限制的創(chuàng)建多個線程下,虛擬機棧也可能會出現(xiàn)OutOfMemoryError異常,此時操作系統(tǒng)會出現(xiàn)假死, CPU 被完全跑滿了,測試過程中發(fā)現(xiàn)操作系統(tǒng)下所有的應用無法正常操作,請謹慎測試。(以上報錯內(nèi)容,引入網(wǎng)上博主的測試結果)
4.3、方法區(qū)和運行時常量池溢出
運行時常量池屬于方法區(qū)的一部分,這兩個區(qū)域中我們抽取運行時常量池區(qū)域來測試內(nèi)存溢出的現(xiàn)象。
針對這個區(qū)域,我們可以采用String.intern()方法進行測試。String.intern()是一個Native方法,意思是如果常量池中有一個String對象的字符串,就返回池中的這個字符串的String對象;否則,將此String對象包含的字符串添加到常量池中去,并且返回此String對象的引用。
測試代碼如下,JVM 相關的參數(shù)設置步驟同上。
/**
* 虛擬機參數(shù):-XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPoolOOMTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
運行后輸出結果如下:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
實際上,這個異常只會出現(xiàn)在 JDK1.6 及之前的版本中,在 JDK1.7 中是不會有這個異常的,它會一直while循環(huán)下去。
在上文中我們介紹過,在 JDK1.7 及之前的版本中,方法區(qū)也被稱為永久代,因此看到的是PermGen space區(qū)域的OutOfMemoryError異常信息。
但在 JDK1.8 及之后的版本中,沒有-XX:PermSize和-XX:MaxPermSize這兩個參數(shù),取而代之的是-XX:MetaspaceSize和-XX:MaxMetaspaceSize這兩個參數(shù),同時方法區(qū)被稱為元空間,并劃入到本地內(nèi)存中。
4.4、直接內(nèi)存溢出
直接內(nèi)存溢出測試類如下,JVM 相關的參數(shù)設置步驟同上。
/**
* 虛擬機參數(shù): -XX:MaxDirectMemorySize=2048k
*/
public class DirectMemoryTest {
public static void main(String[] args){
int i = 0;
List<ByteBuffer> buffers = new ArrayList<>();
while (true) {
ByteBuffer bb = ByteBuffer.allocateDirect(1024 * 1024 * 1);
buffers.add(bb);
System.out.println(i++);
}
}
}
運行后輸出結果如下:
0
1
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at DirectMemoryTest.main(DirectMemoryTest.java:22)
從日志上可以清晰的看到,OutOfMemoryError的異常區(qū)域為Direct buffer memory,也就是直接內(nèi)存區(qū)域。
五、JDK 各版本內(nèi)存布局變化
在上文中我們也提及到過,不同的版本 JVM 內(nèi)存布局可能有所不同。最后,我們再一起來看下 JDK 1.6、1.7、1.8 的內(nèi)存模型演變過程。
每一次的調(diào)整改動,都是為了更好的適應當下 CPU 性能,最大限度的提升 JVM 運行效率,各個版本的差異如下:
- 在 JDK1.6 中,常量池存放于方法區(qū),也被稱為永久代
- 在 JDK1.7 中,將常量池進行細分,字符串常量池存放于堆中,運行時常量池和類常量池,存放于方法區(qū)中
- 在 JDK1.8 中,無永久代,將運行時常量池和類常量池都保存在元數(shù)據(jù)區(qū)中,也就是大家常說的元空間,但字符串常量池仍然存放在堆上
關于各個內(nèi)存區(qū)域的變化,有些面試官會提出以下一些問題,我們一起來看下。
問題一:在 JDK 1.7 中,為什么要將字符串常量池移動到堆中?
這個問題的主要原因在于 GC 的回收效率上,在永久代中的數(shù)據(jù), GC 回收效率非常低,只有在整堆收集 (Full GC) 的時候才會被執(zhí)行 GC;而 Java 程序中通常會有大量的被創(chuàng)建的字符串需要等待回收,將字符串常量池放到堆中,能夠更高效及時的回收字符串,釋放內(nèi)存。
問題二:JDK 1.8 為什么要廢棄永久代,用元空間取而代之?
HotSpot 團隊選擇移除永久代,簡單的說有兩個因素:
- 外因:在之前的文章中我們說到過,Oralce 擁有 JRockit 與 HotSpot 兩款優(yōu)秀的虛擬機,在 JRockit 中并沒有永久代,為了將 JRockit 優(yōu)秀的設計融入 HotSpot 中,在 JDK 1.8 中 HotSpot 移除了永久代
- 內(nèi)因:JDK 1.7 中永久代大小受-XX:PermSize和-XX:MaxPermSize這兩個參數(shù)的限制,這兩個參數(shù)在物理空間上又受到 JVM 設定的內(nèi)存大小限制,這就會導致在使用中永久代可能出現(xiàn)內(nèi)存溢出的問題,因此在 JDK 1.8 及之后的版本中徹底移除了永久代,用元空間來進行替代,其中元空間并不在虛擬機內(nèi)存中而是使用本地內(nèi)存,相比 JDK 1.7 而言,出現(xiàn)內(nèi)存溢出的風險要小很多,但也不是完全不限制,其大小受操作系統(tǒng)可用內(nèi)存大小的限制,也支持通過-XX:MetaspaceSize和-XX:MaxMetaspaceSize這兩個參數(shù)來配置
如果想要在 JDK1.8 中測試元空間的內(nèi)存溢出現(xiàn)象,可以通過 Cglib 動態(tài)代理框架來創(chuàng)建類,它會將類存放在元空間,測試示例如下。
/**
* 虛擬機參數(shù): -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*/
public class MetaspaceOOMTest {
public static void main(String[] args) {
int i = 0;
try {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
// 創(chuàng)建一個動態(tài)代理類
enhancer.create();
i++;
}
} catch (Throwable e) {
System.out.println("第" + i + "次時發(fā)生異常");
e.printStackTrace();
}
}
private static class OOMObject {
public OOMObject() {
}
}
}
運行后輸出結果如下:
第546次時發(fā)生異常
net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
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.create(Enhancer.java:305)
at MetaspaceOOMTest.main(MetaspaceOOMTest.java:26)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:459)
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:339)
... 6 more
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
... 11 more
從日志上可以清晰的看到,執(zhí)行到第 546 次時出現(xiàn)OutOfMemoryError,內(nèi)存區(qū)域在Metaspace。
六、參考
1.https://zhuanlan.zhihu.com/p/43279292
2.http://www.ityouknow.com/jvm/2017/08/25/jvm-memory-structure.html
3.https://www.cnblogs.com/xrq730/p/4827590.html
4.https://www.cnblogs.com/aflyun/p/10575740.html
5.https://zhuanlan.zhihu.com/p/371778309