方法調(diào)用——多態(tài)
方法調(diào)用并不等同于方法執(zhí)行,方法調(diào)用階段唯一的任務(wù)就是確定調(diào)用方法的版本(即調(diào)用哪一個(gè)方法),暫時(shí)還不涉及方法內(nèi)部的具體運(yùn)行過程。在程序運(yùn)行時(shí),進(jìn)行方法調(diào)用是最普遍、最頻繁的操作。在Class文件的編譯過程中不包含傳統(tǒng)編譯中的連接步驟,一切方法調(diào)用在Class文件里存儲(chǔ)的都只是符號(hào)引用,而不是方法在實(shí)際運(yùn)行時(shí)內(nèi)存布局中的入口地址(相當(dāng)于直接引用)。這個(gè)特性給Java帶來了更強(qiáng)大的動(dòng)態(tài)擴(kuò)展能力,但也使得Java方法的調(diào)用過程變得相對(duì)復(fù)雜,需要在類加載期間甚至到運(yùn)行期間才能確定目標(biāo)方法的直接引用。
一、方法解析
所有方法調(diào)用中的目標(biāo)方法在Class文件里面都是一個(gè)常量池中的符號(hào)引用,在類加載的解析階段,會(huì)將其中一部分符號(hào)引用轉(zhuǎn)化為直接引用,這種解析能成立的前提是:方法在程序真正運(yùn)行之前就有一可確定的調(diào)用版本,并且這個(gè)方法的調(diào)用版本是運(yùn)行期是不可改變的。換句話說,調(diào)用目標(biāo)在程序代碼寫好、編譯器進(jìn)行編譯時(shí)就必須確定下來。這類方法的調(diào)用稱為解析(Resolution)。
在Java語言中,符合“編譯期可知,運(yùn)行期不可變”這個(gè)要求的方法有靜態(tài)方法和私有方法兩大類,前者與類型直接相關(guān)聯(lián),后者在外部不可被訪問,這兩種方法都不可能通過繼承或者別的方式重寫出其它版本,因此它們都適合在類加載階段進(jìn)行靜態(tài)解析。
與之相對(duì)應(yīng),在Java虛擬機(jī)里提供了四條方法調(diào)用字節(jié)碼指令,分別是:
invokestatic:調(diào)用靜態(tài)方法
invokespecial:調(diào)用實(shí)例構(gòu)造器<init>方法,私有方法和父類方法。
invokevirtual:調(diào)用虛方法。
invokeinterface:調(diào)用接口方法,會(huì)在運(yùn)行時(shí)再確定一個(gè)實(shí)現(xiàn)此接口的對(duì)象。
只要能被invokestatic
與invokespecial
指令調(diào)用的方法,都可以在解析階段確定唯一的調(diào)用版本,符合這個(gè)條件的有靜態(tài)方法,私有方法,實(shí)例構(gòu)造器和父類方法四類,它們?cè)陬惣虞d的時(shí)候就會(huì)把符號(hào)引用解析為該方法的直接引用。這些方法可以統(tǒng)稱為非虛方法,與之相反,其它方法就稱為虛方法(除去final方法)。
Java中的非虛方法除了使用invokestatic
與invokespecial
指令調(diào)用的方法之后還有一種,就是被final
修飾的方法。雖然final方法是使用invokevirtual指令來調(diào)用的,但是由于它無法被覆蓋,沒有其它版本,所以也無須對(duì)方法接收都進(jìn)行多態(tài)選擇,又或者說多態(tài)選擇的結(jié)果是唯一的。在Java語言規(guī)范中明確說明了final方法是一種非虛方法。
解析調(diào)用一定是個(gè)靜態(tài)過程,在編譯期間就完全確定,在類裝載的解析階段就會(huì)把涉及的符號(hào)引用全部轉(zhuǎn)變?yōu)榭纱_定的直接引用,不會(huì)延遲到運(yùn)行期再去完成。而分派(Dispatch)調(diào)用則可能是靜態(tài)的也可能是動(dòng)態(tài)的,根據(jù)分派依據(jù)的宗量數(shù)可分為單分派與多分派。這兩類分派方式兩兩組合就構(gòu)成了靜態(tài)單分派,靜態(tài)多分派,動(dòng)態(tài)單分派與動(dòng)態(tài)多分派情況。
二、分派
1.靜態(tài)分派——重載
下面是一段程序代碼:
<pre class="md-fences md-end-block" lang="java" contenteditable="false" cid="n1099" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
package com.xtayfjpk.jvm.chapter8;
?
public class StaticDispatch {
static abstract class Human {
?
}
static class Man extends Human {
?
}
static class Woman extends Human {
?
}
?
public void sayHello(Human guy) {
System.out.println("hello guy...");
}
public void sayHello(Man man) {
System.out.println("hello man...");
}
public void sayHello(Woman woman) {
System.out.println("hello woman...");
}
?
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sd = new StaticDispatch();
sd.sayHello(man);
sd.sayHello(woman);
}
}</pre>
執(zhí)行結(jié)果為:
hello guy...hello guy...
但為什么會(huì)選擇執(zhí)行參數(shù)為Human的重載呢?在這之前,先按如下代碼定義兩個(gè)重要的概念:Human man = new Man();
上面代碼中的“Human”稱為變量的靜態(tài)類型(Static Type)或者外觀類型(Apparent Type),后面的“Man”則稱為變量的實(shí)際類型(Actual Type),靜態(tài)類型和實(shí)際類型在程序中都可以發(fā)生一些變化,區(qū)別是靜態(tài)類型的變化僅僅在使用時(shí)發(fā)生,變量本身的靜態(tài)類型不會(huì)被改變,并且最終的靜態(tài)類型是編譯期可知的;而實(shí)際類型變化的結(jié)果在運(yùn)行期才可確定,編譯期在編譯程序的時(shí)候并不知道一個(gè)對(duì)象的實(shí)際類型是什么?如下面的代碼:
<pre class="md-fences md-end-block" lang="java" contenteditable="false" cid="n1109" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
//實(shí)際類型變化
Human man = new Man();
man = new Woman();
?
//靜態(tài)類型變化
sd.sayHello((Man)man);
sd.sayHello((Woman)man);</pre>
解釋了這兩個(gè)概念,再回到上述代碼中。main()里面的兩次sayHello()方法調(diào)用,在方法接收者已經(jīng)確定是對(duì)象“sd”的前提下,使用哪個(gè)重載版本,就完全取決于傳入?yún)?shù)的數(shù)量和數(shù)據(jù)類型。代碼中刻意定義了兩個(gè)靜態(tài)類型相同,實(shí)際類型不同的變量,但虛擬機(jī)(準(zhǔn)確地說是編譯器)在重載時(shí)是通過參數(shù)的靜態(tài)類型而不是實(shí)際類型作為判定依據(jù)的。并且靜態(tài)類型在編譯期是可知的,所以在編譯階段,Javac編譯器就根據(jù)參數(shù)的靜態(tài)類型決定使用哪個(gè)重載版本,所以選擇了sayHello(Human)作為調(diào)用目標(biāo),并把這個(gè)方法的符號(hào)引用寫到main()方法的兩條invokevirual指令的參數(shù)中。
所有依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動(dòng)作,都稱為靜態(tài)分派。靜態(tài)分派的最典型應(yīng)用就是方法重載。靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分派的動(dòng)作實(shí)際上不是由虛擬機(jī)來執(zhí)行的。另外,編譯器雖然能確定出方法的重載版本,但是很多情況下,這個(gè)重載版本并不是“唯一的”,往往只能確定一個(gè)“更適合的”版本。這種模糊的結(jié)論在0和1構(gòu)成的計(jì)算機(jī)世界中算是個(gè)比較“稀罕”的事件,產(chǎn)生這種模糊結(jié)論的主要原因是字面量不需要定義,所以字面量沒有顯式的靜態(tài)類型,它的靜態(tài)類型只能通過語言上的規(guī)則去理解和推斷。
2.動(dòng)態(tài)分派——重寫
動(dòng)態(tài)分派與重寫(Override)有著很密切的關(guān)聯(lián)。如下代碼:
<pre class="md-fences md-end-block" lang="java" contenteditable="false" cid="n1117" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
package com.xtayfjpk.jvm.chapter8;
?
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
?
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}</pre>
這里顯然不可能是根據(jù)靜態(tài)類型來決定的,因?yàn)殪o態(tài)類型都是Human的兩個(gè)變量man和woman在調(diào)用sayHello()方法時(shí)執(zhí)行了不同的行為,并且變量man在兩次調(diào)用中執(zhí)行了不同的方法。導(dǎo)致這個(gè)現(xiàn)象的原是是這兩個(gè)變量的實(shí)際類型不同。那么Java虛擬機(jī)是如何根據(jù)實(shí)際類型來分派方法執(zhí)行版本的呢,我們使用javap命令輸出這段代碼的字節(jié)碼,結(jié)果如下:
<pre class="md-fences md-end-block" lang="java" contenteditable="false" cid="n1120" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #16 // class com/xtayfjpk/jvm/chapter8/DynamicDispatchMan."<init>":()V
7: astore_1
8: new #19 // class com/xtayfjpk/jvm/chapter8/DynamicDispatchWoman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #22 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatchHuman.sayHello:()V
24: new #19 // class com/xtayfjpk/jvm/chapter8/DynamicDispatchWoman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #22 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Human.sayHello:()V
36: return</pre>
0-15行的字節(jié)碼是準(zhǔn)備動(dòng)作,作用是建立man和woman的內(nèi)存空間,調(diào)用Man和Woman類的實(shí)例構(gòu)造器,將這兩個(gè)實(shí)例的引用存放在第1和第2個(gè)局部變量表Slot之中,這個(gè)動(dòng)作對(duì)應(yīng)了代碼中這兩句:
<pre class="md-fences md-end-block" lang="java" contenteditable="false" cid="n1123" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
Human man = new Man();
Human woman = new Woman();</pre>
接下來的第16-21行是關(guān)鍵部分,第16和第20兩行分別把剛剛創(chuàng)建的兩個(gè)對(duì)象的引用壓到棧頂,這兩個(gè)對(duì)象是將執(zhí)行的sayHello()方法的所有者,稱為接收者(Receiver),第17和第21兩行是方法調(diào)用指令,單從字節(jié)碼的角度來看,這兩條調(diào)用指令無論是指令(都是invokevirtual)還是參數(shù)(都是常量池中Human.sayHello()的符號(hào)引用)都完全一樣,但是這兩條指令最終執(zhí)行的目標(biāo)方法并不相同,其原因需要從invokevirutal指令的多態(tài)查找過程開始說起,invokevirtual指令的運(yùn)行時(shí)解析過程大致分為以下步驟:
找到操作數(shù)棧頂?shù)牡谝粋€(gè)元素所指向的對(duì)象實(shí)際類型,記作C。
如果在類型C中找到與常量中描述符和簡(jiǎn)單名稱都相同的方法,則進(jìn)行訪問權(quán)限校驗(yàn),如果通過則返回這個(gè)方法的直接引用,查找結(jié)束;不通過則返回java.lang.IllegalAccessError錯(cuò)誤。
否則,按照繼承關(guān)系從下往上依次對(duì)C的各個(gè)父類進(jìn)行第2步的搜索與校驗(yàn)過程。
如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError錯(cuò)誤。
由于invokevirtual指令執(zhí)行的第一步就是在運(yùn)行期確定接收者的實(shí)際類型,所以兩次調(diào)用中的invokevirtual指令把常量池中的類方法符號(hào)引用解析到了不同的直接引用上,這個(gè)過程就是Java語言中方法重寫的本質(zhì)。我們把這種在運(yùn)行期根據(jù)實(shí)際類型確定方法執(zhí)行版本的分派過程稱為動(dòng)態(tài)分派。
3.單分派與多分派
方法的接收者與方法的參數(shù)統(tǒng)稱為方法的宗量。根據(jù)分派基于多少種宗量,可以將分派劃分為單分派與多分派兩種。單分派是根據(jù)一個(gè)宗量來對(duì)目標(biāo)方法進(jìn)行選擇,多分派則是根據(jù)多于一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇。
在編譯期的靜態(tài)分派過程選擇目標(biāo)方法的依據(jù)有兩點(diǎn):一是靜態(tài)類型;二是方法參數(shù),所以Java語言的靜態(tài)分派屬于多分派類型。在運(yùn)行階段虛擬機(jī)的動(dòng)態(tài)分派過程只有接收者的實(shí)際類型一個(gè)宗量作為目標(biāo)方法選擇依據(jù),所以Java語言的動(dòng)態(tài)分派屬于單分派類型。所在Java語言是一門靜態(tài)多分派,動(dòng)態(tài)單分派語言。
4.虛擬機(jī)動(dòng)態(tài)分派的實(shí)現(xiàn)
由于動(dòng)態(tài)分派是非常頻繁的動(dòng)作,而且動(dòng)態(tài)分派的方法版本選擇過程需要在運(yùn)行時(shí)在類的方法元數(shù)據(jù)中搜索合適的目標(biāo)方法,因此在虛擬機(jī)的實(shí)際實(shí)現(xiàn)中基于性能的考慮,大部分實(shí)現(xiàn)都不會(huì)真的進(jìn)行如此頻繁的搜索。面對(duì)這種情況,最常用的優(yōu)化手段就是在類的方法區(qū)中建立一個(gè)虛方法表(Virtual Method Table,也稱vtable,與此對(duì)應(yīng),在invokeinterface執(zhí)行時(shí)也會(huì)用到接口方法表,Interface Method Table,也稱itable),使用虛方法表索引來代替元數(shù)據(jù)據(jù)查找以提高性能。
虛方法表中存放著各個(gè)方法的實(shí)際入口地址。如果某個(gè)方法在子類中沒有被重寫,那么子類的虛方法表里面的地址入口和父類方法的地址入口是一致的,都指向父類的實(shí)現(xiàn)入口。如果子類中重寫了這個(gè)方法,子類方法表中的地址將會(huì)被替換為指向子類實(shí)現(xiàn)版本的地址入口。
疑問:
Q: 而分派(Dispatch)調(diào)用則可能是靜態(tài)的也可能是動(dòng)態(tài)的,根據(jù)分派依據(jù)的宗量數(shù)可分為單分派與多分派。這兩類分派方式兩兩組合就構(gòu)成了靜態(tài)單分派,靜態(tài)多分派,動(dòng)態(tài)單分派與動(dòng)態(tài)多分派情況。什么叫宗量數(shù)?
內(nèi)存模型JMM——定義程序中各個(gè)變量的訪問規(guī)則
主內(nèi)存與工作內(nèi)存
Java內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量值這樣的底層細(xì)節(jié)。此處的變量(Variable)與Java編譯中所說的變量略有區(qū)別,它包括了實(shí)例字段,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但是不包括局部變量與方法參數(shù),因?yàn)楹笳呤蔷€程私有的,不會(huì)被共享,自然就不存在競(jìng)爭(zhēng)的問題。為了獲得比較好的執(zhí)行效率,Java內(nèi)存模型并沒有限制執(zhí)行引擎使用處理器的特定寄存器或緩存來和主內(nèi)存進(jìn)行交互,也沒有限制即時(shí)編譯器調(diào)整代碼執(zhí)行順序這類權(quán)限。
Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(Main Memory)中。每條線程還有自己的工作內(nèi)存(Working Memory),線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝,(內(nèi)存交互規(guī)則:)1、線程對(duì)變量的所有操作(讀取,賦值等)都必須是工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。2、不同的線程之間也無法直接訪問對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過主內(nèi)存來完成,線程、主內(nèi)存、工作內(nèi)存三者的交互關(guān)系如下圖:
內(nèi)存間交互操作
一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存,如何從工作內(nèi)存同步回主內(nèi)存之類的實(shí)現(xiàn)細(xì)節(jié),Java內(nèi)存模型中定義了以下8種操作來完成。
lock(鎖定):作用于主內(nèi)存變量,它把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)。
unlock(解鎖):作用于主內(nèi)存變量,它把一個(gè)處理鎖定的狀態(tài)的變量釋放出來,釋放后的變量才可以被其它線程鎖定,unlock之前必須將變量值同步回主內(nèi)存。
read(讀取):作用于主內(nèi)存變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用。
load(載入):作用于工作內(nèi)存變量,它把read操作從主內(nèi)存中得到的值放入工作內(nèi)存的變量副本中。
use(使用):作用于工作內(nèi)存中的變量,它把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
assign(賦值):作用于工作內(nèi)存變量,它把一個(gè)從執(zhí)行引擎接到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
store(存儲(chǔ)):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write操作使用。
write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的值放入主內(nèi)存的變量中。
如果要把一個(gè)變量從主內(nèi)存復(fù)制到工作內(nèi)存,那就要順序地執(zhí)行read和load操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要順序地執(zhí)行store和write操作。Java內(nèi)存模型只是要求上述兩個(gè)操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行。也就是說read與load之間、store與write之間是可以插入其它指令的,如果對(duì)主在內(nèi)中的變量a,b進(jìn)行訪問時(shí),一種可能出現(xiàn)的順序是read a、read b、load b、load a。除此之外,Java內(nèi)存模型還規(guī)定了執(zhí)行上述八種基礎(chǔ)操作時(shí)必須滿足如下規(guī)則:
不允許read和load、store和write操作之一單獨(dú)出現(xiàn),即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者從工作內(nèi)存發(fā)起回寫但主內(nèi)存不接受的情況出現(xiàn)。
不允許一個(gè)線程丟棄它的最近的assign操作,即變量在工作內(nèi)存中改變(為工作內(nèi)存變量賦值)了之后必須把該變化同步回主內(nèi)存。
不允許一個(gè)線程無原因地(沒有發(fā)生任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中。
一個(gè)新變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load和assign)的變量,換話說就是一個(gè)變量在實(shí)施use和store操作之前,必須先執(zhí)行過了assign和load操作。
一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一個(gè)線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖。
如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值。
如果一個(gè)變量事先沒有被lock操作鎖定,則不允許對(duì)它執(zhí)行unlock操作;也不允許去unlock一個(gè)被其它線程鎖定的變量。
對(duì)一個(gè)變量執(zhí)行unloack之前,必須把此變量同步回主內(nèi)存中(執(zhí)行store和write操作)
疑問:
Q: unlock(解鎖):作用于主內(nèi)存變量,它把一個(gè)處理鎖定的狀態(tài)的變量釋放出來,釋放后的變量才可以被其它線程鎖定,unlock之前必須將變量值同步回主內(nèi)存。unlock之前必須將變量值同步回主內(nèi)存是指調(diào)用unlock之前必須先調(diào)用store,然后調(diào)用write操作,還是說unlock之前會(huì)自動(dòng)調(diào)用store,然后調(diào)用write操作?
volatile變量的特殊規(guī)則——保證變量對(duì)所有線程的可見性及禁止指令重排序優(yōu)化
當(dāng)一個(gè)變量定義成volatile之后,它將具備兩種特性:第一是保證此變量對(duì)所有線程的可見性,這里的“可見性”是指當(dāng)一條線程修改了這個(gè)變量的值,新值對(duì)于其它線程是可以立即得知的,變量值在線程間傳遞均需要通過主內(nèi)存來完成,如:線程A修改一個(gè)普通變量的值,然后向主內(nèi)存進(jìn)行回寫,另外一條線程B在線程A回寫完成了之后再從主內(nèi)存進(jìn)行讀取操作,新變量的值才會(huì)對(duì)線程B可見。
關(guān)于volatile變量的可見性,很多人誤以為以下描述成立:“volatile對(duì)所有線程是立即可見的,對(duì)volatile變量所有的寫操作都能立即返回到其它線程之中,換句話說,volatile變量在各個(gè)線程中是一致的,所以基于volatile變量的運(yùn)算在并發(fā)下是安全的”。這句話的論據(jù)部分并沒有錯(cuò),但是其論據(jù)并不能得出“基于基于volatile變量的運(yùn)算在并發(fā)下是安全的”這個(gè)結(jié)論。volatile變量在各個(gè)線程的工作內(nèi)存中不存在一致性問題(在各個(gè)線程的工作內(nèi)存中volatile變量也可以存在不一致的情況,但由于每次使用之前都要先刷新,執(zhí)行引擎看不到不致的情況,因此可以認(rèn)為不存在一致性問題),但是Java里的運(yùn)算并非原子操作,導(dǎo)致volatile變量的運(yùn)算在并發(fā)下一樣是不安全的。
由于volatile變量只能保證可見性,在不符合以下條件規(guī)則的兩處場(chǎng)景中,仍然需要通過加鎖來保證原子性。
這兩種場(chǎng)景下volatile變量可以保證安全:
運(yùn)算結(jié)果不依賴變量的當(dāng)前值,或者能確保只有單一的線程改變變量的值。
變量不需要與其它的狀態(tài)變量共同參與不變約束。
使用volatile變量的第二個(gè)語義是禁止指令重排序優(yōu)化,普通的變量?jī)H僅會(huì)保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方能獲取到正確的結(jié)果,而不能保證變量的賦值操作的順序與程序代碼中的執(zhí)行順序一致。因?yàn)樵谝粋€(gè)線程的方法執(zhí)行過程中無法感知到這一點(diǎn),這也就是Java內(nèi)存模型中描述的所謂的”線程內(nèi)表現(xiàn)為串行的語義“(Within-Thread As-If-Serial Sematics)。
<pre class="md-fences md-end-block" lang="" contenteditable="false" cid="n1249" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
Map configOptions;
char[] configText;
//此變量必須定義為volatile
volatile boolean initialized = false;
//假設(shè)以下代碼在線程A中執(zhí)行
//模擬讀取配置信息,當(dāng)讀取完成后
//將initialized設(shè)置為true來通知其它線程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
?
//假設(shè)以下代碼在線程B中執(zhí)行
//等線程A待initialized為true,代表線程A已經(jīng)把配置信息初始化完成
while(!initialized) {
sleep();
}
//使用線程A中初始化好的配置信息
doSomethingWithConfig();</pre>
上面為一段偽代碼,其中描述的場(chǎng)景十分常見,只是我們?cè)谔幚砼渲梦募r(shí)一般不會(huì)出現(xiàn)并發(fā)而已。如果定義initialized變量時(shí)沒有使用volatile修飾,就可能會(huì)由于指令重排序的優(yōu)化,導(dǎo)致位于線程A中最后一句的代碼”initialized = true“被提前執(zhí)行,這樣在線程B中使用配置信息的代碼就可能出現(xiàn)錯(cuò)誤,而volatile關(guān)鍵字則可以避免此類情況的發(fā)生。
Java內(nèi)存模型中對(duì)volatile變量定義的特殊規(guī)則。假定T表示一個(gè)線程,V和W分別表示兩個(gè)volatile變量,那么在進(jìn)行read、load、use、assign、store、write操作時(shí)需要滿足如下的規(guī)則:
只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作是load的時(shí)候,線程T才能對(duì)變量V執(zhí)行use動(dòng)作;并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是use的時(shí)候,線程T才能對(duì)變量V執(zhí)行l(wèi)oad操作。線程T對(duì)變量V的use操作可以認(rèn)為是與線程T對(duì)變量V的load和read操作相關(guān)聯(lián)的,必須一起連續(xù)出現(xiàn)。這條規(guī)則要求在工作內(nèi)存中,每次使用變量V之前都必須先從主內(nèi)存刷新最新值,用于保證能看到其它線程對(duì)變量V所作的修改后的值。
只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作是assign的時(shí)候,線程T才能對(duì)變量V執(zhí)行store操作;并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是store操作的時(shí)候,線程T才能對(duì)變量V執(zhí)行assign操作。線程T對(duì)變量V的assign操作可以認(rèn)為是與線程T對(duì)變量V的store和write操作相關(guān)聯(lián)的,必須一起連續(xù)出現(xiàn)。這一條規(guī)則要求在工作內(nèi)存中,每次修改V后都必須立即同步回主內(nèi)存中,用于保證其它線程可以看到自己對(duì)變量V的修改。
假定操作A是線程T對(duì)變量V實(shí)施的use或assign動(dòng)作,假定操作F是操作A相關(guān)聯(lián)的load或store操作,假定操作P是與操作F相應(yīng)的對(duì)變量V的read或write操作;類型地,假定動(dòng)作B是線程T對(duì)變量W實(shí)施的use或assign動(dòng)作,假定操作G是操作B相關(guān)聯(lián)的load或store操作,假定操作Q是與操作G相應(yīng)的對(duì)變量V的read或write操作。如果A先于B,那么P先于Q。這條規(guī)則要求valitile修改的變量不會(huì)被指令重排序優(yōu)化,保證代碼的執(zhí)行順序與程序的順序相同。
原子性、可見性、有序性
volatile只能保證可見性和有序性;
synchronized可以保證原子性,可見性以及有序性。
Java內(nèi)存模型是圍繞著并發(fā)過程中如何處理原子性、可見性、有序性這三個(gè)特征來建立的,下面是這三個(gè)特性的實(shí)現(xiàn)原理:
1.原子性(Atomicity)——原子性變量操作包括read、load、use、assign、store和write六個(gè)
由Java內(nèi)存模型來直接保證的原子性變量操作包括read、load、use、assign、store和write六個(gè),大致可以認(rèn)為基礎(chǔ)數(shù)據(jù)類型的訪問和讀寫是具備原子性的。如果應(yīng)用場(chǎng)景需要一個(gè)更大范圍的原子性保證,Java內(nèi)存模型還提供了lock和unlock操作來滿足這種需求,盡管虛擬機(jī)未把lock與unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來隱匿地使用這兩個(gè)操作,這兩個(gè)字節(jié)碼指令反映到Java代碼中就是同步塊---synchronized關(guān)鍵字,因此在synchronized塊之間的操作也具備原子性。
2.可見性(Visibility)——volatile、synchronized、final
可見性就是指當(dāng)一個(gè)線程修改了線程共享變量的值,其它線程能夠立即得知這個(gè)修改。Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方法來實(shí)現(xiàn)可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區(qū)別是volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每使用前立即從內(nèi)存刷新。因?yàn)槲覀兛梢哉fvolatile保證了線程操作時(shí)變量的可見性,而普通變量則不能保證這一點(diǎn)。除了volatile之外,Java還有兩個(gè)關(guān)鍵字能實(shí)現(xiàn)可見性,它們是synchronized。同步塊的可見性是由“對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store和write操作)”這條規(guī)則獲得的,而final關(guān)鍵字的可見性是指:被final修飾的字段是構(gòu)造器一旦初始化完成,并且構(gòu)造器沒有把“this”引用傳遞出去,那么在其它線程中就能看見final字段的值。
3.有序性(Ordering)
Java內(nèi)存模型中的程序天然有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有操作都是有序的;如果在一個(gè)線程中觀察另一個(gè)線程,所有操作都是無序的。前半句是指“線程內(nèi)表現(xiàn)為串行語義”,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。Java語言提供了volatile和synchronized兩個(gè)關(guān)鍵字來保證線程之間操作的有序性,volatile關(guān)鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作”這條規(guī)則來獲得的,這個(gè)規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行地進(jìn)入。
先行發(fā)生原則:Happen-Before:——線程執(zhí)行節(jié)點(diǎn)的不確定便不能保證先行發(fā)生原則如果Java內(nèi)存模型中所有的有序性都只靠volatile和synchronized來完成,那么有一些操作將會(huì)變得很啰嗦,但是我們?cè)诰帉慗ava并發(fā)代碼的時(shí)候并沒有感覺到這一點(diǎn),這是因?yàn)镴ava語言中有一個(gè)“先行發(fā)生”(Happen-Before)的原則。這個(gè)原則非常重要,它是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng),線程是否安全的主要依賴。先行發(fā)生原則是指Java內(nèi)存模型中定義的兩項(xiàng)操作之間的依序關(guān)系,如果說操作A先行發(fā)生于操作B,其實(shí)就是說發(fā)生操作B之前,操作A產(chǎn)生的影響能被操作B觀察到,“影響”包含了修改了內(nèi)存中共享變量的值、發(fā)送了消息、調(diào)用了方法等。它意味著什么呢?如下例:
<pre class="md-fences md-end-block" lang="" contenteditable="false" cid="n1290" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
//線程A中執(zhí)行
i = 1;
?
//線程B中執(zhí)行
j = i;
?
//線程C中執(zhí)行
i = 2;</pre>
假設(shè)線程A中的操作”i=1“先行發(fā)生于線程B的操作”j=i“,那么我們就可以確定在線程B的操作執(zhí)行后,變量j的值一定是等于1,結(jié)出這個(gè)結(jié)論的依據(jù)有兩個(gè),一是根據(jù)先行發(fā)生原則,”i=1“的結(jié)果可以被觀察到;二是線程C登場(chǎng)之前,線程A操作結(jié)束之后沒有其它線程會(huì)修改變量i的值。現(xiàn)在再來考慮線程C,我們依然保持線程A和B之間的先行發(fā)生關(guān)系,而線程C出現(xiàn)在線程A和B操作之間,但是C與B沒有先行發(fā)生關(guān)系,那么j的值可能是1,也可能是2,因?yàn)榫€程C對(duì)應(yīng)變量i的影響可能會(huì)被線程B觀察到,也可能觀察不到,這時(shí)線程B就存在讀取到過期數(shù)據(jù)的風(fēng)險(xiǎn),不具備多線程的安全性。
下面是Java內(nèi)存模型下一些”天然的“先行發(fā)生關(guān)系,這些先行發(fā)生關(guān)系無須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用。如果兩個(gè)操作之間的關(guān)系不在此列,并且無法從下列規(guī)則推導(dǎo)出來的話,它們就沒有順序性保障,虛擬機(jī)可以對(duì)它們進(jìn)行隨意地重排序。
程序次序規(guī)則(Pragram Order Rule):在一個(gè)線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準(zhǔn)確地說應(yīng)該是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支、循環(huán)結(jié)構(gòu)。
管程鎖定規(guī)則(Monitor Lock Rule):一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作。這里必須強(qiáng)調(diào)的是同一個(gè)鎖,而”后面“是指時(shí)間上的先后順序。
volatile變量規(guī)則(Volatile Variable Rule):對(duì)一個(gè)volatile變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀取操作,這里的”后面“同樣指時(shí)間上的先后順序。
線程啟動(dòng)規(guī)則(Thread Start Rule):Thread對(duì)象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作。
線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對(duì)此線程的終止檢測(cè),我們可以通過Thread.join()方法結(jié)束,Thread.isAlive()的返回值等作段檢測(cè)到線程已經(jīng)終止執(zhí)行。
線程中斷規(guī)則(Thread Interruption Rule):對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測(cè)是否有中斷發(fā)生。
對(duì)象終結(jié)規(guī)則(Finalizer Rule):一個(gè)對(duì)象初始化完成(構(gòu)造方法執(zhí)行完成)先行發(fā)生于它的finalize()方法的開始。
傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。
一個(gè)操作”時(shí)間上的先發(fā)生“不代表這個(gè)操作會(huì)是”先行發(fā)生“,那如果一個(gè)操作”先行發(fā)生“是否就能推導(dǎo)出這個(gè)操作必定是”時(shí)間上的先發(fā)生“呢?也是不成立的,一個(gè)典型的例子就是指令重排序。所以時(shí)間上的先后順序與先生發(fā)生原則之間基本沒有什么關(guān)系,所以衡量并發(fā)安全問題一切必須以先行發(fā)生原則為準(zhǔn)。
疑問:
Q: final關(guān)鍵字的可見性是指:被final修飾的字段是構(gòu)造器一旦初始化完成,并且構(gòu)造器沒有把“this”引用傳遞出去,那么在其它線程中就能看見final字段的值。final關(guān)鍵字保證可見性的原因是因?yàn)槠洳豢勺儼桑吭趺蠢斫膺@句話?
Hotspot JVM的常用選項(xiàng)
本文將介紹Hotspot JVM的常用選項(xiàng)。
選項(xiàng)的分類
Hotspot JVM提供以下三大類選項(xiàng):
標(biāo)準(zhǔn)選項(xiàng):這類選項(xiàng)的功能是很穩(wěn)定的,在后續(xù)版本中也不太會(huì)發(fā)生變化。運(yùn)行java或者java -help可以看到所有的標(biāo)準(zhǔn)選項(xiàng)。所有的標(biāo)準(zhǔn)選項(xiàng)都是以-開頭,比如-version, -server等。X選項(xiàng):比如-Xms。這類選項(xiàng)都是以-X開頭,可能由于這個(gè)原因它們被稱為X選項(xiàng)。運(yùn)行java -X命令可以看到所有的X選項(xiàng)。這類選項(xiàng)的功能還是很穩(wěn)定,但官方的說法是它們的行為可能會(huì)在后續(xù)版本中改變,也有可能不在后續(xù)版本中提供了。XX選項(xiàng):這類選項(xiàng)是屬于實(shí)驗(yàn)性,主要是給JVM開發(fā)者用于開發(fā)和調(diào)試JVM的,在后續(xù)的版本中行為有可能會(huì)變化。
XX選項(xiàng)的語法
如果是布爾類型的選項(xiàng),它的格式為-XX:+flag或者-XX:-flag,分別表示開啟和關(guān)閉該選項(xiàng)。針對(duì)非布爾類型的選項(xiàng),它的格式為-XX:flag=value在了解這些約定的規(guī)范后,我們就可以來看看一些比較常用的選項(xiàng)了。
指定JVM的類型:-server,-client
Hotspot JVM有兩種類型,分別是server和client。它們的區(qū)別是Server VM的初始堆空間會(huì)大一些,默認(rèn)使用的是并行垃圾回收器。Client VM相對(duì)來講會(huì)保守一些,初始堆空間會(huì)小一些,使用串行的垃圾回收器,它的目標(biāo)是為了讓JVM的啟動(dòng)速度更快。
JVM在啟動(dòng)的時(shí)候會(huì)根據(jù)硬件和操作系統(tǒng)會(huì)自動(dòng)選擇使用Server還是Client類型的JVM。
在32位Windows系統(tǒng)上,不論硬件配置如何,都默認(rèn)使用Client類型的JVM。在其他32位操作系統(tǒng)上,如果機(jī)器配置有2GB及以上的內(nèi)存同時(shí)有2個(gè)以上的CPU,則默認(rèn)會(huì)使用Server類型的JVM64位機(jī)器上只有Server類型的JVM。也就是說Client類型的JVM只在32位機(jī)器上提供。你也可以使用-server和-client選項(xiàng)來指定JVM的類型,不過只在32位的機(jī)器上有效,原因見上面一條。詳細(xì)內(nèi)容請(qǐng)參見:http://docs.oracle.com/javase/7/docs/technotes/guides/vm/server-class.html
指定JIT編譯器的模式:-Xint,-Xcomp,-Xmixed
我們知道Java是一種解釋型語言,但是隨著JIT技術(shù)的進(jìn)步,它能在運(yùn)行時(shí)將Java的字節(jié)碼編譯成本地代碼。以下是幾個(gè)相關(guān)的選項(xiàng):
-Xint表示禁用JIT,所有字節(jié)碼都被解釋執(zhí)行,這個(gè)模式的速度最慢的。****-Xcomp表示所有字節(jié)碼都首先被編譯成本地代碼,然后再執(zhí)行。****-Xmixed,默認(rèn)模式,讓JIT根據(jù)程序運(yùn)行的情況,有選擇地將某些代碼編譯成本地代碼。-Xcomp和-Xmixed到底誰的速度快,針對(duì)不同的程序可能有不同的結(jié)果,基本還是推薦用默認(rèn)模式。
-version和-showversion
-version就是查看當(dāng)前機(jī)器的java是什么版本,是什么類型的JVM(Server/Client),采用的是什么執(zhí)行模式。比如,在我的機(jī)器上的結(jié)果如下:
$ java -versionjava version "1.7.0_71"Java(TM) SE Runtime Environment (build 1.7.0_71-b14)Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, mixed mode)表示我機(jī)器上java是運(yùn)行在mixed模式下的Server VM。
-showversion的作用是在運(yùn)行一個(gè)程序的時(shí)候首先把JVM的版本信息打印出來,這樣便于問題診斷。個(gè)人建議Server類型的程序都把這個(gè)選項(xiàng)打開,這樣可以發(fā)現(xiàn)一些配置問題,比如程序需要JDK1.7才能運(yùn)行,而有的機(jī)器上裝有多個(gè)JDK的版本,打開這個(gè)選項(xiàng)可以避免使用了錯(cuò)誤版本的Java。
查看XX選項(xiàng)的值: -XX:+PrintCommandLineFlags, -XX:+PrintFlagsInitial和-XX:+PrintFlagsFinal
與-showversion類似,-XX:+PrintCommandLineFlags可以讓在程序運(yùn)行前打印出用戶手動(dòng)設(shè)置或者JVM自動(dòng)設(shè)置的XX選項(xiàng),建議加上這個(gè)選項(xiàng)以輔助問題診斷。比如在我的機(jī)器上,JVM自動(dòng)給配置了初始的和最大的HeapSize以及其他的一些選項(xiàng):
<pre class="md-fences md-end-block" lang="" contenteditable="false" cid="n2994" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
$ java -XX:+PrintCommandLineFlags -version
?
-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags -XX:+UseCompressedOops -XX:+UseParallelGC
?
java version "1.7.0_71"
?
Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
?
Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, mixed mode)
?</pre>
相關(guān)另外兩個(gè)選項(xiàng):-XX:+PrintFlagsInitial表示打印出所有XX選項(xiàng)的默認(rèn)值,-XX:+PrintFlagsFinal表示打印出XX選項(xiàng)在運(yùn)行程序時(shí)生效的值。
內(nèi)存大小相關(guān)的選項(xiàng)
-Xms 設(shè)置初始堆的大小,也是最小堆的大小,它等價(jià)于:-XX:InitialHeapSize-Xmx 設(shè)置最大堆的大小,它等價(jià)于-XX:MaxHeapSize。比如,下面這條命令就是設(shè)置堆的初始值為128m,最大值為2g。
<pre class="md-fences md-end-block" lang="" contenteditable="false" cid="n3226" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
java -Xms128m -Xmx2g MyApp</pre>
如果堆的初始值和最大值不一樣的話,JVM會(huì)根據(jù)程序的運(yùn)行情況,自動(dòng)調(diào)整堆的大小,這可能會(huì)影響到一些效率。針對(duì)服務(wù)端程序,一般是把堆的最小值和最大值設(shè)置為一樣來避免堆擴(kuò)展和收縮對(duì)性能的影響。****-XX:PermSize 用來設(shè)置永久區(qū)的初始大小****-XX:MaxPermSize 用來設(shè)置永久區(qū)的最大值永久區(qū)是存放類以及常量池的地方,如果程序需要加載的class數(shù)量非常多的話,就需要增大永久區(qū)的大小。-Xss 設(shè)置線程棧的大小,線程棧的大小會(huì)影響到遞歸調(diào)用的深度,同時(shí)也會(huì)影響到能同時(shí)開啟的線程數(shù)量。
OutofMemory(OOM)相關(guān)的選項(xiàng)
如果程序發(fā)生了OOM后,JVM可以配置一些選項(xiàng)來做些善后工作,比如把內(nèi)存給dump下來,或者自動(dòng)采取一些別的動(dòng)作。
-XX:+HeapDumpOnOutOfMemoryError 表示在內(nèi)存出現(xiàn)OOM的時(shí)候,把Heap轉(zhuǎn)存(Dump)到文件以便后續(xù)分析,文件名通常是java_pid<pid>.hprof,其中pid為該程序的進(jìn)程號(hào)。****-XX:HeapDumpPath=<path>: 用來指定heap轉(zhuǎn)存文件的存儲(chǔ)路徑,需要指定的路徑下有足夠的空間來保存轉(zhuǎn)存文件。****-XX:OnOutOfMemoryError 用來指定一個(gè)可行性程序或者腳本的路徑,當(dāng)發(fā)生OOM的時(shí)候,去執(zhí)行這個(gè)腳本。
比如,下面的命令可以使得在發(fā)生OOM的時(shí)候,Heap被轉(zhuǎn)存到文件/tmp/heapdump.hprof,同時(shí)執(zhí)行Home目錄中的cleanup.sh文件。
<pre class="md-fences md-end-block" lang="" contenteditable="false" cid="n3019" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
$ java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:OnOutOfMemoryError ="sh ~/cleanup.sh" MyApp</pre>
個(gè)人覺得幾個(gè)選項(xiàng)還是非常有用的,它可以使得你有相關(guān)的信息來分析OOM的根源。
新生代相關(guān)的選項(xiàng)
在介紹新生代相關(guān)的選項(xiàng)前,先簡(jiǎn)要介紹下Hotspot VM的Heap分代的背景知識(shí)。很多面向?qū)ο蟪绦蛟谶\(yùn)行時(shí)都具有如下兩點(diǎn)特征:
新創(chuàng)建的對(duì)象通常不會(huì)存活很長(zhǎng)時(shí)間,也就是夭折了。很少有老對(duì)象引用到新對(duì)象。基于這里兩點(diǎn),把新老對(duì)象分別放在不同的區(qū)域(分別叫做新生代和老生代)可以針對(duì)新老對(duì)象的特點(diǎn)使用不同的回收算法,同時(shí)在回收新對(duì)象的時(shí)候不用遍歷老對(duì)象,從而提高垃圾回收的效率。
在Hotspot JVM中,它進(jìn)一步地將新生代分成了三個(gè)區(qū)域,一個(gè)稍大的區(qū)域Eden和兩個(gè)較小但大小相等的Survivor區(qū)域(分別叫做From和To)。一般來講,新對(duì)象首先分配在Eden區(qū)域,當(dāng)Eden區(qū)域滿的時(shí)候,會(huì)執(zhí)行一次Minor GC。MinorGC使用的是標(biāo)記-復(fù)制算法。垃圾回收器會(huì)首先標(biāo)記Eden和From區(qū)域中還存活的對(duì)象,然后把它們?nèi)恳苿?dòng)到To區(qū)域,這樣Eden和From區(qū)域的空間就可以全部回收了,最后再將指向From和To區(qū)域的指針交換一下。
下圖展示了MinorGC的流程,綠色區(qū)域表示空閑空間,紅色表示活動(dòng)對(duì)象,黃色表示可以回收的對(duì)象。
簡(jiǎn)要總結(jié)一下,對(duì)象在新生代的生命周期是,它首先在Eden區(qū)域誕生,如果對(duì)象在MinorGC時(shí)還存活的話,就移動(dòng)到Survivor區(qū)域。在后續(xù)的MinorGC的時(shí)候,如果對(duì)象還繼續(xù)存活的話,就在兩個(gè)Survivor區(qū)域?qū)⒌跪v。那對(duì)象什么時(shí)候會(huì)被移動(dòng)到老生代呢?有以下條件:
1、Survivor區(qū)域中存活對(duì)象占用Survivor空間達(dá)到了指定的閾值。2、對(duì)象在Survivor空間每倒騰一次其年齡就加1,如果一個(gè)對(duì)象的年齡達(dá)到了一個(gè)閾值,也會(huì)被移動(dòng)到老生代。3、大對(duì)象會(huì)在創(chuàng)建的時(shí)候就會(huì)被直接放到老生代。由此可見,新生代的空間大小很重要:如果新生代空間過小,就會(huì)導(dǎo)致對(duì)象很快就被移動(dòng)到老生代,從而使得某些原本可以及時(shí)回收的對(duì)象存活的時(shí)間過長(zhǎng),而且老生代回收的代價(jià)更大。那相反,如果新生代空間過大,就會(huì)使得某些存活時(shí)間長(zhǎng)的對(duì)象在新生代倒騰了很多次,影響到新生代回收垃圾的效率。這就需要根據(jù)應(yīng)用的特點(diǎn),找到一個(gè)合適的值。Hotspot提供了如下一些選項(xiàng)來調(diào)節(jié)新生代的參數(shù):
-XX:NewSize和-XX:MaxNewSize分別用來設(shè)置新生代的最小和最大值。需要注意的是,新生代是JVM堆的一部分,新生代的空間大小不能大于老生代的大小,因?yàn)樵跇O端的情況下,新生代中對(duì)象可能會(huì)被全部移到老生代,因此-XX:MaxNewSize最大只能設(shè)為-Xmx的一半。****-XX:NewRatio用來設(shè)置老生代和新生代大小的比例,比如-XX:NewRatio=2表示1/3的Heap是新生代,2/3的Heap是老生代。使用這個(gè)選項(xiàng)的好處是新生代的大小也能隨著Heap的變化而變化。
- -XX:SurvivorRatio用來設(shè)置新生代中Eden和Survivor空間大小的比例,需要注意的是有兩個(gè)Survivor。比如-XX:SurvivorRatio=8表示Eden區(qū)域在新生代的8/10,兩個(gè)Survivor分別占1/10。調(diào)節(jié)Survivor空間的時(shí)候也注意要折中,如果Survivor空間小的話,那么很有可能在一次MinorGC的時(shí)候Survivor空間就滿了,從而對(duì)象就被移到了老生代;如果Survivor空間大的話,那么Eden區(qū)域就小了,從而導(dǎo)致MinorGC的發(fā)生得更頻繁。總得來說,調(diào)節(jié)新生代的目標(biāo)是:1)避免對(duì)象過早地被移到了老生代 2)也要避免需要長(zhǎng)期存活的對(duì)象在新生代呆的時(shí)間過長(zhǎng),這會(huì)提高M(jìn)inorGC發(fā)生的頻率以及增加單次MinorGC的時(shí)間。這需要針對(duì)程序的運(yùn)行情況做一些分析。接下來就介紹了一個(gè)參數(shù)來分析新生代對(duì)象年齡的分布。
-XX:+PrintTenuringDistribution
-XX:+PrintTenuringDistribution讓JVM在每次MinorGC后打印出Survivor空間中的對(duì)象的年齡分布。比如:
Desired survivor size 75497472 bytes, new threshold 15 (max 15)
age 1: 19321624 bytes, 19321624 total
age 2: 79376 bytes, 19401000 total
age 3: 2904256 bytes, 22305256 total從第一行中可以看出JVM期望的Survivor空間占用為72M,對(duì)象被移到老年代中的年齡閾值為15。其中期望的Survivor空間大小為Survivor空間大小 x -XX:TargetSurvivorRatio的值。
接下來的一行,表示年齡為1的對(duì)象約19M,年齡為2的對(duì)象約79k,年齡為3的對(duì)象約為2.9M,每行后面的數(shù)值表示所有小于等于該行年齡的對(duì)象的總共大小,比如最后一行就表示所有年齡小于等于3的對(duì)象的總共大小為約22M(等于所有年齡對(duì)象大小的和)。因?yàn)槟壳癝urvivor空間中對(duì)象的大小22M小于期望Survivor空間的大小72M,所以沒有對(duì)象會(huì)被移到老年代。
假設(shè)下一次MinorGC后的輸出結(jié)果為:
Desired survivor size 75497472 bytes, new threshold 2 (max 15)
age 1: 68407384 bytes, 68407384 total
age 2: 12494576 bytes, 80901960 total
age 3: 79376 bytes, 80981336 total
age 4: 2904256 bytes, 83885592 total上次MinorGC后還存活的對(duì)象在這次MinorGC年齡都增加了1,可以看到上次年齡為2和3的對(duì)象(對(duì)應(yīng)在這次GC后的年齡為3和4)依然存在(大小未變),而一部分上次對(duì)象年齡為1的對(duì)象在這次GC時(shí)被回收了。同時(shí)可以看到這次新增了約68M的新對(duì)象。這次MinorGC后Survivor區(qū)域中對(duì)象總的大小為約83M,大于了期望的Survivor空間的大小72M,因此它就把對(duì)象移到老年代的年齡的閾值調(diào)整為2,在下次MinorGC時(shí)一部分對(duì)象就會(huì)被移到老年代了。(動(dòng)態(tài)對(duì)象年齡判斷:如果在Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,那么年齡大于或等于該年齡的對(duì)象就直接進(jìn)行老年代,無須等到MaxTenuringThreshold中要求的年齡。)
相關(guān)的調(diào)整選項(xiàng)有:
-XX:InitialTenuringThreshold 表示對(duì)象被移到老年代的年齡閾值的初始值****-XX:MaxTenuringThreshold 表示對(duì)象被移到老年代的年齡閾值的最大值****-XX:TargetSurvivorRatio 表示MinorGC結(jié)束了Survivor區(qū)域中占用空間的期望比例。這些參數(shù)的調(diào)節(jié)沒有統(tǒng)一的標(biāo)準(zhǔn),但是有兩點(diǎn)可以借鑒:
如果Survivor中對(duì)象的年齡分布顯示很多對(duì)象在經(jīng)歷了多次GC最終年齡達(dá)到了-XX:MaxTenuringThreshold(年齡閾值的最大值)才被移到老年代,這可能說明-XX:MaxTenuringThreshold設(shè)置得過大,也有可能是Survivor的空間過大。如果-XX:MaxTenuringThreshold的值大于1,但是很多對(duì)象年齡都不大于1,那就得關(guān)注一下期望的Survivor空間。如果每次GC后Survivor中對(duì)象的大小都沒有超過期望的Survivor空間大小,則說明GC工作得很好。反之,則說明可能Survivor空間小了,使得新生成的對(duì)象很快就被移到了老年代了。
吞吐量?jī)?yōu)先收集器的相關(guān)選項(xiàng)
衡量JVM垃圾收集器的兩個(gè)基本指標(biāo)是吞吐量和停頓時(shí)間。吞吐量是指執(zhí)行用戶代碼的時(shí)間占總的時(shí)間的比例,總的時(shí)間包括執(zhí)行用戶代碼的時(shí)間和垃圾回收占用的時(shí)間。在垃圾回收的時(shí)候執(zhí)行用戶代碼的線程必須暫停,這會(huì)導(dǎo)致程序暫時(shí)失去響應(yīng)。停頓時(shí)間就是衡量垃圾回收時(shí)造成的用戶線程暫停的時(shí)間。這兩個(gè)指標(biāo)是在一定程度是相互矛盾的,不可能讓一個(gè)程序的吞吐量很高的同時(shí)停頓時(shí)間也短,只能以優(yōu)先選擇一個(gè)目標(biāo)或者折中一下。因此,不同的垃圾回收器會(huì)有不同的側(cè)重點(diǎn)。
在Hotspot JVM中,側(cè)重于吞吐量的垃圾回收器是Parallel Scavenge,它的相關(guān)選項(xiàng)如下:
-XX:+UseParallelOldGC 表示新生代和老生代都使用并行回收器,其中的Old表示老年代的意思,而不是舊的意思。****-XX:ParallelGCThreads=n 表示配置多少個(gè)線程來回收垃圾。默認(rèn)的配置是如果處理器的個(gè)數(shù)小于8,那么就是處理器的個(gè)數(shù);如果處理器大于8,它的值就是3+5N/8。也可以根據(jù)程序的需要去設(shè)置這個(gè)值,比如你的機(jī)器有16核,上面有4個(gè)Java程序,那么設(shè)置將這個(gè)值設(shè)置為4比較合理,因?yàn)镴VM不會(huì)去探測(cè)同一機(jī)器上有多少個(gè)Java程序。-XX:UseAdaptiveSizePolicy 表示是否開啟自適應(yīng)策略,打開這個(gè)開關(guān)后,JVM自動(dòng)調(diào)節(jié)JVM的新生代大小,Eden和Survivor的比例等參數(shù)。用戶只需要設(shè)置期望的吞吐量(-XX:GCTimeRatio)和期望的停頓時(shí)間(-XX:MaxGCPauseMillis)。然后,JVM會(huì)盡量去向用戶期望的方向去優(yōu)化。此外,如果機(jī)器只有一個(gè)核的話,采用并行回收器可能得不償失,因?yàn)?strong>多個(gè)回收線程會(huì)爭(zhēng)搶CPU資源,反而造成更大的消耗。這時(shí),就最好采用串行回收器,相關(guān)的參數(shù)是-XX:+UseSerialGC
CMS收集器
CMS收集器(ConcurrentMarkandSweep),是一個(gè)關(guān)注系統(tǒng)停頓時(shí)間的收集器。它的主要思想是把收集器分成了不同的階段,其中某些階段是可以用戶程序并行的,從而減少了整體的系統(tǒng)停頓時(shí)間。它主要分成了以下幾個(gè)階段:
初始標(biāo)記 initial mark并發(fā)標(biāo)記 concurrent mark重新標(biāo)記 remark并發(fā)清理 concurrent clean并發(fā)重置 concurrent reset凡是名字以并發(fā)開頭的階段都是可以和用戶線程并行的,其他階段也是要暫停用戶程序線程。CMS雖然能減少系統(tǒng)的停頓時(shí)間,但是它也有其缺點(diǎn):
從它的名字可以看出,它是一個(gè)標(biāo)記-清除收集器,也就說運(yùn)行了一段時(shí)間后,內(nèi)存會(huì)產(chǎn)生碎片,從而導(dǎo)致無法找到連續(xù)空間來分配大對(duì)象。CMS收集器在運(yùn)行過程中會(huì)占用一些內(nèi)存,同時(shí)系統(tǒng)還在運(yùn)行,如果系統(tǒng)產(chǎn)生新對(duì)象的速度比CMS清理的速度快的話,會(huì)導(dǎo)致CMS運(yùn)行失敗。當(dāng)上面的任何一種情況發(fā)生的時(shí)候,JVM就會(huì)觸發(fā)一次Full GC,會(huì)導(dǎo)致JVM停頓較長(zhǎng)時(shí)間。
它的相關(guān)選項(xiàng)如下:
-XX:+UseConcMarkSweepGC 表示老年代開啟CMS收集器,而新生代默認(rèn)會(huì)使用并行收集器。****-XX:ConcGCThreads 指定用多少個(gè)線程來執(zhí)行CMS的并發(fā)階段。****-XX:CMSInitiatingOccupancyFraction 指定在老生代用掉多少內(nèi)存后開始進(jìn)行垃圾回收。與吞吐量?jī)?yōu)先的回收器不同的是,吞吐量?jī)?yōu)先的回收器在老生代內(nèi)存用盡了以后才開始進(jìn)行收集,這對(duì)CMS來講是不行的,因?yàn)橥掏铝績(jī)?yōu)先的垃圾回收器運(yùn)行的時(shí)候會(huì)停止所有用戶線程,所以不會(huì)產(chǎn)生新的對(duì)象,而CMS運(yùn)行的時(shí)候,用戶線程還有可能產(chǎn)生新的對(duì)象,所以不能等到內(nèi)存用光后才開始運(yùn)行。比如-XX:CMSInitiatingOccupancyFraction=75表示老生代用掉75%后開始回收垃圾。默認(rèn)值是68。****-XX:+ExplicitGCInvokesConcurrent 如果在代碼里面顯式調(diào)用System.gc(),那么它還是會(huì)執(zhí)行Full GC從而導(dǎo)致用戶線程被暫停。采用這個(gè)選項(xiàng)使得顯式觸發(fā)GC的時(shí)候還是使用CMS收集器。****-XX:+DisableExplicitGC 一個(gè)相關(guān)的選項(xiàng),這個(gè)選項(xiàng)是禁止顯式調(diào)用GC
GC日志相關(guān)的選項(xiàng)
分析GC問題不可避免地要查看GC日志,下面是一些GC日志相關(guān)的選項(xiàng):
-XX:+PrintGC,等同于-verbose:gc 表示打開簡(jiǎn)化的GC日志,相關(guān)輸出如下:
<pre class="md-fences md-end-block" lang="" contenteditable="false" cid="n3132" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
[GC 425355K->351685K(506816K), 0.2175300 secs]
?
[Full GC 500561K->456058K(506816K), 0.6421920 secs]</pre>
其中以GC開頭的行表示發(fā)生了一次Minor GC,后面的數(shù)字表示收集前后Heap空間的占用量,圓括號(hào)里面表示Heap大小,最后的數(shù)字表示用了多少時(shí)間。比如:上面的例子中,表示在這次GC新生代空間占用從425355K降到了351685K,總的新生代空間為506816K,這次GC耗時(shí)0.22秒。通過這個(gè)選項(xiàng)只能看到一些基本信息,而且所有收集器的輸出在這個(gè)模式下都是一樣的。
-XX:+PrintGCDetails 這個(gè)選項(xiàng)會(huì)打印出更多的GC日志,不同的收集器產(chǎn)生的日志會(huì)不一樣。因此,在后續(xù)的文章中再介紹不同收集器的日志格式。-XX:+PrintGCTimeStamps and -XX:+PrintGCDateStamps 這兩個(gè)選項(xiàng)把GC的時(shí)間戳顯示在GC的日志中。
其中,-XX:+PrintGCTimeStamps打印GC發(fā)生的時(shí)間相對(duì)于JVM啟動(dòng)的時(shí)間,-XX:+PrintGCDateStamps表示打印出GC發(fā)生的具體時(shí)間。比如,以下是-XX:+PrintGCTimeStamps的輸出
<pre class="md-fences md-end-block" lang="" contenteditable="false" cid="n3142" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">0,185: [GC 66048K->53077K(251392K), 0,0977580 secs]
?
0,323: [GC 119125K->114661K(317440K), 0,1448850 secs]
?
0,603: [GC 246757K->243133K(375296K), 0,2860800 secs]
?</pre>
以下是-XX:+PrintGCDateStamps打開后的輸出
<pre class="md-fences md-end-block" lang="" contenteditable="false" cid="n3147" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Consolas, "Liberation Mono", Courier, monospace; font-size: 0.9em; white-space: pre; text-align: left; break-inside: avoid; display: block; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(221, 221, 221); border-radius: 3px; padding: 8px 1em 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
2014-12-26T17:52:38.613-0800: 3.395: [GC 139776K->58339K(506816K), 0.1442900 secs]</pre>
-Xloggc:<file> 表示把GC日志寫入到一個(gè)文件中去,而不是打印到標(biāo)準(zhǔn)輸出中。需要注意的是:這些和GC日志相關(guān)的選項(xiàng)可以在JVM已經(jīng)啟動(dòng)后再開啟,可以通過jinfo這個(gè)工具去設(shè)置。具體可以參見jinfo的幫助文件。這樣就可以在需要診斷問題的時(shí)候再開啟GC日志。
疑問:
Q: Xint 中int的詳細(xì)名詞是什么?有沒有所有參賽詳細(xì)名詞的全稱,方便記憶?
Q: -showversion的作用是在運(yùn)行一個(gè)程序的時(shí)候首先把JVM的版本信息打印出來,這樣便于問題診斷。
-showversion是配置在哪里,怎么使用?
Q: -XX:+PrintGC,等同于-verbose:gc 表示打開簡(jiǎn)化的GC日志。這個(gè)怎么使用,放在配置參數(shù)上?
jvm新生代中為什么要有Survivor區(qū),且必須是2個(gè)
一、為什么會(huì)有年輕代
我們先來屢屢,為什么需要把堆分代?不分代不能完成他所做的事情么?其實(shí)不分代完全可以,分代的唯一理由就是優(yōu)化GC性能。你先想想,如果沒有分代,那我們所有的對(duì)象都在一塊,GC的時(shí)候我們要找到哪些對(duì)象沒用,這樣就會(huì)對(duì)堆的所有區(qū)域進(jìn)行掃描。而我們的很多對(duì)象都是朝生夕死的,如果分代的話,我們把新創(chuàng)建的對(duì)象放到某一地方,當(dāng)GC的時(shí)候先把這塊存“朝生夕死”對(duì)象的區(qū)域進(jìn)行回收,這樣就會(huì)騰出很大的空間出來。
二、年輕代中的GC
新生代大小=eden大小+1個(gè)survivor大小
新生代大小(PSYoungGen total 9216K)=eden大小(eden space 8192K)+1個(gè)survivor大小(from space 1024K)
HotSpot JVM把年輕代分為了三部分:1個(gè)Eden區(qū)和2個(gè)Survivor區(qū)(分別叫from和to)。默認(rèn)比例為8(Eden):1(一個(gè)survivor),為啥默認(rèn)會(huì)是這個(gè)比例,接下來我們會(huì)聊到。一般情況下,新創(chuàng)建的對(duì)象都會(huì)被分配到Eden區(qū)(一些大對(duì)象特殊處理),這些對(duì)象經(jīng)過第一次Minor GC后,如果仍然存活,將會(huì)被移到Survivor區(qū)。對(duì)象在Survivor區(qū)中每熬過一次Minor GC,年齡就會(huì)增加1歲,當(dāng)它的年齡增加到一定程度時(shí),就會(huì)被移動(dòng)到年老代中。因?yàn)槟贻p代中的對(duì)象基本都是朝生夕死的(90%以上),所以在年輕代的垃圾回收算法使用的是復(fù)制算法,復(fù)制算法的基本思想就是將內(nèi)存分為兩塊,每次只用其中一塊,當(dāng)這一塊內(nèi)存用完,就將還活著的對(duì)象復(fù)制到另外一塊上面。復(fù)制算法不會(huì)產(chǎn)生內(nèi)存碎片,因?yàn)樾律鷮?duì)象大部分是朝生夕死的,復(fù)制的對(duì)象很少,效率高。在GC開始的時(shí)候,對(duì)象只會(huì)存在于Eden區(qū)和名為“From”的Survivor區(qū),Survivor區(qū)“To”是空的。緊接著進(jìn)行GC,Eden區(qū)中所有存活的對(duì)象都會(huì)被復(fù)制到“To”,而在“From”區(qū)中,仍存活的對(duì)象會(huì)根據(jù)他們的年齡值來決定去向。年齡達(dá)到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設(shè)置)的對(duì)象會(huì)被移動(dòng)到年老代中,沒有達(dá)到閾值的對(duì)象會(huì)被復(fù)制到“To”區(qū)域。經(jīng)過這次GC后,Eden區(qū)和From區(qū)已經(jīng)被清空。這個(gè)時(shí)候,“From”和“To”會(huì)交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會(huì)保證名為To的Survivor區(qū)域是空的。Minor GC會(huì)一直重復(fù)這樣的過程,直到“To”區(qū)被填滿,“To”區(qū)被填滿之后,會(huì)將所有對(duì)象移動(dòng)到年老代中。
三、一個(gè)對(duì)象的這一輩子
我是一個(gè)普通的Java對(duì)象,我出生在Eden區(qū),在Eden區(qū)我還看到和我長(zhǎng)的很像的小兄弟,我們?cè)贓den區(qū)中玩了挺長(zhǎng)時(shí)間。有一天Eden區(qū)中的人實(shí)在是太多了,我就被迫去了Survivor區(qū)的“From”區(qū),自從去了Survivor區(qū),我就開始漂了,有時(shí)候在Survivor的“From”區(qū),有時(shí)候在Survivor的“To”區(qū),居無定所。直到我15歲的時(shí)候,爸爸說我成人了,該去社會(huì)上闖闖了。于是我就去了年老代那邊,年老代里,人很多,并且年齡都挺大的,我在這里也認(rèn)識(shí)了很多人。在年老代里,我生活了20年(每次GC加一歲),然后被回收。
四、為什么要有Survivor區(qū)
先不去想為什么有兩個(gè)Survivor區(qū),第一個(gè)問題是,設(shè)置Survivor區(qū)的意義在哪里?如果沒有Survivor,Eden區(qū)每進(jìn)行一次Minor GC,存活的對(duì)象就會(huì)被送到老年代。老年代很快被填滿,觸發(fā)Major GC(因?yàn)镸ajor GC一般伴隨著Minor GC,也可以看做觸發(fā)了Full GC)。老年代的內(nèi)存空間遠(yuǎn)大于新生代,進(jìn)行一次Full GC消耗的時(shí)間比Minor GC長(zhǎng)得多。你也許會(huì)問,執(zhí)行時(shí)間長(zhǎng)有什么壞處?頻發(fā)的Full GC消耗的時(shí)間是非常可觀的,這一點(diǎn)會(huì)影響大型程序的執(zhí)行和響應(yīng)速度,更不要說某些連接會(huì)因?yàn)槌瑫r(shí)發(fā)生連接錯(cuò)誤了。
好,那我們來想想在沒有Survivor的情況下,有沒有什么解決辦法,可以避免上述情況:
方案 | 優(yōu)點(diǎn) | 缺點(diǎn) |
---|---|---|
增加老年代空間 | 更多存活對(duì)象才能填滿老年代。降低Full GC頻率 | 隨著老年代空間加大,一旦發(fā)生Full GC,執(zhí)行所需要的時(shí)間更長(zhǎng) |
減少老年代空間 | Full GC所需時(shí)間減少 | 老年代很快被存活對(duì)象填滿,F(xiàn)ull GC頻率增加 |
顯而易見,沒有Survivor的話,上述兩種解決方案都不能從根本上解決問題。
我們可以得到第一條結(jié)論:Survivor的存在意義,就是減少被送到老年代的對(duì)象,進(jìn)而減少Full GC的發(fā)生,Survivor的預(yù)篩選保證,只有經(jīng)歷15次Minor GC還能在新生代中存活的對(duì)象,才會(huì)被送到老年代。
五、為什么要設(shè)置兩個(gè)Survivor區(qū)
設(shè)置兩個(gè)Survivor區(qū)最大的好處就是解決了碎片化,下面我們來分析一下。
為什么一個(gè)Survivor區(qū)不行?第一部分中,我們知道了必須設(shè)置Survivor區(qū)。假設(shè)現(xiàn)在只有一個(gè)survivor區(qū),我們來模擬一下流程:剛剛新建的對(duì)象在Eden中,一旦Eden滿了,觸發(fā)一次Minor GC,Eden中的存活對(duì)象就會(huì)被移動(dòng)到Survivor區(qū)。這樣繼續(xù)循環(huán)下去,下一次Eden滿了的時(shí)候,問題來了,此時(shí)進(jìn)行Minor GC,Eden和Survivor各有一些存活對(duì)象,如果此時(shí)把Eden區(qū)的存活對(duì)象硬放到Survivor區(qū),很明顯這兩部分對(duì)象所占有的內(nèi)存是不連續(xù)的,也就導(dǎo)致了內(nèi)存碎片化。我繪制了一幅圖來表明這個(gè)過程。其中色塊代表對(duì)象,白色框分別代表Eden區(qū)(大)和Survivor區(qū)(小)。Eden區(qū)理所當(dāng)然大一些,否則新建對(duì)象很快就導(dǎo)致Eden區(qū)滿,進(jìn)而觸發(fā)Minor GC,有悖于初衷。
碎片化帶來的風(fēng)險(xiǎn)是極大的,嚴(yán)重影響Java程序的性能。堆空間被散布的對(duì)象占據(jù)不連續(xù)的內(nèi)存,最直接的結(jié)果就是,堆中沒有足夠大的連續(xù)內(nèi)存空間,接下去如果程序需要給一個(gè)內(nèi)存需求很大的對(duì)象分配內(nèi)存。。。畫面太美不敢看。。。這就好比我們爬山的時(shí)候,背包里所有東西緊挨著放,最后就可能省出一塊完整的空間放相機(jī)。如果每件行李之間隔一點(diǎn)空隙亂放,很可能最后就要一路把相機(jī)掛在脖子上了。
那么,順理成章的,應(yīng)該建立兩塊Survivor區(qū),剛剛新建的對(duì)象在Eden中,經(jīng)歷一次Minor GC,Eden中的存活對(duì)象就會(huì)被移動(dòng)到第一塊survivor space S0,Eden被清空;等Eden區(qū)再滿了,就再觸發(fā)一次Minor GC,Eden和S0中的存活對(duì)象又會(huì)被復(fù)制送入第二塊survivor space S1(這個(gè)過程非常重要,因?yàn)檫@種復(fù)制算法保證了S1中來自S0和Eden兩部分的存活對(duì)象占用連續(xù)的內(nèi)存空間,避免了碎片化的發(fā)生)。S0和Eden被清空,然后下一輪S0與S1交換角色,如此循環(huán)往復(fù)。如果對(duì)象的復(fù)制次數(shù)達(dá)到15次,該對(duì)象就會(huì)被送到老年代中。下圖中每部分的意義和上一張圖一樣,就不加注釋了。
上述機(jī)制最大的好處就是,整個(gè)過程中,永遠(yuǎn)有一個(gè)survivor space是空的,另一個(gè)非空的survivor space無碎片。
那么,Survivor為什么不分更多塊呢?比方說分成三個(gè)、四個(gè)、五個(gè)?顯然,如果Survivor區(qū)再細(xì)分下去,每一塊的空間就會(huì)比較小,很容易導(dǎo)致Survivor區(qū)滿,因此,我認(rèn)為兩塊Survivor區(qū)是經(jīng)過權(quán)衡之后的最佳方案。