JVM
內存區域包括PC計數器、Java虛擬機棧、本地方法棧、堆、方法區、運行時常量池和直接內存。java
本文主要介紹各個內存區域的做用和特性,同時分別闡述各個區域發生內存溢出的可能性和異常類型。編程
Java
虛擬機執行Java
程序的過程當中,會把所管理的內存劃分爲若干不一樣的數據區域。這些內存區域各有各的用途,以及建立和銷燬時間。有的區域隨着虛擬機進程的啓動而存在,有的區域伴隨着用戶線程的啓動和結束而建立和銷燬。後端
JVM
內存區域也稱爲Java
運行時數據區域。其中包括:程序計數器、虛擬機棧、本地方法棧、堆、靜態方法區、靜態常量池等。數組
注意:程序計數器、虛擬機棧、本地方法棧屬於每一個線程私有的;堆和方法區屬於線程共享訪問的。緩存
程序計數器(Program Counter Register
)是一塊較小的內存空間,它的做用能夠看作是當前線程所執行的字節碼行號指示器。bash
PC
計數器。JVM
啓動而生,JVM
關閉而死。Java
方法時,記錄其正在執行的虛擬機字節碼指令地址。Native
方法時,計數器記錄爲空(Undefined
)。Java
虛擬機規範中沒有規定任何OutOfMemoryError
狀況區域。線程私有內存空間,它的生命週期和線程相同。線程執行期間,每一個方法執行時都會建立一個棧幀(Stack Frame) ,用於存儲 局部變量表、操做數棧 、動態連接 、方法出口 等信息。多線程
每個方法從調用直到執行完成的過程,就對應着一個棧幀在虛擬機棧中的入棧和出棧的全過程。架構
下面依次解釋棧幀裏的四種組成元素的具體結構和功能:框架
局部變量表是一組變量值的存儲空間,用於存儲方法參數和局部變量。 在 Class
文件的方法表的 Code
屬性的 max_locals
指定了該方法所需局部變量表的最大容量。異步
局部變量表在編譯期間分配內存空間,能夠存放編譯期的各類變量類型:
boolean
, byte
, char
, short
, int
, float
, long
, double
等8
種;reference
,指向對象起始地址的引用指針;returnAddress
,返回地址的類型。變量槽(Variable Slot
):
變量槽是局部變量表的最小單位,規定大小爲
32
位。對於64
位的long
和double
變量而言,虛擬機會爲其分配兩個連續的Slot
空間。
操做數棧(Operand Stack
)也常稱爲操做棧,是一個後入先出棧。在 Class
文件的 Code
屬性的 max_stacks
指定了執行過程當中最大的棧深度。Java
虛擬機的解釋執行引擎被稱爲基於棧的執行引擎 ,其中所指的棧就是指-操做數棧。
32
字長爲單位的數組。int
、long
、float
、double
、reference
和returnType
等類型 (對於byte
、short
以及char
類型的值在壓入到操做數棧以前,也會被轉換爲int
)。虛擬機把操做數棧做爲它的工做區——大多數指令都要從這裏彈出數據,執行運算,而後把結果壓回操做數棧。
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
複製代碼
在這個字節碼序列裏,前兩個指令
iload_0
和iload_1
將存儲在局部變量表中索引爲0
和1
的整數壓入操做數棧中,其後iadd
指令從操做數棧中彈出那兩個整數相加,再將結果壓入操做數棧。第四條指令istore_2
則從操做數棧中彈出結果,並把它存儲到局部變量表索引爲2
的位置。
下圖詳細表述了這個過程當中局部變量表和操做數棧的狀態變化(圖中沒有使用的局部變量表和操做數棧區域以空白表示)。
每一個棧幀都包含一個指向運行時常量池中所屬的方法引用,持有這個引用是爲了支持方法調用過程當中的動態連接。
Class
文件的常量池中存在有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用:
final
、static
域等),稱爲靜態解析,當一個方法開始執行之後,只有兩種方法能夠退出當前方法:
Normal Method Invocation Completion
),通常來講,調用者的PC
計數器能夠做爲返回地址。Abrupt Method Invocation Completion
),返回地址要經過異常處理器表來肯定。當一個方法返回時,可能依次進行如下3
個操做:
PC
計數器的值指向下一條方法指令位置。小結:
注意:在Java虛擬機規範中,對這個區域規定了兩種異常。 其一:若是當前線程請求的棧深度大於虛擬機棧所容許的深度,將會拋出
StackOverflowError
異常(在虛擬機棧不容許動態擴展的狀況下);其二:若是擴展時沒法申請到足夠的內存空間,就會拋出OutOfMemoryError
異常。
本地方法棧和Java
虛擬機棧發揮的做用很是類似,主要區別是Java
虛擬機棧執行的是Java
方法服務,而本地方法棧執行Native
方法服務(一般用C編寫)。
有些虛擬機發行版本(譬如
Sun HotSpot
虛擬機)直接將本地方法棧和Java
虛擬機棧合二爲一。與虛擬機棧同樣,本地方法棧也會拋出StackOverflowError
和OutOfMemoryError
異常。
Java
堆是被全部線程共享的最大的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。
在Java
中,堆被劃分紅兩個不一樣的區域:新生代 (Young Generation
) 、老年代 (Old Generation
) 。新生代 (Young
) 又被劃分爲三個區域:一個Eden
區和兩個Survivor
區 - From Survivor
區和To Survivor
區。
簡要概括:新的對象分配是首先放在年輕代 (
Young Generation
) 的Eden
區,Survivor
區做爲Eden
區和Old
區的緩衝,在Survivor
區的對象經歷若干次收集仍然存活的,就會被轉移到老年代Old
中。
這樣劃分的目的是爲了使JVM
可以更好的管理堆內存中的對象,包括內存的分配以及回收。
方法區和Java
堆同樣,爲多個線程共享,它用於存儲類信息、常量、靜態常量和即時編譯後的代碼等數據。
運行時常量池是方法區的一部分,Class
文件中除了有類的版本、字段、方法和接口等描述信息外, 還有一類信息是常量池,用於存儲編譯期間生成的各類字面量和符號引用。
直接內存不屬於虛擬機運行時數據區的一部分,也不是Java
虛擬機規範中定義的內存區域。 Java NIO
容許Java
程序直接訪問直接內存,一般直接內存的速度會優於Java堆內存。所以,對於讀寫頻繁、性能要求高的場景,能夠考慮使用直接內存。
除了程序計數器外,Java
虛擬機的其餘運行時區域都有可能發生OutOfMemoryError
的異常,下面分別給出驗證:
Java
堆可以存儲對象實例。經過不斷地建立對象,並保證GC Roots
到對象有可達路徑來避免垃圾回收機制清除這些對象。 當對象數量到達最大堆的容量限制時就會產生OutOfMemoryError
異常。
設置JVM
啓動參數:-Xms20M
設置堆的最小內存爲20M
,-Xmx20M
設置堆的最大內存和最小內存同樣,這樣能夠防止Java
堆在內存不足時自動擴容。 -XX:+HeapDumpOnOutOfMemoryError
參數可讓虛擬機在出現內存溢出異常時Dump
出內存堆運行時快照。
HeapOOM.java
/** * VM Args: -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError */
public class HeapOOM {
public static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
複製代碼
測試運行結果:
打開Java VisualVM
導出Heap
內存運行時的dump
文件。
HeapOOM
對象不停地被建立,堆內存使用達到
99%
。
垃圾回收器不斷地嘗試回收但都以失敗了結。
分析:遇到這種狀況,一般要考慮內存泄露和內存溢出兩種可能性。
進一步使用
Java VisualVM
工具進行分析,查看泄露對象是經過怎樣的路徑
與GC Roots
關聯而致使垃圾回收器沒法回收的。
經過
Java VisualVM
工具分析,不存在泄露對象,也就是說堆內存中的對象必須得存活着。就要考慮以下措施:
- 從代碼上檢查是否存在某些對象生命週期過長、持續狀態時間過長的狀況,嘗試減小程序運行期的內存。
- 檢查虛擬機的堆參數(
-Xmx
與-Xms
),對比機器的物理內存看是否還能夠調大。
關於虛擬機棧和本地方法棧,分析內存異常類型可能存在如下兩種:
StackOverflowError
異常。OutOfMemoryError
異常。能夠劃分爲兩類問題,當棧空間沒法分配時,到底時棧內存過小,仍是已使用的棧內存過大。
測試方案一:
-Xss
參數減小棧內存的容量,異常發生時打印棧的深度。設置JVM
啓動參數:-Xss128k
設置棧內存的大小爲128k
。
JavaVMStackSOF.java
/** * VM Args: -Xss128k */
public class JavaVMStackSOF {
private int stackLength = 1;
private void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("Stack length: " + oom.stackLength);
throw e;
}
}
}
複製代碼
測試結果:
分析:在單個線程下,不管是棧幀太大仍是虛擬機棧容量過小,當沒法分配內存的時候,虛擬機拋出的都是
StackOverflowError
異常。
測試方案二:
JavaVMStackOOM.java
/** * VM Args: -Xss2M */
public class JavaVMStackOOM {
private void running() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
new Thread(new Runnable() {
@Override
public void run() {
running();
}
}).start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
複製代碼
測試結果:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
複製代碼
上述測試代碼運行時存在較大的風險,可能會致使操做系統假死,這裏就不親自測試了,引用做者的測試結果。
運行時常量和字面量都存放於運行時常量池中,常量池又是方法區的一部分,所以兩個區域的測試是同樣的。 這裏採用String.intern()
進行測試:
String.intern()是一個native方法,它的做用是:若是字符串常量池中存在一個String對象的字符串,那麼直接返回常量池中的這個String對象; 不然,將此String對象包含的字符串放入常量池中,而且返回這個String對象的引用。
設置JVM
啓動參數:經過-XX:PermSize=10M
和-XX:MaxPermSize=10M
限制方法區的大小爲10M
,從而間接的限制其中常量池的容量。
RuntimeConstantPoolOOM.java
/** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M */
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持着常量池的引用,避免Full GC回收常量池
List<String> list = new ArrayList<>();
// 10MB的PermSize在Integer範圍內足夠產生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
複製代碼
測試結果分析:
JDK1.6
版本運行結果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
複製代碼
JDK1.6
版本運行結果顯示常量池會溢出並拋出永久帶的OutOfMemoryError
異常。 而JDK1.7
及以上的版本則不會獲得相同的結果,它會一直循環下去。
方法區存放Class
相關的信息,好比類名、訪問修飾符、常量池、字段描述、方法描述等。 對於方法區的內存溢出的測試,基本思路是在運行時產生大量類字節碼區填充方法區。
這裏引入Spring
框架的CGLib
動態代理的字節碼技術,經過循環不斷生成新的代理類,達到方法區內存溢出的效果。
JavaMethodAreaOOM.java
/** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M */
public class JavaMethodAreaOOM {
public static void main(String[] args) {
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);
}
});
enhancer.create();
}
}
private static class OOMObject {
public OOMObject() {
}
}
}
複製代碼
JDK1.6
版本運行結果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
複製代碼
測試結果分析:
JDK1.6
版本運行結果顯示常量池會溢出並拋出永久帶的OutOfMemoryError
異常。 而JDK1.7
及以上的版本則不會獲得相同的結果,它會一直循環下去。
本機直接內存的容量可經過-XX:MaxDirectMemorySize
指定,若是不指定,則默認與Java
堆最大值(-Xmx指定)同樣。
測試場景:
直接經過反射獲取Unsafe
實例,經過反射向操做系統申請分配內存:
設置JVM
啓動參數:-Xmx20M
指定Java
堆的最大內存,-XX:MaxDirectMemorySize=10M
指定直接內存的大小。
DirectMemoryOOM.java
/** * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M */
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
複製代碼
測試結果:
測試結果分析:
由DirectMemory
致使的內存溢出,一個明顯的特徵是Heap Dump
文件中不會看到明顯的異常信息。 若是OOM
發生後Dump
文件很小,而且程序中直接或者間接地使用了NIO
,那麼就能夠考慮一下這方面的問題。
歡迎關注技術公衆號: 零壹技術棧
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。