深刻理解JAVA虛擬機(內存模型+GC算法+JVM調優)

目錄java

1.Java虛擬機內存模型

JVM虛擬機將內存數據分爲程序計數器、虛擬機棧、本地方法棧、Java堆和方法區等部分。程序員

程序計數器用於存放下一條運行的指令;虛擬機棧和本地方法棧用於存放函數調用堆棧信息;Java堆用於存放Java程序運行時所需的對象等數據;方法區用於存放程序的類元數據信息。算法

1.1 程序計數器

每個線程都必須有一個獨立的程序計數器,用於記錄下一條要運行的指令。各個線程之間的計數器互不影響,獨立工做;是一塊線程私有的內存空間。數據庫

若是當前程序正在執行一個Java方法,則程序計數器記錄正在執行的Java字節碼地址,若是當前線程正在執行一個Native方法,則程序計數器爲空。apache

1.2 Java虛擬機棧

Java虛擬機棧也是線程的私有空間,它和Java線程在同一時間建立,它保存方法的局部變量、部分結果,並參與方法的調用和返回。windows

在Java虛擬機規範中,定義了兩種異常與棧空間有關:StackOverflowError 和 OutOfMemoryError。數組

在 HotSpot 虛擬機中,可使用 -Xss 參數(如:-Xss1M)來設置棧的大小。棧的大小直接決定了函數調用的可達深度。tomcat

虛擬機棧在運行時使用一種叫作棧幀的數據結構保存上下文數據。在棧幀中,存放了方法的局部變量表、操做數棧、動態鏈接方法和返回地址等信息。每個方法的調用都伴隨着棧幀的入棧操做。相應的,方法的返回則表示棧幀的出棧操做。安全

若是方法調用時,方法的參數和局部變量相對較多,那麼棧幀中的局部變量表就會比較大,棧幀會膨脹以知足方法調用所需傳遞的信息。所以,單個方法調用所需的棧空間大小也會比較多。性能優化

局部變量

若是一個局部變量被保存在局部變量表中,那麼GC根就能引用到這個局部變量所指向的內存空間,從而在GC時,沒法回收這部分空間。這裏有一個很是簡單的示例來講明局部變量對GC的影響。

public void test(){
        {
            byte[] b = new byte[1024 * 1024 * 60]; // 1024*60 KB = 60 MB
        }
        System.gc();
        System.out.println("gc over");
    }

在運行Java程序時設置參數-XX:+PrintGC打印GC日誌,運行結果:

[GC (System.gc())  64775K->62176K(125952K), 0.0011984 secs]
[Full GC (System.gc())  62176K->62097K(125952K), 0.0063403 secs]
gc over

很明顯,顯示的Full GC並無能釋放它所佔用的堆空間。這是由於,變量b仍在該棧幀的局部變量表中。所以GC能夠引用到該內存塊,阻礙了回收過程。

假設在該變量失效後,在這個函數體內,又未能有定義足夠多的局部變量來複用該變量所佔的字,那麼,在整個函數體中,這塊內存區域是不會被回收的。在這種環境下,手工對要釋放的變量賦值爲null,是一種有效的作法。

public void test(){
        {
            byte[] b = new byte[1024 * 1024 * 5]; // 5MB
            b = null;
        }
        System.gc();
        System.out.println("gc over");
    }

運行結果:

[GC (Allocation Failure)  1513K->616K(7680K), 0.0011590 secs]
[GC (System.gc())  6191K->5880K(7680K), 0.0011550 secs]
[Full GC (System.gc())  5880K->651K(7680K), 0.0095708 secs]
gc over

在實際開發中,遇到上述狀況的可能性並不大。由於在大多數狀況下,若是後續仍然須要進行大量的操做,那麼極有可能會申明新的局部變量,從而複用變量b的字,使b佔的內存空間能夠被GC回收。

public void test(){
        {
            byte[] b = new byte[1024 * 1024 * 5];
        }
        int a = 0;
        System.gc();
        System.out.println("gc over");
    }

運行結果:

[GC (Allocation Failure)  1530K->656K(7680K), 0.0011337 secs]
[GC (System.gc())  6189K->5824K(7680K), 0.0010571 secs]
[Full GC (System.gc())  5824K->651K(7680K), 0.0077965 secs]
gc over

很明顯,變量b因爲變量a的做用被回收了。

完整代碼以下:

package com.oscar999.performance.JVMTune;
 
public class SystemGC {
    /**
     * GC 沒法回收b, 由於b 還在局部變量中
     */
    public static void test1() {
        {
            byte[] b = new byte[6 * 1024 * 1024];
        }
        System.gc();
        System.out.println("first explict gc over");
    }
    /**
     * GC 沒法回收, 由於 賦值爲null將銷燬局部變量表中的數據
     */
    public static void test2() {
        {
            byte[] b = new byte[6 * 1024 * 1024];
            b=null;
        }
        System.gc();
        System.out.println("first explict gc over");
    }
    /**
     * GC 能夠回收, 由於變量a 複用了變量b 的字,GC根沒法找到b
     */
    public static void test3() {
        {
            byte[] b = new byte[6 * 1024 * 1024];
            
        }
        int a=0;
        System.gc();
        System.out.println("first explict gc over");
    }
    /**
     * GC 沒法回收, 由於變量a 複用了變量c 的字,b 仍然存在
     */
    public static void test4() {
        {
            int c = 0;
            byte[] b = new byte[6 * 1024 * 1024];           
        }
        int a=0;
        System.gc();
        System.out.println("first explict gc over");
    }
    /**
     * GC 能夠回收, 由於變量a 複用了變量c 的字,變量d 複用了變量b 的字
     */
    public static void test5() {
        {
            int c = 0;
            byte[] b = new byte[6 * 1024 * 1024];           
        }
        int a=0;
        int d=0;
        System.gc();
        System.out.println("first explict gc over");
    }
    
    /**
     * 
     * 老是能夠回收b , 由於上層函數的棧幀已經銷燬
     */
    public static void main(String args[]){
        test1();
        System.gc();
        System.out.println("second explict gc over");
    }
}

1.3 本地方法棧

本地方法棧和Java虛擬機棧的功能很類似,也屬於線程的私有空間。Java虛擬機棧用於管理Java函數的調用,而本地方法棧用於管理本地方法的調用。本地方法並非用Java實現的,而是使用C實現的。在SUN的Hot Spot虛擬機中,不區分本地方法棧和虛擬機棧。所以,和虛擬機棧同樣,他也會拋出 StackOverflowError 和 OutOfMemoryError。

1.4 Java堆

Java堆能夠說是Java運行時內存中最爲重要的部分,幾乎全部的對象和數組都是在堆中分配空間的。Java堆分爲新生代和老年代兩個部分,新生代用於存放剛剛產生的對象和年輕的對象,若是對象一直沒有被回收,生存得足夠長,老年對象就會被移入老年代。

新生代又可進一步細分爲 eden、survivor space0(s0 或者 from space)和 survivor space1(s1或者to space)。

eden:對象的出生地,大部分對象剛剛創建時,一般會存放在這裏。s0 和 s1 爲 survivor(倖存者)空間,存放其中的對象至少經歷過一次垃圾回收,並得以倖存。若是在倖存區的對象到了指定年齡仍未被回收,則有機會進入老年代(tenured)。

使用JVM參數-XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -Xms40M -Xmx40M -Xmn20M運行這段代碼:

public void test2(){
        byte[] byte1 = new byte[1024*1024/2];
        byte[] byte2 = new byte[1024*1024*8];
        byte2 = null;
        byte2 = new byte[1024*1024*8];
        System.gc();    //註釋此行
    }

運行結果:

[GC (System.gc()) [PSYoungGen: 11004K->1256K(18432K)] 19196K->9456K(38912K), 0.0013429 secs][Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 1256K->0K(18432K)] [ParOldGen: 8200K->9361K(20480K)] 9456K->9361K(38912K), [Metaspace: 3478K->3478K(1056768K)], 0.0072324 secs][Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 18432K, used 164K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
  eden space 16384K, 1% used [0x00000000fec00000,0x00000000fec290d0,0x00000000ffc00000)
  from space 2048K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x00000000ffe00000)
  to   space 2048K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x0000000100000000)
 ParOldGen       total 20480K, used 9361K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
  object space 20480K, 45% used [0x00000000fd800000,0x00000000fe124740,0x00000000fec00000)
 Metaspace       used 3485K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 390K, committed 512K, reserved 1048576K

能夠看到,在通過Full GC以後,新生代空間所有被清空,未被回收的對象所有被移入老年代。

1.5 方法區(永久區、元空間)

方法區也是 JVM 內存區中很是重要的一塊內存區域,與堆空間相似,它也是被 JVM 中全部的線程共享的。方法區主要保存的信息是類的元數據。

方法區中最爲重要的是類的類型信息、常量池、域信息、方法信息。類型信息包括類的完整名稱、父類的完整名稱、類型修飾符(public/protected/private)和類型的直接接口類表;常量池包括這個類方法、域等信息所引用的常量信息;域信息包括域名稱、域類型和域修飾符;方法信息包括方法名稱名稱、返回類型、方法參數、方法修飾符、方法字節碼、操做數棧和方法幀棧的局部變量區大小以及異常表。總之,方法區保存的信息,大部分來自於 class 文件,是 Java 應用程序運行必不可少的重要數據。

在JDK8以前的Host Spot虛擬機的實現中,方法區也被稱爲永久區,是一塊獨立於 Java 堆的內存空間。雖然叫永久區,可是永久區中的對象一樣能夠被 GC 回收的。對永久區 GC 的回收,一般主要從兩個方面分析:一是 GC 對永久區常量池的回收;二是永久區對類元數據的回收。永久區的垃圾回收是和老年代(old generation)捆綁在一塊兒的,所以不管誰滿了,都會觸發永久代和老年代的垃圾收集。

方法區是 JVM 的一種規範,永久區是一種具體實現,在 Java8 中,永久區已經被 Metaspace 元空間取而代之。原永久區的數據被分到了堆和元空間中:元空間存儲類的元信息,靜態變量和常量池等放入堆中。相應的,JVM參數 PermSize 和 MaxPermSize 被 MetaSpaceSize 和 MaxMetaSpaceSize 取代。

至於爲何要使用元空間替換永久區,無非是永久區帶來了一些問題,元空間解決了一些問題。好比永久區的大小是在啓動時固定好的——很難驗證並進行調優,由於永久區的大小依賴於不少因素,例如JVM加載的class的總數,常量池的大小,方法的大小等,因此容易致使內存泄露或內存溢出(java.lang.OutOfMemoryError: PermGen)。

元空間的本質和永久區相似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。理論上取決於32位/64位系統可虛擬的內存大小。不過,能夠經過設置參數 MaxMetaspaceSize 限制本地內存分配給類元數據的大小。若是沒有指定這個參數,元空間會在運行時根據須要動態調整。

元空間認爲,類和它的元數據的生命週期是和它的類加載器的生命週期一致的。每個類加載器的存儲區域都稱做一個元空間,全部的元空間合在一塊兒就是咱們一直說的元空間。當一個類加載器被垃圾回收器標記爲再也不存活,其對應的元空間會被回收。

使用JVM參數-XX:+PrintGCDetails -XX:MetaspaceSize=4M -XX:MaxMetaspaceSize=5M運行這段代碼:

@Test
    public void test4() {
        for (int i=0; i<Integer.MAX_VALUE;i++){
            String s = String.valueOf(i).intern();  //加入常量池並返回
        }
    }

運行結果:

[GC (Metadata GC Threshold) [PSYoungGen: 1331K->696K(38400K)] 1331K->696K(125952K), 0.0011365 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Metadata GC Threshold) [PSYoungGen: 696K->0K(38400K)] [ParOldGen: 0K->550K(62976K)] 696K->550K(101376K), [Metaspace: 2875K->2875K(1056768K)], 0.0062965 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Metadata GC Threshold) [PSYoungGen: 4672K->640K(38400K)] 5222K->1198K(101376K), 0.0016105 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Metadata GC Threshold) [PSYoungGen: 640K->0K(38400K)] [ParOldGen: 558K->1131K(118272K)] 1198K->1131K(156672K), [Metaspace: 4647K->4647K(1056768K)], 0.0139785 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
....

能夠看到,持久代已經飽和,並拋出 「java.lang.OutOfMemoryError: Metaspace」 異常顯示持久代溢出。

Full GC 在這種狀況下不能回收類的元數據。

事實上,若是虛擬機確認該類的全部實例已經被回收,而且加載該類的 ClassLoader 已經被回收,GC 就有可能回收該類型。

附圖

圖片來源於網絡

2.JVM內存分配參數

2.1 設置最大堆內存

設置 參數 示例
最大堆內存 -Xmx -Xmx3m

在運行時,可使用 Runtime.getRuntime().maxMemory() 取得系統可用的最大堆內存。

好比在運行時設置參數 -Xmx3M

@Test
    public void test5(){
        System.out.println(Runtime.getRuntime().maxMemory()/1024/1024);
    }

運行結果:

3

2.2 設置最小堆內存

設置 參數 示例
最小堆內存 -Xms -Xms1m

最小堆內存,也就是JVM啓動時,所佔據的操做系統內存大小。

Java應用程序在運行時,首先被分配-Xms指定的內存大小,並儘量嘗試在這個空間段內運行程序。當-Xms指定的內存大小確實沒法知足應用程序時,JVM 纔會向操做系統申請更多的內存,直到內存大小達到-Xmx指定的最大內存爲止。若超過-Xmx的值,則拋出 OutOfMemoryError 異常。

若是 -Xms 的數值較小,那麼JVM爲了保證系統儘量地在指定內存範圍內運行,就會更加頻繁地進行GC操做,以釋放失效的內存空間,從而,會增長 Minor GC 和 Full GC的次數,對系統性能產生必定的影響。所以把 -Xms 值設置爲 -Xmx 時,能夠在系統運行初期減小 GC 的次數和耗時。

2.3 設置新生代

設置 參數 示例
新生代 -Xmn -Xmn2m

設置一個較大的新生代會減小老年代的大小,這個參數對系統性能以及 GC 行爲有很大的影響。新生代的大小通常設置爲整個堆空間的1/4到1/3左右。

在 Hot Spot 虛擬機中,-XX:NewSize 用於設置新生代的初始大小,-XX:MaxNewSize用於設置新生代的最大值。但一般狀況下,只設置 -Xmn 以及能夠知足絕大部分應用的須要。設置 -Xmn 的效果等同於設置了相同的-XX:NewSize-XX:MaxNewSize

若設置不一樣的-XX:NewSize-XX:MaxNewSize可能會致使內存震盪,從而產生沒必要要的系統開銷。

2.4 設置持久代

持久代(方法區)不屬於Java堆的一部分。在Hot Spot虛擬機中,使用-XX:MaxPermSize能夠設置持久代的最大值,使用-XX:PermSize能夠設置持久代的初始大小。

JDK1.8取消了PermGen,取而代之的是Metaspace(元空間),因此PermSize和MaxPermSize參數失效,取而代之的是 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize

設置 參數 示例
持久代的初始大小 -XX:MetaspaceSize -XX:MetaspaceSize=64M
持久代的最大值 -XX:MaxMetaspaceSize -XX:MaxMetaspaceSize=128M

持久代的大小直接決定了系統能夠支持多少個類定義和多少常量。對於使用 CGLIB 或者 Javassist 等動態字節碼生成工具的應用程序而言,設置合理的持久代大小有助於維持系統穩定。

通常來講,設置MaxMetaspaceSize爲64MB已經能夠知足絕大部分應用程序正常工做。若是依然出現永久區溢出,能夠將MaxMetaspaceSize設置爲128MB。這是兩個很經常使用的永久區取值。

2.5 設置線程棧

線程棧是線程的一塊私有空間。有關描述能夠參考前文的「Java虛擬機棧」。

設置 參數 示例
線程棧 -Xss -Xss1M

在線程中進行局部變量分配,函數調用時,都須要在棧中開闢空間。若是棧的空間分配過小,那麼線程在運行時,可能沒有足夠的空間分配局部變量或者達不到足夠的函數調用深度,致使程序異常退出;若是棧空間過大,那麼開設線程所需的內存成本就會上升,系統所能支持的線程總數就會降低。

因爲Java堆也是向操做系統申請內存空間的,所以,若是堆空間過大,就會致使操做系統可用於線程棧的內存減小,從而間接減小程序所能支持的線程數量。

當系統因爲內存不夠沒法建立新的線程時,會拋出 OOM 異常以下:

java.lang.OutOfMemoryError: unable to create new native thread

根據以上內容可知,這並非因爲堆內存不夠而致使的 OOM,而是由於操做系統內存減去堆內存後,剩餘的系統內存不足而沒法建立新的線程。在這種狀況下,能夠嘗試減小堆內存,以換取更多的系統空間,來解決這個問題。

若是系統確實須要大量的線程併發執行,那麼設置一個較小的堆和較小的棧,有助於提供系統所能承受的最大線程數。

2.6 堆的內存分配

設置 參數 示例
eden區/survivor區 -XX:SurvivorRatio -XX:SurvivorRatio=8
老年代/新生代 -XX:NewRatio -XX:NewRatio=2

參數 -XX:SurvivorRatio 是用來設置新生代中,eden空間和s0空間的比例關係。s0 和 s1 空間又被稱爲 from 空間和 to 空間。它們的大小是相同的,職能也是同樣的,並在 Minor GC後,會互換角色。

公式:-XX:SurvivorRatio = eden/s0 = eden/s1

舉例:當設置JVM參數 -Xmn10M -XX:SurvivorRatio=8 就等於設置 eden=8M,s0=1M,s1=1M。

參數 -XX:NewRatio 是用來設置新生代與老年代的比例:

公式:-XX:NewRatio = 老年代 / 新生代

舉例:當設置JVM參數 -Xms18M -Xmx18M -XX:NewRatio=2運行程序時,新生代約佔6MB,老年代約佔12MB。

2.7 堆分配參數總結

參數 說明
-Xmx 設置Java應用程序能得到的最大堆大小。
-Xms 設置Java應用程序啓動時的初始堆大小。
-Xss 設置線程棧的大小。
-Xmn 設置新生代的初始大小與最大值。
-XX:NewSize 設置新生代的大小。
-XX:NewRatio 設置老年代與新生代的比例,它等於老年代大小除以新生代大小。
-XX:SurvivorRatio 設置新生代中eden區和survivor區的比例。
-XX:MetaspaceSize (Java8)設置永久區的初始值。。
-XX:MaxMetaspaceSize (Java8)最大的持久區大小。
-XX:MinHeapFreeRatio 設置堆空間的最大空閒比例。
當堆空間的空閒內存小於這個數值時,JVM便會擴展堆空間
-XX:MaxHeapFreeRatio 設置堆空間的最大空閒比例。
當堆空間的空閒內存大於這個數值時,便會壓縮堆空間,獲得一個較小的堆。
-XX:TargetSurvivorRatio 設置survivor區的可以使用率。
當survivor區的空間使用率達到這個值時,會將對象送入老年代。

3.垃圾收集基礎

Java語言的一大特色就是能夠進行自動垃圾回收處理,而無需開發人員過於關注系統資源(尤爲是內存資源)的釋放狀況。自動垃圾收集雖然大大減輕了開發人員的工做量,但同時,它也增長了軟件系統的負擔。一個不合適的垃圾回收方法和策略會對系統性能形成不良影響。

3.1 垃圾收集的做用

在C++語言中,程序員必須當心謹慎地處理每一項內存分配,且內存使用完後,必須手工釋放曾經佔用的內存空間,當內存釋放不夠徹底時,即存在分配但永不釋放的內存塊,就會引發內存泄漏,嚴重時,會致使程序癱瘓。

雖然,目前有許多自動化檢測工具能夠識別這些內存泄漏的代碼點,可是這種純手工的管理內存的方法,依然被很多人所詬病。爲了解決這個問題,Java語言使用了垃圾收集器用來替代C++時代的純手工的內存管理,以減輕程序員的負擔,減小出錯的機率。

垃圾收集器要處理的基本問題是:

  1. 哪些對象須要回收?
  2. 什麼時候回收這些對象?
  3. 如何回收這些對象?

3.2 垃圾回收算法與思想

3.2.1 引用計數法(Reference Counting)

引用計數法的實現很簡單,對於一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1,當引用失效時,引用計數器就減一。只要對象A的引用計數器的值爲0,則對象A就不能再被使用。

引用計數器的缺點是沒法處理循環引用的狀況。所以,在Java的垃圾回收器中,沒有使用這種算法。

一個簡單的循環引用問題描述以下:有對象A和對象B,對象A中含有對象B的引用,對象B中含有對象A的引用。此時,對象A和B的引用計數器都不爲0。可是,在系統中,卻不存在任何第3個對象引用了A或B。也就是說,A和B是應該被回收的垃圾對象,但因爲垃圾對象間的相互引用,從而使垃圾回收器沒法識別,引發內存泄漏。

3.2.2 標記-清除算法(Mark-Sweep)

標記-清除算法是現代垃圾回收算法的思想基礎。

標記-清除算法將垃圾回收分爲兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段,首先經過根節點,標記全部從根節點開始的可達對象。所以,未被標記的對象就是未被引用的垃圾對象。而後,在清除階段,清除全部未被標記的對象。標記-清除算法可能產生的最大問題就是空間碎片。

由圖能夠看到,回收後的空間是不連續的。不連續的內存空間的工做效率要低於連續的空間。所以,這也是該算法的最大缺點。

3.2.3 複製算法(Copying)

與標記-清除算法相比,複製算法是一種相對高效的回收方法。它的核心思想是:將原有的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的內存中的存活對象複製到未使用的內存塊中,以後,清除正在使用的內存塊中的全部對象,交換兩個內存中的角色,完成垃圾回收。

若是系統中的垃圾對象不少,複製算法須要複製的存活對象數量並不會太大。所以,在真正須要垃圾回收的時刻,複製算法的效率是很高的。又因爲對象是在垃圾回收過程當中統一複製到新的內存空間中,所以,可確保回收後的內存空間是沒有碎片的。可是,複製算法的缺點是將系統內存摺半,所以,單純的複製算法也很難讓人接受。如圖:

在Java的新生代串行垃圾回收器中,使用了複製算法的思想。新生代分爲 eden 空間、form 空間和 to空間3個部分。其中 from 和 to 空間能夠視爲用於複製的兩塊大小相同、地位相等,且可進行角色互換的空間塊。from 和 to 空間也稱爲 survivor 空間,即倖存者空間,用於存放未被回收的對象。

在垃圾回收時,eden空間中存活的對象會被複制到未使用的survivor空間中(假設是 to),正在使用的survivor空間(假設是 from)中的年輕對象也會被複制到to空間中(大對象或者老年對象會直接進入老年代,若是to空間已滿,則對象也會進入老年代)。此時eden和from空間中剩餘對象就是垃圾對象,直接清空,to空間則存放這次回收後存活下來的對象。

這種複製算法保證了內存空間的連續性,又避免了大量的空間浪費。

複製算法比較適用於新生代。由於在新生代中,垃圾對象一般會多於存活對象,複製算法的效果會比較好。

3.2.4 標記-壓縮算法(Mark-Compact)

複製算法的高效性是創建在存活對象少、垃圾對象多的前提下的。這種狀況在年輕代常常發生,可是在老年代中,大部分對象都是存活的對象,若是仍是有複製算法的話,成本會比較高。所以,基於老年代這種特性,應該使用其餘的回收算法。

標記壓縮算法是一種老年代的回收算法,它在標記清除算法的基礎上作了優化。(標記清除算法的缺點,垃圾回收後內存空間再也不連續,影響了內存空間的使用效率)和標記清除算法同樣,標記壓縮算法也首先從根節點開始,對全部可達的對象作一次標記,但以後,它並非簡單的清理未標記的對象,而是將全部的存活對象壓縮到內存空間的一端。以後,清理邊界外全部的空間。這樣作避免的碎片的產生,又不須要兩塊相同的內存空間,所以,其性價比高。

3.2.5 增量算法(Incremental Collecting)

對大部分垃圾回收算法而言,在垃圾回收的過程當中,應用系統軟件處於一種「Stop the World」的狀態。在「Stop the World」狀態下應用程序的全部線程都會掛起,暫停一切正常的工做,等待垃圾回收的完成。若是垃圾回收時間很長,應用系統會被掛起好久,將嚴重影響用戶體驗或者系統的穩定性。

增量算法的基本思想是,若是一次性將全部垃圾進行處理,須要形成系統長時間的停頓,那麼可讓垃圾收集和應用程序交替止執行。每次,垃圾收集線程只收集一小片區域的內存空間,接着切換到應用程序執行。如此反覆,直到垃圾收集完成。使用這種方式,因爲在垃圾回收過程當中,間斷性的還執行了應用程序代碼,因此能減小系統的停頓時間。由於線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本升高,形成系統吞吐量的降低。

3.2.6 分代(Generation Collection)

前面介紹的複製、標記-清除、標記-壓縮等算法,並無一種算法能夠徹底替換其餘算法。它們都有各自的優缺點。所以根據垃圾回收對象的特色不一樣,使用不一樣的回收算法纔是明智之舉。

分代就是基於這種思想,它將內存區域根據對象的特色分紅不一樣的內存區域,根據每塊區域對象的特徵不一樣使用不一樣的回收算法,以提升垃圾回收的效率。

以Hot Spot虛擬機爲例,它將全部的新建對象都放入稱爲年輕代的內存區域,年輕代的特色是對象朝生夕滅,大約 90% 的新建對象會被很快回收,所以,在年輕代就選擇效率較高的複製算法。當一個對象通過幾回垃圾回收後依然存活,對象就會放入老年代的內存空間,在老年代中,幾乎全部的對象都是通過幾回垃圾回收後依然得以存活的,所以,認爲這些對象在一段時間內,甚至在程序的整個生命週期將是常駐內存的。

老年代的存活率是很高的,若是依然使用複製算法回收老年代,將須要複製大量的對象。這種作法是不可取的,根據分代的思想,對老年代的回收使用與新生代不一樣的標記-壓縮算法能夠提升垃圾回收效率。如圖:

注意:分代的思想被現有的Hot Spot虛擬機普遍使用,幾乎全部的垃圾回收器都區分新生代和老年代。

3.3 垃圾收集器的類型

  • 按線程數分:

串行:串行垃圾回收器一次只使用一個線程繼續垃圾回收。

並行:並行垃圾回收器一次開啓多個線程同時進行垃圾回收。在cup能力較強時使用並行能夠提升垃圾收集效率,縮短GC停頓時間。

  • 按工做模式分:

併發式:併發式垃圾回收器與應用系統交替工做,以儘量減小應用系統的停頓時間。

獨佔式:獨佔式垃圾回收器(Stop the World)一旦運行就中止應用程序運行,直到垃圾收集徹底結束,才容許應用程序執行。

  • 按碎片處理方式分:

壓縮式:壓縮式垃圾回收器會在回收完成後,對存活對象進行壓縮整理,消除回收後的碎片。

非壓縮式:非壓縮式垃圾回收器不進行壓縮操做。

  • 按工做內存區間分:

新生代垃圾回收器:只在新生代工做。

老年代垃圾回收器:只在老年代工做。

3.4 評價GC策略的指標

  • 吞吐量:指在應用程序的生命週期內,應用程序所花費的時間和系統總運行時間的比值。 系統總運行時間 = 應用程序耗時 + GC 耗時。若是系統運行了 100min,GC 耗時 1min,那麼系統的吞吐量就是(100-1)/100=99%。

  • 垃圾回收器負載:和吞吐量相反,垃圾回收器負載指垃圾回收器耗時與系統運行總時間的比值。

  • 停頓時間:指垃圾回收器正在運行時,應用程序的暫停時間。對於獨佔回收器而言,停頓時間可能會比較長。使用併發的回收器時,因爲垃圾回收器和應用程序交替運行,程序的停頓時間會變短,可是,因爲其效率極可能不如獨佔垃圾回收器,故系統的吞吐量可能會較低。

  • 垃圾回收頻率:指垃圾回收器多長時間會運行一次。通常來講,對於固定的應用而言,垃圾回收器的頻率應該是越低越好。一般增大堆空間能夠有效下降垃圾回收發生的頻率,可是可能會增長回收產生的停頓時間。

  • 反應時間:指當一個對象被稱爲垃圾後多長時間內,它所佔據的內存空間會被釋放。

  • 堆分配:不一樣的垃圾回收器對堆內存的分配方式多是不一樣的。一個良好的垃圾收集器應該有一個合理的堆內存區間劃分。

一般狀況下,很難讓一個應用程序在全部的指標上都達到最優,所以,只能根據應用自己的特色,儘量使垃圾回收器配合應用程序的工做。好比,對於客戶端應用而言,應該儘量下降其停頓時間,給用戶良好的使用體驗,爲此,能夠犧牲垃圾回收的吞吐量;對後臺服務程序來講,可能會更加關注吞吐量,因此,能夠適當延長系統的停頓時間。

3.5 新生代串行收集器

串行收集器是全部垃圾收集器中最古老的一種,也是JDK中最基本的垃圾收集器之一。串行回收器主要有兩個特色:第一:使用單線程進行垃圾回收;第二:獨佔式垃圾回收。

在串行收集器進行垃圾回收時,Java應用程序中的線程都須要暫停,等待垃圾回收完成。這種現象成爲Stop-The-World。它將形成很是糟糕的用戶體驗,在實時性要求較高的應用場景中,這種現象每每是不能被接受的。

在諸如單CPU處理器或者較小的應用內存等硬件平臺不是特別優越的場合,它的性能表現能夠超過並行回收器和併發回收器。

在 Hot Spot虛擬機中,使用-XX:+UseSerialGC參數能夠指定使用新生代串行收集器和老年代串行收集器。當JVM虛擬機在Client模式下運行時,它是默認的垃圾收集器。

3.6 老年代串行收集器

老年代串行收集器使用的是標記壓縮算法,它也是一個串行的、獨佔式的垃圾回收器。因爲老年代垃圾回收一般會使用比新生代垃圾回收更長的時間。所以,在堆空間較大的應用程序中,一旦老年串行收集器啓動,應用程序極可能會所以停頓幾秒甚至更長時間。雖然如此,做爲老牌的垃圾回收器,老年代串行回收器能夠和多種新生代回收器配合使用,同時它也能夠做爲CMS回收器的備用回收器。

可使用如下參數啓用老年代串行回收器。

  • -XX:+UseSerialGC:新生代老年代都使用串行回收去器。

  • -XX:+UseParNewGC:新生代使用並行回收器,老年代使用串行回收器。

  • -XX:+UseParallelGC:新生代使用並行回收收集器,老年代使用串行回收收集器。

3.7 並行收集器

並行收集器是工做在新生代的垃圾收集器,它只是簡單地將串行回收器多線程化。它的回收策略、算法以及參數和串行回收器同樣。並行回收器也是獨佔式的回收器,在收集過程當中,應用程序會所有暫停。但因爲並行回收器使用多線程進行垃圾回收,所以,在併發能力比較強的 CPU 上,它產生的停頓時間要短於串行回收器,而在單 CPU 或者併發能力較弱的系統中,並行回收器的效果不會比串行回收器好,因爲多線程的壓力,它的實際表現極可能比串行回收器差。

開啓並行回收器可使用如下參數:

  • -XX:+UseParNewGC:新生代使用並行收集器,老年代使用串行回收器。

  • -XX:+UseConcMarkSweepGC:新生代使用並行回收器,老年代使用CMS。

並行收集器工做時的線程數量可使用 -XX:ParallelGCThreads 參數指定。通常最好與CPU數量至關,避免過多的線程數,影響垃圾收集性能。 在默認狀況下,當CPU數量小於8個時,ParallelGCThreads 的值等於 CPU 數量;當 CPU 數量大於8個時,ParallelGCThreads 的值等於 3+[(5*CPU_Count)/8]。

3.8 新生代並行回收(Parallel Scavenge)收集器

新生代並行回收器也是使用複製算法的收集器。與並行回收器不一樣的是它很是關注系統的吞吐量。

開啓並行回收收集器可使用如下參數:

  • -XX:+UseParallelGC:新生代使用並行回收收集器,老年代使用串行回收器。

  • -XX:+UseParallelOldGC:新生代與老年代都使用並行回收收集器。

並行回收收集器提供了兩個重要的參數用於控制系統的吞吐量:

  • -XX:+MaxGCPauseMills:設置最大垃圾收集停頓時間,它的值是一個大於 0 的整數。收集器在工做時會調整 Java 堆大小或者其餘一些參數,儘量地把停頓時間控制在 MaxGCPauseMills 之內。若是但願減小停頓時間,而把這個值設置得很小,爲了達到預期的停頓時間,JVM 可能會使用一個較小的堆 (一個小堆比一個大堆回收快),而這將致使垃圾回收變得很頻繁,從而增長了垃圾回收總時間,下降了吞吐量。

  • -XX:+GCTimeRatio:設置吞吐量大小,它的值是一個 0-100 之間的整數。假設 GCTimeRatio 的值爲 n,那麼系統將花費不超過 1/(1+n) 的時間用於垃圾收集。好比 GCTimeRatio 等於 19,則系統用於垃圾收集的時間不超過 1/(1+19)=5%。默認狀況下,它的取值是 99,即不超過 1%的時間用於垃圾收集。

除此以外,並行回收收集器與並行收集器另外一個不一樣之處在於,它支持一種自適應的 GC 調節策略,使用-XX:+UseAdaptiveSizePolicy 能夠打開自適應 GC 策略。在這種模式下,新生代的大小、eden 和 survivor 的比例、晉升老年代的對象年齡等參數會被自動調整,以達到在堆大小、吞吐量和停頓時間之間的平衡點。在手工調優比較困難的場合,能夠直接使用這種自適應的方式,僅指定虛擬機的最大堆、目標的吞吐量 (GCTimeRatio) 和停頓時間 (MaxGCPauseMills),讓虛擬機本身完成調優工做。

3.9 老年代並行回收收集器

老年代的並行回收收集器也是一種多線程併發的收集器。和新生代並行回收收集器同樣,它也是一種關注吞吐量的收集器。老年代並行回收收集器使用標記-壓縮算法,JDK1.6 以後開始啓用。

使用-XX:+UseParallelOldGC 能夠在新生代和老生代都使用並行回收收集器,這是一對很是關注吞吐量的垃圾收集器組合,在對吞吐量敏感的系統中,能夠考慮使用。參數-XX:ParallelGCThreads 也能夠用於設置垃圾回收時的線程數量。

3.10 CMS收集器

與並行回收收集器不一樣,CMS 收集器主要關注於系統停頓時間。CMS 是 Concurrent Mark Sweep 的縮寫,意爲併發標記清除,從名稱上能夠得知,它使用的是標記-清除算法,同時它又是一個使用多線程併發回收的垃圾收集器。

CMS 工做時,主要步驟有:初始標記、併發標記、從新標記、併發清除和併發重置。其中初始標記和從新標記是獨佔系統資源的,而併發標記、併發清除和併發重置是能夠和用戶線程一塊兒執行的。所以,從總體上來講,CMS 收集不是獨佔式的,它能夠在應用程序運行過程當中進行垃圾回收。

根據標記-清除算法,初始標記、併發標記和從新標記都是爲了標記出須要回收的對象。併發清理則是在標記完成後,正式回收垃圾對象;併發重置是指在垃圾回收完成後,從新初始化 CMS 數據結構和數據,爲下一次垃圾回收作好準備。併發標記、併發清理和併發重置都是能夠和應用程序線程一塊兒執行的。

CMS收集器的主要缺點:

對CPU資源很是敏感:CMS 收集器在其主要的工做階段雖然沒有暴力地完全暫停應用程序線程,可是因爲它和應用程序線程併發執行,相互搶佔 CPU,因此在 CMS 執行期內對應用程序吞吐量形成必定影響。CMS 默認啓動的線程數是 (ParallelGCThreads+3)/4),ParallelGCThreads 是新生代並行收集器的線程數,也能夠經過-XX:ParallelCMSThreads 參數手工設定 CMS 的線程數量。當 CPU 資源比較緊張時,受到 CMS 收集器線程的影響,應用程序的性能在垃圾回收階段可能會很是糟糕。

沒法處理浮動垃圾:因爲 CMS 收集器不是獨佔式的回收器,在 CMS 回收過程當中,應用程序仍然在不停地工做。在應用程序工做過程當中,又會不斷地產生垃圾。這些新生成的垃圾在當前 CMS 回收過程當中是沒法清除的。同時,由於應用程序沒有中斷,因此在 CMS 回收過程當中,還應該確保應用程序有足夠的內存可用。所以,CMS 收集器不會等待堆內存飽和時才進行垃圾回收,而是當前堆內存使用率達到某一閾值時,便開始進行回收,以確保應用程序在 CMS 工做過程當中依然有足夠的空間支持應用程序運行。

這個回收閾值可使用-XX:CMSInitiatingOccupancyFraction 來指定,默認是 68。即當老年代的空間使用率達到 68%時,會執行一次 CMS 回收。若是應用程序的內存使用率增加很快,在 CMS 的執行過程當中,已經出現了內存不足的狀況,此時,CMS 回收將會失敗,JVM 將啓動老年代串行收集器進行垃圾回收。若是這樣,應用程序將徹底中斷,直到垃圾收集完成,這時,應用程序的停頓時間可能很長。

所以,根據應用程序的特色,能夠對 -XX:CMSInitiatingOccupancyFraction 進行調優。若是內存增加緩慢,則能夠設置一個稍大的值,大的閾值能夠有效下降 CMS 的觸發頻率,減小老年代回收的次數能夠較爲明顯地改善應用程序性能。反之,若是應用程序內存使用率增加很快,則應該下降這個閾值,以免頻繁觸發老年代串行收集器。

標記-清除算法致使的空間碎片:CMS是一個基於標記-清除算法的回收器。標記-清除算法會形成大量內存碎片,離散的可用空間沒法分配較大的對象。在這種狀況下,即便堆內存仍然有較大的剩餘空間,也可能會被迫進行一次垃圾回收,以換取一塊可用的連續內存,這種現象對系統性能是至關不利的,爲了解決這個問題,CMS 收集器還提供了幾個用於內存壓縮整理的參數。

-XX:+UseCMSCompactAtFullCollection 參數可使 CMS 在垃圾收集完成後,進行一次內存碎片整理。內存碎片的整理並非併發進行的。-XX:CMSFullGCsBeforeCompaction 參數能夠用於設定進行多少次 CMS 回收後,進行一次內存壓縮。

若是CMS收集器併發收集失敗。這極可能是因爲應用程序在運行過程當中老年代空間不夠所致使。若是在CMS工做過程當中,出現很是頻繁的併發模式失敗,就應該考慮進行調整,儘量預留一個較大的老年代空間。或者能夠設置一個較小的 -XX:CMSInitiatingOccupancyFraction 參數,下降CMS觸發的閾值,使CMS在執行過程當中,仍然有較大的老年代空閒空間供應用程序使用。

3.11 G1收集器

G1(Garbage First)收集器在JDK 1.7中發佈,目標是做爲一款面向服務端應用的垃圾收集器,HotSpot開發團隊賦予它的使命是在將來能夠替換掉JDK 1.5中發佈的CMS收集器。與其餘GC收集器相比,G1具有以下特色:

  • 並行與併發:G1能充分利用多CPU、多核環境下的硬件優點,使用多個CPU來縮短「Stop The World」停頓時間,部分其餘收集器本來須要停頓Java線程執行的GC動做,G1收集器仍然能夠經過併發的方式讓Java程序繼續執行。
  • 分代收集:與其餘收集器同樣,分代概念在G1中依然得以保留。雖然G1能夠不須要其餘收集器配合就能獨立管理整個GC堆,但它可以採用不一樣方式去處理新建立的對象和已存活一段時間、熬過屢次GC的舊對象來獲取更好的收集效果。
  • 空間整合:G1從總體來看是基於「標記-整理」算法實現的收集器,從局部(兩個Region之間)上來看是基於「複製」算法實現的。這意味着G1運行期間不會產生內存空間碎片,收集後能提供規整的可用內存。此特性有利於程序長時間運行,分配大對象時不會由於沒法找到連續內存空間而提早觸發下一次GC。
  • 可預測的停頓:這是G1相對CMS的一大優點,下降停頓時間是G1和CMS共同的關注點,但G1除了下降停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷內,消耗在GC上的時間不得超過N毫秒,這幾乎已是實時Java(RTSJ)的垃圾收集器的特徵了。

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

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

每一個 Region 都有一個 Remembered Set,用來記錄該 Region 對象的引用對象所在的 Region。經過使用 Remembered Set,在作可達性分析的時候就能夠避免全堆掃描。

若是不計算維護Remembered Set的操做,G1收集器的運做大體可劃分爲如下幾個步驟:

  • 初始標記(Initial Marking):僅僅只是標記一下GC Roots 能直接關聯到的對象,而且修改TAMS(Nest Top Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確能夠的Region中建立對象,此階段須要停頓線程,但耗時很短。
  • 併發標記(Concurrent Marking):從GC Root 開始對堆中對象進行可達性分析,找到存活對象,此階段耗時較長,但可與用戶程序併發執行。
  • 最終標記(Final Marking):爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的Remembered Set Logs裏面,最終標記階段須要把Remembered Set Logs的數據合併到Remembered Set中,這階段須要停頓線程,可是可並行執行。
  • 篩選回收(Live Data Counting and Evacuation):首先對各個Region中的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃。此階段其實也能夠作到與用戶程序一塊兒併發執行,可是由於只回收一部分Region,時間是用戶可控制的,並且停頓用戶線程將大幅度提升收集效率。

使用如下參數能夠啓用 G1 回收器:

-XX:+UnlockExperimentalVMOptions –XX:+UseG1GC

設置 G1 回收器的目標停頓時間:

-XX:MaxGCPauseMills = 20

-XX:GCPauseIntervalMills = 200。

以上參數指定在200ms內,停頓時間不超過50ms。這兩個參數是G1回收器的目標,G1回收器並不保證能執行它們。

3.12 收集器對系統性能的影響

本示例將展現不一樣垃圾收集器對應用軟件性能的影響,而不是篩選出最優秀的垃圾回收器。事實上,在衆多的垃圾回收器中,並無最好的,只有最適合應用的回收器。根據應用軟件的特性以及硬件平臺的特色,選擇不一樣的垃圾回收器,纔能有效地提升系統性能。

測試代碼:

import java.util.HashMap;

public class  GCTimeTest {
    private static HashMap<Long, byte[]> map = new HashMap<>();
    public static  void main(String[] args){
        long beginTime = System.currentTimeMillis();
        for(int i=0;i<10000;i++){
            if(map.size()*512/1024/1024>=400){
                map.clear();//保護內存不溢出
                System.out.println("clean map");
            }
            byte[] b1;
            for(int j=0;j<100;j++){
                b1 = new  byte[512];
                map.put(System.nanoTime(), b1);//不斷消耗內存
            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println(endTime-beginTime);
    }
}

使用不一樣的垃圾回收器測試表:

回收器 耗時/ms
-Xmx512M -Xms512M -XX:+UseParNewGC 515
-Xmx512M -Xms512M -XX:+UseParallelOldGC -XX:ParallelGCThreads=8 547
-Xmx512M -Xms512M -XX:+UseSerialGC 730
-Xmx512M -Xms512M -XX:+UseConcMarkSweepGC 686

能夠看到,不一樣的回收器對代碼的執行時間有着較爲明顯的差異。若是讀者親自測試一番,可能結果又有所不一樣。本例只能說明在新生代使用並行收集器更適合筆者的測試硬件環境和樣例代碼。

根據應用的不一樣特色,能夠選擇不一樣的垃圾回收器,以提升應用程序的性能。

3.13 GC相關參數總結

1.與串行回收器相關的參數

  • -XX:+UseSerialGC:在新生代和老年代使用串行回收器。

  • -XX:+SuivivorRatio:設置 eden 區大小和 survivor 區大小的比例。

  • -XX:+PretenureSizeThreshold:設置大對象直接進入老年代的閾值。當對象的大小超過這個值時,將直接在老年代分配。

  • -XX:MaxTenuringThreshold:設置對象進入老年代的年齡的最大值。每一次 Minor GC 後,對象年齡就加 1。任何大於這個年齡的對象,必定會進入老年代。

2.與並行 GC 相關的參數

  • -XX:+UseParNewGC:在新生代使用並行回收收集器。

  • -XX:+UseParallelOldGC:老年代使用並行回收收集器。

  • -XX:ParallelGCThreads:設置用於垃圾回收的線程數。一般狀況下能夠和 CPU 數量相等。但在 CPU 數量比較多的狀況下,設置相對較小的數值也是合理的。

  • -XX:MaxGCPauseMills:設置最大垃圾收集停頓時間。它的值是一個大於 0 的整數。收集器在工做時,會調整 Java 堆大小或者其餘一些參數,儘量地把停頓時間控制在 MaxGCPauseMills 之內。

  • -XX:GCTimeRatio:設置吞吐量大小,它的值是一個 0-100 之間的整數。假設 GCTimeRatio 的值爲 n,那麼系統將花費不超過 1/(1+n) 的時間用於垃圾收集。

  • -XX:+UseAdaptiveSizePolicy:打開自適應 GC 策略。在這種模式下,新生代的大小,eden 和 survivor 的比例、晉升老年代的對象年齡等參數會被自動調整,以達到在堆大小、吞吐量和停頓時間之間的平衡點。

3.與 CMS 回收器相關的參數

  • -XX:+UseConcMarkSweepGC:新生代使用並行收集器,老年代使用 CMS+串行收集器。

  • -XX:+ParallelCMSThreads:設定 CMS 的線程數量。

  • -XX:+CMSInitiatingOccupancyFraction:設置 CMS 收集器在老年代空間被使用多少後觸發,默認爲 68%。

  • -XX:+UseFullGCsBeforeCompaction:設定進行多少次 CMS 垃圾回收後,進行一次內存壓縮。

  • -XX:+CMSClassUnloadingEnabled:容許對類元數據進行回收。

  • -XX:+CMSParallelRemarkEndable:啓用並行重標記。

  • -XX:CMSInitatingPermOccupancyFraction:當永久區佔用率達到這一百分比後,啓動 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。

  • -XX:UseCMSInitatingOccupancyOnly:表示只在到達閾值的時候,才進行 CMS 回收。

  • -XX:+CMSIncrementalMode:使用增量模式,比較適合單 CPU。

4.與 G1 回收器相關的參數

  • -XX:+UseG1GC:使用 G1 回收器。

  • -XX:+UnlockExperimentalVMOptions:容許使用實驗性參數。

  • -XX:+MaxGCPauseMills:設置最大垃圾收集停頓時間。

  • -XX:+GCPauseIntervalMills:設置停頓間隔時間。

5.其餘參數

  • -XX:+DisableExplicitGC: 禁用顯示 GC。

4.經常使用調優案例和方法

在實際調優過程當中,須要根據具體狀況進行權衡和折中。

4.1 將新對象預留在新生代

因爲 Full GC 的成本遠遠高於 Minor GC,所以某些狀況下須要儘量將對象分配在年輕代。所以,在 JVM 參數調優時能夠爲應用程序分配一個合理的年輕代空間,以最大限度避免新對象直接進入年老代的狀況發生。

合理設置一個年輕代的空間大小。-Xmn 調整這個參數,最好設置成堆內存的3/8,例如最大-Xmx5G,那麼 -Xmn應該設置成3/8*2大約在2G左右

設置合理的survivor區並提升survivor區的使用率。 第一種是經過參數-XX:TargetSurvivorRatio提升from區的利用率;第二種方法經過-XX:SurvivorRatio,設置一個更大的from區。

4.2 大對象進入老年代

由於大對象出如今年輕代極可能擾亂年輕代 GC,並破壞年輕代原有的對象結構。由於嘗試在年輕代分配大對象,極可能致使空間不足,爲了有足夠的空間容納大對象,JVM 不得不將年輕代中的年輕對象挪到年老代。由於大對象佔用空間多,因此可能須要移動大量小的年輕對象進入年老代,這對 GC 至關不利。基於以上緣由,能夠將大對象直接分配到年老代,保持年輕代對象結構的完整性,這樣能夠提升 GC 的效率。

可使用-XX:PetenureSizeThreshold 設置大對象直接進入老年代的閾值。

舉例:-XX:PetenureSizeThreshold=1000000 當對象大小超過這個值時,將直接在老年代分配。

4.3 設置對象進入老年代的年齡

對象在新生代通過一次GC依然存活,則年齡+1,當年齡達到閥值,就移入老年代。 閥值的最大值經過參數: -XX:MaxTenuringThreshold 來設置,它默認是15。 在實際虛擬機運行過程當中,並非按照這個年齡閥值來判斷,而是依據內存使用狀況來判斷,但這個年齡閥值是最大值,也就說到達這個年齡的對象必定會被移到老年代。

舉例:-XX:MaxTenuringThreshold=1 即全部通過一次GC的對象均可以直接進入老年代。

4.4 穩定與震盪的堆大小

當 -Xms與 -Xmx設置大小同樣,是一個穩定的堆,這樣作的好處是,減小GC的次數。

當 -Xms與 -Xmx設置大小不同,是一個不穩定的堆,它會增長GC的次數,可是它在系統不須要使用大內存時,壓縮堆空間,使得GC應對一個較小的堆,能夠加快單次GC的次數。

能夠經過兩個參數設置用語壓縮和擴展堆空間:

  • -XX:MinHeapFreeRatio: 設置堆的最小空閒比例,默認是40,當堆空間的空閒空間小於這個數值時,jvm會自動擴展空間。

  • -XX:MaxHeapFreeRatio: 設置堆的最大空閒比例,默認是70,當堆空間的空閒空間大於這個數值時,jvm會自動壓縮空間。

4.5 吞吐量優先案例

吞吐量優先的方案將會盡量減小系統執行垃圾回收的總時間,故能夠考慮關注系統吞吐量的並行回收收集器。在擁有4GB內存和32核CPU的計算機上,進行吞吐量的優化,可使用參數:

java –Xmx3800m –Xms3800m –Xmn2G –Xss128k –XX:+UseParallelGC 
   –XX:ParallelGCThreads=20 –XX:+UseParallelOldGC
  • –Xmx380m –Xms3800m:設置 Java 堆的最大值和初始值。通常狀況下,爲了不堆內存的頻繁震盪,致使系統性能降低,咱們的作法是設置最大堆等於最小堆。假設這裏把最小堆減小爲最大堆的一半,即 1900m,那麼 JVM 會盡量在 1900MB 堆空間中運行,若是這樣,發生 GC 的可能性就會比較高;

  • -Xss128k:減小線程棧的大小,這樣可使剩餘的系統內存支持更多的線程;

  • -Xmn2g:設置年輕代區域大小爲 2GB;

  • –XX:+UseParallelGC:年輕代使用並行垃圾回收收集器。這是一個關注吞吐量的收集器,能夠儘量地減小 GC 時間。

  • –XX:ParallelGCThreads:設置用於垃圾回收的線程數,一般狀況下,能夠設置和 CPU 數量相等。但在 CPU 數量比較多的狀況下,設置相對較小的數值也是合理的;

  • –XX:+UseParallelOldGC:設置年老代使用並行回收收集器。

4.6 使用大頁案例

在 Solaris 系統中,JVM 能夠支持 Large Page Size 的使用。使用大的內存分頁能夠加強 CPU 的內存尋址能力,從而提高系統的性能。

java –Xmx2506m –Xms2506m –Xmn1536m –Xss128k -XX:++UseParallelGC
 –XX:ParallelGCThreads=20 –XX:+UseParallelOldGC –XX:+LargePageSizeInBytes=256m

–XX:+LargePageSizeInBytes:設置大頁的大小。

4.7 下降停頓案例

爲下降應用軟件的垃圾回收時的停頓,首先考慮的是使用關注系統停頓的 CMS 回收器,其次,爲了減小 Full GC 次數,應儘量將對象預留在年輕代,由於年輕代 Minor GC 的成本遠遠小於年老代的 Full GC。

java –Xmx3550m –Xms3550m –Xmn2g –Xss128k –XX:ParallelGCThreads=20
 –XX:+UseConcMarkSweepGC –XX:+UseParNewGC –XX:+SurvivorRatio=8 –XX:TargetSurvivorRatio=90
 –XX:MaxTenuringThreshold=31
  • –XX:ParallelGCThreads=20:設置 20 個線程進行垃圾回收;

  • –XX:+UseParNewGC:新生代使用並行回收器;

  • –XX:+UseConcMarkSweepGC:老年代使用 CMS 收集器下降停頓;

  • –XX:+SurvivorRatio:設置 Eden 區和 Survivor 區的比例爲 8:1。稍大的 Survivor 空間能夠提升在新生代回收生命週期較短的對象的可能性(若是 Survivor 不夠大,一些短命的對象可能直接進入老年代,這對系統來講是不利的)。

  • –XX:TargetSurvivorRatio:設置 Survivor 區的可以使用率。這裏設置爲 90%,則容許 90%的 Survivor 空間被使用。默認值是 50%。故該設置提升了 Survivor 區的使用率。當存放的對象超過這個百分比,則對象會向老年代壓縮。所以,這個選項更有助於將對象留在新生代。

  • –XX:MaxTenuringThreshold:設置年輕對象晉升到老年代的年齡。默認值是 15 次,即對象通過 15 次 Minor GC 依然存活,則進入老年代。這裏設置爲 31,即儘量地讓對象保存在新生代。

5.實用JVM參數

5.1 JIT編譯參數

JVM的JIT(Just-In-Time)編譯器,能夠在運行時將字節碼編譯成本地代碼,從而提升函數的執行效率。-XX:CompileThreshold 爲 JIT編譯的閾值, 當函數的調用次數超過 -XX:CompileThreshold 時,JIT就將字節碼編譯成本地機器碼。 在Client 模式下, XX:CompileThreshold 的取值爲1500;在Server 模式下, 取值是10000。JIT編譯完成後, JVM便會使用本地代碼代替原來的字節碼解釋執行。

設置 參數 示例
JIT編譯閾值 -XX:CompileThreshold -XX:CompileThreshold=1500
打印耗時 -XX:CITime -XX:CITime
打印編譯信息 -XX:PrintCompilation -XX:PrintCompilation

5.2 堆快照(堆Dump)

得到程序的堆快照文件有不少方法, 比較經常使用的取得堆快照文件的方法是使用-XX:+HeapDumpOnOutOfMemoryError 參數在程序發生OOM時,導出應用程序的當前堆快照。

經過參數 -XX:heapDumpPath 能夠指定堆快照的保存位置。

-Xmx10m -XX:+HeapDumpOnOutOfMemoryError -XX:heapDumpPath=C:\m.hprof

5.3 錯誤處理

當系統發生OOM錯誤時,虛擬機在錯誤發生時運行一段第三方腳本, 好比, 當OOM發生時,重置系統

-XX:OnOutOfMemoryError=c:\reset.bat

5.4 取得GC信息

JVM虛擬機提供了許多參數幫助開發人員獲取GC信息。

獲取一段簡要的GC信息,可使用 -verbose:gc 或者 -XX:+PrintGC。它們的輸出以下:

[GC 118250K->113543K(130112K), 0.0094143 secs]
[Full GC 121376K->10414K(130112K), 0.0650971 secs]

這段輸出,顯示了GC前的堆棧狀況以及GC後的堆棧大小和堆棧的總大小。

若是要得到更加詳細的信息, 可使用 -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]

它不只包含了GC的整體狀況,還分別給出了新生代、老年代以及永久區各自的GC信息,以及GC的消耗時間。

若是想要在GC發生的時刻打印GC發生的時間,則能夠追加使用-XX:+PrintGCTimeStamps選項。所以,能夠知道GC的頻率和間隔。打印輸出以下:

11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

若是須要查看新生對象晉升老年代的實際閾值, 可使用參數 -XX:+PrintTenuringDistribution 。

若是須要在GC時打印詳細的堆信息,則能夠打開 -XX:+PrintHeapAtGC 開關。

若是須要查看GC與實際程序相互執行的耗時, 可使用 -XX:+PrintGCApplicationtStoppedTime 和 -XX:+PrintGCApplicationConcurrentTime參數。它們將分別顯示應用程序在GC發生時的停頓時間和應用程序在GC停頓期間的執行時間。它們的輸出以下:

Total time for which application threads were stopped: 0.0468229 seconds
Application time: 0.5291524 seconds

爲了能將以上的輸出信息保存到文件,可使用 -Xloggc 參數指定GC日誌的輸出位置。 如 -Xloggc:C:\gc.log。

5.5 類和對象跟蹤

JVM 還提供了一組參數用於獲取系統運行時的加載、卸載類的信息。

-XX:+TraceClassLoading 參數用於跟蹤類加載狀況,-XX:+TraceClassUnloading 用於跟蹤類卸載狀況。若是須要同時跟蹤類的加載和卸載信息,能夠同時打開這兩個開關,也可使用 -verbose:class 參數。

除了類的跟蹤, JVM 還提供了 -XX:+PrintClassHistogram 開關用於打印運行時實例的信息。 當此開關被打開時,  當Ctrl+Break 被按下, 會輸出系統內類的統計信息。

...
 4: 990 23760 java.lang.String
...

從左到右,依次是 序號、實例數量、總大小和類名等信息。

5.6 控制GC

-XX:+DisableExplicitGC 選項用於禁止顯式的GC操做, 即禁止在程序中使用System.gc() 觸發Full GC。

-Xnoclassgc 參數用於禁止系統進行類的回收, 即系統不會卸載任何類,進而提高GC的性能。

-Xincgc 參數,一旦啓用這個參數,系統便會進行增量式的 GC,增量式的GC使用特定算法讓GC線程和應用程序線程交叉執行,從而減少應用程序因GC而產生的停頓時間。

5.7 選擇類校驗器

爲確保class文件的正確和安全,JVM須要經過類校驗器對class文件進行驗證。目前,JVM中有兩套校驗器。

在JDK1.6中默認開啓新的類校驗器,加速類的記載, 可使用 -XX:-UseSplitVerifier 參數指定使用舊的類校驗器(注意是關閉選項)。若是新的校驗器校驗失敗,可使用老的校驗器再次校驗。可使用開關 -XX:-FailOverToOldVerifier關閉再次校驗的功能。

5.8 Solaris下線程控制

在solaris下,JVM提供了幾個用於線程控制的開關:

  • -XX:+UseBoundTreads: 綁定全部用戶線程到內核線程, 減小線程進入飢餓狀態的次數 。
  • -XX:+UserLWPSynchronization: 使用內核線程替換線程同步 。
  • -XX:+UserVMInterruptibleIO: 容許運行時中斷線程。

5.9 使用大頁

對一樣大小的內存空間, 使用大頁後, 內存分頁的表項就會減小, 從而能夠提高CPU從虛擬內存地址映射到物理內存地址的能力。 在支持大頁的操做系統中,使用JVM參數讓虛擬機使用大頁,從而提高系統性能。

  • -XX:+UserlargePages: 啓用大頁。

  • -XX:LargePageSizeInBytes: 指定大頁的大小。

5.10 壓縮指針

在64位虛擬機上, 應用程序所佔內存的大小要遠遠超出其32位版本(約1.5 倍左右)。這是由於64位系統擁有更寬的尋址空間, 與32位系統相比,指針對象的長度進行了翻倍。爲了解決這個問題,64位的JVM虛擬機可使用 -XX:+UseCompressedOops 參數打開指針壓縮,從必定程度上減小內存的消耗,能夠對如下指針進行壓縮:

  • Class的屬性指針(靜態成員變量)
  • 對象的屬性指針
  • 普通對象數組的每一個元素指針

雖然壓縮指針能夠節省內存,可是壓縮和解壓指針也會對JVM形成必定的性能損失。

6.實戰JVM調優

6.1 Tomcat啓動加速

使用 startup.bat 啓動Tomcat 服務器時,start.bat  調用了bin 目錄下的calalina.bat 文件。 若是須要配置 Tomcat的JVM參數,能夠將參數寫入 catalina.bat 中。打開 catalina.bat,能夠看到:

這段說明顯示,配置環境變量CATALINA_OPTS或者JAVA_OPTS均可以設置Tomcat的JVM優化參數。根聽說明建議,相似堆大小、GC日誌和 JMX 端口等推薦配置在 CATALIN_OPTS 中。

獲取GC信息能夠加入:

set CATALINA_OPTS=-Xloggc:gc.log -XX:+PrintGCDetails

爲了減小Minor GC的次數, 增大新生代:

set CATALINA_OPTS=%CATALINA_OPTS% -Xmx32M -Xms32M

禁用顯示GC:

set CATALINA_OPTS=%CATALINA_OPTS% -XX:+DisableExplicitGC

在堆內存不變的前提下,爲了能進一步減小Minor GC的次數,能夠擴大新生代的大小:

set CATALINA_OPTS=%CATALINA_OPTS% -XX:NewRation=2

爲了加快Minor GC的速度,在多核計算機上能夠考慮使用新生代並行回收收集器,加快Minor GC 的速度:

set CATALINA_OPTS=%CATALINA_OPTS% -XX:+UseParallelGC

因爲JVM虛擬機在加載類時,處於徹底考慮,會對Class進行校驗和認證,若是類文件是可信任的, 爲了加快程序的運行速度,也能夠考慮禁用這些效應:

set CATALINA_OPTS=%CATALINA_OPTS% -Xverify:none

6.2 JMeter介紹和使用

JMeter是Apache 下基於Java 的一款性能測試和壓力測試工具。它基於Java 開發,可對HTTP 服務器和FTP服務器,甚至是數據庫進行壓力測試。

下載地址:http://jmeter.apache.org/download_jmeter.cgi

中文教程:https://www.yiibai.com/jmeter/

1)如何切換中文界面?

編輯/bin/jmeter.properties文件,

找到被註釋的#language那一行,更改成 language=zh_CN

2)入門HTTP測試

使用版本:5.0 ,環境:windows

第一步:新建線程組

第二步:配置線程數10,每條線程循環200次。

第三步:配置取樣器,這裏是HTTP請求。

第四步:配置HTTP請求參數,服務器IP,端口號,路徑,HTTP參數等。

第五步:生成測試報告。JMeter提供圖形、表格等多種形式的報告,報告有各項參數,包括平均響應時間、錯誤數和吞吐量。這裏是生成聚合報告。

第六步:配置完成後,單機頂部綠色的三角圖形,啓動,便可進行測試。測試完成後,查看吞吐量那一欄(Throughput)。

3)調優過程示例

爲了減小GC次數, 可使用合理的堆大小和永久區大小。這裏將堆大小設置爲512MB, 永久區使用32MB, 同時, 禁用顯示GC, 並去掉類校驗。參數以下:

set CATALINA_OPTS=%CATALINA_OPTS% "-Xmx512M"
set CATALINA_OPTS=%CATALINA_OPTS% "-Xms512M"
set CATALINA_OPTS=%CATALINA_OPTS% "-XX:PermSize=32M"
set CATALINA_OPTS=%CATALINA_OPTS% "-XX:MaxPermSize=32M"
set CATALINA_OPTS=%CATALINA_OPTS% "-XX:+DisableExplicitGC"
set CATALINA_OPTS=%CATALINA_OPTS% "-Xverify:none"

爲了進一步提升系統的吞吐量, 能夠嘗試使用並行回收收集器代替串行收集器。

set CATALINA_OPTS=%CATALINA_OPTS% "-Xmx512M"
set CATALINA_OPTS=%CATALINA_OPTS% "-Xms512M"
set CATALINA_OPTS=%CATALINA_OPTS% "-XX:PermSize=32M"
set CATALINA_OPTS=%CATALINA_OPTS% "-XX:MaxPermSize=32M"
set CATALINA_OPTS=%CATALINA_OPTS% "-XX:+DisableExplicitGC"
set CATALINA_OPTS=%CATALINA_OPTS% "-Xverify:none"
set CATALINA_OPTS=%CATALINA_OPTS% -XX:+UseParallelGC
set CATALINA_OPTS=%CATALINA_OPTS% -XX:+UseParallelOldGC
set CATALINA_OPTS=%CATALINA_OPTS% -XX:ParallelGCThreads=8

總結一下 JVM調優的主要過程有: 肯定堆內存大小(-Xmx, -Xms)、合理分配新生代和老生代(-XX:NewRation, -Xmn, -XX:SurvivorRatio)、肯定永久區大小: -XX:Permsize, -XX:MaxPermSize、選擇垃圾收集器、對垃圾收集器進行合理的設置,除此以外,禁用顯示GC(-XX:+DisableExplicitGC), 禁用類元數據回收(-Xnoclassgc), 禁用類驗證(-Xverfy:none)等設置, 對提高系統性能也有必定的幫助。

本文參考

《深刻理解Java虛擬機》周志明
《Java程序性能優化》葛一鳴

相關文章
相關標籤/搜索