最詳細的JVM&GC講解

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

不過文中一些JVM參數示例都是實際項目裏調優的結果,還是經受過實戰考驗的。

目錄


  1. JVM簡介
  2. 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 堆內存與棧內存的區別
  3. JIT編譯器
  4. 類加載機制 4.1 類加載的時機 4.2 類加載過程
  5. 垃圾回收 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收集器
  6. JVM參數 6.1 典型配置 6.1.1 堆大小設置 6.1.2 回收器選擇 6.1.3 輔助信息 6.2 參數詳細說明
  7. JVM性能調優 7.1 堆設置調優 7.2 GC策略調優 7.3 JIT調優 7.4 JVM線程調優 7.5 典型案例
  8. 常見問題 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編譯器。

image.png

image.png

2.JVM結構

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異常。

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棧幀裏的操作數棧,操作數棧被組織成一個以字長爲單位的數組,它是通過標準的棧操作-入棧和出棧來進行訪問,而不是通過索引訪問。

看一個例子:

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

發現,整個計算過程的參數傳遞和操作數棧密切相關!如圖:

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\0在常量區,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),這七個階段的發生順序如下圖所示:

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 按代實現垃圾回收

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中幾種常見的垃圾收集器:

image.png

5.7.1 Serial收集器

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

image.png

特性:

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

應用場景:

Serial收集器是虛擬機運行在Client模式下的默認新生代收集器。

優勢:

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

5.7.2 ParNew收集器

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收集器

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收集器

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收集器

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收集器

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
  • 說明:

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減小單個線程大小。以便能生產更多的線程
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章