最詳細的JVM&GC講解

高廣超發表於2017-04-12

這篇文章是我之前翻閱了不少的書籍以及從網路上收集的一些資料的整理,因此不免有一些不準確的地方,同時不同JDK版本的差異也比較大。

不過文中一些JVM引數示例都是實際專案裡調優的結果,還是經受過實戰考驗的。

目錄


  1. JVM簡介
  1. JVM結構
    2.1 方法區
    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 區域性變數區
    2.3.1.2 運算元棧
    2.3.1.3 棧資料區
    2.4 本地方法棧
    2.5 PC暫存器
    2.6 堆與棧
    2.6.1 堆與棧裡存什麼
    2.6.2 堆記憶體與棧記憶體的區別
  2. JIT編譯器
  3. 類載入機制
    4.1 類載入的時機
    4.2 類載入過程
  4. 垃圾回收
    5.1 按代實現垃圾回收
    5.2 怎樣判斷物件是否已經死亡
    5.3 java中的引用
    5.4 finalize方法什麼作用
    5.5 垃圾收集演算法
    5.6 Hotspot實現垃圾回收細節
    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收集器
  5. JVM引數
    6.1 典型配置
    6.1.1 堆大小設定
    6.1.2 回收器選擇
    6.1.3 輔助資訊
    6.2 引數詳細說明
  6. JVM效能調優
    7.1 堆設定調優
    7.2 GC策略調優
    7.3 JIT調優
    7.4 JVM執行緒調優
    7.5 典型案例
  7. 常見問題
    8.1 記憶體洩漏及解決方法
    8.2 年老代堆空間被佔滿
    8.3 持久代被佔滿
    8.4 堆疊溢位
    8.5 執行緒堆疊滿
    8.6 系統記憶體被佔滿

1.JVM簡介

JVM是java的核心和基礎,在java編譯器和os平臺之間的虛擬處理器。它是一種利用軟體方法實現的抽象的計算機基於下層的作業系統和硬體平臺,可以在上面執行java的位元組碼程式。

java編譯器只要面向JVM,生成JVM能理解的程式碼或位元組碼檔案。Java原始檔經編譯成位元組碼程式,通過JVM將每一條指令翻譯成不同平臺機器碼,通過特定平臺執行。

執行過程

Java語言寫的源程式通過Java編譯器,編譯成與平臺無關的‘位元組碼程式’(.class檔案,也就是0,1二進位制程式),然後在OS之上的Java直譯器中解釋執行。

C++以及Fortran這類編譯型語言都會通過一個靜態的編譯器將程式編譯成CPU相關的二進位制程式碼。

PHP以及Perl這列語言則是解釋型語言,只需要安裝正確的直譯器,它們就能執行在任何CPU之上。當程式被執行的時候,程式程式碼會被逐行解釋並執行。


  1. 編譯型語言的優缺點:
    • 速度快:因為在編譯的時候它們能夠獲取到更多的有關程式結構的資訊,從而有機會對它們進行優化。
    • 適用性差:它們編譯得到的二進位制程式碼往往是CPU相關的,在需要適配多種CPU時,可能需要編譯多次。
  2. 解釋型語言的優缺點:
    • 適應性強:只需要安裝正確的直譯器,程式在任何CPU上都能夠被執行
    • 速度慢:因為程式需要被逐行翻譯,導致速度變慢。同時因為缺乏編譯這一過程,執行程式碼不能通過編譯器進行優化。
  3. Java的做法是找到編譯型語言和解釋性語言的一箇中間點:
    • Java程式碼會被編譯:被編譯成Java位元組碼,而不是針對某種CPU的二進位制程式碼。
    • Java程式碼會被解釋:Java位元組碼需要被java程式解釋執行,此時,Java位元組碼被翻譯成CPU相關的二進位制程式碼。
    • JIT編譯器的作用:在程式執行期間,將Java位元組碼編譯成平臺相關的二進位制程式碼。正因為此編譯行為發生在程式執行期間,所以該編譯器被稱為Just-In-Time編譯器。

img_1355ed4848ef4879c2796d4ded1f6754.png
image.png
img_0d7667c448ad619e9e824cd83f93af05.png
image.png

2.JVM結構

img_3684e3fc3a8fc29a744f4c1f1207a2f2.png
image.png

java是基於一門虛擬機器的語言,所以瞭解並且熟知虛擬機器執行原理非常重要。

2.1 方法區

方法區,Method Area, 對於習慣在HotSpot虛擬機器上開發和部署程式的開發者來說,很多人願意把方法區稱為“永久代”(Permanent Generation),本質上兩者並不等價,僅僅是因為HotSpot虛擬機器的設計團隊選擇把GC分代收集擴充套件至方法區,或者說使用永久代來實現方法區而已。對於其他虛擬機器(如BEA JRockit、IBM J9等)來說是不存在永久代的概念的。

主要存放已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料(比如spring 使用IOC或者AOP建立bean時,或者使用cglib,反射的形式動態生成class資訊等)。

注意:JDK 6 時,String等字串常量的資訊是置於方法區中的,但是到了JDK 7 時,已經移動到了Java堆。所以,方法區也好,Java堆也罷,到底詳細的儲存了什麼,其實沒有具體定論,要結合不同的JVM版本來分析。

異常

當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError。
執行時常量池溢位:比如一直往常量池加入資料,就會引起OutOfMemoryError異常。

類資訊

  1. 型別全限定名。
  2. 型別的直接超類的全限定名(除非這個型別是java.lang.Object,它沒有超類)。
  3. 型別是類型別還是介面型別。
  4. 型別的訪問修飾符(public、abstract或final的某個子集)。
  5. 任何直接超介面的全限定名的有序列表。
  6. 型別的常量池。
  7. 欄位資訊。
  8. 方法資訊。
  9. 除了常量意外的所有類(靜態)變數。
  10. 一個到類ClassLoader的引用。
  11. 一個到Class類的引用。

2.1.1 常量池

2.1.1.1 Class檔案中的常量池

在Class檔案結構中,最頭的4個位元組用於儲存Megic Number,用於確定一個檔案是否能被JVM接受,再接著4個位元組用於儲存版本號,前2個位元組儲存次版本號,後2個儲存主版本號,再接著是用於存放常量的常量池,由於常量的數量是不固定的,所以常量池的入口放置一個U2型別的資料(constant_pool_count)儲存常量池容量計數值。

常量池主要用於存放兩大類常量:字面量(Literal)和符號引用量(Symbolic References),字面量相當於Java語言層面常量的概念,如文字字串,宣告為final的常量值等,符號引用則屬於編譯原理方面的概念,包括瞭如下三種型別的常量:

  • 類和介面的全限定名
  • 欄位名稱和描述符
  • 方法名稱和描述符

2.1.1.2 執行時常量池

CLass檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。

執行時常量池相對於CLass檔案常量池的另外一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是並非預置入CLass檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法。

2.1.1.3 常量池的好處

常量池是為了避免頻繁的建立和銷燬物件而影響系統效能,其實現了物件的共享。

例如字串常量池,在編譯階段就把所有的字串文字放到一個常量池中。

  • (1)節省記憶體空間:常量池中所有相同的字串常量被合併,只佔用一個空間。
  • (2)節省執行時間:比較字串時,==比equals()快。對於兩個引用變數,只用==判斷引用是否相等,也就可以判斷實際值是否相等。

雙等號==的含義

  • 基本資料型別之間應用雙等號,比較的是他們的數值。
  • 複合資料型別(類)之間應用雙等號,比較的是他們在記憶體中的存放地址。

2.1.1.4 基本型別的包裝類和常量池

java中基本型別的包裝類的大部分都實現了常量池技術,即Byte,Short,Integer,Long,Character,Boolean。

這5種包裝類預設建立了數值[-128,127]的相應型別的快取資料,但是超出此範圍仍然會去建立新的物件。 兩種浮點數型別的包裝類Float,Double並沒有實現常量池技術。

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);這種情況下會建立新的物件。
  • (3)語句i4 == i5 + i6,因為+這個操作符不適用於Integer物件,首先i5和i6進行自動拆箱操作,進行數值相加,即i4 == 40。然後Integer物件無法與數值進行直接比較,所以i4自動拆箱轉為int值40,最終這條語句轉為40 == 40進行數值比較。

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”是直接在堆記憶體空間建立一個新的物件。只要使用new方法,便需要建立新的物件。
  • (2)連線表示式 +
    只有使用引號包含文字的方式建立的String物件之間使用“+”連線產生的新物件才會被加入字串池中。
    對於所有包含new方式新建物件(包括null)的“+”連線表示式,它所產生的新物件都不會被加入字串池中。
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的值之前,他們何時被賦值,以及被賦予什麼樣的值,都是個變數。因此A和B在被賦值之前,性質類似於一個變數。那麼s就不能在編譯期被確定,而只能在執行時被建立了。

String s1 = new String("xyz"); //建立了幾個物件?

解釋:

考慮類載入階段和實際執行時。

  • (1)類載入對一個類只會進行一次。”xyz”在類載入時就已經建立並駐留了(如果該類被載入之前已經有”xyz”字串被駐留過則不需要重複建立用於駐留的”xyz”例項)。駐留的字串是放在全域性共享的字串常量池中的。
  • (2)在這段程式碼後續被執行的時候,”xyz”字面量對應的String例項已經固定了,不會再被重複建立。所以這段程式碼將常量池中的物件複製一份放到heap中,並且把heap中的這個物件的引用交給s1 持有。

這條語句建立了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時能夠識別為同一字串的,自動優化成常量,引用自同一String物件.

在執行時建立的字串具有獨立的記憶體地址,所以不引用自同一String物件.

2.2 堆

Heap(堆)是JVM的記憶體資料區。

一個虛擬機器例項只對應一個堆空間,堆是執行緒共享的。堆空間是存放物件例項的地方,幾乎所有物件例項都在這裡分配。堆也是垃圾收集器管理的主要區域(也被稱為GC堆)。堆可以處於物理上不連續的記憶體空間中,只要邏輯上相連就行。

Heap 的管理很複雜,每次分配不定長的記憶體空間,專門用來儲存物件的例項。在Heap 中分配一定的記憶體來儲存物件例項,實際上也只是儲存物件例項的屬性值,屬性的型別和物件本身的型別標記等,並不儲存物件的方法(方法是指令,儲存在Stack中)。而物件例項在Heap中分配好以後,需要在Stack中儲存一個4位元組的Heap 記憶體地址,用來定位該物件例項在Heap 中的位置,便於找到該物件例項。

異常

堆中沒有足夠的記憶體進行物件例項分配時,並且堆也無法擴充套件時,會丟擲OutOfMemoryError異常。

img_36185f6ec733cda34a24ce9dff7338e8.png
image.png

2.3 Java棧

Stack(棧)是JVM的記憶體指令區。

描述的是java方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀,用於存放區域性變數表(基本型別、物件引用)、運算元棧、方法返回、常量池指標等資訊。 由編譯器自動分配釋放, 記憶體的分配是連續的。Stack的速度很快,管理很簡單,並且每次操作的資料或者指令位元組長度是已知的。所以Java 基本資料型別,Java 指令程式碼,常量都儲存在Stack中。

虛擬機器只會對棧進行兩種操作,以幀為單位的入棧和出棧。Java棧中的每個幀都儲存一個方法呼叫的區域性變數、運算元棧、指向常量池的指標等,且每一次方法呼叫都會建立一個幀,並壓棧。

異常

  • 如果一個執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常, 比如遞迴呼叫。
  • 如果執行緒生成數量過多,無法申請足夠多的記憶體時,則會丟擲OutOfMemoryError異常。比如tomcat請求數量非常多時,設定最大請求數。

2.3.1 棧幀

棧幀由三部分組成:區域性變數區、運算元棧、幀資料區。

2.3.1.1 區域性變數區

包含方法的引數和區域性變數。

以一個靜態方法為例

public class Demo {
     public static int doStaticMethod(int i, long l, float f, Object o, byte b) {
         return 0;
     }
 }

編譯之後的具備變數表位元組碼如下:

LOCALVARIABLEiIL0L10
LOCALVARIABLElJL0L11
LOCALVARIABLEfFL0L13
LOCALVARIABLEoLjava/lang/Object;L0L14
LOCALVARIABLEbBL0L15
MAXSTACK=1    //該方法操作棧的最大深度
MAXLOCALS=6  //確定了該方法所需要分配的最大區域性變數表的容量

可以認為Java棧幀裡的區域性變數表有很多的槽位組成,每個槽最大可以容納32位的資料型別,故方法引數裡的int i 引數佔據了一個槽位,而long l 引數就佔據了兩個槽(1和2),Object物件型別的引數其實是一個引用,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

例項方法的區域性變數表和靜態方法基本一樣,唯一區別就是例項方法在Java棧幀的區域性變數表裡第一個槽位(0位置)存的是一個this引用(當前物件的引用),後面就和靜態方法的一樣了。

2.3.1.2 運算元棧

Java沒有暫存器,故所有引數傳遞使用Java棧幀裡的運算元棧,運算元棧被組織成一個以字長為單位的陣列,它是通過標準的棧操作-入棧和出棧來進行訪問,而不是通過索引訪問。

看一個例子:

img_f904c14313a4136e0317d54c8f5db1bb.png
image.png

注意,對於區域性變數表的槽位,按照從0開始的順序,依次是方法引數,之後是方法內的區域性變數,區域性變數0就是a,1就是b,2就是c…… 編譯之後的位元組碼為:

// access flags 0x9
  public static add(II)I
   L0
    LINENUMBER 18 L0 // 對應原始碼第18行,以此類推
    ICONST_0 // 把常量0 push 到Java棧幀的運算元棧裡
    ISTORE 2 // 將0從運算元棧pop到區域性變數表槽2裡(c),完成賦值
   L1
    LINENUMBER 19 L1
    ILOAD 0 // 將區域性變數槽位0(a)push 到Java棧幀的運算元棧裡
    ILOAD 1 // 把區域性變數槽1(b)push到運算元棧 
    IADD // pop出a和b兩個變數,求和,把結果push到運算元棧
    ISTORE 2 // 把結果從運算元棧pop到區域性變數2(a+b的和給c賦值)
   L2
    LINENUMBER 21 L2
    ILOAD 2 // 區域性變數2(c)push 到運算元棧
    IRETURN // 返回結果
   L3
    LOCALVARIABLE a I L0 L3 0
    LOCALVARIABLE b I L0 L3 1
    LOCALVARIABLE c I L1 L3 2
    MAXSTACK = 2
    MAXLOCALS = 3
    

發現,整個計算過程的引數傳遞和運算元棧密切相關!如圖:

img_4c7bdea1ae8418a784d6b0d04ec067d7.png
image.png

2.3.1.3 棧資料區

存放一些用於支援常量池解析(常量池指標)、正常方法返回以及異常派發機制的資訊。即將常量池的符號引用轉化為直接地址引用、恢復發起呼叫的方法的幀進行正常返回,發生異常時轉交異常表進行處理。

2.4 本地方法棧

Native Method Stack

訪問本地方式時使用到的棧,為本地方法服務, 也就是呼叫虛擬機器使用到的Native方法服務。也會丟擲StackOverflowError和OutOfMemoryError異常。

2.5 PC暫存器

每個執行緒都擁有一個PC暫存器,執行緒私有的。
PC暫存器的內容總是下一條將被執行指令的”地址”,這裡的”地址”可以是一個本地指標,也可以是在方法位元組碼中相對於該方法起始指令的偏移量。如果該執行緒正在執行一個本地方法,則程式計數器內容為undefined,區域在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。

2.6 堆與棧

2.6.1 堆與棧裡存什麼

  • 1)堆中存的是物件。棧中存的是基本資料型別和堆中物件的引用。一個物件的大小是不可估計的,或者說是可以動態變化的,但是在棧中,一個物件只對應了一個4btye的引用。
  • 2)為什麼不把基本型別放堆中呢?因為其佔用的空間一般是1~8個位元組——需要空間比較少,而且因為是基本型別,所以不會出現動態增長的情況——長度固定,因此棧中儲存就夠了,如果把他存在堆中是沒有什麼意義的。可以這麼說,基本型別和物件的引用都是存放在棧中,而且都是幾個位元組的一個數,因此在程式執行時,他們的處理方式是統一的。但是基本型別、物件引用和物件本身就有所區別了,因為一個是棧中的資料一個是堆中的資料。最常見的一個問題就是,Java中引數傳遞時的問題。
  • 3)Java中的引數傳遞時傳值呢?還是傳引用?程式執行永遠都是在棧中進行的,因而引數傳遞時,只存在傳遞基本型別和物件引用的問題。不會直接傳物件本身。
int a = 0; //全域性初始化區
 
char p1; //全域性未初始化區
 
main(){
 
  int b; //棧
 
  char s[] = "abc"; //棧
 
  char p2; //棧
 
  char p3 = "123456"; //123456 在常量區,p3在棧上。
 
  static int c =0; //全域性(靜態)初始化區
 
  p1 = (char *)malloc(10); //堆
 
  p2 = (char *)malloc(20); //堆
 
}

2.6.2 堆記憶體與棧記憶體的區別

  • 申請和回收方式不同:棧上的空間是自動分配自動回收的,所以棧上的資料的生存週期只是在函式的執行過程中,執行後就釋放掉,不可以再訪問。而堆上的資料只要程式設計師不釋放空間,就一直可以訪問到,不過缺點是一旦忘記釋放會造成記憶體洩露。
  • 碎片問題:對於棧,不會產生不連續的記憶體塊;但是對於堆來說,不斷的new、delete勢必會產生上面所述的內部碎片和外部碎片。
  • 申請大小的限制:棧是向低地址擴充套件的資料結構,是一塊連續的記憶體的區域。棧頂的地址和棧的最大容量是系統預先規定好的,如果申請的空間超過棧的剩餘空間,就會產生棧溢位;對於堆,是向高地址擴充套件的資料結構,是不連續的記憶體區域。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。
  • 申請效率的比較:棧由系統自動分配,速度較快。但程式設計師是無法控制的;堆:是由new分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便。

3.JIT編譯器

  1. JIT編譯器是JVM的核心。它對於程式效能的影響最大。
  2. CPU只能執行彙編程式碼或者二進位制程式碼,所有程式都需要被翻譯成它們,然後才能被CPU執行。
  3. C++以及Fortran這類編譯型語言都會通過一個靜態的編譯器將程式編譯成CPU相關的二進位制程式碼。
  4. PHP以及Perl這列語言則是解釋型語言,只需要安裝正確的直譯器,它們就能執行在任何CPU之上。當程式被執行的時候,程式程式碼會被逐行解釋並執行。
  5. 編譯型語言的優缺點:
    • 速度快:因為在編譯的時候它們能夠獲取到更多的有關程式結構的資訊,從而有機會對它們進行優化。
    • 適用性差:它們編譯得到的二進位制程式碼往往是CPU相關的,在需要適配多種CPU時,可能需要編譯多次。
  6. 解釋型語言的優缺點:
    • 適應性強:只需要安裝正確的直譯器,程式在任何CPU上都能夠被執行
    • 速度慢:因為程式需要被逐行翻譯,導致速度變慢。同時因為缺乏編譯這一過程,執行程式碼不能通過編譯器進行優化。
  7. Java的做法是找到編譯型語言和解釋性語言的一箇中間點:
    • Java程式碼會被編譯:被編譯成Java位元組碼,而不是針對某種CPU的二進位制程式碼。
    • Java程式碼會被解釋:Java位元組碼需要被java程式解釋執行,此時,Java位元組碼被翻譯成CPU相關的二進位制程式碼。
    • JIT編譯器的作用:在程式執行期間,將Java位元組碼編譯成平臺相關的二進位制程式碼。正因為此編譯行為發生在程式執行期間,所以該編譯器被稱為Just-In-Time編譯器。

HotSpot 編譯

HotSpot VM名字也體現了JIT編譯器的工作方式。在VM開始執行一段程式碼時,並不會立即對它們進行編譯。在程式中,總有那麼一些“熱點”區域,該區域的程式碼會被反覆的執行。而JIT編譯器只會編譯這些“熱點”區域的程式碼。

這麼做的原因在於:
* 編譯那些只會被執行一次的程式碼價效比太低,直接解釋執行Java位元組碼反而更快。
* JVM在執行這些程式碼的時候,能獲取到這些程式碼的資訊,一段程式碼被執行的次數越多,JVM也對它們愈加熟悉,因此能夠在對它們進行編譯的時候做出一些優化。
在HotSpot VM中內嵌有兩個JIT編譯器,分別為Client Compiler和Server Compiler,但大多數情況下我們簡稱為C1編譯器和C2編譯器。開發人員可以通過如下命令顯式指定Java虛擬機器在執行時到底使用哪一種即時編譯器,如下所示:

-client:指定Java虛擬機器執行在Client模式下,並使用C1編譯器;
-server:指定Java虛擬機器執行在Server模式下,並使用C2編譯器。

除了可以顯式指定Java虛擬機器在執行時到底使用哪一種即時編譯器外,預設情況下HotSpot VM則會根據作業系統版本與物理機器的硬體效能自動選擇執行在哪一種模式下,以及採用哪一種即時編譯器。簡單來說,C1編譯器會對位元組碼進行簡單和可靠的優化,以達到更快的編譯速度;而C2編譯器會啟動一些編譯耗時更長的優化,以獲取更好的編譯質量。不過在Java7版本之後,一旦開發人員在程式中顯式指定命令“-server”時,預設將會開啟分層編譯(Tiered Compilation)策略,由C1編譯器和C2編譯器相互協作共同來執行編譯任務。不過在早期版本中,開發人員則只能夠通過命令“-XX:+TieredCompilation”手動開啟分層編譯策略。

總結

  1. Java綜合了編譯型語言和解釋性語言的優勢。
  2. Java會將類檔案編譯成為Java位元組碼,然後Java位元組碼會被JIT編譯器選擇性地編譯成為CPU能夠直接執行的二進位制程式碼。
  3. 將Java位元組碼編譯成二進位制程式碼後,效能會被大幅度提升。

4.類載入機制

Java虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的載入機制。

類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期包括了:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和解除安裝(Unloading)七個階段。其中驗證、準備和解析三個部分統稱為連線(Linking),這七個階段的發生順序如下圖所示:

img_6b3d88f87d05a48088005bddc275fb30.png
image.png

如上圖所示,載入、驗證、準備、初始化和解除安裝這五個階段的順序是確定的,類的載入過程必須按照這個順序來按部就班地開始,而解析階段則不一定,它在某些情況下可以在初始化階段後再開始。

類的生命週期的每一個階段通常都是互相交叉混合式進行的,通常會在一個階段執行的過程中呼叫或啟用另外一個階段。

4.1 類載入的時機

主動引用

一個類被主動引用之後會觸發初始化過程(載入,驗證,準備需再此之前開始)

  • 1)遇到new、get static、put static或invoke static這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令最常見的Java程式碼場景是:使用new關鍵字例項化物件時、讀取或者設定一個類的靜態欄位(被final修飾、已在編譯器把結果放入常量池的靜態欄位除外)時、以及呼叫一個類的靜態方法的時候。
  • 2)使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  • 3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要觸發父類的初始化。
  • 4)當虛擬機器啟動時,使用者需要指定一個執行的主類(包含main()方法的類),虛擬機器會先初始化這個類。
  • 5)當使用jdk7+的動態語言支援時,如果java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有進行過初始化,則需要先觸發器 初始化。

被動引用

一個類如果是被動引用的話,該類不會觸發初始化過程

  • 1)通過子類引用父類的靜態欄位,不會導致子類初始化。對於靜態欄位,只有直接定義該欄位的類才會被初始化,因此當我們通過子類來引用父類中定義的靜態欄位時,只會觸發父類的初始化,而不會觸發子類的初始化。
  • 2)通過陣列定義來引用類,不會觸發此類的初始化。
  • 3)常量在編譯階段會存入呼叫類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。

4.2 類載入過程

1、載入

在載入階段,虛擬機器需要完成以下三件事情:

  • 1)通過一個類的全限定名稱來獲取定義此類的二進位制位元組流。
  • 2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
  • 3)在java堆中生成一個代表這個類的java.lang.Class物件,作為方法區這些資料的訪問入口。
    相對於類載入過程的其他階段,載入階段是開發期相對來說可控性比較強,該階段既可以使用系統提供的類載入器完成,也可以由使用者自定義的類載入器來完成,開發人員可以通過定義自己的類載入器去控制位元組流的獲取方式。

2、驗證

驗證的目的是為了確保Class檔案中的位元組流包含的資訊符合當前虛擬機器的要求,而且不會危害虛擬機器自身的安全。不同的虛擬機器對類驗證的實現可能會有所不同,但大致都會完成以下四個階段的驗證:檔案格式的驗證、後設資料的驗證、位元組碼驗證和符號引用驗證。

  • 1)檔案格式的驗證:驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理,該驗證的主要目的是保證輸入的位元組流能正確地解析並儲存
    於方法區之內。經過該階段的驗證後,位元組流才會進入記憶體的方法區中進行儲存,後面的三個驗證都是基於方法區的儲存結構進行的。
  • 2)後設資料驗證:對類的後設資料資訊進行語義校驗(其實就是對類中的各資料型別進行語法校驗),保證不存在不符合Java語法規範的後設資料資訊。
  • 3)位元組碼驗證:該階段驗證的主要工作是進行資料流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在執行時不會做出危害虛擬機器安全的行為。
  • 4)符號引用驗證:這是最後一個階段的驗證,它發生在虛擬機器將符號引用轉化為直接引用的時候(解析階段中發生該轉化,後面會有講解),主要是對類自身以外的資訊(常量池中的各種符號引用)進行匹配性的校驗。

3、準備

準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中進行分配。

注:

  • 1)這時候進行記憶體分配的僅包括類變數(static),而不包括例項變數,例項變數會在物件例項化時隨著物件一塊分配在Java堆中。
  • 2)這裡所設定的初始值通常情況下是資料型別預設的零值(如0、0L、、false等),而不是被在Java程式碼中被顯式地賦予的值。

4、解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程

符號引用(Symbolic Reference):

符號引用以一組符號來描述所引用的目標,符號引用可以是任何形式的字面量,符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經在記憶體中。

直接引用(Direct Reference):

直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制程式碼。直接引用是與虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同的虛擬機器例項上翻譯出來的直接引用一般都不相同,如果有了直接引用,那引用的目標必定已經在記憶體中存在。

  • 1)類或介面的解析:判斷所要轉化成的直接引用是對陣列型別,還是普通的物件型別的引用,從而進行不同的解析。
  • 2)欄位解析:對欄位進行解析時,會先在本類中查詢是否包含有簡單名稱和欄位描述符都與目標相匹配的欄位,如果有,則查詢結束;如果沒有,則會按照繼承關係從上往下遞迴搜尋該類所實現的各個介面和它們的父介面,還沒有,則按照繼承關係從上往下遞迴搜尋其父類,直至查詢結束。
  • 3)類方法解析:對類方法的解析與對欄位解析的搜尋步驟差不多,只是多了判斷該方法所處的是類還是介面的步驟,而且對類方法的匹配搜尋,是先搜尋父類,再搜尋介面。
  • 4)介面方法解析:與類方法解析步驟類似,只是介面不會有父類,因此,只遞迴向上搜尋父介面就行了。

5、初始化

類初始化階段是類載入過程的最後一步,前面的類載入過程中,除了載入(Loading)階段使用者應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,才真正開始執行類中定義的Java程式程式碼。

初始化階段是執行類構造器<clinit>方法的過程。

  • 1)<clinit>方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序由語句在原始檔中出現的順序所決定。
  • 2)<clinit>方法與類的建構函式不同,它不需要顯式地呼叫父類構造器,虛擬機器會保證在子類的<clinit>方法執行之前,父類的<clinit>方法已經執行完畢,因此在虛擬機器中第一個執行的<clinit>方法的類一定是java.lang.Object。
  • 3)由於父類的<clinit>方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變數賦值操作。
  • 4)<clinit>方法對於類或者介面來說並不是必需的,如果一個類中沒有靜態語句塊也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成<clinit>方法。
  • 5)介面中可能會有變數賦值操作,因此介面也會生成<clinit>方法。但是介面與類不同,執行介面的<clinit>方法不需要先執行父介面的<clinit>方法。只有當父介面中定義的變數被使用時,父介面才會被初始化。另外,介面的實現類在初始化時也不會執行介面的<clinit>方法。
  • 6)虛擬機器會保證一個類的<clinit>方法在多執行緒環境中被正確地加鎖和同步。如果有多個執行緒去同時初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>方法,其它執行緒都需要阻塞等待,直到活動執行緒執行<clinit>方法完畢。如果在一個類的<clinit>方法中有耗時很長的操作,那麼就可能造成多個程式阻塞。

5.垃圾回收

5.1 按代實現垃圾回收

img_47e3660d78c4886a4f31bf6e53388f05.png
image.png

新生代(Young generation):

絕大多數最新被建立的物件會被分配到這裡,由於大部分物件在建立後會很快變得不可到達,所以很多物件被建立在新生代,然後消失。物件從這個區域消失的過程我們稱之為”minor GC“。

新生代中存在一個Eden區和兩個Survivor區。新物件會首先分配在 Eden 中(如果新物件過大,會直接分配在老年代中)。在GC中,Eden 中的物件會被移動到survivor中,直至物件滿足一定的年紀(定義為熬過GC的次數),會被移動到老年代(具體細節將在下邊垃圾收集演算法中討論)。

可以設定新生代和老年代的相對大小。這種方式的優點是新生代大小會隨著整個堆大小動態擴充套件。引數 -XX:NewRatio 設定老年代與新生代的比例。例如 -XX:NewRatio=8 指定老年代/新生代為8/1. 老年代佔堆大小的 7/8 ,新生代佔 1/8 .(預設即使1/8)
例如:-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8

老年代(Old generation):

物件沒有變得不可達,並且從新生代中存活下來,會被拷貝到這裡。其所佔用的空間要比新生代多。也正由於其相對較大的空間,發生在老年代上的GC要比新生代少得多。物件從老年代中消失的過程,可以稱之為”major GC“(或者”full GC“)

永久代(permanent generation):

像一些類的層級資訊,方法資料和方法資訊(如位元組碼,棧和變數大小),執行時常量池(jdk7之後移出永久代),已確定的符號引用和虛方法表等等,它們幾乎都是靜態的並且很少被解除安裝和回收,在JDK8之前的HotSpot虛擬機器中,類的這些“永久的”資料存放在一個叫做永久代的區域。永久代一段連續的記憶體空間,我們在JVM啟動之前可以通過設定-XX:MaxPermSize的值來控制永久代的大小。但是jdk8之後取消了永久代,這些後設資料被移到了一個與堆不相連的本地記憶體區域 。

5.2 怎樣判斷物件是否已經死亡

引用計數收集演算法

用計數是垃圾收集器中的早期策略。在這種方法中,堆中每個物件(不是引用)都有一個引用計數。當一個物件被建立時,且將該物件分配給一個變數,該變數計數設定為1。當任何其它變數被賦值為這個物件的引用時,計數加1(a = b,則b引用的物件+1),但當一個物件的某個引用超過了生命週期或者被設定為一個新值時,物件的引用計數減1。任何引用計數為0的物件可以被當作垃圾收集。當一個物件被垃圾收集時,它引用的任何物件計數減1。

  • 優點:引用計數收集器可以很快的執行,交織在程式執行中。對程式不被長時間打斷的實時環境比較有利。
  • 缺點: 無法檢測出迴圈引用。如父物件有一個對子物件的引用,子物件反過來引用父物件。這樣,他們的引用計數永遠不可能為0.

可達性分析演算法

通過一系列稱為”GC Roots”的物件作為起點,從這些節點開始向下搜尋,搜尋所有走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時(從GC Roots到此物件不可達),則證明此物件是不可用的。
可作為GC Roots的物件包括:

  • 虛擬機器棧中所引用的物件(本地變數表)
  • 方法區中類靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧中JNI引用的物件(Native物件)

5.3 java中的引用

強引用(Strong Reference):

在程式碼中普遍存在的,類似”Object obj = new Object”這類引用,只要強引用還在,垃圾收集器永遠不會回收掉被引用的物件

軟引用(Sofe Reference):

有用但並非必須的物件,可用SoftReference類來實現軟引用,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體異常異常。

弱引用(Weak Reference):

被弱引用關聯的物件只能生存到下一次垃圾收集發生之前,JDK提供了WeakReference類來實現弱引用。

虛引用(Phantom Reference):

也稱為幽靈引用或幻影引用,是最弱的一種引用關係,JDK提供了PhantomReference類來實現虛引用。

5.4 finalize方法什麼作用

對於一個物件來說,在被判斷沒有 GCroots 與其相關聯時,被第一次標記,然後判斷該物件是否應該執行finalize方法(判斷依據:如果物件的finalize方法被複寫,並且沒有執行過,則可以被執行)。如果允許執行那麼這個物件將會被放到一個叫F-Query的佇列中,等待被執行。(注意:由於finalize的優先順序比較低,所以該物件的的finalize方法不一定被執行,即使被執行了,也不保證finalize方法一定會執行完)

5.5 垃圾收集演算法

標記-清除演算法:

標記-清除演算法採用從根集合進行掃描,對存活的物件進行標記,標記完畢後,再掃描整個空間中未被標記的物件,進行回收。標記-清除演算法不需要進行物件的移動,並且僅對不存活的物件進行處理,在存活物件比較多的情況下極為高效,但由於標記-清除演算法直接回收不存活的物件,因此會造成記憶體碎片。

複製演算法:

這種收集演算法將堆疊分為兩個域,常稱為半空間。每次僅使用一半的空間,JVM生成的新物件則放在另一半空間中。GC執行時,它把可到達物件複製到另一半空間,從而壓縮了堆疊。這種方法適用於短生存期的物件,持續複製長生存期的物件則導致效率降低。並且對於指定大小堆來說,需要兩倍大小的記憶體,因為任何時候都只使用其中的一半。

標記整理演算法:

標記-整理演算法採用標記-清除演算法一樣的方式進行物件的標記,但在清除時不同,在回收不存活的物件佔用的空間後,會將所有的存活物件往一端空閒空間移動,並更新對應的指標。標記-整理演算法是在標記-清除演算法的基礎上,又進行了物件的移動,因此成本更高,但是卻解決了記憶體碎片的問題。

分代收集演算法:

在上邊三種收集思想中加入了分代的思想。

5.6 Hotspot實現垃圾回收細節

一致性:

在可達性分析期間整個系統看起來就像被凍結在某個時間點上,不可以出現分析過程中物件引用關係還在不斷變化的情況。

一致性要求導致GC進行時必須停頓所有Java執行執行緒。(Stop The World)即使在號稱不會發生停頓的CMS收集器中,列舉根節點時也是必須停頓的。

HotSpot使用的是準確式GC,當執行系統停頓下來後,並不需要一個不漏地檢查完所有執行上下文和全域性的引用位置,這是通過一組稱為OopMap的資料結構來達到的。

安全點(Safe Point):

程式只有在到達安全點時才能暫停。安全點的選定標準是“是否具有讓程式長時間執行的特徵”。“長時間執行”的最明顯特徵就是指令序列的複用,如方法呼叫、迴圈跳轉等,具有這些功能的指令才會產生安全點。

讓程式暫停的兩種方式:

* 搶先式中斷(Preemptive Suspension):在GC發生時,主動中斷所有執行緒,不需要執行緒執行的程式碼主動配合。如果發現有執行緒中斷的地方不在安全點上,就恢復執行緒讓它跑到安全點上。(不推薦)
* 主動式中斷(Voluntary Suspension):設一個標誌,各個執行緒主動去輪詢這個標誌,遇到中斷則暫停。輪詢地方與安全點重合。

5.7 垃圾收集器

HotSpot中幾種常見的垃圾收集器:

img_e6c2bb37aa82e7ce529b85a6464a023f.png
image.png

5.7.1 Serial收集器

Serial收集器是最基本、發展歷史最悠久的收集器,曾經(在JDK 1.3.1之前)是虛擬機器新生代收集的唯一選擇。

img_38833f388b890cb99324f788b22bc833.png
image.png

特性:

這個收集器是一個單執行緒的收集器,但它的“單執行緒”的意義並不僅僅說明它只會使用一個CPU或一條收集執行緒去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束。Stop The World

應用場景:

Serial收集器是虛擬機器執行在Client模式下的預設新生代收集器。

優勢:

簡單而高效(與其他收集器的單執行緒比),對於限定單個CPU的環境來說,Serial收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率。

5.7.2 ParNew收集器

img_b3015ed59e4ff8094dd6009ccde0da1c.png
image.png

特性:

ParNew收集器其實就是Serial收集器的多執行緒版本,除了使用多條執行緒進行垃圾收集之外,其餘行為包括Serial收集器可用的所有控制引數、收集演算法、Stop The World、物件分配規則、回收策略等都與Serial收集器完全一樣,在實現上,這兩種收集器也共用了相當多的程式碼。

應用場景:

ParNew收集器是許多執行在Server模式下的虛擬機器中首選的新生代收集器。有一個很重要的原因是除了Serial收集器外,目前只有它能與CMS收集器配合工作。

Serial收集器 VS ParNew收集器:

ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由於存線上程互動的開銷,該收集器在通過超執行緒技術實現的兩個CPU的環境中都不能百分之百地保證可以超越Serial收集器。然而,隨著可以使用的CPU的數量的增加,它對於GC時系統資源的有效利用還是很有好處的。

5.7.3 Parallel Scavenge收集器

特性:

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製演算法的收集器,又是並行的多執行緒收集器。

應用場景:

停頓時間越短就越適合需要與使用者互動的程式,良好的響應速度能提升使用者體驗,而高吞吐量則可以高效率地利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。

對比分析:

Parallel Scavenge收集器 VS CMS等收集器:

Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關 注點是儘可能地縮短垃圾收集時使用者執行緒的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。

由於與吞吐量關係密切,Parallel Scavenge收集器也經常稱為“吞吐量優先”收集器。

Parallel Scavenge收集器 VS ParNew收集器:

Parallel Scavenge收集器與ParNew收集器的一個重要區別是它具有自適應調節策略。

GC自適應的調節策略:

Parallel Scavenge收集器有一個引數-XX:+UseAdaptiveSizePolicy。當這個引數開啟之後,就不需要手工指定新生代的大小、Eden與Survivor區的比例、晉升老年代物件年齡等細節引數了,虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC自適應的調節策略(GC Ergonomics)。

5.7.4 Serial Old收集器

img_3a40b20e116224dd3f36ef82f3260fa7.png
image.png

特性:

Serial Old是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用標記-整理演算法。

應用場景:

  • Client模式:Serial Old收集器的主要意義也是在於給Client模式下的虛擬機器使用。
  • Server模式:如果在Server模式下,那麼它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

5.7.5 Parallel Old收集器

img_d1df823e18aa1e9b6c26d25cf4ae073d.png
image.png

特性:

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。

應用場景:

在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。

這個收集器是在JDK 1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處於比較尷尬的狀態。原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old收集器外別無選擇(Parallel Scavenge收集器無法與CMS收集器配合工作)。由於老年代Serial Old收集器在服務端應用效能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果,由於單執行緒的老年代收集中無法充分利用伺服器多CPU的處理能力,在老年代很大而且硬體比較高階的環境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合“給力”。直到Parallel Old收集器出現後,“吞吐量優先”收集器終於有了比較名副其實的應用組合。

5.7.6 CMS收集器

img_9678dcd6123347409a839dbda2248208.png
image.png

特性:

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
CMS收集器是基於“標記—清除”演算法實現的,它的運作過程相對於前面幾種收集器來說更復雜一些,整個過程分為4個步驟:

  • 初始標記(CMS initial mark):初始標記僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快,需要“Stop The World”。
  • 併發標記(CMS concurrent mark):併發標記階段就是進行GC Roots Tracing的過程。
  • 重新標記(CMS remark):重新標記階段是為了修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,仍然需要“Stop The World”。
  • 併發清除(CMS concurrent sweep):併發清除階段會清除物件。

由於整個過程中耗時最長的併發標記和併發清除過程收集器執行緒都可以與使用者執行緒一起工作,所以,從總體上來說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。

優點:

CMS是一款優秀的收集器,它的主要優點在名字上已經體現出來了:併發收集、低停頓。

缺點:

  • 1)CMS收集器對CPU資源非常敏感

    其實,面向併發設計的程式都對CPU資源比較敏感。在併發階段,它雖然不會導致使用者執行緒停頓,但是會因為佔用了一部分執行緒(或者說CPU資源)而導致應用程式變慢,總吞吐量會降低。

    CMS預設啟動的回收執行緒數是(CPU數量+3)/ 4,也就是當CPU在4個以上時,併發回收時垃圾收集執行緒不少於25%的CPU資源,並且隨著CPU數量的增加而下降。但是當CPU不足4個(譬如2個)時,CMS對使用者程式的影響就可能變得很大。

  • 2)CMS收集器無法處理浮動垃圾

    CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。

    由於CMS併發清理階段使用者執行緒還在執行著,伴隨程式執行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。

    也是由於在垃圾收集階段使用者執行緒還需要執行,那也就還需要預留有足夠的記憶體空間給使用者執行緒使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程式運作使用。要是CMS執行期間預留的記憶體無法滿足程式需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機器將啟動後備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。

  • 3)CMS收集器會產生大量空間碎片

    CMS是一款基於“標記—清除”演算法實現的收集器,這意味著收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大物件分配帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前物件,不得不提前觸發一次Full GC。

5.7.7 G1收集器

img_84de6844560ae43ee61d778544877d49.png
image.png

特性:

G1(Garbage-First)是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是未來可以替換掉JDK 1.5中釋出的CMS收集器。與其他GC收集器相比,G1具備如下特點。

  • 1)並行與併發

    G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java執行緒執行的GC動作,G1收集器仍然可以通過併發的方式讓Java程式繼續執行。

  • 2)分代收集

    與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間、熬過多次GC的舊物件以獲取更好的收集效果。

  • 3)空間整合

    與CMS的“標記—清理”演算法不同,G1從整體來看是基於“標記—整理”演算法實現的收集器,從區域性(兩個Region之間)上來看是基於“複製”演算法實現的,但無論如何,這兩種演算法都意味著G1運作期間不會產生記憶體空間碎片,收集後能提供規整的可用記憶體。這種特性有利於程式長時間執行,分配大物件時不會因為無法找到連續記憶體空間而提前觸發下一次GC。

  • 4)可預測的停頓

    這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。

在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體佈局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。

G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了G1收集器在有限的時間內可以獲取儘可能高的收集效率。

執行過程:

G1收集器的運作大致可劃分為以下幾個步驟:

  • 1)初始標記(Initial Marking):初始標記階段僅僅只是標記一下GC Roots能直接關聯到的物件,並且修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式併發執行時,能在正確可用的Region中建立新物件,這階段需要停頓執行緒,但耗時很短。
  • 2)併發標記(Concurrent Marking):併發標記階段是從GC Root開始對堆中物件進行可達性分析,找出存活的物件,這階段耗時較長,但可與使用者程式併發執行。
  • 3)最終標記(Final Marking):最終標記階段是為了修正在併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機器將這段時間物件變化記錄線上程Remembered Set Logs裡面,最終標記階段需要把Remembered Set Logs的資料合併到Remembered Set中,這階段需要停頓執行緒,但是可並行執行。
  • 4)篩選回收(Live Data Counting and Evacuation):篩選回收階段首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃,這個階段其實也可以做到與使用者程式一起併發執行,但是因為只回收一部分Region,時間是使用者可控制的,而且停頓使用者執行緒將大幅提高收集效率。
何時會丟擲OutOfMemoryException,並不是記憶體被耗空的時候才丟擲
    * JVM98%的時間都花費在記憶體回收
    * 每次回收的記憶體小於2%

6.JVM引數

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 中最大堆大小有三方面限制:相關作業系統的資料模型(32-bt還是64-bit)限制;系統的可用虛擬記憶體限制;系統的可用實體記憶體限制。32位系統下,一般限制在1.5G~2G;64為作業系統對記憶體無限制。

java -Xmx3550m -Xms3550m -Xmn2g-Xss128k
-Xmx3550m:設定JVM最大可用記憶體為3550M。
-Xms3550m:設定JVM促使記憶體為3550m。此值可以設定與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配記憶體。
-Xmn2g:設定年輕代大小為2G。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為64m,所以增大年輕代後,將會減小年老代大小。此值對系統效能影響較大,Sun官方推薦配置為整個堆的3/8。
-Xss128k:設定每個執行緒的堆疊大小。JDK5.0以後每個執行緒堆疊大小為1M,以前每個執行緒堆疊大小為256K。更具應用的執行緒所需記憶體大小進行調整。在相同實體記憶體下,減小這個值能生成更多的執行緒。但是作業系統對一個程式內的執行緒數還是有限制的,不能無限生成,經驗值在3000~5000左右。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:設定年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設定為4,則年輕代與年老代所佔比值為1:4,年輕代佔整個堆疊的1/5
-XX:SurvivorRatio=4:設定年輕代中Eden區與Survivor區的大小比值。設定為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區佔整個年輕代的1/6
-XX:MaxPermSize=16m:設定持久代大小為16m。
-XX:MaxTenuringThreshold=0:設定垃圾最大年齡。如果設定為0的話,則年輕代物件不經過Survivor區,直接進入年老代。對於年老代比較多的應用,可以提高效率。如果將此值設定為一個較大值,則年輕代物件會在Survivor區進行多次複製,這樣可以增加物件再年輕代的存活時間,增加在年輕代即被回收的概論。

6.1.2 回收器選擇

JVM給了三種選擇:序列收集器、並行收集器、併發收集器,但是序列收集器只適用於小資料量的情況,所以這裡的選擇主要針對並行收集器和併發收集器。預設情況下,JDK5.0以前都是使用序列收集器,如果想使用其他收集器需要在啟動時加入相應引數。JDK5.0以後,JVM會根據當前系統配置進行判斷。

吞吐量優先的並行收集器

如上文所述,並行收集器主要以到達一定的吞吐量為目標,適用於科學技術和後臺處理等。

java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC:選擇垃圾收集器為並行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用併發收集,而年老代仍舊使用序列收集。
-XX:ParallelGCThreads=20:配置並行收集器的執行緒數,即:同時多少個執行緒一起進行垃圾回收。此值最好配置與處理器數目相等。
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會自動調整年輕代大小,以滿足此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100-XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy:設定此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低相應時間或者收集頻率等,此值建議使用並行收集器時,一直開啟。

響應時間優先的併發收集器

如上文所述,併發收集器主要是保證系統的響應時間,減少垃圾收集時的停頓時間。適用於應用伺服器、電信領域等。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:設定年老代為併發收集。測試中配置這個以後,-XX:NewRatio=4的配置失效了,原因不明。所以,此時年輕代大小最好用-Xmn設定。
-XX:+UseParNewGC:設定年輕代為並行收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設定,所以無需再設定此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:由於併發收集器不對記憶體空間進行壓縮、整理,所以執行一段時間以後會產生“碎片”,使得執行效率降低。此值設定執行多少次GC以後對記憶體空間進行壓縮、整理。
-XX:+UseCMSCompactAtFullCollection:開啟對年老代的壓縮。可能會影響效能,但是可以消除碎片

6.1.3輔助資訊

JVM提供了大量命令列引數,列印資訊,供除錯使用。主要有以下一些:

-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:列印每次垃圾回收前,程式未中斷的執行時間。可與上面混合使用
輸出形式: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 引數詳細說明

引數名稱 含義 預設值 說明
-Xms 初始堆大小 實體記憶體的1/64(<1GB) 預設(MinHeapFreeRatio引數可以調整)空餘堆記憶體小於40%時,JVM就會增大堆直到-Xmx的最大限制.
-Xmx 最大堆大小 實體記憶體的1/4(<1GB) 預設(MaxHeapFreeRatio引數可以調整)空餘堆記憶體大於70%時,JVM會減少堆直到-Xms的最小限制
-Xmn 年輕代大小(1.4or lator) 注意:此處的大小是(eden+ 2 survivor space).與jmap -heap中顯示的New gen是不同的。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小.增大年輕代後,將會減小年老代大小.此值對系統效能影響較大,Sun官方推薦配置為整個堆的3/8
-XX:NewSize 設定年輕代大小(for 1.3/1.4)
-XX:MaxNewSize 年輕代最大值(for 1.3/1.4)
-XX:PermSize 設定持久代(perm gen)初始值 實體記憶體的1/64
-XX:MaxPermSize 設定持久代最大值 實體記憶體的1/4
-Xss 每個執行緒的堆疊大小 JDK5.0以後每個執行緒堆疊大小為1M,以前每個執行緒堆疊大小為256K.更具應用的執行緒所需記憶體大小進行調整.在相同實體記憶體下,減小這個值能生成更多的執行緒.但是作業系統對一個程式內的執行緒數還是有限制的,不能無限生成,經驗值在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區)與年老代的比值(除去持久代) -XX:NewRatio=4表示年輕代與年老代所佔比值為1:4,年輕代佔整個堆疊的1/5Xms=Xmx並且設定了Xmn的情況下,該引數不需要進行設定。
-XX:SurvivorRatio Eden區與Survivor區的大小比值 設定為8,則兩個Survivor區與一個Eden區的比值為2:8,一個Survivor區佔整個年輕代的1/10
-XX:LargePageSizeInBytes 記憶體頁的大小不可設定過大, 會影響Perm的大小 =128m
-XX:+UseFastAccessorMethods 原始型別的快速優化
-XX:+DisableExplicitGC 關閉System.gc() 這個引數需要嚴格的測試
-XX:MaxTenuringThreshold 垃圾最大年齡 如果設定為0的話,則年輕代物件不經過Survivor區,直接進入年老代.對於年老代比較多的應用,可以提高效率.如果將此值設定為一個較大值,則年輕代物件會在Survivor區進行多次複製,這樣可以增加物件再年輕代的存活時間,增加在年輕代即被回收的概率.該引數只有在序列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 單位位元組 新生代採用Parallel Scavenge GC時無效,另一種直接在舊生代分配的情況是大的陣列物件,且陣列中無外部引用物件.
-XX:TLABWasteTargetPercent TLAB佔eden區的百分比 1%
-XX:+CollectGen0First FullGC時是否先YGC false

並行收集器相關引數

引數名稱 含義 預設值 說明
-XX:+UseParallelGC Full GC採用parallel MSC(此項待驗證) 選擇垃圾收集器為並行收集器.此配置僅對年輕代有效.即上述配置下,年輕代使用併發收集,而年老代仍舊使用序列收集.(此項待驗證)
-XX:+UseParNewGC 設定年輕代為並行收集 可與CMS收集同時使用,JDK5.0以上,JVM會根據系統配置自行設定,所以無需再設定此值
-XX:ParallelGCThreads 並行收集器的執行緒數 此值最好配置與處理器數目相等 同樣適用於CMS
-XX:+UseParallelOldGC 年老代垃圾收集方式為並行收集(Parallel Compacting) 這個是JAVA 6出現的引數選項
-XX:MaxGCPauseMillis 每次年輕代垃圾回收的最長時間(最大暫停時間) 如果無法滿足此時間,JVM會自動調整年輕代大小,以滿足此值.
-XX:+UseAdaptiveSizePolicy 自動選擇年輕代區大小和相應的Survivor區比例 設定此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低相應時間或者收集頻率等,此值建議使用並行收集器時,一直開啟.
-XX:GCTimeRatio 設定垃圾回收時間佔程式執行時間的百分比 公式為1/(1+n)
-XX:+ScavengeBeforeFullGC Full GC前呼叫YGC true Do young generation GC prior to a full GC. (Introduced in 1.4.1.)

CMS相關引數

引數名稱 含義 預設值 說明
-XX:+UseConcMarkSweepGC 使用CMS記憶體收集 測試中配置這個以後,-XX:NewRatio=4的配置失效了,原因不明.所以,此時年輕代大小最好用-Xmn設定
-XX:+AggressiveHeap 試圖是使用大量的實體記憶體長時間大記憶體。使用的優化,能檢查計算資源(記憶體, 處理器數量)至少需要256MB記憶體,大量的CPU/記憶體, (在1.4.1在4CPU的機器上已經顯示有提升)
-XX:CMSFullGCsBeforeCompaction 多少次後進行記憶體壓縮 由於併發收集器不對記憶體空間進行壓縮,整理,所以執行一段時間以後會產生”碎片”,使得執行效率降低.此值設定執行多少次GC以後對記憶體空間進行壓縮,整理.
-XX:+CMSParallelRemarkEnabled 降低標記停頓
-XX+UseCMSCompactAtFullCollection 在FULL GC的時候, 對年老代的壓縮 CMS是不會移動記憶體的, 因此, 這個非常容易產生碎片, 導致記憶體不夠用, 因此, 記憶體的壓縮這個時候就會被啟用。 增加這個引數是個好習慣。可能會影響效能,但是可以消除碎片
-XX:+UseCMSInitiatingOccupancyOnly 使用手動定義初始化定義開始CMS收集 禁止hostspot自行觸發CMS GC
-XX:CMSInitiatingOccupancyFraction=70 使用cms作為垃圾回收,使用70%後開始CMS收集 92 為了保證不出現promotion failed(見下面介紹)錯誤,該值的設定需要滿足以下公式CMSInitiatingOccupancyFraction計算公式
-XX:CMSInitiatingPermOccupancyFraction 設定Perm Gen使用到達多少比率時觸發 92
-XX:+CMSIncrementalMode 設定為增量模式 用於單CPU情況
-XX:+CMSClassUnloadingEnabled 相對於並行收集器,CMS收集器預設不會對永久代進行垃圾回收。如果希望對永久代進行垃圾回收,可用設定標誌-XX:+CMSClassUnloadingEnabled。在早期JVM版本中,要求設定額外的標誌-XX:+CMSPermGenSweepingEnabled。注意,即使沒有設定這個標誌,一旦永久代耗盡空間也會嘗試進行垃圾回收,但是收集不會是並行的,而再一次進行Full GC。

輔助資訊

引數名稱 含義 預設值 說明
-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 列印每次垃圾回收前,程式未中斷的執行時間.可與上面混合使用 輸出形式: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效能調優

7.1 堆設定調優

年輕代大小選擇

  • 響應時間優先的應用:儘可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇)。在此種情況下,年輕代收集發生的頻率也是最小的。同時,減少到達年老代的物件。
  • 吞吐量優先的應用:儘可能的設定大,可能到達Gbit的程度。因為對響應時間沒有要求,垃圾收集可以並行進行,一般適合8CPU以上的應用。
    通過-XX:NewRadio設定新生代與老年代的大小比例,通過-Xmn來設定新生代的大小。

年老代大小選擇

  • 響應時間優先的應用:年老代使用併發收集器,所以其大小需要小心設定,一般要考慮併發會話率和會話持續時間等一些引數。如果堆設定小了,可以會造成記憶體碎片、高回收頻率以及應用暫停而使用傳統的標記清除方式;如果堆大了,則需要較長的收集時間。最優化的方案,一般需要參考以下資料獲得:

    • 併發垃圾收集資訊
    • 持久代併發收集次數
    • 傳統GC資訊
    • 花在年輕代和年老代回收上的時間比例
  • 吞吐量優先的應用:一般吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代。原因是,這樣可以儘可能回收掉大部分短期物件,減少中期的物件,而年老代盡存放長期存活物件。

  • 較小堆引起的碎片問題
    因為年老代的併發收集器使用標記、清除演算法,所以不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合併,這樣可以分配給較大的物件。但是,當堆空間較小時,執行一段時間以後,就會出現“碎片”,如果併發收集器找不到足夠的空間,那麼併發收集器將會停止,然後使用傳統的標記、清除方式進行回收。如果出現“碎片”,可能需要進行如下配置:

    • -XX:+UseCMSCompactAtFullCollection:使用併發收集器時,開啟對年老代的壓縮。
    • -XX:CMSFullGCsBeforeCompaction=0:上面配置開啟的情況下,這裡設定多少次Full GC後,對年老代進行壓縮

7.2 GC策略調優

  1. 能夠忍受full gc的停頓?

    是:選擇throughput

    否:如果堆較小,使用CMS或者G1;如果堆較大,選擇G1

  2. 使用預設配置能達到期望目標嗎?

    首先儘量使用預設配置,因為垃圾收集技術在不斷髮展成熟,自動優化大多數的效果是最好的。如果預設配置沒有達到期望,請確認垃圾收集是否是效能瓶頸。如負荷較高的應用,如果垃圾收集上的時間不超過3%,即使進行垃圾回收調優效果也不大。

  3. 應用的停頓時間和預期的目標接近嗎?

    是:調整最大停頓時間設定可能是需要做的

    否:需要進行其他調整

    如果停頓時間太長,但是吞吐量正常,可以嘗試減少新生代大小(如果是full gc,則減少老年代大小),這樣停頓時間變短,但是單次時間變長

  4. GC停頓很短了,但是吞吐量上不去?

    增大堆的大小,但是單次停頓時間會加長

  5. 使用併發收集器,發生了由併發模式失敗引發的full gc?

    如果CPU資源充足,可以增加併發GC的執行緒數數

  6. 使用併發收集器,發生由晉升失敗引起的full gc?

    如果是CMS,意味著發生了碎片化,這種情況下:使用跟大的堆;儘早啟動後臺回收
    如果堆空間較大,可以選擇使用G1

7.3 JIT調優

  1. 一般只需要選擇是使用客戶端版或者伺服器版的JIT編譯器即可。
  2. 客戶端版的JIT編譯器使用:-client指定,伺服器版的使用:-server。
  3. 選擇哪種型別一般和硬體的配置相關,當然隨著硬體的發展,也沒有一個確定的標準哪種硬體適合哪種配置。
  4. 兩種JIT編譯器的區別:
    • Client版對於程式碼的編譯早於Server版,也意味著程式碼的執行速度在程式執行早期Client版更快。
    • Server版對程式碼的編譯會稍晚一些,這是為了獲取到程式本身的更多資訊,以便編譯得到優化程度更高的程式碼。因為執行在Server上的程式通常都會持續很久。
  5. Tiered編譯的原理:
    • JVM啟動之初使用Client版JIT編譯器
    • 當HotSpot形成之後使用Server版JIT編譯器再次編譯
  6. 在Java 8中,預設使用Tiered編譯方式。

不過在Java7版本之後,一旦開發人員在程式中顯式指定命令“-server”時,預設將會開啟分層編譯(Tiered Compilation)策略,由client編譯器和server編譯器相互協作共同來執行編譯任務。不過在早期版本中,開發人員則只能夠通過命令“-XX:+TieredCompilation”手動開啟分層編譯策略。

  • -Xint:完全採用直譯器模式執行程式;
  • -Xcomp:完全採用即時編譯器模式執行程式;
  • -Xmixed:採用直譯器+即時編譯器的混合模式共同執行程式。

啟動優化

Application -client -server -XX:+TieredCompilation 類數量
HelloWorld 0.08s 0.08s 0.08s Few
NetBeans 2.83s 3.92s 3.07s ~10000
HelloWorld 51.5s 54.0s 52.0s ~20000

總結

  1. 當程式的啟動速度越快越好時,使用Client版的JIT編譯器更好。
  2. 就啟動速度而言,Tiered編譯方式的效能和只使用Client的方式十分接近,因為Tiered編譯本質上也會在啟動是使用Client JIT編譯器。

批處理優化

對於批處理任務,任務量的大小是決定執行時間和使用哪種編譯策略的最重要因素:

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

可以發現幾個結論:

  1. 當任務數量小的時候,使用Client或者Tiered方式的效能類似,而當任務數量大的時候,使用Tiered會獲得最好的效能,因為它綜合使用了Client和Server兩種編譯器,在程式執行之初,使用Client JIT編譯器得到一部分編譯過的程式碼,在程式“熱點”逐漸形成之後,使用Server JIT編譯器得到高度優化的編譯後程式碼。
  2. Tiered編譯方式的效能總是好於單獨使用Server JIT編譯器。
  3. Tiered編譯方式在任務量不大的時候,和單獨使用Client JIT編譯器的效能相當。

總結

  1. 當一段批處理程式需要被執行時,使用不同的策略進行測試,使用速度最快的那一種。
  2. 對於批處理程式,考慮使用Tiered編譯方式作為預設選項。

長時間執行應用的優化

對於長時間執行的應用,比如Servlet程式等,一般會使用吞吐量來測試它們的效能。 以下的一組資料表示了一個典型的資料獲取程式在使用不同“熱身時間”以及不同編譯策略時,對吞吐量(OPS)的影響(執行時間為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秒,因為執行時間為60秒,所以編譯器也有機會在次期間做出優化。

從上面的資料可以發現的幾個結論:

  1. 對於典型的資料獲取程式,編譯器對程式碼編譯和優化發生的十分迅速,當“熱身時間”顯著增加時,如從60秒增加到300秒,最後得到的OPS差異並不明顯。
  2. -server JIT編譯器和Tiered編譯的效能顯著優於-client JIT編譯器。

總結

  1. 對於長時間執行的應用,總是使用-server JIT編譯器或者Tiered編譯策略。

程式碼快取調優(Tuning the Code Cache)

當JVM對程式碼進行編譯後,被編譯的程式碼以彙編指令的形式存在於程式碼快取中(Code Cache),顯然這個快取區域也是有大小限制的,當此區域被填滿了之後,編譯器就不能夠再編譯其他Java位元組碼了。

Code Cache的最大空間可以通過:-XX:ReservedCodeCacheSize=N來進行設定。

7.4 JVM執行緒調優

調節執行緒棧大小

通過設定-Xss引數,在記憶體比較稀缺的機器上,可以減少執行緒棧的大小,在32位的JVM上,可以減少執行緒棧大小,可以稍稍增加堆的可用記憶體。每個執行緒預設會開啟1M的堆疊,用於存放棧幀、呼叫引數、區域性變數等,對大多數應用而言這個預設值太了,一般256K就足用。

偏向鎖

使用-XX:UseBiasedLocking選項來禁用偏向鎖,偏向鎖預設開啟。偏向鎖可以提高快取命中率,但是因為偏向鎖也需要一些簿記資訊,有時候效能會更糟,比如使用了某些執行緒池,同步資源或程式碼一直都是多執行緒訪問的,那麼消除偏向鎖這一步驟對你來說就是多餘的。

自旋鎖

使用-XX:UseSpinning引數可以設定自旋鎖是否開啟,但是Java7以後自旋鎖無法禁用。

執行緒優先順序

每個執行緒都可以由開發人員指定優先順序,不過真正執行時的優先順序還取決於作業系統為每個執行緒計算的當前優先順序。開發人員不能依賴執行緒優先順序來影響其效能,如果要提高某些任務的優先順序,就必須使用應用層邏輯來劃分優先順序,可以通過將任務指派給不同執行緒池並修改哪些池子大小來實現。

總結

理解執行緒如何運作,可以獲得很大的效能優勢,不過就執行緒的效能而言,沒有太多可以調優的:可以修改的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執行頻率變小,CMS沒有停滯,也不會有promotion failed問題,記憶體回收得很乾淨

8.常見問題

8.1 記憶體洩漏及解決方法

  • 1.系統崩潰前的一些現象:

    • 每次垃圾回收的時間越來越長,由之前的10ms延長到50ms左右,FullGC的時間也有之前的0.5s延長到4、5s
    • FullGC的次數越來越多,最頻繁時隔不到1分鐘就進行一次FullGC
    • 年老代的記憶體越來越大並且每次FullGC後年老代沒有記憶體被釋放

    之後系統會無法響應新的請求,逐漸到達OutOfMemoryError的臨界值。

  • 2.生成堆的dump檔案

    通過JMX的MBean生成當前的Heap資訊,大小為一個3G(整個堆的大小)的hprof檔案,如果沒有啟動JMX可以通過Java的jmap命令來生成該檔案。

  • 3.分析dump檔案

    下面要考慮的是如何開啟這個3G的堆資訊檔案,顯然一般的Window系統沒有這麼大的記憶體,必須藉助高配置的Linux。當然我們可以藉助X-Window把Linux上的圖形匯入到Window。
    我們考慮用下面幾種工具開啟該檔案:

    • Visual VM
    • IBM HeapAnalyzer
    • JDK 自帶的Hprof工具

    使用這些工具時為了確保載入速度,建議設定最大記憶體為6G。使用後發現,這些工具都無法直觀地觀察到記憶體洩漏,Visual VM雖能觀察到物件大小,但看不到呼叫堆疊;HeapAnalyzer雖然能看到呼叫堆疊,卻無法正確開啟一個3G的檔案。因此,我們又選用了Eclipse專門的靜態記憶體分析工具:Mat

  • 4.分析記憶體洩漏

    通過Mat我們能清楚地看到,哪些物件被懷疑為記憶體洩漏,哪些物件佔的空間最大及物件的呼叫關係。針對本案,在ThreadLocal中有很多的JbpmContext例項,經過調查是JBPM的Context沒有關閉所致。

    另,通過Mat或JMX我們還可以分析執行緒狀態,可以觀察到執行緒被阻塞在哪個物件上,從而判斷系統的瓶頸。

  • 5.迴歸問題

    • Q:為什麼崩潰前垃圾回收的時間越來越長?
    • A:根據記憶體模型和垃圾回收演算法,垃圾回收分兩部分:記憶體標記、清除(複製),標記部分只要記憶體大小固定時間是不變的,變的是複製部分,因為每次垃圾回收都有一些回收不掉的記憶體,所以增加了複製量,導致時間延長。所以,垃圾回收的時間也可以作為判斷記憶體洩漏的依據
    • Q:為什麼Full GC的次數越來越多?
    • A:因此記憶體的積累,逐漸耗盡了年老代的記憶體,導致新物件分配沒有更多的空間,從而導致頻繁的垃圾回收
    • Q:為什麼年老代佔用的記憶體越來越大?
    • A:因為年輕代的記憶體無法被回收,越來越多地被Copy到年老代

8.2 年老代堆空間被佔滿

  • 異常: java.lang.OutOfMemoryError: Java heap space
  • 說明:
img_f85a0959401762bf4d016260c6813ab5.png
image.png
這是最典型的記憶體洩漏方式,簡單說就是所有堆空間都被無法回收的垃圾物件佔滿,虛擬機器無法再在分配新空間。

如上圖所示,這是非常典型的記憶體洩漏的垃圾回收情況圖。所有峰值部分都是一次垃圾回收點,所有谷底部分表示是一次垃圾回收後剩餘的記憶體。連線所有谷底的點,可以發現一條由底到高的線,這說明,隨時間的推移,系統的堆空間被不斷佔滿,最終會佔滿整個堆空間。因此可以初步認為系統內部可能有記憶體洩漏。(上面的圖僅供示例,在實際情況下收集資料的時間需要更長,比如幾個小時或者幾天)
  • 解決:

    這種方式解決起來也比較容易,一般就是根據垃圾回收前後情況對比,同時根據物件引用情況(常見的集合物件引用)分析,基本都可以找到洩漏點。

8.3 持久代被佔滿

  • 異常:java.lang.OutOfMemoryError: PermGen space

  • 說明:

    Perm空間被佔滿。無法為新的class分配儲存空間而引發的異常。這個異常以前是沒有的,但是在Java反射大量使用的今天這個異常比較常見了。主要原因就是大量動態反射生成的類不斷被載入,最終導致Perm區被佔滿。
    更可怕的是,不同的classLoader即便使用了相同的類,但是都會對其進行載入,相當於同一個東西,如果有N個classLoader那麼他將會被載入N次。因此,某些情況下,這個問題基本視為無解。當然,存在大量classLoader和大量反射類的情況其實也不多。

  • 解決:

    • 1.-XX:MaxPermSize=16m
    • 2.換用JDK。比如JRocket

8.4 堆疊溢位

  • 異常:java.lang.StackOverflowError
  • 說明:這個就不多說了,一般就是遞迴沒返回,或者迴圈呼叫造成

8.5 執行緒堆疊滿

  • 異常:Fatal: Stack size too small

  • 說明:java中一個執行緒的空間大小是有限制的。JDK5.0以後這個值是1M。與這個執行緒相關的資料將會儲存在其中。但是當執行緒空間滿了以後,將會出現上面異常。

  • 解決:增加執行緒棧大小。-Xss2m。但這個配置無法解決根本問題,還要看程式碼部分是否有造成洩漏的部分。

8.6 系統記憶體被佔滿

  • 異常:java.lang.OutOfMemoryError: unable to create new native thread

  • 說明:
    這個異常是由於作業系統沒有足夠的資源來產生這個執行緒造成的。系統建立執行緒時,除了要在Java堆中分配記憶體外,作業系統本身也需要分配資源來建立執行緒。因此,當執行緒數量大到一定程度以後,堆中或許還有空間,但是作業系統分配不出資源來了,就出現這個異常了。

    分配給Java虛擬機器的記憶體愈多,系統剩餘的資源就越少,因此,當系統記憶體固定時,分配給Java虛擬機器的記憶體越多,那麼,系統總共能夠產生的執行緒也就越少,兩者成反比的關係。同時,可以通過修改-Xss來減少分配給單個執行緒的空間,也可以增加系統總共內生產的執行緒數。

  • 解決:

    • 1.重新設計系統減少執行緒數量。
    • 2.執行緒數量不能減少的情況下,通過-Xss減小單個執行緒大小。以便能生產更多的執行緒。

歡迎關注 高廣超的簡書部落格 與 收藏文章 !
歡迎關注 頭條號:網際網路技術棧

個人介紹:

高廣超 :多年一線網際網路研發與架構設計經驗,擅長設計與落地高可用、高效能網際網路架構。目前就職於美團網,負責核心業務研發工作。

本文首發在 高廣超的簡書部落格 轉載請註明!

img_7015b3c64a6b1e4a95d4739adf2bbaa0.png
image.png


相關文章