這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡上收集的一些資料的整理褒纲,因此不免有一些不準確的地方代承,同時不同JDK版本的差異也比較大。
不過文中一些JVM參數(shù)示例都是實際項目里調(diào)優(yōu)的結果,還是經(jīng)受過實戰(zhàn)考驗的。
目錄
- JVM簡介
- JVM結構
2.1 方法區(qū)
2.1.1 常量池
2.1.1.1 Class文件中的常量池
2.1.1.2 運行時常量池
2.1.1.3 常量池的好處
2.1.1.4 基本類型的包裝類和常量池
2.2 堆
2.3 Java棧
2.3.1 棧幀
2.3.1.1 局部變量區(qū)
2.3.1.2 操作數(shù)棧
2.3.1.3 棧數(shù)據(jù)區(qū)
2.4 本地方法棧
2.5 PC寄存器
2.6 堆與棧
2.6.1 堆與棧里存什么
2.6.2 堆內(nèi)存與棧內(nèi)存的區(qū)別 - JIT編譯器
- 類加載機制
4.1 類加載的時機
4.2 類加載過程 - 垃圾回收
5.1 按代實現(xiàn)垃圾回收
5.2 怎樣判斷對象是否已經(jīng)死亡
5.3 java中的引用
5.4 finalize方法什么作用
5.5 垃圾收集算法
5.6 Hotspot實現(xiàn)垃圾回收細節(jié)
5.7 垃圾收集器
5.7.1 Serial收集器
5.7.2 ParNew收集器
5.7.3 Parallel Scavenge收集器
5.7.4 Serial Old收集器
5.7.5 Parallel Old收集器
5.7.6 CMS收集器
5.7.7 G1收集器 - JVM參數(shù)
6.1 典型配置
6.1.1 堆大小設置
6.1.2 回收器選擇
6.1.3 輔助信息
6.2 參數(shù)詳細說明 - JVM性能調(diào)優(yōu)
7.1 堆設置調(diào)優(yōu)
7.2 GC策略調(diào)優(yōu)
7.3 JIT調(diào)優(yōu)
7.4 JVM線程調(diào)優(yōu)
7.5 典型案例 - 常見問題
8.1 內(nèi)存泄漏及解決方法
8.2 年老代堆空間被占滿
8.3 持久代被占滿
8.4 堆棧溢出
8.5 線程堆棧滿
8.6 系統(tǒng)內(nèi)存被占滿
1.JVM簡介
JVM是java的核心和基礎具则,在java編譯器和os平臺之間的虛擬處理器蜂厅。它是一種利用軟件方法實現(xiàn)的抽象的計算機基于下層的操作系統(tǒng)和硬件平臺术奖,可以在上面執(zhí)行java的字節(jié)碼程序政勃。
java編譯器只要面向JVM讽挟,生成JVM能理解的代碼或字節(jié)碼文件。Java源文件經(jīng)編譯成字節(jié)碼程序,通過JVM將每一條指令翻譯成不同平臺機器碼,通過特定平臺運行。
運行過程
Java語言寫的源程序通過Java編譯器绍弟,編譯成與平臺無關的‘字節(jié)碼程序’(.class文件郭脂,也就是0埃难,1二進制程序),然后在OS之上的Java解釋器中解釋執(zhí)行。
C++以及Fortran這類編譯型語言都會通過一個靜態(tài)的編譯器將程序編譯成CPU相關的二進制代碼贫途。
PHP以及Perl這列語言則是解釋型語言,只需要安裝正確的解釋器,它們就能運行在任何CPU之上盛险。當程序被執(zhí)行的時候,程序代碼會被逐行解釋并執(zhí)行。
- 編譯型語言的優(yōu)缺點:
- 速度快:因為在編譯的時候它們能夠獲取到更多的有關程序結構的信息,從而有機會對它們進行優(yōu)化。
- 適用性差:它們編譯得到的二進制代碼往往是CPU相關的,在需要適配多種CPU時,可能需要編譯多次谆趾。
- 解釋型語言的優(yōu)缺點:
- 適應性強:只需要安裝正確的解釋器来候,程序在任何CPU上都能夠被運行
- 速度慢:因為程序需要被逐行翻譯转质,導致速度變慢。同時因為缺乏編譯這一過程盈魁,執(zhí)行代碼不能通過編譯器進行優(yōu)化飘痛。
- Java的做法是找到編譯型語言和解釋性語言的一個中間點:
- Java代碼會被編譯:被編譯成Java字節(jié)碼脖旱,而不是針對某種CPU的二進制代碼践险。
- Java代碼會被解釋:Java字節(jié)碼需要被java程序解釋執(zhí)行占遥,此時尤揣,Java字節(jié)碼被翻譯成CPU相關的二進制代碼嗜愈。
- JIT編譯器的作用:在程序運行期間拌阴,將Java字節(jié)碼編譯成平臺相關的二進制代碼。正因為此編譯行為發(fā)生在程序運行期間,所以該編譯器被稱為Just-In-Time編譯器欠痴。
2.JVM結構
java是基于一門虛擬機的語言迄靠,所以了解并且熟知虛擬機運行原理非常重要。
2.1 方法區(qū)
方法區(qū)掌挚,Method Area, 對于習慣在HotSpot虛擬機上開發(fā)和部署程序的開發(fā)者來說菩咨,很多人愿意把方法區(qū)稱為“永久代”(Permanent Generation)吠式,本質(zhì)上兩者并不等價,僅僅是因為HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區(qū)抽米,或者說使用永久代來實現(xiàn)方法區(qū)而已特占。對于其他虛擬機(如BEA JRockit、IBM J9等)來說是不存在永久代的概念的云茸。
主要存放已被虛擬機加載的類信息是目、常量、靜態(tài)變量查辩、即時編譯器編譯后的代碼等數(shù)據(jù)(比如spring 使用IOC或者AOP創(chuàng)建bean時胖笛,或者使用cglib网持,反射的形式動態(tài)生成class信息等)宜岛。
注意:JDK 6 時,String等字符串常量的信息是置于方法區(qū)中的功舀,但是到了JDK 7 時萍倡,已經(jīng)移動到了Java堆。所以辟汰,方法區(qū)也好列敲,Java堆也罷,到底詳細的保存了什么帖汞,其實沒有具體定論戴而,要結合不同的JVM版本來分析。
異常
當方法區(qū)無法滿足內(nèi)存分配需求時翩蘸,將拋出OutOfMemoryError所意。
運行時常量池溢出:比如一直往常量池加入數(shù)據(jù),就會引起OutOfMemoryError異常。
類信息
- 類型全限定名扶踊。
- 類型的直接超類的全限定名(除非這個類型是java.lang.Object泄鹏,它沒有超類)。
- 類型是類類型還是接口類型秧耗。
- 類型的訪問修飾符(public备籽、abstract或final的某個子集)。
- 任何直接超接口的全限定名的有序列表分井。
- 類型的常量池车猬。
- 字段信息。
- 方法信息杂抽。
- 除了常量意外的所有類(靜態(tài))變量诈唬。
- 一個到類ClassLoader的引用。
- 一個到Class類的引用缩麸。
2.1.1 常量池
2.1.1.1 Class文件中的常量池
在Class文件結構中铸磅,最頭的4個字節(jié)用于存儲Megic Number,用于確定一個文件是否能被JVM接受杭朱,再接著4個字節(jié)用于存儲版本號阅仔,前2個字節(jié)存儲次版本號,后2個存儲主版本號弧械,再接著是用于存放常量的常量池八酒,由于常量的數(shù)量是不固定的,所以常量池的入口放置一個U2類型的數(shù)據(jù)(constant_pool_count)存儲常量池容量計數(shù)值刃唐。
常量池主要用于存放兩大類常量:字面量(Literal)和符號引用量(Symbolic References)羞迷,字面量相當于Java語言層面常量的概念,如文本字符串画饥,聲明為final的常量值等衔瓮,符號引用則屬于編譯原理方面的概念,包括了如下三種類型的常量:
- 類和接口的全限定名
- 字段名稱和描述符
- 方法名稱和描述符
2.1.1.2 運行時常量池
CLass文件中除了有類的版本抖甘、字段热鞍、方法、接口等描述信息外衔彻,還有一項信息是常量池薇宠,用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放艰额。
運行時常量池相對于CLass文件常量池的另外一個重要特征是具備動態(tài)性澄港,Java語言并不要求常量一定只有編譯期才能產(chǎn)生,也就是并非預置入CLass文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池柄沮,運行期間也可能將新的常量放入池中回梧,這種特性被開發(fā)人員利用比較多的就是String類的intern()方法逐工。
2.1.1.3 常量池的好處
常量池是為了避免頻繁的創(chuàng)建和銷毀對象而影響系統(tǒng)性能,其實現(xiàn)了對象的共享漂辐。
例如字符串常量池泪喊,在編譯階段就把所有的字符串文字放到一個常量池中。
- (1)節(jié)省內(nèi)存空間:常量池中所有相同的字符串常量被合并髓涯,只占用一個空間袒啼。
- (2)節(jié)省運行時間:比較字符串時,==比equals()快纬纪。對于兩個引用變量蚓再,只用==判斷引用是否相等,也就可以判斷實際值是否相等包各。
雙等號==的含義
- 基本數(shù)據(jù)類型之間應用雙等號摘仅,比較的是他們的數(shù)值。
- 復合數(shù)據(jù)類型(類)之間應用雙等號问畅,比較的是他們在內(nèi)存中的存放地址娃属。
2.1.1.4 基本類型的包裝類和常量池
java中基本類型的包裝類的大部分都實現(xiàn)了常量池技術,即Byte,Short,Integer,Long,Character,Boolean护姆。
這5種包裝類默認創(chuàng)建了數(shù)值[-128矾端,127]的相應類型的緩存數(shù)據(jù),但是超出此范圍仍然會去創(chuàng)建新的對象卵皂。 兩種浮點數(shù)類型的包裝類Float,Double并沒有實現(xiàn)常量池技術秩铆。
Integer與常量池
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));
System.out.println("i1=i4 " + (i1 == i4));
System.out.println("i4=i5 " + (i4 == i5));
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));
System.out.println("40=i5+i6 " + (40 == i5 + i6));
i1=i2 true
i1=i2+i3 true
i1=i4 false
i4=i5 false
i4=i5+i6 true
40=i5+i6 true
解釋:
- (1)Integer i1=40;Java在編譯的時候會直接將代碼封裝成Integer i1=Integer.valueOf(40);灯变,從而使用常量池中的對象殴玛。
- (2)Integer i1 = new Integer(40);這種情況下會創(chuàng)建新的對象。
- (3)語句i4 == i5 + i6添祸,因為+這個操作符不適用于Integer對象滚粟,首先i5和i6進行自動拆箱操作,進行數(shù)值相加膝捞,即i4 == 40坦刀。然后Integer對象無法與數(shù)值進行直接比較愧沟,所以i4自動拆箱轉(zhuǎn)為int值40蔬咬,最終這條語句轉(zhuǎn)為40 == 40進行數(shù)值比較。
String與常量池
String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);//false
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
System.out.println(str3 == str4);//false
String str5 = "string";
System.out.println(str3 == str5);//true
解釋:
- (1)new String("abcd")是在常量池中拿對象沐寺,"abcd"是直接在堆內(nèi)存空間創(chuàng)建一個新的對象林艘。只要使用new方法,便需要創(chuàng)建新的對象混坞。
- (2)連接表達式 +
只有使用引號包含文本的方式創(chuàng)建的String對象之間使用“+”連接產(chǎn)生的新對象才會被加入字符串池中狐援。
對于所有包含new方式新建對象(包括null)的“+”連接表達式钢坦,它所產(chǎn)生的新對象都不會被加入字符串池中。
public static final String A; // 常量A
public static final String B; // 常量B
static {
A = "ab";
B = "cd";
}
public static void main(String[] args) {
// 將兩個常量用+連接對s進行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t啥酱,它們是同一個對象");
} else {
System.out.println("s不等于t爹凹,它們不是同一個對象");
}
}
解釋:
s不等于t,它們不是同一個對象镶殷。
A和B雖然被定義為常量禾酱,但是它們都沒有馬上被賦值。在運算出s的值之前绘趋,他們何時被賦值颤陶,以及被賦予什么樣的值,都是個變數(shù)陷遮。因此A和B在被賦值之前滓走,性質(zhì)類似于一個變量。那么s就不能在編譯期被確定帽馋,而只能在運行時被創(chuàng)建了搅方。
String s1 = new String("xyz"); //創(chuàng)建了幾個對象?
解釋:
考慮類加載階段和實際執(zhí)行時绽族。
- (1)類加載對一個類只會進行一次腰懂。”xyz”在類加載時就已經(jīng)創(chuàng)建并駐留了(如果該類被加載之前已經(jīng)有”xyz”字符串被駐留過則不需要重復創(chuàng)建用于駐留的”xyz”實例)项秉。駐留的字符串是放在全局共享的字符串常量池中的绣溜。
- (2)在這段代碼后續(xù)被運行的時候,”xyz”字面量對應的String實例已經(jīng)固定了娄蔼,不會再被重復創(chuàng)建怖喻。所以這段代碼將常量池中的對象復制一份放到heap中涛目,并且把heap中的這個對象的引用交給s1 持有缩滨。
這條語句創(chuàng)建了2個對象匹厘。
public static void main(String[] args) {
String s1 = new String("計算機");
String s2 = s1.intern();
String s3 = "計算機";
System.out.println("s1 == s2? " + (s1 == s2));
System.out.println("s3 == s2? " + (s3 == s2));
}
s1 == s2? false
s3 == s2? true
解釋:
String的intern()方法會查找在常量池中是否存在一份equal相等的字符串,如果有則返回該字符串的引用,如果沒有則添加自己的字符串進入常量池凳兵。
public class Test {public static void main(String[] args) {
String hello = "Hello", lo = "lo";
System.out.println((hello == "Hello") + " "); //true
System.out.println((Other.hello == hello) + " "); //true
System.out.println((other.Other.hello == hello) + " "); //true
System.out.println((hello == ("Hel"+"lo")) + " "); //true
System.out.println((hello == ("Hel"+lo)) + " "); //false
System.out.println(hello == ("Hel"+lo).intern()); //true
}
}
class Other {
static String hello = "Hello";
}
package other;
public class Other {
public static String hello = "Hello";
}
解釋:
在同包同類下,引用自同一String對象.
在同包不同類下,引用自同一String對象.
在不同包不同類下,依然引用自同一String對象.
在編譯成.class時能夠識別為同一字符串的,自動優(yōu)化成常量,引用自同一String對象.
在運行時創(chuàng)建的字符串具有獨立的內(nèi)存地址,所以不引用自同一String對象.
2.2 堆
Heap(堆)是JVM的內(nèi)存數(shù)據(jù)區(qū)卷仑。
一個虛擬機實例只對應一個堆空間仗岸,堆是線程共享的茁瘦。堆空間是存放對象實例的地方涝焙,幾乎所有對象實例都在這里分配坠韩。堆也是垃圾收集器管理的主要區(qū)域(也被稱為GC堆)距潘。堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上相連就行只搁。
Heap 的管理很復雜音比,每次分配不定長的內(nèi)存空間,專門用來保存對象的實例氢惋。在Heap 中分配一定的內(nèi)存來保存對象實例洞翩,實際上也只是保存對象實例的屬性值稽犁,屬性的類型和對象本身的類型標記等,并不保存對象的方法(方法是指令骚亿,保存在Stack中)已亥。而對象實例在Heap中分配好以后,需要在Stack中保存一個4字節(jié)的Heap 內(nèi)存地址来屠,用來定位該對象實例在Heap 中的位置陷猫,便于找到該對象實例。
異常
堆中沒有足夠的內(nèi)存進行對象實例分配時的妖,并且堆也無法擴展時绣檬,會拋出OutOfMemoryError異常。
2.3 Java棧
Stack(棧)是JVM的內(nèi)存指令區(qū)嫂粟。
描述的是java方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會同時創(chuàng)建一個棧幀娇未,用于存放局部變量表(基本類型、對象引用)星虹、操作數(shù)棧零抬、方法返回、常量池指針等信息宽涌。 由編譯器自動分配釋放平夜, 內(nèi)存的分配是連續(xù)的。Stack的速度很快卸亮,管理很簡單忽妒,并且每次操作的數(shù)據(jù)或者指令字節(jié)長度是已知的。所以Java 基本數(shù)據(jù)類型兼贸,Java 指令代碼段直,常量都保存在Stack中。
虛擬機只會對棧進行兩種操作溶诞,以幀為單位的入棧和出棧鸯檬。Java棧中的每個幀都保存一個方法調(diào)用的局部變量、操作數(shù)棧螺垢、指向常量池的指針等喧务,且每一次方法調(diào)用都會創(chuàng)建一個幀,并壓棧枉圃。
異常
- 如果一個線程請求的棧深度大于虛擬機所允許的深度功茴,將拋出StackOverflowError異常, 比如遞歸調(diào)用讯蒲。
- 如果線程生成數(shù)量過多痊土,無法申請足夠多的內(nèi)存時肄扎,則會拋出OutOfMemoryError異常墨林。比如tomcat請求數(shù)量非常多時赁酝,設置最大請求數(shù)。
2.3.1 棧幀
棧幀由三部分組成:局部變量區(qū)旭等、操作數(shù)棧酌呆、幀數(shù)據(jù)區(qū)。
2.3.1.1 局部變量區(qū)
包含方法的參數(shù)和局部變量搔耕。
以一個靜態(tài)方法為例
public class Demo {
public static int doStaticMethod(int i, long l, float f, Object o, byte b) {
return 0;
}
}
編譯之后的具備變量表字節(jié)碼如下:
LOCALVARIABLEiIL0L10
LOCALVARIABLElJL0L11
LOCALVARIABLEfFL0L13
LOCALVARIABLEoLjava/lang/Object;L0L14
LOCALVARIABLEbBL0L15
MAXSTACK=1 //該方法操作棧的最大深度
MAXLOCALS=6 //確定了該方法所需要分配的最大局部變量表的容量
可以認為Java棧幀里的局部變量表有很多的槽位組成隙袁,每個槽最大可以容納32位的數(shù)據(jù)類型,故方法參數(shù)里的int i 參數(shù)占據(jù)了一個槽位弃榨,而long l 參數(shù)就占據(jù)了兩個槽(1和2)菩收,Object對象類型的參數(shù)其實是一個引用,o相當于一個指針鲸睛,也就是32位大小娜饵。byte類型升為int,也是32位大小官辈。如下:
0 int int i
1 long long l
3 float float f
4 reference Object o
5 int byte b
實例方法的局部變量表和靜態(tài)方法基本一樣箱舞,唯一區(qū)別就是實例方法在Java棧幀的局部變量表里第一個槽位(0位置)存的是一個this引用(當前對象的引用),后面就和靜態(tài)方法的一樣了拳亿。
2.3.1.2 操作數(shù)棧
Java沒有寄存器晴股,故所有參數(shù)傳遞使用Java棧幀里的操作數(shù)棧,操作數(shù)棧被組織成一個以字長為單位的數(shù)組肺魁,它是通過標準的棧操作-入棧和出棧來進行訪問电湘,而不是通過索引訪問。
看一個例子:
注意鹅经,對于局部變量表的槽位胡桨,按照從0開始的順序,依次是方法參數(shù)瞬雹,之后是方法內(nèi)的局部變量昧谊,局部變量0就是a,1就是b酗捌,2就是c…… 編譯之后的字節(jié)碼為:
// access flags 0x9
public static add(II)I
L0
LINENUMBER 18 L0 // 對應源代碼第18行呢诬,以此類推
ICONST_0 // 把常量0 push 到Java棧幀的操作數(shù)棧里
ISTORE 2 // 將0從操作數(shù)棧pop到局部變量表槽2里(c),完成賦值
L1
LINENUMBER 19 L1
ILOAD 0 // 將局部變量槽位0(a)push 到Java棧幀的操作數(shù)棧里
ILOAD 1 // 把局部變量槽1(b)push到操作數(shù)棧
IADD // pop出a和b兩個變量胖缤,求和尚镰,把結果push到操作數(shù)棧
ISTORE 2 // 把結果從操作數(shù)棧pop到局部變量2(a+b的和給c賦值)
L2
LINENUMBER 21 L2
ILOAD 2 // 局部變量2(c)push 到操作數(shù)棧
IRETURN // 返回結果
L3
LOCALVARIABLE a I L0 L3 0
LOCALVARIABLE b I L0 L3 1
LOCALVARIABLE c I L1 L3 2
MAXSTACK = 2
MAXLOCALS = 3
發(fā)現(xiàn),整個計算過程的參數(shù)傳遞和操作數(shù)棧密切相關哪廓!如圖:
2.3.1.3 棧數(shù)據(jù)區(qū)
存放一些用于支持常量池解析(常量池指針)狗唉、正常方法返回以及異常派發(fā)機制的信息。即將常量池的符號引用轉(zhuǎn)化為直接地址引用涡真、恢復發(fā)起調(diào)用的方法的幀進行正常返回分俯,發(fā)生異常時轉(zhuǎn)交異常表進行處理肾筐。
2.4 本地方法棧
Native Method Stack
訪問本地方式時使用到的棧,為本地方法服務缸剪, 也就是調(diào)用虛擬機使用到的Native方法服務吗铐。也會拋出StackOverflowError和OutOfMemoryError異常。
2.5 PC寄存器
每個線程都擁有一個PC寄存器杏节,線程私有的唬渗。
PC寄存器的內(nèi)容總是下一條將被執(zhí)行指令的"地址",這里的"地址"可以是一個本地指針奋渔,也可以是在方法字節(jié)碼中相對于該方法起始指令的偏移量镊逝。如果該線程正在執(zhí)行一個本地方法,則程序計數(shù)器內(nèi)容為undefined嫉鲸,區(qū)域在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域蹋半。
2.6 堆與棧
2.6.1 堆與棧里存什么
- 1)堆中存的是對象。棧中存的是基本數(shù)據(jù)類型和堆中對象的引用充坑。一個對象的大小是不可估計的减江,或者說是可以動態(tài)變化的,但是在棧中捻爷,一個對象只對應了一個4btye的引用辈灼。
- 2)為什么不把基本類型放堆中呢?因為其占用的空間一般是1~8個字節(jié)——需要空間比較少也榄,而且因為是基本類型巡莹,所以不會出現(xiàn)動態(tài)增長的情況——長度固定,因此棧中存儲就夠了甜紫,如果把他存在堆中是沒有什么意義的降宅。可以這么說囚霸,基本類型和對象的引用都是存放在棧中腰根,而且都是幾個字節(jié)的一個數(shù),因此在程序運行時拓型,他們的處理方式是統(tǒng)一的额嘿。但是基本類型、對象引用和對象本身就有所區(qū)別了劣挫,因為一個是棧中的數(shù)據(jù)一個是堆中的數(shù)據(jù)册养。最常見的一個問題就是,Java中參數(shù)傳遞時的問題压固。
- 3)Java中的參數(shù)傳遞時傳值呢球拦?還是傳引用?程序運行永遠都是在棧中進行的,因而參數(shù)傳遞時坎炼,只存在傳遞基本類型和對象引用的問題愧膀。不會直接傳對象本身。
int a = 0; //全局初始化區(qū)
char p1; //全局未初始化區(qū)
main(){
int b; //棧
char s[] = "abc"; //棧
char p2; //棧
char p3 = "123456"; //123456\0在常量區(qū)点弯,p3在棧上扇调。
static int c =0矿咕; //全局(靜態(tài))初始化區(qū)
p1 = (char *)malloc(10); //堆
p2 = (char *)malloc(20); //堆
}
2.6.2 堆內(nèi)存與棧內(nèi)存的區(qū)別
- 申請和回收方式不同:棧上的空間是自動分配自動回收的抢肛,所以棧上的數(shù)據(jù)的生存周期只是在函數(shù)的運行過程中,運行后就釋放掉碳柱,不可以再訪問捡絮。而堆上的數(shù)據(jù)只要程序員不釋放空間,就一直可以訪問到莲镣,不過缺點是一旦忘記釋放會造成內(nèi)存泄露福稳。
- 碎片問題:對于棧,不會產(chǎn)生不連續(xù)的內(nèi)存塊瑞侮;但是對于堆來說的圆,不斷的new、delete勢必會產(chǎn)生上面所述的內(nèi)部碎片和外部碎片半火。
- 申請大小的限制:棧是向低地址擴展的數(shù)據(jù)結構越妈,是一塊連續(xù)的內(nèi)存的區(qū)域。棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預先規(guī)定好的钮糖,如果申請的空間超過棧的剩余空間梅掠,就會產(chǎn)生棧溢出;對于堆店归,是向高地址擴展的數(shù)據(jù)結構阎抒,是不連續(xù)的內(nèi)存區(qū)域。堆的大小受限于計算機系統(tǒng)中有效的虛擬內(nèi)存消痛。由此可見且叁,堆獲得的空間比較靈活,也比較大秩伞。
- 申請效率的比較:棧由系統(tǒng)自動分配谴古,速度較快。但程序員是無法控制的稠歉;堆:是由new分配的內(nèi)存掰担,一般速度比較慢,而且容易產(chǎn)生內(nèi)存碎片,不過用起來最方便怒炸。
3.JIT編譯器
- JIT編譯器是JVM的核心带饱。它對于程序性能的影響最大。
- CPU只能執(zhí)行匯編代碼或者二進制代碼,所有程序都需要被翻譯成它們勺疼,然后才能被CPU執(zhí)行教寂。
- C++以及Fortran這類編譯型語言都會通過一個靜態(tài)的編譯器將程序編譯成CPU相關的二進制代碼。
- PHP以及Perl這列語言則是解釋型語言执庐,只需要安裝正確的解釋器酪耕,它們就能運行在任何CPU之上。當程序被執(zhí)行的時候轨淌,程序代碼會被逐行解釋并執(zhí)行迂烁。
- 編譯型語言的優(yōu)缺點:
- 速度快:因為在編譯的時候它們能夠獲取到更多的有關程序結構的信息,從而有機會對它們進行優(yōu)化递鹉。
- 適用性差:它們編譯得到的二進制代碼往往是CPU相關的盟步,在需要適配多種CPU時,可能需要編譯多次躏结。
- 解釋型語言的優(yōu)缺點:
- 適應性強:只需要安裝正確的解釋器却盘,程序在任何CPU上都能夠被運行
- 速度慢:因為程序需要被逐行翻譯,導致速度變慢媳拴。同時因為缺乏編譯這一過程黄橘,執(zhí)行代碼不能通過編譯器進行優(yōu)化。
- Java的做法是找到編譯型語言和解釋性語言的一個中間點:
- Java代碼會被編譯:被編譯成Java字節(jié)碼屈溉,而不是針對某種CPU的二進制代碼塞关。
- Java代碼會被解釋:Java字節(jié)碼需要被java程序解釋執(zhí)行,此時语婴,Java字節(jié)碼被翻譯成CPU相關的二進制代碼描孟。
- JIT編譯器的作用:在程序運行期間,將Java字節(jié)碼編譯成平臺相關的二進制代碼砰左。正因為此編譯行為發(fā)生在程序運行期間匿醒,所以該編譯器被稱為Just-In-Time編譯器。
HotSpot 編譯
HotSpot VM名字也體現(xiàn)了JIT編譯器的工作方式缠导。在VM開始運行一段代碼時廉羔,并不會立即對它們進行編譯。在程序中僻造,總有那么一些“熱點”區(qū)域憋他,該區(qū)域的代碼會被反復的執(zhí)行。而JIT編譯器只會編譯這些“熱點”區(qū)域的代碼髓削。
這么做的原因在于:
* 編譯那些只會被運行一次的代碼性價比太低竹挡,直接解釋執(zhí)行Java字節(jié)碼反而更快。
* JVM在執(zhí)行這些代碼的時候立膛,能獲取到這些代碼的信息揪罕,一段代碼被執(zhí)行的次數(shù)越多梯码,JVM也對它們愈加熟悉,因此能夠在對它們進行編譯的時候做出一些優(yōu)化好啰。
在HotSpot VM中內(nèi)嵌有兩個JIT編譯器轩娶,分別為Client Compiler和Server Compiler,但大多數(shù)情況下我們簡稱為C1編譯器和C2編譯器框往。開發(fā)人員可以通過如下命令顯式指定Java虛擬機在運行時到底使用哪一種即時編譯器鳄抒,如下所示:
-client:指定Java虛擬機運行在Client模式下,并使用C1編譯器椰弊;
-server:指定Java虛擬機運行在Server模式下许溅,并使用C2編譯器。
除了可以顯式指定Java虛擬機在運行時到底使用哪一種即時編譯器外男应,默認情況下HotSpot VM則會根據(jù)操作系統(tǒng)版本與物理機器的硬件性能自動選擇運行在哪一種模式下闹司,以及采用哪一種即時編譯器娱仔。簡單來說沐飘,C1編譯器會對字節(jié)碼進行簡單和可靠的優(yōu)化,以達到更快的編譯速度牲迫;而C2編譯器會啟動一些編譯耗時更長的優(yōu)化耐朴,以獲取更好的編譯質(zhì)量。不過在Java7版本之后盹憎,一旦開發(fā)人員在程序中顯式指定命令“-server”時筛峭,缺省將會開啟分層編譯(Tiered Compilation)策略,由C1編譯器和C2編譯器相互協(xié)作共同來執(zhí)行編譯任務陪每。不過在早期版本中影晓,開發(fā)人員則只能夠通過命令“-XX:+TieredCompilation”手動開啟分層編譯策略。
總結
- Java綜合了編譯型語言和解釋性語言的優(yōu)勢檩禾。
- Java會將類文件編譯成為Java字節(jié)碼挂签,然后Java字節(jié)碼會被JIT編譯器選擇性地編譯成為CPU能夠直接運行的二進制代碼。
- 將Java字節(jié)碼編譯成二進制代碼后盼产,性能會被大幅度提升。
4.類加載機制
Java虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗粘茄、轉(zhuǎn)換解析和初始化圣絮,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的加載機制灌灾。
類從被加載到虛擬機內(nèi)存中開始搓译,到卸載出內(nèi)存為止,它的整個生命周期包括了:加載(Loading)锋喜、驗證(Verification)些己、準備(Preparation)、解析(Resolution)、初始化(Initialization)轴总、使用(using)直颅、和卸載(Unloading)七個階段。其中驗證怀樟、準備和解析三個部分統(tǒng)稱為連接(Linking)功偿,這七個階段的發(fā)生順序如下圖所示:
如上圖所示往堡,加載吨瞎、驗證、準備穆咐、初始化和卸載這五個階段的順序是確定的颤诀,類的加載過程必須按照這個順序來按部就班地開始,而解析階段則不一定对湃,它在某些情況下可以在初始化階段后再開始崖叫。
類的生命周期的每一個階段通常都是互相交叉混合式進行的,通常會在一個階段執(zhí)行的過程中調(diào)用或激活另外一個階段拍柒。
4.1 類加載的時機
主動引用
一個類被主動引用之后會觸發(fā)初始化過程(加載心傀,驗證,準備需再此之前開始)
- 1)遇到new拆讯、get static脂男、put static或invoke static這4條字節(jié)碼指令時,如果類沒有進行過初始化种呐,則需要先觸發(fā)其初始化宰翅。生成這4條指令最常見的Java代碼場景是:使用new關鍵字實例化對象時、讀取或者設置一個類的靜態(tài)字段(被final修飾陕贮、已在編譯器把結果放入常量池的靜態(tài)字段除外)時堕油、以及調(diào)用一個類的靜態(tài)方法的時候。
- 2)使用java.lang.reflect包的方法對類進行反射調(diào)用的時候肮之,如果類沒有進行過初始化掉缺,則需要先觸發(fā)其初始化。
- 3)當初始化一個類的時候戈擒,如果發(fā)現(xiàn)其父類還沒有進行過初始化眶明,則需要觸發(fā)父類的初始化。
- 4)當虛擬機啟動時筐高,用戶需要指定一個執(zhí)行的主類(包含main()方法的類)搜囱,虛擬機會先初始化這個類丑瞧。
- 5)當使用jdk7+的動態(tài)語言支持時,如果java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic蜀肘、REF_putStatic绊汹、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化扮宠,則需要先觸發(fā)器 初始化西乖。
被動引用
一個類如果是被動引用的話,該類不會觸發(fā)初始化過程
- 1)通過子類引用父類的靜態(tài)字段坛增,不會導致子類初始化获雕。對于靜態(tài)字段,只有直接定義該字段的類才會被初始化收捣,因此當我們通過子類來引用父類中定義的靜態(tài)字段時届案,只會觸發(fā)父類的初始化,而不會觸發(fā)子類的初始化罢艾。
- 2)通過數(shù)組定義來引用類楣颠,不會觸發(fā)此類的初始化。
- 3)常量在編譯階段會存入調(diào)用類的常量池中昆婿,本質(zhì)上沒有直接引用到定義常量的類球碉,因此不會觸發(fā)定義常量的類的初始化蜓斧。
4.2 類加載過程
1仓蛆、加載
在加載階段,虛擬機需要完成以下三件事情:
- 1)通過一個類的全限定名稱來獲取定義此類的二進制字節(jié)流挎春。
- 2)將這個字節(jié)流所代表的靜態(tài)存儲結構轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結構看疙。
- 3)在java堆中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這些數(shù)據(jù)的訪問入口直奋。
相對于類加載過程的其他階段能庆,加載階段是開發(fā)期相對來說可控性比較強,該階段既可以使用系統(tǒng)提供的類加載器完成脚线,也可以由用戶自定義的類加載器來完成搁胆,開發(fā)人員可以通過定義自己的類加載器去控制字節(jié)流的獲取方式。
2邮绿、驗證
驗證的目的是為了確保Class文件中的字節(jié)流包含的信息符合當前虛擬機的要求渠旁,而且不會危害虛擬機自身的安全。不同的虛擬機對類驗證的實現(xiàn)可能會有所不同船逮,但大致都會完成以下四個階段的驗證:文件格式的驗證顾腊、元數(shù)據(jù)的驗證、字節(jié)碼驗證和符號引用驗證挖胃。
- 1)文件格式的驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范杂靶,并且能被當前版本的虛擬機處理梆惯,該驗證的主要目的是保證輸入的字節(jié)流能正確地解析并存儲
于方法區(qū)之內(nèi)。經(jīng)過該階段的驗證后吗垮,字節(jié)流才會進入內(nèi)存的方法區(qū)中進行存儲垛吗,后面的三個驗證都是基于方法區(qū)的存儲結構進行的。 - 2)元數(shù)據(jù)驗證:對類的元數(shù)據(jù)信息進行語義校驗(其實就是對類中的各數(shù)據(jù)類型進行語法校驗)烁登,保證不存在不符合Java語法規(guī)范的元數(shù)據(jù)信息职烧。
- 3)字節(jié)碼驗證:該階段驗證的主要工作是進行數(shù)據(jù)流和控制流分析,對類的方法體進行校驗分析防泵,以保證被校驗的類的方法在運行時不會做出危害虛擬機安全的行為蚀之。
- 4)符號引用驗證:這是最后一個階段的驗證,它發(fā)生在虛擬機將符號引用轉(zhuǎn)化為直接引用的時候(解析階段中發(fā)生該轉(zhuǎn)化捷泞,后面會有講解)足删,主要是對類自身以外的信息(常量池中的各種符號引用)進行匹配性的校驗。
3锁右、準備
準備階段是正式為類變量分配內(nèi)存并設置類變量初始值的階段失受,這些內(nèi)存都將在方法區(qū)中進行分配。
注:
- 1)這時候進行內(nèi)存分配的僅包括類變量(static)咏瑟,而不包括實例變量拂到,實例變量會在對象實例化時隨著對象一塊分配在Java堆中。
- 2)這里所設置的初始值通常情況下是數(shù)據(jù)類型默認的零值(如0码泞、0L兄旬、、false等)余寥,而不是被在Java代碼中被顯式地賦予的值领铐。
4、解析
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程
宋舷。
符號引用(Symbolic Reference):
符號引用以一組符號來描述所引用的目標绪撵,符號引用可以是任何形式的字面量,符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關祝蝠,引用的目標并不一定已經(jīng)在內(nèi)存中音诈。
直接引用(Direct Reference):
直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄绎狭。直接引用是與虛擬機實現(xiàn)的內(nèi)存布局相關的细溅,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般都不相同,如果有了直接引用坟岔,那引用的目標必定已經(jīng)在內(nèi)存中存在谒兄。
- 1)類或接口的解析:判斷所要轉(zhuǎn)化成的直接引用是對數(shù)組類型,還是普通的對象類型的引用社付,從而進行不同的解析承疲。
- 2)字段解析:對字段進行解析時邻耕,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,如果有燕鸽,則查找結束兄世;如果沒有,則會按照繼承關系從上往下遞歸搜索該類所實現(xiàn)的各個接口和它們的父接口啊研,還沒有御滩,則按照繼承關系從上往下遞歸搜索其父類,直至查找結束党远。
- 3)類方法解析:對類方法的解析與對字段解析的搜索步驟差不多削解,只是多了判斷該方法所處的是類還是接口的步驟,而且對類方法的匹配搜索沟娱,是先搜索父類氛驮,再搜索接口。
- 4)接口方法解析:與類方法解析步驟類似济似,只是接口不會有父類矫废,因此,只遞歸向上搜索父接口就行了砰蠢。
5蓖扑、初始化
類初始化階段是類加載過程的最后一步,前面的類加載過程中台舱,除了加載(Loading)階段用戶應用程序可以通過自定義類加載器參與之外律杠,其余動作完全由虛擬機主導和控制。到了初始化階段柿赊,才真正開始執(zhí)行類中定義的Java程序代碼俩功。
初始化階段是執(zhí)行類構造器<clinit>方法的過程。
- 1)<clinit>方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的碰声,編譯器收集的順序由語句在源文件中出現(xiàn)的順序所決定。
- 2)<clinit>方法與類的構造函數(shù)不同熬甫,它不需要顯式地調(diào)用父類構造器胰挑,虛擬機會保證在子類的<clinit>方法執(zhí)行之前,父類的<clinit>方法已經(jīng)執(zhí)行完畢椿肩,因此在虛擬機中第一個執(zhí)行的<clinit>方法的類一定是java.lang.Object瞻颂。
- 3)由于父類的<clinit>方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作郑象。
- 4)<clinit>方法對于類或者接口來說并不是必需的贡这,如果一個類中沒有靜態(tài)語句塊也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<clinit>方法厂榛。
- 5)接口中可能會有變量賦值操作盖矫,因此接口也會生成<clinit>方法丽惭。但是接口與類不同,執(zhí)行接口的<clinit>方法不需要先執(zhí)行父接口的<clinit>方法辈双。只有當父接口中定義的變量被使用時责掏,父接口才會被初始化。另外湃望,接口的實現(xiàn)類在初始化時也不會執(zhí)行接口的<clinit>方法换衬。
- 6)虛擬機會保證一個類的<clinit>方法在多線程環(huán)境中被正確地加鎖和同步。如果有多個線程去同時初始化一個類证芭,那么只會有一個線程去執(zhí)行這個類的<clinit>方法瞳浦,其它線程都需要阻塞等待,直到活動線程執(zhí)行<clinit>方法完畢废士。如果在一個類的<clinit>方法中有耗時很長的操作术幔,那么就可能造成多個進程阻塞。
5.垃圾回收
5.1 按代實現(xiàn)垃圾回收
新生代(Young generation):
絕大多數(shù)最新被創(chuàng)建的對象會被分配到這里湃密,由于大部分對象在創(chuàng)建后會很快變得不可到達诅挑,所以很多對象被創(chuàng)建在新生代,然后消失泛源。對象從這個區(qū)域消失的過程我們稱之為”minor GC“拔妥。
新生代中存在一個Eden區(qū)和兩個Survivor區(qū)。新對象會首先分配在 Eden 中(如果新對象過大达箍,會直接分配在老年代中)没龙。在GC中,Eden 中的對象會被移動到survivor中缎玫,直至對象滿足一定的年紀(定義為熬過GC的次數(shù)),會被移動到老年代(具體細節(jié)將在下邊垃圾收集算法中討論)硬纤。
可以設置新生代和老年代的相對大小。這種方式的優(yōu)點是新生代大小會隨著整個堆大小動態(tài)擴展赃磨。參數(shù) -XX:NewRatio 設置老年代與新生代的比例筝家。例如 -XX:NewRatio=8 指定老年代/新生代為8/1. 老年代占堆大小的 7/8 ,新生代占 1/8 .(默認即使1/8)
例如:-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8
老年代(Old generation):
對象沒有變得不可達邻辉,并且從新生代中存活下來溪王,會被拷貝到這里。其所占用的空間要比新生代多值骇。也正由于其相對較大的空間莹菱,發(fā)生在老年代上的GC要比新生代少得多。對象從老年代中消失的過程吱瘩,可以稱之為”major GC“(或者”full GC“)
永久代(permanent generation):
像一些類的層級信息道伟,方法數(shù)據(jù)和方法信息(如字節(jié)碼,棧和變量大惺鼓搿)蜜徽,運行時常量池(jdk7之后移出永久代)祝懂,已確定的符號引用和虛方法表等等,它們幾乎都是靜態(tài)的并且很少被卸載和回收娜汁,在JDK8之前的HotSpot虛擬機中嫂易,類的這些“永久的”數(shù)據(jù)存放在一個叫做永久代的區(qū)域。永久代一段連續(xù)的內(nèi)存空間掐禁,我們在JVM啟動之前可以通過設置-XX:MaxPermSize的值來控制永久代的大小怜械。但是jdk8之后取消了永久代,這些元數(shù)據(jù)被移到了一個與堆不相連的本地內(nèi)存區(qū)域 傅事。
5.2 怎樣判斷對象是否已經(jīng)死亡
引用計數(shù)收集算法
用計數(shù)是垃圾收集器中的早期策略缕允。在這種方法中,堆中每個對象(不是引用)都有一個引用計數(shù)蹭越。當一個對象被創(chuàng)建時障本,且將該對象分配給一個變量,該變量計數(shù)設置為1响鹃。當任何其它變量被賦值為這個對象的引用時驾霜,計數(shù)加1(a = b,則b引用的對象+1),但當一個對象的某個引用超過了生命周期或者被設置為一個新值時买置,對象的引用計數(shù)減1粪糙。任何引用計數(shù)為0的對象可以被當作垃圾收集。當一個對象被垃圾收集時忿项,它引用的任何對象計數(shù)減1蓉冈。
- 優(yōu)點:引用計數(shù)收集器可以很快的執(zhí)行,交織在程序運行中轩触。對程序不被長時間打斷的實時環(huán)境比較有利寞酿。
- 缺點: 無法檢測出循環(huán)引用。如父對象有一個對子對象的引用脱柱,子對象反過來引用父對象伐弹。這樣,他們的引用計數(shù)永遠不可能為0.
可達性分析算法
通過一系列稱為”GC Roots”的對象作為起點褐捻,從這些節(jié)點開始向下搜索掸茅,搜索所有走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時(從GC Roots到此對象不可達)柠逞,則證明此對象是不可用的。
可作為GC Roots的對象包括:
- 虛擬機棧中所引用的對象(本地變量表)
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中JNI引用的對象(Native對象)
5.3 java中的引用
強引用(Strong Reference):
在代碼中普遍存在的景馁,類似”O(jiān)bject obj = new Object”這類引用板壮,只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象
軟引用(Sofe Reference):
有用但并非必須的對象合住,可用SoftReference類來實現(xiàn)軟引用绰精,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前撒璧,將會把這些對象列進回收范圍之中進行二次回收。如果這次回收還沒有足夠的內(nèi)存笨使,才會拋出內(nèi)存異常異常卿樱。
弱引用(Weak Reference):
被弱引用關聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前,JDK提供了WeakReference類來實現(xiàn)弱引用硫椰。
虛引用(Phantom Reference):
也稱為幽靈引用或幻影引用繁调,是最弱的一種引用關系,JDK提供了PhantomReference類來實現(xiàn)虛引用靶草。
5.4 finalize方法什么作用
對于一個對象來說蹄胰,在被判斷沒有 GCroots 與其相關聯(lián)時,被第一次標記奕翔,然后判斷該對象是否應該執(zhí)行finalize方法(判斷依據(jù):如果對象的finalize方法被復寫,并且沒有執(zhí)行過,則可以被執(zhí)行)运褪。如果允許執(zhí)行那么這個對象將會被放到一個叫F-Query的隊列中仇奶,等待被執(zhí)行。(注意:由于finalize的優(yōu)先級比較低驾窟,所以該對象的的finalize方法不一定被執(zhí)行庆猫,即使被執(zhí)行了,也不保證finalize方法一定會執(zhí)行完)
5.5 垃圾收集算法
標記-清除算法:
標記-清除算法采用從根集合進行掃描纫普,對存活的對象進行標記阅悍,標記完畢后,再掃描整個空間中未被標記的對象昨稼,進行回收节视。標記-清除算法不需要進行對象的移動,并且僅對不存活的對象進行處理假栓,在存活對象比較多的情況下極為高效寻行,但由于標記-清除算法直接回收不存活的對象,因此會造成內(nèi)存碎片匾荆。
復制算法:
這種收集算法將堆棧分為兩個域拌蜘,常稱為半空間。每次僅使用一半的空間牙丽,JVM生成的新對象則放在另一半空間中简卧。GC運行時,它把可到達對象復制到另一半空間烤芦,從而壓縮了堆棧举娩。這種方法適用于短生存期的對象,持續(xù)復制長生存期的對象則導致效率降低。并且對于指定大小堆來說铜涉,需要兩倍大小的內(nèi)存智玻,因為任何時候都只使用其中的一半。
標記整理算法:
標記-整理算法采用標記-清除算法一樣的方式進行對象的標記芙代,但在清除時不同吊奢,在回收不存活的對象占用的空間后,會將所有的存活對象往一端空閑空間移動纹烹,并更新對應的指針页滚。標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動滔韵,因此成本更高逻谦,但是卻解決了內(nèi)存碎片的問題。
分代收集算法:
在上邊三種收集思想中加入了分代的思想陪蜻。
5.6 Hotspot實現(xiàn)垃圾回收細節(jié)
一致性:
在可達性分析期間整個系統(tǒng)看起來就像被凍結在某個時間點上邦马,不可以出現(xiàn)分析過程中對象引用關系還在不斷變化的情況。
一致性要求導致GC進行時必須停頓所有Java執(zhí)行線程宴卖。(Stop The World)即使在號稱不會發(fā)生停頓的CMS收集器中滋将,枚舉根節(jié)點時也是必須停頓的。
HotSpot使用的是準確式GC症昏,當執(zhí)行系統(tǒng)停頓下來后随闽,并不需要一個不漏地檢查完所有執(zhí)行上下文和全局的引用位置,這是通過一組稱為OopMap的數(shù)據(jù)結構來達到的肝谭。
安全點(Safe Point):
程序只有在到達安全點時才能暫停掘宪。安全點的選定標準是“是否具有讓程序長時間執(zhí)行的特征”∪林颍“長時間執(zhí)行”的最明顯特征就是指令序列的復用魏滚,如方法調(diào)用、循環(huán)跳轉(zhuǎn)等坟漱,具有這些功能的指令才會產(chǎn)生安全點鼠次。
讓程序暫停的兩種方式:
* 搶先式中斷(Preemptive Suspension):在GC發(fā)生時,主動中斷所有線程芋齿,不需要線程執(zhí)行的代碼主動配合腥寇。如果發(fā)現(xiàn)有線程中斷的地方不在安全點上,就恢復線程讓它跑到安全點上觅捆。(不推薦)
* 主動式中斷(Voluntary Suspension):設一個標志赦役,各個線程主動去輪詢這個標志,遇到中斷則暫停栅炒。輪詢地方與安全點重合扩劝。
5.7 垃圾收集器
HotSpot中幾種常見的垃圾收集器:
5.7.1 Serial收集器
Serial收集器是最基本庸论、發(fā)展歷史最悠久的收集器职辅,曾經(jīng)(在JDK 1.3.1之前)是虛擬機新生代收集的唯一選擇棒呛。
特性:
這個收集器是一個單線程的收集器,但它的“單線程”的意義并不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作域携,更重要的是在它進行垃圾收集時簇秒,必須暫停其他所有的工作線程,直到它收集結束秀鞭。Stop The World
應用場景:
Serial收集器是虛擬機運行在Client模式下的默認新生代收集器趋观。
優(yōu)勢:
簡單而高效(與其他收集器的單線程比),對于限定單個CPU的環(huán)境來說锋边,Serial收集器由于沒有線程交互的開銷皱坛,專心做垃圾收集自然可以獲得最高的單線程收集效率。
5.7.2 ParNew收集器
特性:
ParNew收集器其實就是Serial收集器的多線程版本豆巨,除了使用多條線程進行垃圾收集之外剩辟,其余行為包括Serial收集器可用的所有控制參數(shù)、收集算法往扔、Stop The World贩猎、對象分配規(guī)則、回收策略等都與Serial收集器完全一樣萍膛,在實現(xiàn)上吭服,這兩種收集器也共用了相當多的代碼。
應用場景:
ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器蝗罗。有一個很重要的原因是除了Serial收集器外艇棕,目前只有它能與CMS收集器配合工作。
Serial收集器 VS ParNew收集器:
ParNew收集器在單CPU的環(huán)境中絕對不會有比Serial收集器更好的效果串塑,甚至由于存在線程交互的開銷沼琉,該收集器在通過超線程技術實現(xiàn)的兩個CPU的環(huán)境中都不能百分之百地保證可以超越Serial收集器。然而拟赊,隨著可以使用的CPU的數(shù)量的增加刺桃,它對于GC時系統(tǒng)資源的有效利用還是很有好處的。
5.7.3 Parallel Scavenge收集器
特性:
Parallel Scavenge收集器是一個新生代收集器吸祟,它也是使用復制算法的收集器瑟慈,又是并行的多線程收集器。
應用場景:
停頓時間越短就越適合需要與用戶交互的程序屋匕,良好的響應速度能提升用戶體驗葛碧,而高吞吐量則可以高效率地利用CPU時間,盡快完成程序的運算任務过吻,主要適合在后臺運算而不需要太多交互的任務进泼。
對比分析:
Parallel Scavenge收集器 VS CMS等收集器:
Parallel Scavenge收集器的特點是它的關注點與其他收集器不同蔗衡,CMS等收集器的關 注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)乳绕。
由于與吞吐量關系密切绞惦,Parallel Scavenge收集器也經(jīng)常稱為“吞吐量優(yōu)先”收集器。
Parallel Scavenge收集器 VS ParNew收集器:
Parallel Scavenge收集器與ParNew收集器的一個重要區(qū)別是它具有自適應調(diào)節(jié)策略洋措。
GC自適應的調(diào)節(jié)策略:
Parallel Scavenge收集器有一個參數(shù)-XX:+UseAdaptiveSizePolicy济蝉。當這個參數(shù)打開之后,就不需要手工指定新生代的大小菠发、Eden與Survivor區(qū)的比例王滤、晉升老年代對象年齡等細節(jié)參數(shù)了,虛擬機會根據(jù)當前系統(tǒng)的運行情況收集性能監(jiān)控信息滓鸠,動態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時間或者最大的吞吐量雁乡,這種調(diào)節(jié)方式稱為GC自適應的調(diào)節(jié)策略(GC Ergonomics)。
5.7.4 Serial Old收集器
特性:
Serial Old是Serial收集器的老年代版本糜俗,它同樣是一個單線程收集器踱稍,使用標記-整理算法。
應用場景:
- Client模式:Serial Old收集器的主要意義也是在于給Client模式下的虛擬機使用吩跋。
- Server模式:如果在Server模式下寞射,那么它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作為CMS收集器的后備預案锌钮,在并發(fā)收集發(fā)生Concurrent Mode Failure時使用桥温。
5.7.5 Parallel Old收集器
特性:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法梁丘。
應用場景:
在注重吞吐量以及CPU資源敏感的場合侵浸,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器。
這個收集器是在JDK 1.6中才開始提供的氛谜,在此之前掏觉,新生代的Parallel Scavenge收集器一直處于比較尷尬的狀態(tài)。原因是值漫,如果新生代選擇了Parallel Scavenge收集器澳腹,老年代除了Serial Old收集器外別無選擇(Parallel Scavenge收集器無法與CMS收集器配合工作)。由于老年代Serial Old收集器在服務端應用性能上的“拖累”杨何,使用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果酱塔,由于單線程的老年代收集中無法充分利用服務器多CPU的處理能力,在老年代很大而且硬件比較高級的環(huán)境中危虱,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合“給力”羊娃。直到Parallel Old收集器出現(xiàn)后,“吞吐量優(yōu)先”收集器終于有了比較名副其實的應用組合埃跷。
5.7.6 CMS收集器
特性:
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器蕊玷。目前很大一部分的Java應用集中在互聯(lián)網(wǎng)站或者B/S系統(tǒng)的服務端上邮利,這類應用尤其重視服務的響應速度,希望系統(tǒng)停頓時間最短垃帅,以給用戶帶來較好的體驗延届。CMS收集器就非常符合這類應用的需求。
CMS收集器是基于“標記—清除”算法實現(xiàn)的挺智,它的運作過程相對于前面幾種收集器來說更復雜一些祷愉,整個過程分為4個步驟:
- 初始標記(CMS initial mark):初始標記僅僅只是標記一下GC Roots能直接關聯(lián)到的對象,速度很快赦颇,需要“Stop The World”。
- 并發(fā)標記(CMS concurrent mark):并發(fā)標記階段就是進行GC Roots Tracing的過程赴涵。
- 重新標記(CMS remark):重新標記階段是為了修正并發(fā)標記期間因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分對象的標記記錄媒怯,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發(fā)標記的時間短髓窜,仍然需要“Stop The World”扇苞。
- 并發(fā)清除(CMS concurrent sweep):并發(fā)清除階段會清除對象。
由于整個過程中耗時最長的并發(fā)標記和并發(fā)清除過程收集器線程都可以與用戶線程一起工作寄纵,所以鳖敷,從總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的程拭。
優(yōu)點:
CMS是一款優(yōu)秀的收集器定踱,它的主要優(yōu)點在名字上已經(jīng)體現(xiàn)出來了:并發(fā)收集、低停頓恃鞋。
缺點:
-
1)CMS收集器對CPU資源非常敏感
其實崖媚,面向并發(fā)設計的程序都對CPU資源比較敏感。在并發(fā)階段恤浪,它雖然不會導致用戶線程停頓畅哑,但是會因為占用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低水由。
CMS默認啟動的回收線程數(shù)是(CPU數(shù)量+3)/ 4荠呐,也就是當CPU在4個以上時,并發(fā)回收時垃圾收集線程不少于25%的CPU資源砂客,并且隨著CPU數(shù)量的增加而下降泥张。但是當CPU不足4個(譬如2個)時,CMS對用戶程序的影響就可能變得很大鞭盟。
-
2)CMS收集器無法處理浮動垃圾
CMS收集器無法處理浮動垃圾圾结,可能出現(xiàn)“Concurrent Mode Failure”失敗而導致另一次Full GC的產(chǎn)生。
由于CMS并發(fā)清理階段用戶線程還在運行著齿诉,伴隨程序運行自然就還會有新的垃圾不斷產(chǎn)生筝野,這一部分垃圾出現(xiàn)在標記過程之后晌姚,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉歇竟。這一部分垃圾就稱為“浮動垃圾”挥唠。
也是由于在垃圾收集階段用戶線程還需要運行,那也就還需要預留有足夠的內(nèi)存空間給用戶線程使用焕议,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集宝磨,需要預留一部分空間提供并發(fā)收集時的程序運作使用。要是CMS運行期間預留的內(nèi)存無法滿足程序需要盅安,就會出現(xiàn)一次“Concurrent Mode Failure”失敗唤锉,這時虛擬機將啟動后備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了别瞭。
-
3)CMS收集器會產(chǎn)生大量空間碎片
CMS是一款基于“標記—清除”算法實現(xiàn)的收集器窿祥,這意味著收集結束時會有大量空間碎片產(chǎn)生◎空間碎片過多時晒衩,將會給大對象分配帶來很大麻煩,往往會出現(xiàn)老年代還有很大空間剩余墙歪,但是無法找到足夠大的連續(xù)空間來分配當前對象听系,不得不提前觸發(fā)一次Full GC。
5.7.7 G1收集器
特性:
G1(Garbage-First)是一款面向服務端應用的垃圾收集器虹菲。HotSpot開發(fā)團隊賦予它的使命是未來可以替換掉JDK 1.5中發(fā)布的CMS收集器靠胜。與其他GC收集器相比,G1具備如下特點届惋。
-
1)并行與并發(fā)
G1能充分利用多CPU髓帽、多核環(huán)境下的硬件優(yōu)勢,使用多個CPU來縮短Stop-The-World停頓的時間脑豹,部分其他收集器原本需要停頓Java線程執(zhí)行的GC動作郑藏,G1收集器仍然可以通過并發(fā)的方式讓Java程序繼續(xù)執(zhí)行。
-
2)分代收集
與其他收集器一樣瘩欺,分代概念在G1中依然得以保留必盖。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新創(chuàng)建的對象和已經(jīng)存活了一段時間俱饿、熬過多次GC的舊對象以獲取更好的收集效果歌粥。
-
3)空間整合
與CMS的“標記—清理”算法不同,G1從整體來看是基于“標記—整理”算法實現(xiàn)的收集器拍埠,從局部(兩個Region之間)上來看是基于“復制”算法實現(xiàn)的失驶,但無論如何,這兩種算法都意味著G1運作期間不會產(chǎn)生內(nèi)存空間碎片枣购,收集后能提供規(guī)整的可用內(nèi)存嬉探。這種特性有利于程序長時間運行擦耀,分配大對象時不會因為無法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次GC。
-
4)可預測的停頓
這是G1相對于CMS的另一大優(yōu)勢涩堤,降低停頓時間是G1和CMS共同的關注點眷蜓,但G1除了追求低停頓外,還能建立可預測的停頓時間模型胎围,能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi)吁系,消耗在垃圾收集上的時間不得超過N毫秒。
在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代白魂,而G1不再是這樣汽纤。使用G1收集器時,Java堆的內(nèi)存布局就與其他收集器有很大差別碧聪,它將整個Java堆劃分為多個大小相等的獨立區(qū)域(Region)冒版,雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了逞姿,它們都是一部分Region(不需要連續(xù))的集合。
G1收集器之所以能建立可預測的停頓時間模型捆等,是因為它可以有計劃地避免在整個Java堆中進行全區(qū)域的垃圾收集滞造。G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經(jīng)驗值)栋烤,在后臺維護一個優(yōu)先列表谒养,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的Region(這也就是Garbage-First名稱的來由)明郭。這種使用Region劃分內(nèi)存空間以及有優(yōu)先級的區(qū)域回收方式买窟,保證了G1收集器在有限的時間內(nèi)可以獲取盡可能高的收集效率。
執(zhí)行過程:
G1收集器的運作大致可劃分為以下幾個步驟:
- 1)初始標記(Initial Marking):初始標記階段僅僅只是標記一下GC Roots能直接關聯(lián)到的對象薯定,并且修改TAMS(Next Top at Mark Start)的值始绍,讓下一階段用戶程序并發(fā)運行時,能在正確可用的Region中創(chuàng)建新對象话侄,這階段需要停頓線程亏推,但耗時很短。
- 2)并發(fā)標記(Concurrent Marking):并發(fā)標記階段是從GC Root開始對堆中對象進行可達性分析年堆,找出存活的對象吞杭,這階段耗時較長,但可與用戶程序并發(fā)執(zhí)行变丧。
- 3)最終標記(Final Marking):最終標記階段是為了修正在并發(fā)標記期間因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分標記記錄芽狗,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs里面,最終標記階段需要把Remembered Set Logs的數(shù)據(jù)合并到Remembered Set中痒蓬,這階段需要停頓線程童擎,但是可并行執(zhí)行滴劲。
- 4)篩選回收(Live Data Counting and Evacuation):篩選回收階段首先對各個Region的回收價值和成本進行排序,根據(jù)用戶所期望的GC停頓時間來制定回收計劃柔昼,這個階段其實也可以做到與用戶程序一起并發(fā)執(zhí)行哑芹,但是因為只回收一部分Region,時間是用戶可控制的捕透,而且停頓用戶線程將大幅提高收集效率聪姿。
何時會拋出OutOfMemoryException,并不是內(nèi)存被耗空的時候才拋出
* JVM98%的時間都花費在內(nèi)存回收
* 每次回收的內(nèi)存小于2%
6.JVM參數(shù)
6.1 典型配置
/usr/local/jdk/bin/java
-Dresin.home=/usr/local/resin
-server
-Xms1800M
-Xmx1800M
-Xmn300M
-Xss512K
-XX:PermSize=300M
-XX:MaxPermSize=300M
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=5
-XX:GCTimeRatio=19
-Xnoclassgc
-XX:+DisableExplicitGC
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:-CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=70
-XX:SoftRefLRUPolicyMSPerMB=0
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:log/gc.log
6.1.1 堆大小設置
JVM 中最大堆大小有三方面限制:相關操作系統(tǒng)的數(shù)據(jù)模型(32-bt還是64-bit)限制乙嘀;系統(tǒng)的可用虛擬內(nèi)存限制末购;系統(tǒng)的可用物理內(nèi)存限制。32位系統(tǒng)下虎谢,一般限制在1.5G~2G盟榴;64為操作系統(tǒng)對內(nèi)存無限制。
java -Xmx3550m -Xms3550m -Xmn2g-Xss128k
-Xmx3550m:設置JVM最大可用內(nèi)存為3550M婴噩。
-Xms3550m:設置JVM促使內(nèi)存為3550m擎场。此值可以設置與-Xmx相同,以避免每次垃圾回收完成后JVM重新分配內(nèi)存几莽。
-Xmn2g:設置年輕代大小為2G迅办。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為64m章蚣,所以增大年輕代后站欺,將會減小年老代大小。此值對系統(tǒng)性能影響較大纤垂,Sun官方推薦配置為整個堆的3/8矾策。
-Xss128k:設置每個線程的堆棧大小。JDK5.0以后每個線程堆棧大小為1M峭沦,以前每個線程堆棧大小為256K贾虽。更具應用的線程所需內(nèi)存大小進行調(diào)整崇呵。在相同物理內(nèi)存下撩炊,減小這個值能生成更多的線程熄驼。但是操作系統(tǒng)對一個進程內(nèi)的線程數(shù)還是有限制的隅津,不能無限生成税朴,經(jīng)驗值在3000~5000左右撑碴。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:設置年輕代(包括Eden和兩個Survivor區(qū))與年老代的比值(除去持久代)允懂。設置為4空骚,則年輕代與年老代所占比值為1:4巷送,年輕代占整個堆棧的1/5
-XX:SurvivorRatio=4:設置年輕代中Eden區(qū)與Survivor區(qū)的大小比值驶忌。設置為4,則兩個Survivor區(qū)與一個Eden區(qū)的比值為2:4,一個Survivor區(qū)占整個年輕代的1/6
-XX:MaxPermSize=16m:設置持久代大小為16m付魔。
-XX:MaxTenuringThreshold=0:設置垃圾最大年齡聊品。如果設置為0的話,則年輕代對象不經(jīng)過Survivor區(qū)几苍,直接進入年老代翻屈。對于年老代比較多的應用,可以提高效率妻坝。如果將此值設置為一個較大值伸眶,則年輕代對象會在Survivor區(qū)進行多次復制,這樣可以增加對象再年輕代的存活時間刽宪,增加在年輕代即被回收的概論厘贼。
6.1.2 回收器選擇
JVM給了三種選擇:串行收集器、并行收集器圣拄、并發(fā)收集器嘴秸,但是串行收集器只適用于小數(shù)據(jù)量的情況,所以這里的選擇主要針對并行收集器和并發(fā)收集器庇谆。默認情況下岳掐,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在啟動時加入相應參數(shù)饭耳。JDK5.0以后岩四,JVM會根據(jù)當前系統(tǒng)配置進行判斷。
吞吐量優(yōu)先的并行收集器
如上文所述哥攘,并行收集器主要以到達一定的吞吐量為目標,適用于科學技術和后臺處理等材鹦。
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC:選擇垃圾收集器為并行收集器逝淹。此配置僅對年輕代有效。即上述配置下桶唐,年輕代使用并發(fā)收集栅葡,而年老代仍舊使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的線程數(shù)尤泽,即:同時多少個線程一起進行垃圾回收欣簇。此值最好配置與處理器數(shù)目相等。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
-XX:+UseParallelOldGC:配置年老代垃圾收集方式為并行收集坯约。JDK6.0支持對年老代并行收集熊咽。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100:設置每次年輕代垃圾回收的最長時間,如果無法滿足此時間闹丐,JVM會自動調(diào)整年輕代大小横殴,以滿足此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100-XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy:設置此選項后卿拴,并行收集器會自動選擇年輕代區(qū)大小和相應的Survivor區(qū)比例衫仑,以達到目標系統(tǒng)規(guī)定的最低相應時間或者收集頻率等梨与,此值建議使用并行收集器時,一直打開文狱。
響應時間優(yōu)先的并發(fā)收集器
如上文所述粥鞋,并發(fā)收集器主要是保證系統(tǒng)的響應時間,減少垃圾收集時的停頓時間瞄崇。適用于應用服務器呻粹、電信領域等。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:設置年老代為并發(fā)收集杠袱。測試中配置這個以后尚猿,-XX:NewRatio=4的配置失效了,原因不明楣富。所以凿掂,此時年輕代大小最好用-Xmn設置。
-XX:+UseParNewGC:設置年輕代為并行收集纹蝴∽可與CMS收集同時使用。JDK5.0以上塘安,JVM會根據(jù)系統(tǒng)配置自行設置糠涛,所以無需再設置此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:由于并發(fā)收集器不對內(nèi)存空間進行壓縮兼犯、整理忍捡,所以運行一段時間以后會產(chǎn)生“碎片”,使得運行效率降低切黔。此值設置運行多少次GC以后對內(nèi)存空間進行壓縮砸脊、整理。
-XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮纬霞×韫。可能會影響性能,但是可以消除碎片
6.1.3輔助信息
JVM提供了大量命令行參數(shù)诗芜,打印信息瞳抓,供調(diào)試使用。主要有以下一些:
-XX:+PrintGC
輸出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]
[Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails
輸出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]
-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可與上面兩個混合使用
輸出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前伏恐,程序未中斷的執(zhí)行時間孩哑。可與上面混合使用
輸出形式:Application time: 0.5291524 seconds
-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期間程序暫停的時間脐湾〕舭剩可與上面混合使用
輸出形式:Total time for which application threads were stopped: 0.0468229 seconds
-XX:PrintHeapAtGC:打印GC前后的詳細堆棧信息
-Xloggc:filename:與上面幾個配合使用,把相關日志信息記錄到文件以便分析。
6.2 參數(shù)詳細說明
參數(shù)名稱 | 含義 | 默認值 | 說明 |
---|---|---|---|
-Xms | 初始堆大小 | 物理內(nèi)存的1/64(<1GB) | 默認(MinHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存小于40%時愁铺,JVM就會增大堆直到-Xmx的最大限制. |
-Xmx | 最大堆大小 | 物理內(nèi)存的1/4(<1GB) | 默認(MaxHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存大于70%時鹰霍,JVM會減少堆直到-Xms的最小限制 |
-Xmn | 年輕代大小(1.4or lator) | 注意:此處的大小是(eden+ 2 survivor space).與jmap -heap中顯示的New gen是不同的。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小.增大年輕代后,將會減小年老代大小.此值對系統(tǒng)性能影響較大,Sun官方推薦配置為整個堆的3/8 | |
-XX:NewSize | 設置年輕代大小(for 1.3/1.4) | ||
-XX:MaxNewSize | 年輕代最大值(for 1.3/1.4) | ||
-XX:PermSize | 設置持久代(perm gen)初始值 | 物理內(nèi)存的1/64 | |
-XX:MaxPermSize | 設置持久代最大值 | 物理內(nèi)存的1/4 | |
-Xss | 每個線程的堆棧大小 | JDK5.0以后每個線程堆棧大小為1M,以前每個線程堆棧大小為256K.更具應用的線程所需內(nèi)存大小進行調(diào)整.在相同物理內(nèi)存下,減小這個值能生成更多的線程.但是操作系統(tǒng)對一個進程內(nèi)的線程數(shù)還是有限制的,不能無限生成,經(jīng)驗值在3000~5000左右.一般小的應用茵乱, 如果棧不是很深茂洒, 應該是128k夠用的.大的應用建議使用256k。這個選項對性能影響比較大瓶竭,需要嚴格的測試督勺。 | |
-XX:ThreadStackSize | Thread Stack Size | (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.] | |
-XX:NewRatio | 年輕代(包括Eden和兩個Survivor區(qū))與年老代的比值(除去持久代) | -XX:NewRatio=4表示年輕代與年老代所占比值為1:4,年輕代占整個堆棧的1/5Xms=Xmx并且設置了Xmn的情況下,該參數(shù)不需要進行設置斤贰。 | |
-XX:SurvivorRatio | Eden區(qū)與Survivor區(qū)的大小比值 | 設置為8,則兩個Survivor區(qū)與一個Eden區(qū)的比值為2:8,一個Survivor區(qū)占整個年輕代的1/10 | |
-XX:LargePageSizeInBytes | 內(nèi)存頁的大小不可設置過大智哀, 會影響Perm的大小 | =128m | |
-XX:+UseFastAccessorMethods | 原始類型的快速優(yōu)化 | ||
-XX:+DisableExplicitGC | 關閉System.gc() | 這個參數(shù)需要嚴格的測試 | |
-XX:MaxTenuringThreshold | 垃圾最大年齡 | 如果設置為0的話,則年輕代對象不經(jīng)過Survivor區(qū),直接進入年老代.對于年老代比較多的應用,可以提高效率.如果將此值設置為一個較大值,則年輕代對象會在Survivor區(qū)進行多次復制,這樣可以增加對象再年輕代的存活時間,增加在年輕代即被回收的概率.該參數(shù)只有在串行GC時才有效. | |
-XX:+AggressiveOpts | 加快編譯 | ||
-XX:+UseBiasedLocking | 鎖機制的性能改善 | ||
-Xnoclassgc | 禁用垃圾回收 | ||
-XX:SoftRefLRUPolicyMSPerMB | 每兆堆空閑空間中SoftReference的存活時間 | 1s | softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap |
-XX:PretenureSizeThreshold | 對象超過多大是直接在舊生代分配 | 0 | 單位字節(jié) 新生代采用Parallel Scavenge GC時無效,另一種直接在舊生代分配的情況是大的數(shù)組對象,且數(shù)組中無外部引用對象. |
-XX:TLABWasteTargetPercent | TLAB占eden區(qū)的百分比 | 1% | |
-XX:+CollectGen0First | FullGC時是否先YGC | false |
并行收集器相關參數(shù)
參數(shù)名稱 | 含義 | 默認值 | 說明 |
---|---|---|---|
-XX:+UseParallelGC | Full GC采用parallel MSC(此項待驗證) | 選擇垃圾收集器為并行收集器.此配置僅對年輕代有效.即上述配置下,年輕代使用并發(fā)收集,而年老代仍舊使用串行收集.(此項待驗證) | |
-XX:+UseParNewGC | 設置年輕代為并行收集 | 可與CMS收集同時使用,JDK5.0以上,JVM會根據(jù)系統(tǒng)配置自行設置,所以無需再設置此值 | |
-XX:ParallelGCThreads | 并行收集器的線程數(shù) | 此值最好配置與處理器數(shù)目相等 同樣適用于CMS | |
-XX:+UseParallelOldGC | 年老代垃圾收集方式為并行收集(Parallel Compacting) | 這個是JAVA 6出現(xiàn)的參數(shù)選項 | |
-XX:MaxGCPauseMillis | 每次年輕代垃圾回收的最長時間(最大暫停時間) | 如果無法滿足此時間,JVM會自動調(diào)整年輕代大小,以滿足此值. | |
-XX:+UseAdaptiveSizePolicy | 自動選擇年輕代區(qū)大小和相應的Survivor區(qū)比例 | 設置此選項后,并行收集器會自動選擇年輕代區(qū)大小和相應的Survivor區(qū)比例,以達到目標系統(tǒng)規(guī)定的最低相應時間或者收集頻率等,此值建議使用并行收集器時,一直打開. | |
-XX:GCTimeRatio | 設置垃圾回收時間占程序運行時間的百分比 | 公式為1/(1+n) | |
-XX:+ScavengeBeforeFullGC | Full GC前調(diào)用YGC | true | Do young generation GC prior to a full GC. (Introduced in 1.4.1.) |
CMS相關參數(shù)
參數(shù)名稱 | 含義 | 默認值 | 說明 |
---|---|---|---|
-XX:+UseConcMarkSweepGC | 使用CMS內(nèi)存收集 | 測試中配置這個以后,-XX:NewRatio=4的配置失效了,原因不明.所以,此時年輕代大小最好用-Xmn設置 | |
-XX:+AggressiveHeap | 試圖是使用大量的物理內(nèi)存長時間大內(nèi)存。使用的優(yōu)化荧恍,能檢查計算資源(內(nèi)存瓷叫, 處理器數(shù)量)至少需要256MB內(nèi)存,大量的CPU/內(nèi)存送巡, (在1.4.1在4CPU的機器上已經(jīng)顯示有提升) | ||
-XX:CMSFullGCsBeforeCompaction | 多少次后進行內(nèi)存壓縮 | 由于并發(fā)收集器不對內(nèi)存空間進行壓縮,整理,所以運行一段時間以后會產(chǎn)生"碎片",使得運行效率降低.此值設置運行多少次GC以后對內(nèi)存空間進行壓縮,整理. | |
-XX:+CMSParallelRemarkEnabled | 降低標記停頓 | ||
-XX+UseCMSCompactAtFullCollection | 在FULL GC的時候摹菠, 對年老代的壓縮 | CMS是不會移動內(nèi)存的, 因此骗爆, 這個非常容易產(chǎn)生碎片次氨, 導致內(nèi)存不夠用, 因此摘投, 內(nèi)存的壓縮這個時候就會被啟用煮寡。 增加這個參數(shù)是個好習慣∠簦可能會影響性能,但是可以消除碎片 | |
-XX:+UseCMSInitiatingOccupancyOnly | 使用手動定義初始化定義開始CMS收集 | 禁止hostspot自行觸發(fā)CMS GC | |
-XX:CMSInitiatingOccupancyFraction=70 | 使用cms作為垃圾回收洲押,使用70%后開始CMS收集 | 92 | 為了保證不出現(xiàn)promotion failed(見下面介紹)錯誤,該值的設置需要滿足以下公式CMSInitiatingOccupancyFraction計算公式 |
-XX:CMSInitiatingPermOccupancyFraction | 設置Perm Gen使用到達多少比率時觸發(fā) | 92 | |
-XX:+CMSIncrementalMode | 設置為增量模式 | 用于單CPU情況 | |
-XX:+CMSClassUnloadingEnabled | 相對于并行收集器,CMS收集器默認不會對永久代進行垃圾回收圆凰。如果希望對永久代進行垃圾回收,可用設置標志-XX:+CMSClassUnloadingEnabled体箕。在早期JVM版本中专钉,要求設置額外的標志-XX:+CMSPermGenSweepingEnabled。注意累铅,即使沒有設置這個標志跃须,一旦永久代耗盡空間也會嘗試進行垃圾回收,但是收集不會是并行的娃兽,而再一次進行Full GC菇民。 |
輔助信息
參數(shù)名稱 | 含義 | 默認值 | 說明 |
---|---|---|---|
-XX:+PrintGC | 輸出形式:[GC 118250K->113543K(130112K), 0.0094143 secs][Full GC 121376K->10414K(130112K), 0.0650971 secs] | ||
-XX:+PrintGCDetails | 輸出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs]118250K->113543K(130112K), 0.0124633 secs][GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs]121376K->10414K(130112K), 0.0436268 secs] | ||
-XX:+PrintGCTimeStamps | |||
-XX:+PrintGC:PrintGCTimeStamps | 可與-XX:+PrintGC -XX:+PrintGCDetails混合使用。輸出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs] | ||
-XX:+PrintGCApplicationStoppedTime | 打印垃圾回收期間程序暫停的時間.可與上面混合使用 | 輸出形式:Total time for which application threads were stopped: 0.0468229 seconds | |
-XX:+PrintGCApplicationConcurrentTime | 打印每次垃圾回收前,程序未中斷的執(zhí)行時間.可與上面混合使用 | 輸出形式:Application time: 0.5291524 seconds | |
-XX:+PrintHeapAtGC | 打印GC前后的詳細堆棧信息 | ||
-Xloggc:filename | 把相關日志信息記錄到文件以便分析.與上面幾個配合使用 | ||
-XX:+PrintClassHistogram | garbage collects before printing the histogram. | ||
-XX:+PrintTLAB | 查看TLAB空間的使用情況 | ||
XX:+PrintTenuringDistribution | 查看每次minor GC后新的存活周期的閾值 | Desired survivor size 1048576 bytes, new threshold 7 (max 15) |
new threshold 7即標識新的存活周期的閾值為7。
7.JVM性能調(diào)優(yōu)
7.1 堆設置調(diào)優(yōu)
年輕代大小選擇
- 響應時間優(yōu)先的應用:盡可能設大第练,直到接近系統(tǒng)的最低響應時間限制(根據(jù)實際情況選擇)阔馋。在此種情況下,年輕代收集發(fā)生的頻率也是最小的娇掏。同時呕寝,減少到達年老代的對象。
- 吞吐量優(yōu)先的應用:盡可能的設置大婴梧,可能到達Gbit的程度下梢。因為對響應時間沒有要求,垃圾收集可以并行進行塞蹭,一般適合8CPU以上的應用孽江。
通過-XX:NewRadio設置新生代與老年代的大小比例,通過-Xmn來設置新生代的大小番电。
年老代大小選擇
-
響應時間優(yōu)先的應用:年老代使用并發(fā)收集器岗屏,所以其大小需要小心設置,一般要考慮并發(fā)會話率和會話持續(xù)時間等一些參數(shù)钧舌。如果堆設置小了担汤,可以會造成內(nèi)存碎片、高回收頻率以及應用暫停而使用傳統(tǒng)的標記清除方式洼冻;如果堆大了崭歧,則需要較長的收集時間。最優(yōu)化的方案撞牢,一般需要參考以下數(shù)據(jù)獲得:
- 并發(fā)垃圾收集信息
- 持久代并發(fā)收集次數(shù)
- 傳統(tǒng)GC信息
- 花在年輕代和年老代回收上的時間比例
吞吐量優(yōu)先的應用:一般吞吐量優(yōu)先的應用都有一個很大的年輕代和一個較小的年老代率碾。原因是,這樣可以盡可能回收掉大部分短期對象屋彪,減少中期的對象所宰,而年老代盡存放長期存活對象。
-
較小堆引起的碎片問題
因為年老代的并發(fā)收集器使用標記畜挥、清除算法仔粥,所以不會對堆進行壓縮。當收集器回收時蟹但,他會把相鄰的空間進行合并躯泰,這樣可以分配給較大的對象。但是华糖,當堆空間較小時麦向,運行一段時間以后,就會出現(xiàn)“碎片”客叉,如果并發(fā)收集器找不到足夠的空間诵竭,那么并發(fā)收集器將會停止话告,然后使用傳統(tǒng)的標記、清除方式進行回收卵慰。如果出現(xiàn)“碎片”沙郭,可能需要進行如下配置:- -XX:+UseCMSCompactAtFullCollection:使用并發(fā)收集器時,開啟對年老代的壓縮呵燕。
- -XX:CMSFullGCsBeforeCompaction=0:上面配置開啟的情況下棠绘,這里設置多少次Full GC后,對年老代進行壓縮
7.2 GC策略調(diào)優(yōu)
-
能夠忍受full gc的停頓再扭?
是:選擇throughput
否:如果堆較小氧苍,使用CMS或者G1;如果堆較大泛范,選擇G1
-
使用默認配置能達到期望目標嗎让虐?
首先盡量使用默認配置,因為垃圾收集技術在不斷發(fā)展成熟罢荡,自動優(yōu)化大多數(shù)的效果是最好的赡突。如果默認配置沒有達到期望,請確認垃圾收集是否是性能瓶頸区赵。如負荷較高的應用惭缰,如果垃圾收集上的時間不超過3%,即使進行垃圾回收調(diào)優(yōu)效果也不大笼才。
-
應用的停頓時間和預期的目標接近嗎漱受?
是:調(diào)整最大停頓時間設定可能是需要做的
否:需要進行其他調(diào)整
如果停頓時間太長,但是吞吐量正常骡送,可以嘗試減少新生代大邪合邸(如果是full gc,則減少老年代大兴狻)虐先,這樣停頓時間變短,但是單次時間變長
-
GC停頓很短了派敷,但是吞吐量上不去蛹批?
增大堆的大小,但是單次停頓時間會加長
-
使用并發(fā)收集器篮愉,發(fā)生了由并發(fā)模式失敗引發(fā)的full gc般眉?
如果CPU資源充足,可以增加并發(fā)GC的線程數(shù)數(shù)
-
使用并發(fā)收集器潜支,發(fā)生由晉升失敗引起的full gc?
如果是CMS柿汛,意味著發(fā)生了碎片化冗酿,這種情況下:使用跟大的堆埠对;盡早啟動后臺回收
如果堆空間較大,可以選擇使用G1
7.3 JIT調(diào)優(yōu)
- 一般只需要選擇是使用客戶端版或者服務器版的JIT編譯器即可裁替。
- 客戶端版的JIT編譯器使用:-client指定项玛,服務器版的使用:-server。
- 選擇哪種類型一般和硬件的配置相關弱判,當然隨著硬件的發(fā)展襟沮,也沒有一個確定的標準哪種硬件適合哪種配置。
- 兩種JIT編譯器的區(qū)別:
- Client版對于代碼的編譯早于Server版昌腰,也意味著代碼的執(zhí)行速度在程序執(zhí)行早期Client版更快开伏。
- Server版對代碼的編譯會稍晚一些,這是為了獲取到程序本身的更多信息遭商,以便編譯得到優(yōu)化程度更高的代碼固灵。因為運行在Server上的程序通常都會持續(xù)很久。
- Tiered編譯的原理:
- JVM啟動之初使用Client版JIT編譯器
- 當HotSpot形成之后使用Server版JIT編譯器再次編譯
- 在Java 8中劫流,默認使用Tiered編譯方式巫玻。
不過在Java7版本之后,一旦開發(fā)人員在程序中顯式指定命令“-server”時祠汇,缺省將會開啟分層編譯(Tiered Compilation)策略仍秤,由client編譯器和server編譯器相互協(xié)作共同來執(zhí)行編譯任務。不過在早期版本中可很,開發(fā)人員則只能夠通過命令“-XX:+TieredCompilation”手動開啟分層編譯策略诗力。
- -Xint:完全采用解釋器模式執(zhí)行程序;
- -Xcomp:完全采用即時編譯器模式執(zhí)行程序根穷;
- -Xmixed:采用解釋器+即時編譯器的混合模式共同執(zhí)行程序姜骡。
啟動優(yōu)化
Application | -client | -server | -XX:+TieredCompilation | 類數(shù)量 |
---|---|---|---|---|
HelloWorld | 0.08s | 0.08s | 0.08s | Few |
NetBeans | 2.83s | 3.92s | 3.07s | ~10000 |
HelloWorld | 51.5s | 54.0s | 52.0s | ~20000 |
總結
- 當程序的啟動速度越快越好時,使用Client版的JIT編譯器更好屿良。
- 就啟動速度而言圈澈,Tiered編譯方式的性能和只使用Client的方式十分接近,因為Tiered編譯本質(zhì)上也會在啟動是使用Client JIT編譯器尘惧。
批處理優(yōu)化
對于批處理任務康栈,任務量的大小是決定運行時間和使用哪種編譯策略的最重要因素:
Number of Tasks | -client | -server | -XX:+TieredCompilation |
---|---|---|---|
1 | 0.142s | 0.176s | 0.165s |
10 | 0.211s | 0.348s | 0.226s |
100 | 0.454s | 0.674s | 0.472s |
1000 | 2.556s | 2.158s | 1.910s |
10000 | 23.78s | 14.03s | 13.56s |
可以發(fā)現(xiàn)幾個結論:
- 當任務數(shù)量小的時候,使用Client或者Tiered方式的性能類似喷橙,而當任務數(shù)量大的時候啥么,使用Tiered會獲得最好的性能,因為它綜合使用了Client和Server兩種編譯器贰逾,在程序運行之初悬荣,使用Client JIT編譯器得到一部分編譯過的代碼,在程序“熱點”逐漸形成之后疙剑,使用Server JIT編譯器得到高度優(yōu)化的編譯后代碼氯迂。
- Tiered編譯方式的性能總是好于單獨使用Server JIT編譯器践叠。
- Tiered編譯方式在任務量不大的時候,和單獨使用Client JIT編譯器的性能相當嚼蚀。
總結
- 當一段批處理程序需要被執(zhí)行時禁灼,使用不同的策略進行測試,使用速度最快的那一種轿曙。
- 對于批處理程序弄捕,考慮使用Tiered編譯方式作為默認選項。
長時間運行應用的優(yōu)化
對于長時間運行的應用导帝,比如Servlet程序等守谓,一般會使用吞吐量來測試它們的性能。 以下的一組數(shù)據(jù)表示了一個典型的數(shù)據(jù)獲取程序在使用不同“熱身時間”以及不同編譯策略時舟扎,對吞吐量(OPS)的影響(執(zhí)行時間為60s):
Warm-up Period | -client | -server | -XX:+TieredCompilation |
---|---|---|---|
0s | 15.87 | 23.72 | 24.23 |
60s | 16.00 | 23.73 | 24.26 |
300s | 16.85 | 24.42 | 24.43 |
即使當“熱身時間”為0秒分飞,因為執(zhí)行時間為60秒,所以編譯器也有機會在次期間做出優(yōu)化睹限。
從上面的數(shù)據(jù)可以發(fā)現(xiàn)的幾個結論:
- 對于典型的數(shù)據(jù)獲取程序譬猫,編譯器對代碼編譯和優(yōu)化發(fā)生的十分迅速,當“熱身時間”顯著增加時羡疗,如從60秒增加到300秒染服,最后得到的OPS差異并不明顯。
- -server JIT編譯器和Tiered編譯的性能顯著優(yōu)于-client JIT編譯器叨恨。
總結
- 對于長時間運行的應用柳刮,總是使用-server JIT編譯器或者Tiered編譯策略。
代碼緩存調(diào)優(yōu)(Tuning the Code Cache)
當JVM對代碼進行編譯后痒钝,被編譯的代碼以匯編指令的形式存在于代碼緩存中(Code Cache)秉颗,顯然這個緩存區(qū)域也是有大小限制的,當此區(qū)域被填滿了之后送矩,編譯器就不能夠再編譯其他Java字節(jié)碼了蚕甥。
Code Cache的最大空間可以通過:-XX:ReservedCodeCacheSize=N來進行設置。
7.4 JVM線程調(diào)優(yōu)
調(diào)節(jié)線程棧大小
通過設置-Xss參數(shù)栋荸,在內(nèi)存比較稀缺的機器上菇怀,可以減少線程棧的大小,在32位的JVM上晌块,可以減少線程棧大小爱沟,可以稍稍增加堆的可用內(nèi)存。每個線程默認會開啟1M的堆棧匆背,用于存放棧幀呼伸、調(diào)用參數(shù)、局部變量等钝尸,對大多數(shù)應用而言這個默認值太了括享,一般256K就足用闽铐。
偏向鎖
使用-XX:UseBiasedLocking選項來禁用偏向鎖,偏向鎖默認開啟奶浦。偏向鎖可以提高緩存命中率,但是因為偏向鎖也需要一些簿記信息踢星,有時候性能會更糟澳叉,比如使用了某些線程池,同步資源或代碼一直都是多線程訪問的沐悦,那么消除偏向鎖這一步驟對你來說就是多余的成洗。
自旋鎖
使用-XX:UseSpinning參數(shù)可以設置自旋鎖是否開啟,但是Java7以后自旋鎖無法禁用藏否。
線程優(yōu)先級
每個線程都可以由開發(fā)人員指定優(yōu)先級瓶殃,不過真正執(zhí)行時的優(yōu)先級還取決于操作系統(tǒng)為每個線程計算的當前優(yōu)先級。開發(fā)人員不能依賴線程優(yōu)先級來影響其性能副签,如果要提高某些任務的優(yōu)先級遥椿,就必須使用應用層邏輯來劃分優(yōu)先級,可以通過將任務指派給不同線程池并修改哪些池子大小來實現(xiàn)淆储。
總結
理解線程如何運作冠场,可以獲得很大的性能優(yōu)勢,不過就線程的性能而言本砰,沒有太多可以調(diào)優(yōu)的:可以修改的JVM標識相當少碴裙,而且效果不明顯。
7.5 典型案例
$JAVA_ARGS
.=
"
-Dresin.home=$SERVER_ROOT
-server
-Xmx3000M
-Xms3000M
-Xmn600M
-XX:PermSize=500M
-XX:MaxPermSize=500M
-Xss256K
-XX:+DisableExplicitGC
-XX:SurvivorRatio=1
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSClassUnloadingEnabled
-XX:LargePageSizeInBytes=128M
-XX:+UseFastAccessorMethods
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:SoftRefLRUPolicyMSPerMB=0
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:log/gc.log
";
說明:
64位jdk參考設置点额,年老代漲得很慢舔株,CMS執(zhí)行頻率變小,CMS沒有停滯还棱,也不會有promotion failed問題载慈,內(nèi)存回收得很干凈
8.常見問題
8.1 內(nèi)存泄漏及解決方法
-
1.系統(tǒng)崩潰前的一些現(xiàn)象:
- 每次垃圾回收的時間越來越長,由之前的10ms延長到50ms左右诱贿,F(xiàn)ullGC的時間也有之前的0.5s延長到4娃肿、5s
- FullGC的次數(shù)越來越多,最頻繁時隔不到1分鐘就進行一次FullGC
- 年老代的內(nèi)存越來越大并且每次FullGC后年老代沒有內(nèi)存被釋放
之后系統(tǒng)會無法響應新的請求珠十,逐漸到達OutOfMemoryError的臨界值料扰。
-
2.生成堆的dump文件
通過JMX的MBean生成當前的Heap信息,大小為一個3G(整個堆的大斜翰洹)的hprof文件晒杈,如果沒有啟動JMX可以通過Java的jmap命令來生成該文件。
-
3.分析dump文件
下面要考慮的是如何打開這個3G的堆信息文件,顯然一般的Window系統(tǒng)沒有這么大的內(nèi)存憔鬼,必須借助高配置的Linux。當然我們可以借助X-Window把Linux上的圖形導入到Window琳疏。
我們考慮用下面幾種工具打開該文件:- Visual VM
- IBM HeapAnalyzer
- JDK 自帶的Hprof工具
使用這些工具時為了確保加載速度粪般,建議設置最大內(nèi)存為6G拼余。使用后發(fā)現(xiàn),這些工具都無法直觀地觀察到內(nèi)存泄漏亩歹,Visual VM雖能觀察到對象大小匙监,但看不到調(diào)用堆棧;HeapAnalyzer雖然能看到調(diào)用堆棧小作,卻無法正確打開一個3G的文件亭姥。因此,我們又選用了Eclipse專門的靜態(tài)內(nèi)存分析工具:Mat
-
4.分析內(nèi)存泄漏
通過Mat我們能清楚地看到顾稀,哪些對象被懷疑為內(nèi)存泄漏达罗,哪些對象占的空間最大及對象的調(diào)用關系。針對本案静秆,在ThreadLocal中有很多的JbpmContext實例粮揉,經(jīng)過調(diào)查是JBPM的Context沒有關閉所致。
另诡宗,通過Mat或JMX我們還可以分析線程狀態(tài)滔蝉,可以觀察到線程被阻塞在哪個對象上,從而判斷系統(tǒng)的瓶頸塔沃。
-
5.回歸問題
- Q:為什么崩潰前垃圾回收的時間越來越長蝠引?
- A:根據(jù)內(nèi)存模型和垃圾回收算法,垃圾回收分兩部分:內(nèi)存標記蛀柴、清除(復制)螃概,標記部分只要內(nèi)存大小固定時間是不變的,變的是復制部分鸽疾,因為每次垃圾回收都有一些回收不掉的內(nèi)存吊洼,所以增加了復制量,導致時間延長制肮。所以冒窍,垃圾回收的時間也可以作為判斷內(nèi)存泄漏的依據(jù)
- Q:為什么Full GC的次數(shù)越來越多?
- A:因此內(nèi)存的積累豺鼻,逐漸耗盡了年老代的內(nèi)存综液,導致新對象分配沒有更多的空間,從而導致頻繁的垃圾回收
- Q:為什么年老代占用的內(nèi)存越來越大儒飒?
- A:因為年輕代的內(nèi)存無法被回收谬莹,越來越多地被Copy到年老代
8.2 年老代堆空間被占滿
- 異常: java.lang.OutOfMemoryError: Java heap space
- 說明:
這是最典型的內(nèi)存泄漏方式,簡單說就是所有堆空間都被無法回收的垃圾對象占滿,虛擬機無法再在分配新空間附帽。
如上圖所示埠戳,這是非常典型的內(nèi)存泄漏的垃圾回收情況圖。所有峰值部分都是一次垃圾回收點蕉扮,所有谷底部分表示是一次垃圾回收后剩余的內(nèi)存整胃。連接所有谷底的點,可以發(fā)現(xiàn)一條由底到高的線喳钟,這說明爪模,隨時間的推移,系統(tǒng)的堆空間被不斷占滿荚藻,最終會占滿整個堆空間。因此可以初步認為系統(tǒng)內(nèi)部可能有內(nèi)存泄漏洁段。(上面的圖僅供示例应狱,在實際情況下收集數(shù)據(jù)的時間需要更長,比如幾個小時或者幾天)
-
解決:
這種方式解決起來也比較容易祠丝,一般就是根據(jù)垃圾回收前后情況對比疾呻,同時根據(jù)對象引用情況(常見的集合對象引用)分析,基本都可以找到泄漏點写半。
8.3 持久代被占滿
異常:java.lang.OutOfMemoryError: PermGen space
-
說明:
Perm空間被占滿岸蜗。無法為新的class分配存儲空間而引發(fā)的異常。這個異常以前是沒有的叠蝇,但是在Java反射大量使用的今天這個異常比較常見了璃岳。主要原因就是大量動態(tài)反射生成的類不斷被加載,最終導致Perm區(qū)被占滿悔捶。
更可怕的是铃慷,不同的classLoader即便使用了相同的類,但是都會對其進行加載蜕该,相當于同一個東西犁柜,如果有N個classLoader那么他將會被加載N次。因此堂淡,某些情況下馋缅,這個問題基本視為無解。當然绢淀,存在大量classLoader和大量反射類的情況其實也不多萤悴。 -
解決:
- 1.-XX:MaxPermSize=16m
- 2.換用JDK。比如JRocket
8.4 堆棧溢出
- 異常:java.lang.StackOverflowError
- 說明:這個就不多說了更啄,一般就是遞歸沒返回稚疹,或者循環(huán)調(diào)用造成
8.5 線程堆棧滿
異常:Fatal: Stack size too small
說明:java中一個線程的空間大小是有限制的。JDK5.0以后這個值是1M。與這個線程相關的數(shù)據(jù)將會保存在其中内狗。但是當線程空間滿了以后怪嫌,將會出現(xiàn)上面異常。
解決:增加線程棧大小柳沙。-Xss2m岩灭。但這個配置無法解決根本問題,還要看代碼部分是否有造成泄漏的部分赂鲤。
8.6 系統(tǒng)內(nèi)存被占滿
異常:java.lang.OutOfMemoryError: unable to create new native thread
-
說明:
這個異常是由于操作系統(tǒng)沒有足夠的資源來產(chǎn)生這個線程造成的噪径。系統(tǒng)創(chuàng)建線程時,除了要在Java堆中分配內(nèi)存外数初,操作系統(tǒng)本身也需要分配資源來創(chuàng)建線程找爱。因此,當線程數(shù)量大到一定程度以后泡孩,堆中或許還有空間车摄,但是操作系統(tǒng)分配不出資源來了,就出現(xiàn)這個異常了仑鸥。分配給Java虛擬機的內(nèi)存愈多吮播,系統(tǒng)剩余的資源就越少,因此眼俊,當系統(tǒng)內(nèi)存固定時意狠,分配給Java虛擬機的內(nèi)存越多,那么疮胖,系統(tǒng)總共能夠產(chǎn)生的線程也就越少环戈,兩者成反比的關系。同時澎灸,可以通過修改-Xss來減少分配給單個線程的空間谷市,也可以增加系統(tǒng)總共內(nèi)生產(chǎn)的線程數(shù)。
-
解決:
- 1.重新設計系統(tǒng)減少線程數(shù)量击孩。
- 2.線程數(shù)量不能減少的情況下迫悠,通過-Xss減小單個線程大小。以便能生產(chǎn)更多的線程巩梢。
歡迎關注 高廣超的簡書博客 與 收藏文章 创泄!
個人介紹:
高廣超 :多年一線互聯(lián)網(wǎng)研發(fā)與架構設計經(jīng)驗,擅長設計與落地高可用括蝠、高性能互聯(lián)網(wǎng)架構鞠抑。目前就職于美團網(wǎng),負責核心業(yè)務研發(fā)工作忌警。
本文首發(fā)在 高廣超的簡書博客 轉(zhuǎn)載請注明搁拙!