以下內(nèi)容基于Java 8分析和理解缎除。
Java Platform Standard Edition 8 Documentation 官網(wǎng)文檔地址:
https://docs.oracle.com/javase/8/
下圖是Java
概念圖的描述
一. 編譯
JVM運(yùn)行的是.class
文件钦听,我們編寫的是.java
文件炸渡,所以我們需要將.java
文件編譯成.class
文件,然后.class
文件被加載到JVM中運(yùn)行。
假如我們現(xiàn)有一個(gè)Person.java
類破托,如下:
public class Person {
public static final String TAG = "Person";
public int id = 100;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
通過javac
命令編譯成Person.class
文件查坪,使用二進(jìn)制編輯器打開如下:
cafe babe 0000 0034 001d 0a00 0500 1709
0004 0018 0900 0400 1907 001a 0700 1b01
0003 5441 4701 0012 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 0100 0d43 6f6e
7374 616e 7456 616c 7565 0800 1c01 0002
6964 0100 0149 0100 046e 616d 6501 0006
3c69 6e69 743e 0100 0328 2956 0100 0443
6f64 6501 000f 4c69 6e65 4e75 6d62 6572
5461 626c 6501 0007 6765 744e 616d 6501
0014 2829 4c6a 6176 612f 6c61 6e67 2f53
7472 696e 673b 0100 0773 6574 4e61 6d65
0100 1528 4c6a 6176 612f 6c61 6e67 2f53
7472 696e 673b 2956 0100 0a53 6f75 7263
6546 696c 6501 000b 5065 7273 6f6e 2e6a
6176 610c 000d 000e 0c00 0a00 0b0c 000c
0007 0100 1263 6f6d 2f65 7878 2f65 7878
2f50 6572 736f 6e01 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 0650 6572
736f 6e00 2100 0400 0500 0000 0300 1900
0600 0700 0100 0800 0000 0200 0900 0100
0a00 0b00 0000 0200 0c00 0700 0000 0300
0100 0d00 0e00 0100 0f00 0000 2700 0200
0100 0000 0b2a b700 012a 1064 b500 02b1
0000 0001 0010 0000 000a 0002 0000 0008
0004 000a 0001 0011 0012 0001 000f 0000
001d 0001 0001 0000 0005 2ab4 0003 b000
0000 0100 1000 0000 0600 0100 0000 0e00
0100 1300 1400 0100 0f00 0000 2200 0200
0200 0000 062a 2bb5 0003 b100 0000 0100
1000 0000 0a00 0200 0000 1200 0500 1300
0100 1500 0000 0200 16
編譯器的編譯原理大致如下:
- 對(duì)源文件進(jìn)行詞法和語法分析寸宏。
- 形成語法樹。
- 通過字節(jié)碼生成器生成
Person.class
字節(jié)碼文件偿曙。
我們看不懂這些字節(jié)碼文件氮凝,但是JVM是可以看懂的,那JVM是怎樣“看”的呢望忆?
通過官網(wǎng)文檔Java虛擬機(jī)規(guī)范:
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
找到第四章節(jié)罩阵,Chapter 4. The class File Format(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html),來了解一個(gè).class
文件是怎樣被定義的炭臭。
A class file consists of a single ClassFile structure:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
第一行的U4是什么意思永脓?可以簡(jiǎn)單理解成:每2位16進(jìn)制表示U1,圖解如下:
那么U4表示的是:cafe babe
,代表的即是magic
鞋仍,那magic
是什么意思呢常摧?
通過文檔可知:
The magic item supplies the magic number identifying the class file format; it has the value 0xCAFEBABE.
magic
是用來對(duì).class
文件標(biāo)識(shí)的,換言之就是只要是.class
文件威创,它的開頭一定是cafe babe
落午。
再舉栗分析一下minor_version 和 major_version:
The values of the minor_version and major_version items are the minor and major version numbers of this class file
minor_version 和 major_version,表示.class
文件的次要版本和主要版本肚豺。
其他結(jié)構(gòu)按照同樣的方式進(jìn)行分析溃斋,這里不再一一分析了。
這部分就是.java
文件到.class
文件的一個(gè)過程吸申。
接下來我們考慮下梗劫,當(dāng)有了.class
文件后享甸,是怎樣交給JVM
運(yùn)行的呢?是把整個(gè).class
文件直接加載到JVM
中嗎梳侨?顯然不是的蛉威,接下來我們分析下.class
文件加載到JVM
中。
顯然它是需要一種機(jī)制來加載進(jìn)JVM
中的走哺,并不是隨便想怎樣加載就怎樣加載蚯嫌。
這個(gè)機(jī)制是什么呢?答案就是:類加載機(jī)制
二. 類加載機(jī)制
它的作用就是將我們的.class
文件交給JVM
丙躏。
過程是怎樣的择示?
通過官網(wǎng)文檔第5章節(jié)可以了解到:
Chapter 5. Loading, Linking, and Initializing
The Java Virtual Machine dynamically
loads
,links
andinitializes
classes and interfaces. Loading is the process of finding the binary representation of a class or interface type with a particular name and creating a class or interface from that binary representation. Linking is the process of taking a class or interface and combining it into the run-time state of the Java Virtual Machine so that it can be executed. Initialization of a class or interface consists of executing the class or interface initialization method.
意思是類加載分為5個(gè)步驟:
- 裝載
- 鏈接
- 初始化
- 綁定本地方法的實(shí)現(xiàn)
- 退出JVM虛擬機(jī)
接下來詳細(xì)了解下這5個(gè)步驟:
- 裝載(Loading)
通過官網(wǎng)了解到,JVM提供了2中類型的類加載器:-
Bootstrap ClassLoader
晒旅, 由JVM內(nèi)置提供加載器的類型 -
User-defined ClassLoader
栅盲,由用戶自定義加載器的類型,每個(gè)用戶自定義的類加載器都需要繼承抽象類ClassLoader
废恋,例如可以用來加載網(wǎng)絡(luò)下載的剪菱,動(dòng)態(tài)生成的或者從加密文件中解密的。
-
通過不同類型的類加載器來加載不同區(qū)域的類拴签。
JVM內(nèi)置提供加載器的類型,主要有以下3個(gè)類加載器來實(shí)現(xiàn)的:
-
Bootstrap ClassLoader
旗们,用于加載$JAVA_HOME中的 jre/lib/rt.jar中的所有class蚓哩,或者Xbootclassoath選項(xiàng)指定的jar包下的class。 -
Extension ClassLoader
上渴,加載java平臺(tái)中擴(kuò)展功能的一些jar包岸梨,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包日杈。 -
App ClassLoader
挽封,加載ClassPath中指定的jar包及Djava.class.path所指定目錄下的class和jar包皆刺。
用戶自定義加載器的類型:
-
Custom ClassLoader
庇麦,繼承抽象類ClassLoader
實(shí)現(xiàn)自定義加載器扶踊。
裝載過程:
1. 查找.class
的全路徑收津。
2. 將類信息加載進(jìn)JVM中歹颓。
3. 將.class
對(duì)應(yīng)的類加載進(jìn)JVM中铜秆。
這里需要說明一點(diǎn)奢米,第3步只能裝載一次抓韩,如果存在多個(gè),則會(huì)造成運(yùn)行時(shí)混亂鬓长。
如何保證.class
對(duì)應(yīng)的類只裝載一次谒拴?這里就要說下類加載機(jī)制中的委托機(jī)制。
我們從源碼的角度來看下:
protected Class<?> loadClass(String class_name, boolean resolve) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(class_name)) {
Class c = this.findLoadedClass(class_name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (this.parent != null) {
c = this.parent.loadClass(class_name, false);
} else {
c = this.findBootstrapClassOrNull(class_name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
c = this.findClass(class_name);
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
this.resolveClass(c);
}
return c;
}
}
- 判斷是否已經(jīng)加載過涉波,如果已經(jīng)加載過直接返回英上,如果沒有加載過炭序,進(jìn)入第2步。
- 判斷是否有父類苍日,如果有惭聂,則委托父類去加載,如果沒有父類易遣,則表示該類已經(jīng)是最頂層的類加載器
Bootstrap ClassLoader
彼妻。 -
如果經(jīng)過上述2步還沒有加載到,需要自己加載豆茫。
流程圖大致如下:
找到對(duì)應(yīng)的Class
之后侨歉,要怎樣裝載呢?
- 將類文件靜態(tài)存儲(chǔ)結(jié)構(gòu)以流的形式加載到JVM中的方法區(qū)中揩魂,這里存儲(chǔ)的是類結(jié)構(gòu)的信息幽邓,比如,創(chuàng)建時(shí)間火脉,作者等信息牵舵。
- 將類文件對(duì)應(yīng)的對(duì)象加載到JVM中的堆(Heap)區(qū)中。
接下來進(jìn)行鏈接
部分:
- 驗(yàn)證
主要是驗(yàn)證確保類或者接口二進(jìn)制在結(jié)構(gòu)上的正確性倦挂。 - 準(zhǔn)備
為類或者接口創(chuàng)建靜態(tài)字段畸颅,并設(shè)置字段的默認(rèn)值。
舉栗子:代碼中有一個(gè)靜態(tài)變量:static int KEY = 20方援;
到這一步只會(huì)給KEY賦值默認(rèn)值没炒,即KEY=0 - 解析
將符號(hào)引用解析為動(dòng)態(tài)確定值(直接引用)的過程。
這一步會(huì)給第2步的KEY賦值確定的值犯戏,即KEY=20送火。
鏈接完成之后進(jìn)入到初始化
步驟:
初始化:
執(zhí)行類或接口的初始化方法,靜態(tài)變量賦值。
經(jīng)過裝載
先匪,鏈接
种吸,初始化
上述步驟之后,一個(gè)class
文件就被加載到JVM中呀非。
在之后就來到了JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)了坚俗。
三.JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)
記錄的是代碼在執(zhí)行時(shí)的狀態(tài)。
有幾個(gè)問題想一下:
類信息應(yīng)該加載到哪里岸裙?常量應(yīng)該加載到哪里坦冠?方法應(yīng)該加載到哪里?等等哥桥。
帶著上面的問題辙浑,我們想一下,如果要分類加載到JVM不同區(qū)域中拟糕,那么首先我們要對(duì)JVM區(qū)域進(jìn)行劃分判呕,這里我們直接說結(jié)果倦踢,官網(wǎng)文檔,Chapter 2. The Structure of the Java Virtual Machine#2.5. Run-Time Data Areas侠草,這一節(jié)可以看到:
- The pc Register(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1)
程序計(jì)數(shù)器辱挥,用于記錄每個(gè)線程當(dāng)前正在執(zhí)行的程序的位置。
- Java Virtual Machine Stacks(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.2)
Java 虛擬機(jī)棧边涕,用于處理方法晤碘,執(zhí)行方法入棧,執(zhí)行結(jié)束出棧功蜓。線程私有园爷,線程安全,會(huì)出現(xiàn)StackOverflowError異常式撼,當(dāng)沒有足夠的Memory創(chuàng)建Java虛擬機(jī)棧時(shí)也會(huì)出現(xiàn)OutOfMemoryError異常童社。
- Heap(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.3)
堆,用于存放所有的class對(duì)象以及數(shù)組著隆。所有線程共享扰楼,非線程安全,會(huì)出現(xiàn)OutOfMemoryError異常美浦。
- Method Area(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4)
方法區(qū)弦赖,用于存放類的結(jié)構(gòu)信息,常量浦辨,靜態(tài)變量腾节,構(gòu)造函數(shù),特殊初始化方法以及即時(shí)編譯后的代碼等荤牍。所有線程共享,非線程安全庆冕,會(huì)出現(xiàn)OutOfMemoryError異常康吵。
- Run-Time Constant Pool(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.5)
常量池,由方法區(qū)創(chuàng)建的访递,用于存放常量晦嵌,準(zhǔn)確應(yīng)該和方法區(qū)放到一起。
- Native Method Stacks(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.6)
本地方法棧拷姿,用于存放Java
和Native
交互的方法惭载,當(dāng)大小固定時(shí)會(huì)出現(xiàn)StackOverflowError,當(dāng)可以動(dòng)態(tài)擴(kuò)展時(shí)响巢,會(huì)出現(xiàn)OutOfMemoryError描滔。
接下來詳細(xì)聊一下JVM。
- Java虛擬機(jī)棧 -- 棧幀
何為棧幀踪古?每一個(gè)線程都有一個(gè)私有的Java虛擬機(jī)棧含长,當(dāng)執(zhí)行到某一個(gè)方法時(shí)券腔,就將一個(gè)棧幀壓入Java虛擬機(jī)棧,而這個(gè)棧幀就代表一個(gè)方法的執(zhí)行拘泞。
每一個(gè)棧幀由五部分組成:
- 局部變量表
局部變量表中維護(hù)的是一個(gè)數(shù)組纷纫,這個(gè)數(shù)組的長(zhǎng)度在編譯期就確定了。其中有基本類型的變量陪腌,返回值類型的變量以及引用對(duì)象的變量辱魁。
其中引用對(duì)象的變量指向的是堆內(nèi)存中的一個(gè)內(nèi)存地址。
單個(gè)局部變量可以存儲(chǔ):boolean, byte, char, short, int, float, reference, or returnAddress
一對(duì)局部變量可以存儲(chǔ):long or double
當(dāng)然也包含this
诗鸭。
- 操作數(shù)棧
一個(gè)后進(jìn)先出(LIFO)堆棧染簇,最大棧深也是在編譯器確定的。對(duì)每一個(gè)指令進(jìn)行入棧和出棧操作只泼。
- 動(dòng)態(tài)鏈接
在類加載的時(shí)候會(huì)將一些元信息進(jìn)行解析剖笙,一些能夠確定的類型,在類加載的過程中就確定了请唱,不確定的類型就是需要在運(yùn)行時(shí)才可以確定類型的弥咪,就是在此時(shí)進(jìn)行一個(gè)動(dòng)態(tài)鏈接,來確定具體的類型十绑。
- 方法調(diào)用正常完成
棧幀正常執(zhí)行結(jié)束聚至。
- 方法調(diào)用異常完成
棧幀異常執(zhí)行結(jié)束。
接下來我們結(jié)合字節(jié)碼指令來看下本橙,棧幀是怎樣工作的扳躬。
Java 代碼:
public int plus(int op1, int op2) {
op1 = 3;
int result = op1 + op2;
return result;
}
我們放一張Java虛擬機(jī)棧中的該方法的棧幀圖,如下:
字節(jié)碼反編譯后指令:
public int plus(int, int);
Code:
0: iconst_3
1: istore_1
2: iload_1
3: iload_2
4: iadd
5: istore_3
6: iload_3
7: ireturn
我們“翻譯”下這段字節(jié)碼:
iconst_3
表示將int類型的常量3壓入操作數(shù)棧
istore_1
對(duì)操作數(shù)棧進(jìn)行彈出甚亭,將彈出的值賦值給局部變量表索引為1的局部變量贷币。
iload_1
將局部變量表中索引為1的變量的值壓入操作數(shù)棧。
iload_2
將局部變量表中索引為2的變量的值壓入操作數(shù)棧亏狰。
iadd
將操作數(shù)棧中的值彈出來進(jìn)行相加后役纹,得到的結(jié)果再入棧。
istore_3
對(duì)操作數(shù)棧彈出操作暇唾,然后將彈出的值賦值給局部變量表中索引為3的變量促脉。
iload_3
將局部變量表中索引為3的變量的值壓入操作數(shù)棧。
ireturn
將操作數(shù)棧的值彈出并返回給方法策州。
這里我們想一個(gè)問題瘸味,我們有一個(gè)方法里面會(huì)聲明一個(gè)對(duì)象,用于參與計(jì)算够挂,那么這個(gè)對(duì)象是怎樣創(chuàng)建的呢旁仿?
很顯然對(duì)象都是在堆中創(chuàng)建的,那么我們的棧幀中的局部變量中是如果使用這個(gè)對(duì)象的呢孽糖?就是在局部變量表中有一個(gè)引用指向了堆丁逝。大致如下圖:
這里就會(huì)有另一個(gè)問題汁胆,那么堆是怎樣知道自己要?jiǎng)?chuàng)建什么類型的對(duì)象呢?
答案就是堆中對(duì)象實(shí)際上是指向了方法區(qū)中的對(duì)象所對(duì)應(yīng)的類信息霜幼,一個(gè)Java對(duì)象的內(nèi)存布局大致如下圖:
那如果一個(gè)類中的靜態(tài)變量指向了一個(gè)對(duì)象呢 嫩码?這種情況是怎么樣的?
public class Person{
public static Object tag = new Object();
}
這種情況下罪既,即是方法區(qū)也指向了堆铸题。總結(jié)以上的2個(gè)問題琢感,得出如下結(jié)論:
- 虛擬機(jī)椂洌可以指向堆,即局部變量表中的對(duì)象驹针;
- 方法區(qū)可以指向堆烘挫,靜態(tài)對(duì)象;
- 堆可以指向方法區(qū)(通過class pointer 指向java對(duì)象所對(duì)應(yīng)的類信息內(nèi)存地址)柬甥,創(chuàng)建對(duì)象需要知道對(duì)象的具體類型饮六,而這些對(duì)象的類信息是存儲(chǔ)在方法區(qū)中的。
總結(jié)如下:
以上我們理解并分析了Java運(yùn)行時(shí)的數(shù)據(jù)區(qū)卤橄,那么當(dāng)處于非運(yùn)行時(shí)的一個(gè)狀態(tài),或者說內(nèi)存分布臂外,或者說物理上的內(nèi)存分布的一個(gè)模型窟扑,那么接下來我們就來理解和分析下Java的內(nèi)存模型,即JMM漏健。
四. JMM
那么在運(yùn)行時(shí)數(shù)據(jù)區(qū)中的方法區(qū)和堆嚎货,在實(shí)際中的一個(gè)物理落地應(yīng)該是怎樣的呢?
為什么這里只分析方法區(qū)和堆的一個(gè)物理分布的落地蔫浆,因?yàn)檫\(yùn)行時(shí)數(shù)據(jù)區(qū)中的程序計(jì)數(shù)器殖属,java虛擬機(jī)棧以及本地方法棧都是在創(chuàng)建一個(gè)線程時(shí)才有的一個(gè)狀態(tài),當(dāng)程序執(zhí)行結(jié)束克懊,這個(gè)線程也就結(jié)束了。而方法區(qū)和堆是在JVM創(chuàng)建的時(shí)候就需要存在的七蜘,它倆跟隨的是JVM的進(jìn)程谭溉,生命周期更久。
那這個(gè)JMM的是怎樣劃分的呢橡卤?我們可以通過Java自帶的工具可以清楚的看到它的一個(gè)內(nèi)存分布的狀態(tài)扮念。
啟動(dòng)Java Visual VM :
jvisualvm
然后通過安裝Visual GC
插件來觀察它的一個(gè)內(nèi)存模型。
JMM分布如下圖:
我們通過上圖來分析碧库,為什么這樣設(shè)計(jì)內(nèi)存模型柜与。
- JMM 組成:
- Metaspace
- Old
- Eden
- S0
- S1
方法區(qū)和堆我們來找下對(duì)應(yīng)關(guān)系:
1. 方法區(qū) --> Metaspace
2. 堆 --> Old,Eden,S0,S1
我們從方法區(qū)和堆來進(jìn)行分析和理解JMM巧勤。
如下圖:
如上圖,我們的對(duì)象都存放在堆中弄匕,如果觸發(fā)GC颅悉,就需要對(duì)整個(gè)堆內(nèi)存進(jìn)行掃描,然后就行垃圾回收迁匠。假如我們的堆內(nèi)存是2G剩瓶,那這種情況下,顯然是不合理的城丧。
需要將堆進(jìn)行區(qū)域劃分延曙,這樣我們將活躍的對(duì)象丟到一個(gè)區(qū)域中,不活躍的丟到另一個(gè)區(qū)亡哄,如下:
那么問題來了枝缔,我們應(yīng)該有一個(gè)什么規(guī)則來判斷一個(gè)對(duì)象應(yīng)該在
Old
區(qū)還是在Young
區(qū)呢?就是給每一個(gè)對(duì)象加上年齡的標(biāo)識(shí)蚊惯,如果年齡大于15愿卸,則丟到
Old
老年區(qū),否則丟到Young
新生區(qū)拣挪。
后面我們會(huì)知道擦酌,這個(gè)年齡其實(shí)就是這個(gè)對(duì)象被GC 回收的次數(shù)(回收一次加1),前面我們了解到了每個(gè)對(duì)象都有一個(gè)對(duì)象頭信息菠劝,這個(gè)
年齡
就存放在頭信息中的->Mark Word
中的分代年齡信息中赊舶。
其實(shí)還有一種特殊的情況,就是一開始創(chuàng)建對(duì)象的時(shí)候赶诊,這個(gè)對(duì)象需要的內(nèi)存就非常大笼平,超過了Yong
區(qū)的大小,這個(gè)時(shí)候這個(gè)新創(chuàng)建的對(duì)象就要丟到Old
區(qū)舔痪,如果Old
區(qū)也放不下它寓调,只能報(bào)OutOfMemoryError
了。
這樣就看似解決了問題锄码,但是如果遇到這種問題我們應(yīng)該怎么處理夺英?
假如有一個(gè)對(duì)象大小為3個(gè)單元,但是Young
區(qū)的總大小是能夠放得下的滋捶,可是連續(xù)的空間放不下這個(gè)對(duì)象痛悯,所以會(huì)造成創(chuàng)建對(duì)象失敗,需要進(jìn)行垃圾回收重窟,垃圾回收就要啟動(dòng)垃圾回收線程载萌,這個(gè)時(shí)候我們的業(yè)務(wù)代碼的線程就要被迫停掉,這不是我們想要看到的,所以我們一直提倡減少垃圾回收的頻率扭仁。
如下圖:
針對(duì)以上空間不連續(xù)(碎片)情況需要對(duì)Young
區(qū)進(jìn)一步來優(yōu)化垮衷,如下:
我們都只到大部分對(duì)象的生命周期是很短的,大部分都是“朝生夕死”的乖坠,基于這樣一個(gè)前提搀突,我們來分析這樣劃分的好處。
當(dāng)發(fā)生垃圾回收的時(shí)候瓤帚,在 Eden
區(qū)少量沒有被回收掉的對(duì)象描姚,會(huì)丟到Survivor
區(qū),這樣下來戈次,基本上保證了Eden
區(qū)的連續(xù)性轩勘。但是垃圾回收的過程是對(duì)整個(gè)Young
區(qū)進(jìn)行回收的,所以剛剛在Eden
區(qū)存在的空間不連續(xù)(碎片)問題怯邪,在Survivor
區(qū)同樣也存在绊寻,問題又來了,那怎樣解決這個(gè)問題呢悬秉?
同樣我們可以對(duì)Survivor
區(qū)在進(jìn)行劃分澄步,如下圖:
我們來分析下這樣劃分的好處。
當(dāng)觸發(fā)垃圾回收和泌,我們將Eden
區(qū)少量沒有被回收的對(duì)象全部丟到Survivor0
區(qū)村缸,這個(gè)時(shí)候Eden
區(qū)和Survivor1
區(qū)是空間連續(xù)的。當(dāng)再次觸發(fā)垃圾回收武氓,我們將Eden
區(qū)少量沒有被回收的對(duì)象和Survivor0
區(qū)少量沒有被回收的對(duì)象全部放到Survivor1
區(qū)中梯皿,這樣就保證了Eden
區(qū)和Survivor0
區(qū)是連續(xù)可用的空間,再次發(fā)生垃圾回收的時(shí)候也是同樣的操作县恕,這樣始終都保持Survivor0
區(qū)和Survivor1
區(qū)一定有一個(gè)是連續(xù)可用的空的空間东羹,這樣就解決了空間不連續(xù)(碎片)的問題。
通常Eden:Survivor0:Survivor1 = 8:1:1忠烛,這里分給了Eden更多属提,是因?yàn)榇蠖鄶?shù)對(duì)象都是”朝生夕死”的,這樣分配效率更高美尸。
Survivor0(S0)冤议,Survivor1(S1)
那如果S0或S1的空間放不下沒有被回收的對(duì)象呢?對(duì)师坎,找Old
老年代借點(diǎn)空間恕酸,比如:S區(qū)需要12M的空間,但是目前只有10M屹耐,差了2M尸疆,這個(gè)時(shí)候找老年代借2M空間,這就是所謂的“擔(dān)保機(jī)制”惶岭。如果S區(qū)的對(duì)象的年齡大于15了寿弱,就需要移到Old
老年代區(qū)了。
我們畫一個(gè)流程圖來加深下印象按灶。
五症革、垃圾回收
上一個(gè)章節(jié)已經(jīng)說到了對(duì)象的創(chuàng)建,內(nèi)存模型鸯旁,對(duì)垃圾回收沒這么詳細(xì)說噪矛,接下來我們就詳細(xì)說下垃圾回收。
-
什么樣的對(duì)象才是垃圾艇挨?
- 引用計(jì)數(shù)
如果一個(gè)對(duì)象被引用的計(jì)數(shù)為0,則判定為垃圾對(duì)象韭赘,因?yàn)闆]有其他地方在引用它了缩滨,但會(huì)存在循環(huán)引用的情況。 - 可達(dá)性分析
首先要確定GCRoot對(duì)象泉瞻,然后由GCRoot出發(fā)脉漏,是否能到達(dá)某一個(gè)對(duì)象(是否有引用),如果不到達(dá)袖牙,則為垃圾對(duì)象侧巨。
GCRoot對(duì)象特征: 生命周期要長(zhǎng),而且要有存在的意義鞭达。
可以作為GCRoot的對(duì)象:- 虛擬機(jī)棧中的本地變量
- 靜態(tài)變量
- 常量
- 本地方法棧中的變量
- 類加載器
- 線程(Thread)
- 引用計(jì)數(shù)
-
該如何進(jìn)行回收司忱?
垃圾回收算法:-
標(biāo)記回收算法
內(nèi)存空間不連續(xù),碎片化較嚴(yán)重碉怔,效率較低烘贴。
-
復(fù)制算法
使空間連續(xù),解決碎片化的問題撮胧。弊端就是浪費(fèi)空間桨踪,需要把空間一分為二,保證其中有一塊空間是空的芹啥。
-
標(biāo)記整理算法
不用分割空間锻离,直接在原有的基礎(chǔ)上進(jìn)行標(biāo)記,回收對(duì)象之后墓怀,將所有存活的對(duì)象進(jìn)行重新整理到一起汽纠,達(dá)到空間連續(xù)的目的。
-
垃圾收集器
需要根據(jù)不同的分代來采用不同來及回收器傀履。比如說虱朵,新生代使用什么垃圾回收器,老年代使用什么垃圾回收器?
新生代碴犬,使用復(fù)制回收算法絮宁,因?yàn)樾律膶?duì)象大部分都會(huì)被回收掉,存活時(shí)間很多服协,只有少部分對(duì)象存活绍昂,復(fù)制的成本不高。
老年代偿荷,老年代中的對(duì)象都是經(jīng)過了15次回收之后還存在的對(duì)象窘游,生命力頑強(qiáng),即使再回收幾次也可能還是一樣的結(jié)果跳纳,所以這里并不適合使用復(fù)制回收算法忍饰,標(biāo)記算法更合適,不管是標(biāo)記清除還是標(biāo)記整理都可以寺庄。
所以針對(duì)不同的算法去實(shí)現(xiàn)不同的垃圾回收器即可喘批。
復(fù)制回收算法實(shí)現(xiàn)的垃圾回收器:Serial,ParNew铣揉,ParallelScavenge(關(guān)注的是吞吐量)等饶深。
標(biāo)記算法實(shí)現(xiàn)的垃圾回收器:CMS(Concurrent Mark Sweep,關(guān)注的是停頓時(shí)間)逛拱,Serial Old敌厘,Parallel Old(關(guān)注的也是吞吐量)等。-
優(yōu)勢(shì)和劣勢(shì)
各種垃圾回收器的優(yōu)劣勢(shì)朽合。
Serial:?jiǎn)尉€程執(zhí)行俱两,復(fù)制回收算法的實(shí)現(xiàn),需要暫停業(yè)務(wù)線程曹步。
ParNew:多線程執(zhí)行宪彩,復(fù)制回收算法的實(shí)現(xiàn),需要暫停業(yè)務(wù)線程讲婚。
ParallelScavenge:多線程執(zhí)行尿孔,復(fù)制回收算法的實(shí)現(xiàn),需要暫停業(yè)務(wù)線程筹麸,更加關(guān)注吞吐量(吞吐量=業(yè)務(wù)線程執(zhí)行的時(shí)間/(業(yè)務(wù)線程執(zhí)行的時(shí)間+垃圾回收的時(shí)間))活合。
Serial Old:和Serial特性一樣,只不過是標(biāo)記-清除算法的實(shí)現(xiàn)物赶。
ParallelOld: 和ParallelOScavenge的特性基本一致白指,只不過是標(biāo)記-清除算法的實(shí)現(xiàn),更加關(guān)注吞吐量酵紫。
CMS:并行之心的垃圾回收器告嘲,它是標(biāo)記-清除算法的實(shí)現(xiàn)错维,更加關(guān)注的是暫停時(shí)間,屬于并發(fā)垃圾收集器橄唬,它的線程是和業(yè)務(wù)線程一起運(yùn)行的需五。
工作模型如下:
G1(Garbage-First):它會(huì)嘗試最短的暫停時(shí)間以及更高的吞吐量,這個(gè)時(shí)間是可配置的轧坎,也是標(biāo)記-清除算法的實(shí)現(xiàn),JDK1.9之后默認(rèn)的垃圾回收器為G1.
工作模型如下:
對(duì)比下CMS會(huì)發(fā)現(xiàn)多了一步篩選回收泽示,為什么不對(duì)CMS進(jìn)行優(yōu)化下呢缸血?原因是因?yàn)镃MS的堆內(nèi)存布局不滿足G1的需要,所以G1對(duì)堆內(nèi)存重新進(jìn)行了布局械筛。
G1加入了區(qū)域的概念(Region)捎泻,對(duì)堆內(nèi)存進(jìn)行了重新的布局,在邏輯上還是存在Young埋哟,Old笆豁, Eden區(qū),但是實(shí)際物理上來說已經(jīng)不是隔離開的了赤赊。
-
Region
堆內(nèi)存被分成了很多塊大小相同的Region區(qū)域闯狱,每一塊的Region區(qū)域都是連續(xù)的虛擬內(nèi)存。
G1通過執(zhí)行并發(fā)的全局標(biāo)記來確認(rèn)整個(gè)堆內(nèi)存中的存活對(duì)象抛计,標(biāo)記階段結(jié)束之后哄孤,G1就知道哪些區(qū)域大部分是空的,然后優(yōu)先回收那些產(chǎn)生垃圾的Region區(qū)域吹截,這也就是為什么叫垃圾優(yōu)先了瘦陈,從名字上也可以看出。G1會(huì)將1個(gè)或多個(gè)Region區(qū)域復(fù)制到單獨(dú)的一個(gè)Region區(qū)域波俄,并且在這個(gè)過程中會(huì)壓縮和釋放內(nèi)存晨逝,進(jìn)一步來減少碎片。執(zhí)行垃圾回收的過程是利用多處理器進(jìn)行并行回收懦铺,以此來減少垃圾回收帶來的暫停時(shí)間捉貌。
當(dāng)如果出現(xiàn)以下3個(gè)方面時(shí),應(yīng)該更傾向于使用G1垃圾回收器冬念。
- Java的堆內(nèi)存被實(shí)時(shí)數(shù)據(jù)占用超過50%昏翰;
- 對(duì)象分配率或提升率差異很大;
- 當(dāng)應(yīng)用程序的垃圾回收暫停時(shí)間大于0.5到1秒刘急;
所以綜合來說棚菊,G1是為更短的GC暫停時(shí)間而生的。
這里可以順便說下JVM調(diào)優(yōu)維度:
- GC收集器的停頓時(shí)間和吞吐量
兩個(gè)指標(biāo):- 停頓時(shí)間:垃圾回收器進(jìn)行垃圾回收暫停的時(shí)間叔汁,時(shí)間越短越好统求。
- 吞吐量:運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾回收暫停時(shí)間)检碗,吞吐量越大越好。
二者需要?jiǎng)討B(tài)調(diào)整码邻,達(dá)到一個(gè)平衡點(diǎn)折剃。
- 內(nèi)存使用率
垃圾回收器分類:
- 停頓時(shí)間短的垃圾收集器:CMS(20ms),G1(可以設(shè)置暫停時(shí)間,并不是嚴(yán)格上的15ms)像屋,適用于Web應(yīng)用怕犁,屬于并發(fā)類的收集器。
- 吞吐量更好的垃圾收集器:Parallel Scanvent己莺,Parallel Old奏甫,適用于后端跑任務(wù),和用戶很少交互或者沒有交互的凌受,屬于并行類的收集器阵子。
- 串行收集器:Serial,Serial Old胜蛉,內(nèi)存占用比較小挠进,適用于嵌入式開發(fā),屬于串行類的收集器誊册。
垃圾收集器的選擇:
查看當(dāng)前進(jìn)程使用的垃圾收集器的類型:
終端執(zhí)行以下命令,查看當(dāng)前運(yùn)行的進(jìn)程:
jps -l
根據(jù)進(jìn)程ID來查看當(dāng)前進(jìn)程的GC類型案怯, 以G1垃圾收集器為例:
jinfo -flag UseG1GC <pid>
如果是-號(hào)則不是攘须,+號(hào)代表正在使用還有一種方式就是打印JVM的啟動(dòng)參數(shù):
jcmd <pid> VM.flags
最后放一張垃圾回收器的分類圖:
- 查看垃圾回收器日志文件
JVM官網(wǎng)參數(shù)地址:點(diǎn)擊查看
附上一些常用的JVM參數(shù):
- 標(biāo)準(zhǔn)參數(shù),這些參數(shù)通常是很穩(wěn)定的殴泰,不會(huì)隨著JDK的變化而變化
java -version //查看當(dāng)前JDK的版本
java -help //查看幫助
等于宙。
- -X 參數(shù),分標(biāo)準(zhǔn)參數(shù)悍汛,會(huì)隨著JDK的版本的變動(dòng)而變動(dòng)捞魁。
java -Xint -version //改變JVM虛擬雞的模式 -Xint 解釋執(zhí)行 -Xcomp 編譯執(zhí)行 -Xmixed 混合執(zhí)行
- -XX 參數(shù)
1. Boolean 類型
-XX:[+/-]name //這里的+/-表示啟用或者停用
比如設(shè)置垃圾收集器為:G1
-XX:+UseG1GC
2. 非Boolean類型
-XX:name=value //name屬性名,value屬性值
比如設(shè)置堆內(nèi)存最大空間:
-XX:MaxHeapSize=100M
- 其他參數(shù)
-Xms100M //初始化的堆內(nèi)存大小
-Xmx100M //最大堆內(nèi)存大小
-Xss100K //初始化棧幀的深度
常用參數(shù)如下:
-
啟動(dòng)某一進(jìn)程時(shí)打印啟動(dòng)參數(shù):
java -XX:+PrintFlagsFinal -version
-
查看當(dāng)前Java進(jìn)程
jps
-
查看或者實(shí)時(shí)修改JVM參數(shù)
jinfo -flag <參數(shù)名> <PID>
查看所有參數(shù)
jinfo -flags <PID>
例如:查看是否使用了G1垃圾收集器
jinfo -flag UseG1GC 1135
實(shí)時(shí)修改JVM參數(shù)值离咐,條件就是該參數(shù)是可以修改的谱俭,也就是【manageable】
jinfo -flag key=value <PID>
例如修改 -
查看JVM中某指標(biāo)體狀態(tài)和信息(jstat)
查看JVM class 裝載的情況
jstat -class <PID> 1000 10,意思是查看JVM類裝載情況 每隔1000ms輸出一次宵蛀,共輸出10次昆著。
查看GC狀態(tài)
jstat -gc <PID> 1000 10
查看線程堆棧信息,方便排查線程相關(guān)的情況术陶,例如:死鎖
jstack <PID>
查看堆內(nèi)存的相關(guān)信息凑懂,方便排查分析OOM
jmap 生成堆內(nèi)存的快照
jmap -heap <PID>
dump 文件
jmap -dump:format=b,file=heap.hprof <PID>
設(shè)置當(dāng)出現(xiàn)OOM時(shí)自動(dòng)dump堆內(nèi)存信息
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof -
*.hprof文件分析工具
JDK 自帶工具(bin/目錄下)
- jconsole Java監(jiān)視和管理控制臺(tái)
啟動(dòng)命令
jconsole
- jvisualvm JVM虛擬機(jī)可視化工具
啟動(dòng)命令
jvisualvm
- JMC(Java Mission Control),使用 Java Management Extensions (JMX) 代理連接到 JVM
- Arthas
好了梧宫,由于個(gè)人能力有限接谨,暫時(shí)先分析到這里摆碉,下面我們貼圖總結(jié)下:
有些圖片來源網(wǎng)絡(luò),侵刪EШ馈O锏邸!