聲明: 《深入理解Java虛擬機 JVM高級特性與最佳實踐 第2版》。以下內(nèi)容來自書中第二章硼莽。
1. JVM概述
JVM是Java Virtual Machine(Java虛擬機)的縮寫球散,JVM是一種用于計算設(shè)備的規(guī)范绕辖,它是一個虛構(gòu)出來的計算機颊乘,是通過在實際的計算機上仿真模擬各種計算機功能來實現(xiàn)的歧蒋。
引入Java語言虛擬機后,Java語言在不同平臺上運行時不需要重新編譯偿曙。Java語言使用Java虛擬機屏蔽了與具體平臺相關(guān)的信息氮凝,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節(jié)碼),就可以在多種平臺上不加修改地運行???????????? ——百度百科
可以將JVM理解成一臺機器望忆,這個機器可以用來執(zhí)行java程序罩阵。這樣java代碼就可以實現(xiàn)一次編寫竿秆,到處運行了。在不同的機器上配置相應(yīng)的JVM即可稿壁。 在機器上幽钢,JVM實際上也是一個程序。
2. JVM內(nèi)存分配
2.1 運行時數(shù)據(jù)區(qū)域
根據(jù)《Java 虛擬機規(guī)范(Java SE 7 版)》的規(guī)定傅是,Java虛擬機所有管理的內(nèi)存將會包括以下幾個運行時數(shù)據(jù)區(qū)域匪燕,如下圖所示:
簡單說一下各個數(shù)據(jù)區(qū)域的作用:
線程私有的數(shù)據(jù)區(qū)域:線程私有區(qū)域都是與線程同生共死的,即生命周期與線程相同喧笔。
線程共享的數(shù)據(jù)區(qū)域:生命周期與JVM相同
程序計數(shù)器:每個線程都有的"小本本"帽驯,線程不是連續(xù)不斷工作的。再次運行時它就要看看“小本本”里的內(nèi)容才知道自己該從什么地方繼續(xù)做下去书闸。
在JVM中的多線程是通過輪流切換的方式執(zhí)行的尼变,在任何時刻一個CPU(相當于多核CPU的一個核)只能運行一個線程。如果某個進程的某個線程占用CPU很長的時間梗劫,那么其他的線程會一直等下去嗎?為了讓用戶不會有:”哇梳侨!我這個傻X計算機怎么這么卡膀韧蚯嫌!“的錯覺。CPU就給了每個線程分配了一個CPU時間丙躏,當線程的CPU時間用完之后,他就會把CPU的計算資源讓出來給其他線程晒旅,等到下次輪到它的時候它再執(zhí)行。所以每個線程都需要一個私有的程序計數(shù)器來記錄自己執(zhí)行到哪兒了废恋。它實際可以理解成一個記錄程序執(zhí)行的字節(jié)碼的行號的指示器。 計數(shù)器只會占用內(nèi)存中很小的一部分空間鱼鼓。
Java虛擬機棧:程序員口中常說的“堆椖馓蹋”大抵說的就是這個區(qū)域中的局部變量表。虛擬機棧是用來描述Java方法執(zhí)行時的內(nèi)存模型迄本。這種描述是通過存儲方法開始執(zhí)行(入棧)到方法結(jié)束(出棧)過程中的局部變量表硕淑、操作數(shù)棧、動態(tài)鏈接及方法的出口等信息來是實現(xiàn)的。方法在執(zhí)行時會創(chuàng)建一個棧幀置媳,用來存儲前面說的各種信息于樟,方法完成,棧幀出棧拇囊。
局部變量表存放了編譯期間可知的各種基本數(shù)據(jù)類型和引用類型隔披,以前上課的時候說的堆棧的時候,老師可能會畫這樣的一張圖:
Variable | Value |
---|---|
variable1 | 13 |
variable2 | 2.3 |
variable3 | 'a' |
variable4 | true |
zhangsan(Student的一個實例) | 指向堆中張三實例的地址空間 |
說的就是這個局部變量表寂拆。局部變量表所需要的內(nèi)存空間在編譯期間就分配完成。
以上可以對棧的作用做個小小的結(jié)論:
- 只有在方法調(diào)用時抓韩,才為當前棧分配一個幀纠永,然后將該幀壓入棧。
- 幀中存放了方法的局部變量表谒拴,當方法執(zhí)行完之后尝江,對應(yīng)的幀則從棧中彈出。
JVM規(guī)范中英上,對這個數(shù)據(jù)區(qū)域規(guī)定了兩種異常狀況:
- StackOverflowError:線程請求的棧深度大于虛擬機所允許的深度
- OutOfMemoryError:虛擬機棧動態(tài)擴展時無法申請都足夠的內(nèi)存空間
本地方法棧:與虛擬機棧相似炭序,不過虛擬機棧是為JVM執(zhí)行java方法,而本地方法棧則是為虛擬機使用到的Native方法服務(wù)苍日。
本地方法棧也可能會出現(xiàn)StackOverflowError和OutOfMemoryError惭聂。
Java堆:對大多數(shù)應(yīng)用來說,這個區(qū)域是JVM所管理的最大的內(nèi)存空間相恃,也是GC重點關(guān)注的區(qū)域辜纲。該區(qū)域被線程共享,存在的目的就是為了存放對象實例拦耐,幾乎所有的對象實例和數(shù)組都要在堆上分配耕腾。如果Java堆中沒有空間可以用來實例化對象,而且也沒法再申請新的內(nèi)存時杀糯,該區(qū)域會拋出OutOfMemoryError扫俺。
方法區(qū):線程共享區(qū)域,用于存儲已經(jīng)被虛擬機加載的類信息固翰、常量狼纬、靜態(tài)變量、即使編譯器后的代碼等數(shù)據(jù)倦挂。這個區(qū)域也有人稱其為永久代畸颅。方法區(qū)無法滿足內(nèi)存分配需求時也會拋出OutOfMemoryError。 運行時常量池也是方法區(qū)中的一部分方援,class文件會包括類的版本没炒,字段,方法送火,接口等描述信息,也會有個常量池弃衍,用于存放編譯期生成的各種字面量和符號引用。這部分內(nèi)容在類加載之后進入到方法區(qū)的運行時常量池中存放镜盯。該區(qū)域具有動態(tài)性速缆,即常量不一定是編譯期間就確定的恩闻,在運行期間也可以有新的常量產(chǎn)生,進入運行時常量池中破停。
3. 異常代碼實戰(zhàn)
實戰(zhàn)一下數(shù)據(jù)提供的代碼尉剩,旨在對運行時數(shù)據(jù)區(qū)域內(nèi)存分配和使用有更深的理解,當出現(xiàn)相關(guān)異常的時候能夠快速地定位到異常區(qū)域和異常代碼晤碘。
首先了解一下IDEA如何配置JVM啟動時的參數(shù)园爷。Run—>EditConfigurations
JVM相關(guān)配置參數(shù)說明可以通過在CMD中通過java -X
查看
Java 堆溢出
Java堆是用來存儲對象實例和數(shù)組的童社,只要的不停的創(chuàng)建對象扰楼,且確保對象不會被回收美浦,當對象的數(shù)量達到堆最大的容量限制后,就會產(chǎn)生內(nèi)存溢出異常了蹬竖。下面的JVM參數(shù)設(shè)置了Java堆內(nèi)存的大小為20MB,不可擴展(將最大值-Xms和最小值-Xmx設(shè)為一樣即可避免堆自動擴展)列另。通過參數(shù)-XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機在出現(xiàn)內(nèi)存溢出異常的時候Dump出當前的內(nèi)存堆轉(zhuǎn)儲快照旦装,以便我們后面的分析。
JVM參數(shù)配置
-Xms20m
-Xmx20m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:\\heapdump
代碼如下:
public class HeapOOM {
static class OOMObject{}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
//不停地創(chuàng)建對象店乐,直到OOM
while(true){
list.add(new OOMObject());
}
}
}
程序運行結(jié)果如下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to E:\\heapdump\java_pid8948.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:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at per.ling.JVMPractice.HeapOOM.main(HeapOOM.java:13)
Heap dump file created [28866091 bytes in 0.206 secs]
使用JDK自帶的內(nèi)存映像分析工具jvisualVM來分析程序dump出來的堆轉(zhuǎn)儲快照响巢。裝入文件后點擊類,我們可以看到類名和它對應(yīng)的實例個數(shù)及占用的內(nèi)存空間含长。
打開該文件可以看到OOMObject這個類有810326個實例拘泞,占用內(nèi)存13M左右。這里我們可以看到類實例相關(guān)的情況陪腌,查看概要我們還可以看到相關(guān)的線程及可能出現(xiàn)異常的代碼塊。
如果是內(nèi)存泄露的話染簇,我們可能還需要觀察一下相關(guān)的GC Roots锻弓。
Java的內(nèi)存溢出有很多中情況蝌箍,剛興趣可以搜一下,學(xué)習(xí)一波杂拨。
虛擬機棧溢出
虛擬機椕醭模可能拋出的異常:
- StackOverflowError:線程請求的棧深度大于虛擬機所允許的深度
- OutOfMemoryError:虛擬機堆棧動態(tài)擴展時無法申請都足夠的內(nèi)存空間
JVM參數(shù)如下:
-Xss128k
-XX:+HeapDumpOnStackOverflowErrow
-XX:HeapDumpPath=E:\\heapdump
代碼如下:
public class StackOOM {
private static long stackLength = 0L;
public static void main(String[] args) {
try {
stackLeak();
} catch (Throwable e){
System.out.println("The length of statck is " + stackLength);
throw e;
}
}
private static void stackLeak() {
stackLength++;
stackLeak();
}
}
控制臺輸出如下:
The length of statck is 41351
Exception in thread "main" java.lang.StackOverflowError
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
at pers.leo.chapter_02.StackOOM.stackLeak(StackOOM.java:21)
小結(jié):
StackOverflowerError主要是針對運行時的棧而言贷币,而OutOfMemoryError(內(nèi)存溢出)針對的是整個內(nèi)存區(qū)域。后者出現(xiàn)的原因主要是申請內(nèi)存時沒有更多的內(nèi)存空間導(dǎo)致的偶摔,而導(dǎo)致這樣的原因有很多促脉。
詳情可以參見: Java常見的幾種內(nèi)存溢出及解決方案