第5章 JVM调优
2019獨角獸企業重金招聘Python工程師標準>>>
##5.1 Java虛擬機內存模型## Java虛擬機內存模型是Java程序運行的基礎。JVM將其內存數據分為程序計數器,虛擬機棧,本地方法棧,Java堆和方法區等部分。 程序計數器:用于存放下一條運行的指令; 虛擬機棧和本地方法棧:用于存放函數調用堆棧信息; Java堆:用于存放Java程序運行時所需的對象等數據; 方法區:用于存放程序的類元數據信息; ###5.1.1 程序計數器### 程序計數器是一塊很小內存空間。由于Java是支持線程的語言,當線程數量超過CPU數量時,線程之間根據時間片輪詢搶奪CPU資源。對于單核CPU而言,每一時刻,只能有一個線程在運行,而其他線程必須被切換出去。為此,每一個線程都必須用一個獨立的程序計數器,用于記錄下一條要運行的指令。各個線程之間的計數器互不影響,獨立工作,是一塊線程私有的內存空間。
如果當前線程正在執行一個Java方法,則程序計數器記錄正在執行的Java字節碼地址,如果當前線程正在執行一個Native方法,程序計數器為空。 ###5.1.2 Java虛擬機棧### Java虛擬機棧也是線程私有的內存空間,它和Java線程在同一時間創建,它保存方法的局部變量,部分結果,并參與方法的調用與返回。
Java虛擬機規范允許Java棧的大小是動態的或者是固定的。在Java虛擬機規范中,定義了兩種異常與棧空間有關:StackOverflowError和OutOfMemoryError。如果線程在計算過程中,請求的棧深度大于最大可用的棧深度,則拋出StackOverflowError;如果Java??梢詣討B擴展,而在擴展棧的過程中,沒有足夠的內存空間來支持棧的擴展,則拋出OutOfMemoryError。
在HotSpot虛擬機中,可以使用-Xss參數來設置棧的大小。棧的大小直接決定了函數調用的可達深度。
虛擬機棧在運行時使用一種叫做棧幀的數據結構保存上下文數據。在棧幀中,存放了方法的局部變量表,操作數棧,動態連接方法和返回地址等信息。每一個方法的調用都伴隨著棧幀的入棧操作。相應地,方法的返回則表示棧幀的出棧操作。如果方法調用時,方法的參數和局部變量相對較多,那么棧幀中的局部變量表就會比較大,棧幀會膨脹以滿足方法調用所需傳遞的信息。因此,單個方法調用所需的??臻g大小也會比較多。
注意:函數嵌套調用的次數由棧的大小決定。棧越大,函數嵌套調用次數越多。對一個函數而言,它的參數越多,內部局部變量越多,它的棧幀就越大,其嵌套調用次數就會越少。
在棧幀中,與性能調優關系最為密切的部分就是局部變量表。局部變量表用于存放方法的參數和方法內部的局部變量。局部變量表以“字”為單位進行內存的劃分,一個字為32位長度。對于long和double型的變量,則占用2個字,其余類型使用1個字。在方法執行時,虛擬機使用局部變量表完成方法的傳遞,對于非static方法,虛擬機還會將當前對象(this)作為參數通過局部變量表傳遞給當前方法。
注意:使用jclasslib工具可以深入研究Class類文件的結構,有助于讀者對Java語言更深入的了解。
局部變量表的字空間是可以重用的。因為在一個方法體內,局部變量的作用范圍并不一定是整個方法體。
// 最大局部變量表容量:2+1=3 public void test1() {{long a = 0;}long b = 0; } // 最大局部變量表容量:2+2+1=3 public void test2() {long a = 0;long b = 0; }局部變量表的字,對系統GC也有一定影響。如果一個局部變量被保存在局部變量表中,那么GC根就能引用到這個局部變量所指向的內存空間,從而在GC時,無法回收這部分空間。
// GC無法回收,因為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 test1() {{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 test1() {{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 test1() {{int c = 0;byte[] b = new byte[6*1024*1024];}int a = 0; // 復用c的字System.gc();System.out.println("first explict gc over"); }// GC可以回收,因為變量d復用了b的字 public static void test1() {{int c = 0;byte[] b = new byte[6*1024*1024];}int a = 0; // 復用c的字int d = 0; // 復用b的字System.gc();System.out.println("first explict gc over"); }在這個函數體內,即使在變量b失效后,又未能定義足夠多的局部變量來復用該變量所占的字,變量b仍會在該棧幀的局部變量表中。因此GC根可以引用到該內存塊,阻礙了其回收過程。在這種環境下,手工對要釋放的變量賦值為null,是一種有效的做法。
當方法一結束,該方法的棧幀就會被銷毀,即棧幀中的局部變量表也被銷毀。
注意:局部變量表中的字可能會影響GC回收。如果這個字沒有被后續代碼復用,那么它所引用的對象不會被GC釋放。
###5.1.3 本地方法棧### 本地方法棧和Java虛擬機棧的功能很相似,Java虛擬機棧用于管理Java函數的調用,而本地方法棧用于管理本地方法的調用。本地方法并不是用Java實現的,而是使用C語言實現的。
和虛擬機棧一樣,它也會拋出StackOverflowError和OutOfMemoryError。 ###5.1.4 Java堆### Java堆分為新生代和老年代兩個部分,新生代用于存放剛剛產生的對象和年輕的對象,如果對象一直沒有被回收,生存得足夠長,老年對象就會被移入老年代。新生代又可進一步分為:eden,survivor0,survivor1。eden意義為伊甸園,即對象的出生地,大部分對象剛剛建立時,通常會存放在這里。s0和s1為Survivor空間,直譯為幸存者,也就是說存放其中的對象至少經歷了一次垃圾回收,并得以幸存。如果在幸存區的對象到了指定年齡仍未被回收,則有機會進入老年代。
package com.king.gc;/*** -XX:+PrintGCDetails -XX:SurvivorRatio=8* -XX:MaxTenuringThreshold=15 -Xms40M -Xmx40M -Xmn20M* @author taomk* @version 1.0* @since 15-5-10 上午9:46*/ public class TestHeapGC {public static void main(String [] args) {byte[] b1 = new byte[1024 * 1024 / 2];byte[] b2 = new byte[1024 * 1024 * 8];b2 = null;b2 = new byte[1024 * 1024 * 8];System.gc();} }###5.1.5 方法區### 方法區也是JVM內存區中非常重要的一塊內存區域,與堆空間類似,它也是被JVM中所有線程共享的。方法區主要保存的信息是類的元數據。
方法區中最為重要的是類的類型信息,常量池,域信息,方法信息。類型信息包括類的完整名稱,父類的完整名稱,類型修飾符和類型的直接接口類表;常量池包括這個類方法,域等信息所引用的常量信息;域信息包括域名稱,域類型和域修飾符;方法信息包括方法名稱,返回類型,方法參數,方法修飾符,方法字節碼,操作數棧和方法棧幀的局部變量區大小及異常表。總之,方法區內保持的信息,大部分來自于class文件,是Java應用程序運行必不可少的重要數據。
在HotSpot虛擬機中,方法區也稱為永久區,是一塊獨立于Java堆的內存空間。雖然叫做永久區,但是在永久區中的對象,同樣也是可以被GC回收的。只是對于GC的表現也和Java堆空間略有不同。對永久代GC的回收,通常主要從兩個方面分析:(1)GC對永久代常量池的回收;(2)GC對永久代類元數據的回收。
**GC對永久代常量池的回收:**只要常量池中的常量沒有被任何地方引用,就可以被回收。String.intern()方法的含義是:如果常量池中已經存在當前String,則返回池中的對象;如果常量池中不存在當前String的對象,則先將String加入常量池,并返回池中的對象引用。
**GC對永久代類元數據的回收:**如果虛擬機確認該類的所有實例已經被回收,并且加載該來的ClassLoader實例也已經被回收,GC就有可能回收該類型。 ##5.2 JVM內存分配參數## ###5.2.1 設置最大堆內存### 最大堆內存可以用-Xmx參數指定。最大堆指的是新生代和老年代的大小之和的最大值,它是Java應用程序的上限。 ###5.2.2 設置最小堆內存### 最小堆內存可以用-Xms參數指定。也就是JVM啟動時,所占據的操作系統內存大小。
如果-Xms的數值較小,那么JVM為了保證系統盡可能地在指定內存范圍內運行,就會更加頻繁地進行GC操作,以釋放失效的內存空間,從而,會增加Minor GC和Full GC的次數,對系統性能產生一定影響。
注意:JVM會試圖將系統內存盡可能限制在-Xms中,因此,當內存實際使用量觸及-Xms指定的大小時,會觸發Full GC。因此把-Xms值設置為-Xmx時,可以在系統運行初期減少GC的次數和耗時。
###5.2.3 設置新生代### 參數-Xmn用于設置新生代的大小。新生代的大小一般設置為整個堆空間的1/4到1/3左右。設置-Xmn的效果等同于設置了相同的-XX:NewSize和-XX:MaxNewSize。若設置不同的-XX:NewSize和-XX:MaxNewSize可能會導致內存震蕩,從而產生不必要的系統開銷。 ###5.2.4 設置持久代### 持久代(方法區)不屬于堆的一部分。在HotSpot虛擬機中,使用-XX:MaxPermSize可以設置持久代的最大值,使用-XX:PermSize可以設置持久代的初始大小。持久代的大小直接決定了系統可以支持多少個類定義和多次常量。
一般來說,-XX:MaxPermSize設置為64MB已經可以滿足絕大部分應用程序正常工作。如果依然出現永久區溢出,可以將-XX:MaxPermSize設置為128MB。 ###5.2.5 設置線程棧### 線程棧是線程的一塊私有空間。在JVM中,可以使用-Xss參數設置線程棧的大小。
在線程中進行局部變量分配,函數調用時,都需要在棧中開辟空間。如果棧的空間分配太小,那么線程在運行時,可能沒有足夠的空間分配局部變量或者達不到足夠的函數調用深度,導致程序異常退出;如果??臻g過大,那么開設線程所需的內存成本就會上升,系統所能支持的線程總數就會下降。
由于Java堆也是向操作系統申請內存空間的,因此,如果堆空間過大,就會導致操作系統可用于線程棧的內存減少,從而間接減少程序所能支持的線程數量。當系統由于內存空間不夠而無法創建新的線程時,會拋出OOM異常如下:
java.lang.OutOfMemoryError: unable to create new native thread
根據以上可知,這并不是由于堆內存不夠而導致的OOM,而是因為操作系統內存減去堆內存后,剩余的系統內存不足而無法創建新的線程。在這種情況下,可以嘗試減少堆內存,以換取更多的系統空間,來解決這個問題。 ###5.2.6 堆的比例分配### -XX:SurvivorRatio:可以設置eden區與survivor區的比例; -XX:NewRatio:可以設置老年代與新生代的比例; ###5.2.7 堆分配參數總結### -Xms:設置Java應用程序啟動時的初始堆大小; -Xmx:設置Java應用程序能獲得的最大堆大小; -Xss:設置線程棧的大小; -XX:MinHeapFreeRatio:設置堆空間最小空閑比例。當堆空間的空閑內存小于這個數值時,JVM便會擴展堆空間; -XX:MaxHeapFreeRatio:設置堆空間最大空閑比例。當堆空間的空閑內存大于這個數值時,便會壓縮堆空間,得到一個較小的堆; -XX:NewSize:設置新生代的初始大小; -XX:MaxNewSize:設置新生代能獲得的最大大小; -XX:NewRatio:設置老年代與新生代的比例,它等于老年代大小除以新生代大小; -XX:SurvivorRatio:設置新生代中eden區與survivor區的比例; -XX:PermSize:設置永久代的初始大小; -XX:MaxPermSize:設置永久代能獲得的最大大小; -XX:TargetSurvivorRatio:設置Survivor區的可使用率。當Survivor區的空間使用率達到這個數值時,會將對象送入老年代; ##5.3 垃圾收集基礎## Java語言的一大特點就是可以進行自動垃圾回收處理,而無需開發人員過于關注系統資源(尤指內存資源)的釋放情況。 ###5.3.1 垃圾收集的作用### 擁有垃圾收集器可以說是Java語言與C++語言的一項顯著區別。在C++語言中,程序員必須小心謹慎地處理每一項內存分配,且內存使用完后,必須手工釋放曾經占用的內存空間。當內存釋放不夠完全時,即存在分配但永不釋放的內存塊,就會引起內存泄漏,嚴重時,會導致程序崩潰。 ###5.3.2 垃圾回收算法與思想### 1. 引用計數法 引用計數法是最經典也是最古老的一種垃圾收集算法,其實現也非常簡單,只需要為每個對象配備一個整型的計數器即可。但是,引用計數器有一個嚴重的問題,即無法處理循環引用的情況。因此,在Java的垃圾回收器中,沒有使用這種算法。
2. 標記—清除算法 標記—清除算法是現代垃圾回收算法的思想基礎。標記—清除算法將垃圾回收分為兩個階段:標記階段和清除階段。在標記階段,首先通過根節點,標記所有從根節點開始的可達對象。因此,未被標記的對象就是未被引用的垃圾對象。然后,在清除階段,清除所有未被標記的對象。標記—清除算法可能產生的最大問題就是空間碎片。
該算法回收后的空間是不連續的。在對象的堆空間分配過程中,尤其是大對象的內存分配,不連續的內存空間的工作效率要低于連續的空間。因此,這也是該算法的最大缺點。
3. 復制算法 與標記—清除算法,復制算法是一種相對高效的回收方法。它的核心思想是:將原有的內存空間分為兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的內存中的存活對象復制到未使用的內存塊中,之后,清除正在使用的內存塊中的所有對象,交換兩個內存塊的角色,完成垃圾回收。
如果系統中的垃圾對象很多,復制算法需要復制的存活對象數量并不會太大。因此,在真正需要垃圾回收的時刻,復制算法的效率是很高的。又由于對象是在垃圾回收過程中統一被復制到新的內存空間中,因此,可確保回收后的內存空間是沒有碎片的。雖然有以上兩優點,但是,復制算法的代價缺點是將系統內存折半,因此,單純的復制算法也很難讓人接受。
4. 標記—壓縮算法 復制算法的高效性是建立在存活對象少,垃圾對象多的前提下。這種情況在年輕代經常發生,但是在老年代,更常見的情況是大部分對象都是存活對象。如果依然使用復制算法,由于存活對象較多,復制的成本也將很高。因此,基于老年代垃圾回收的特性,需要使用新的算法。
標記—壓縮算法是一種老年代的回收算法,它在標記—清除算法的基礎上做了一些優化。和標記—清除算法一樣,標記—壓縮算法也首先需要從根節點開始,對所有可達對象做吃一次標記。之后,清理未標記的對象,而是將所有的存活對象壓縮到內存的一端。之后,清理邊界外所有的空間。這種方法既避免了碎片的產生,又不需要兩塊相同的內存空間,因此,其性價比較高。
5. 增量算法 對大部分垃圾回收算法而言,在垃圾回收的過程中,應用將處于一種Stop the World的狀態。在Stop the World狀態下,應用所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間很長,應用會被掛起很久,將嚴重影響用戶體驗或者系統的穩定性。
增量算法的基本思想是,如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那么就可以讓垃圾收集線程和應用程序交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接著切換到應用程序線程。以此反復,直到垃圾收集完成。使用這種方式,由于在垃圾回收的過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。
6. 分代 分代算法就是基于這種思想,它將內存區間根據對象的特點分成幾塊,根據每塊內存區間的特點,使用不同的收集算法,以提高垃圾回收的效率。該算法被現有的HotSpot虛擬機廣泛使用,幾乎所有的垃圾回收器都區分年輕代和老年代。 ###5.3.3 垃圾收集器的類型### 按線程數分:串行垃圾回收器和并行垃圾回收器。串行垃圾回收器一次只使用一個線程進行垃圾回收;并行垃圾回收器將開啟多個線程同時進行垃圾回收。在并行能力較強的CPU上,使用并行垃圾回收器可以縮短GC的停頓時間。
按工作模式分:并發式垃圾回收器和獨占式垃圾回收器。并發式垃圾回收器與應用程序線程交替執行,以盡可能減少應用程序的停頓時間;獨占式垃圾回收器一旦運行,就停止應用程序中的其他所有線程,直到垃圾回收過程完全結束。
按碎片處理方式分:壓縮式垃圾回收器和非壓縮式垃圾回收器。壓縮式垃圾回收器會在回收完成后,對存活對象進行壓縮管理,消除回收后的碎片;非壓縮式的垃圾回收器,不進行這步操作。
按工作區間分:新生代垃圾回收器和老年代垃圾回收器。顧名思義,新生代垃圾回收器只在新生代工作;老年代垃圾回收器則工作在老年代。 ###5.3.4 評價GC策略的指標### 吞吐量:指在應用程序的生命周期內,應用程序所花費的時間和系統總運行時間的比值。系統總運行時間=應用程序耗時+GC耗時。如果系統運行了100min,GC耗時1min,那么系統的吞吐量就是(100-1)/ 100=99%。
垃圾回收器負載:和吞吐量相反,垃圾回收器負載指垃圾回收器耗時與系統運行總時間的比值。
停頓時間:指垃圾回收器正在運行時,應用程序的暫停時間。對于獨占回收器而言,停頓時間可能會比較長。使用并發的回收器時,由于垃圾回和應用程序交替執行,程序的停頓時間會變短,但是,由于其效率很可能不如獨占回收器,故系統的吞吐量可能會降低。
垃圾回收頻率:指垃圾回收器多長時間會運行一次。一般來說,對于固定的應用而言,垃圾回收的頻率應該是越低越好。通常增大堆空間可以有效降低垃圾回收發生的頻率,但是可能會增加回收產生的停頓時間。
反應時間:指當一個對象稱為垃圾后,多長時間內,它所占據的內存空間會被釋放。
堆分配:不同的垃圾回收器對堆內存的分配方式可能是不同的。一個良好的垃圾收集器應該有一個合理的堆內存區間劃分。
通常情況下,很難讓一個應用程序在所有的指標上都達到最優。因此,只能根據應用本身的特點,盡可能使垃圾回收器配合應用程序的工作。 ###5.3.5 新生代串行收集器### 串行收集器是所有垃圾收集器中最古老的一種,也是JDK中最基本的垃圾收集器之一。串行收集器主要有兩個特點:第一,它僅僅使用單線程進行垃圾回收;第二,它是獨占式的垃圾回收。
在串行收集器進行垃圾回收時,Java應用程序中的線程都需要暫停,等待垃圾回收的完成。這種現象稱為“Stop the World”。雖然如此,串行收集器卻是一個成熟,經過長時間生產環境考驗的極為高效的收集器。新生代串行處理器使用復制算法,實現相對簡單,邏輯處理特別高效,且沒有線程切換的開銷。在諸如單CPU處理器或者較小的應用內存等硬件平臺不是特別優越的場合,它的性能表現可以超越并行收集器和并發收集器。
在HotSpot虛擬機中,使用-XX:+UseSerialGC參數可以指定使用新生代串行收集器和老年代串行收集器。當JVM在Client模式下運行時,它是默認的垃圾收集器。 ###5.3.6 老年代串行收集器### 老年代串行收集器使用的是標記—壓縮算法。和新生代串行收集器一樣,它也是一個串行的,獨占式的垃圾回收器。若要啟用老年代串行收集器,可以嘗試使用以下參數: **-XX:+UseSerialGC:**新生代,老年代都使用串行收集器; **-XX:+UseParNewGC:**新生代使用并行收集器,老年代使用串行收集器; **-XX:+UseParallelGC:**新生代使用并行收集器,老年代使用串行收集器; ###5.3.7 并行收集器### 并行收集器是工作在新生代的垃圾收集器,它只是簡單地將串行收集器多線程化。它的回收策略,算法以及參數和串行收集器一樣。并行收集器也是獨占式的回收器,在收集過程中,應用程序會全部暫停。但由于并行收集器使用多線程進行垃圾回收,因此,在并發能力比較強的CPU上,它產生的停頓時間要短于串行回收器,而在單CPU或者并發能力較弱的系統中,并行收集器的效果不會比串行收集器好,由于多線程的壓力,它的實際表現很可能比串行收集器差。
開啟并行收集器可以使用以下參數: **-XX:+UseParNewGC:**新生代使用并行收集器,老年代使用串行收集器; **-XX:+UseConcMarkSweepGC:**新生代使用并行收集器,老年代使用CMS;
并行收集器工作時的線程數量可以使用-XX:ParallelGCThreads參數指定。一般,最好與CPU數量相當,避免過多的線程數,影響垃圾收集性能。在默認情況下,當CPU數量小于8個時,ParallelGCThreads的值等于CPU數量;當CPU數量大于8個時,ParallelGCThreads的值等于3+[(5*CPU_Count)/8]。 ###5.3.8 新生代并行回收收集器### 新生代并行回收收集器也是使用復制算法的收集器。從表面上看,它和并行收集器一樣,都是多線程,獨占式的收集器。但是,并行回收收集器有一個重要的特點:它非常關注系統的吞吐量。
新生代并行回收收集器可以使用以下參數啟用: -XX:+UseParallelGC:新生代使用并行回收收集器,老年代使用串行收集器。 -XX:+UseParallelOldGC:新生代和老年代都使用并行回收收集器。
并行回收收集器提供了兩個重要的參數用于控制系統的吞吐量: (1)-XX:MaxGCPauseMillis:設置最大垃圾收集停頓時間,它的值是一個大于0的整數。收集器在工作時,會調整Java堆大小或者其他一些參數,盡可能地把停頓時間控制在MaxGCPauseMillis以內。如果希望減少停頓時間,而把這個值設得很小,為了達到預期的停頓時間,JVM可能會使用一個較小的堆(一個小堆比一個大堆回收快),而這將導致垃圾回收變得很頻繁,從而增加了垃圾回收總時間,降低了吞吐量。 (2)-XX:GCTimeRatio:設置吞吐量大小,它的值一個0~100之間的整數。假設GCTimeRatio的值為n,那么系統將花費不超過1/(1+n)的時間用于垃圾收集。比如GCTimeRatio等于19默認值,則系統用于垃圾收集的時間不超過1/(1+19)=5%。默認情況下,它的取值是99,即不超過1/(1+99)=1%的時間用于垃圾收集。
除此以外,并行回收收集器與并行收集器另一個不同之處在于,它還有一種自適應的GC調節策略。使用-XX:+UseAdaptiveSizePolicy可以打開自適應GC策略。在這種模式下,新生代的大小,eden和survivor的比例,晉升老年代的對象年齡等參數會被自動調整,以達到在堆大小,吞吐量和停頓時間之間的平衡點。在手工調優比較困難的場合,可以直接使用這種自適應的方式,僅指定虛擬機的最大堆,目標吞吐量(GCTimeRatio)和最大停頓時間(MaxGCPauseMillis),讓虛擬機自己完成調優工作。 ###5.3.9 老年代并行回收收集器### 老年代并行回收收集器也是一種多線程并行的收集器。和新生代并行回收收集器一樣,它也是一種關注吞吐量的收集器。老年代并行回收收集器使用標記—壓縮算法。
使用-XX:+UseParallelOldGC可以在新生代和老年代都使用并行回收收集器,這是一對非常關注吞吐量的垃圾收集器組合,在對吞吐量敏感的系統中,可以考慮使用。參數-XX:ParallelGCThreads也可以用于設置垃圾回收時的線程數量。 ###5.3.10 CMS收集器### 與并行回收收集器不同,CMS收集器主要關注于系統停頓時間。CMS是Concurrent Mark Sweep的縮寫,意味并發標記清除。從名稱上就可以得知,它使用的是標記—清除算法,同時它又是一個使用多線程并行回收的垃圾收集器。
CMS收集器的工作過程與其他垃圾收集器相比,略顯復雜。CMS工作時,主要步驟有:初始標記,并發標記,重新標記,并發清除和并發重置。其中初始標記和重新標記是獨占系統資源的,而并發標記,并發清除和并發重置是可以和用戶線程一起執行的。因此,從整體上說,CMS收集不是獨占式的,它可以在應用程序運行過程中進行垃圾回收。
根據標記—清除算法,初始標記,并發標記和重新標記都是為了標記出需要回收的對象。并發清理,則是在標記完成后,正式回收垃圾對象;并發重置是指在垃圾回收完成后,重新初始化CMS數據結構和數據,為下一次垃圾回收做好準備。并發標記,并發清理和并發重置都是可以和應用程序線程一起執行的。
CMS收集器在其主要的工作階段雖然沒有暴力地徹底暫停應用程序線程,但是由于它和應用程序線程并發執行,相互搶占CPU,故在CMS執行期內對應用程序吞吐量將造成一定影響。CMS默認啟動的線程數是((ParallelGCThreads+3)/4),ParallelGCThreads是新生代并行收集器的線程數,也可以通過-XX:ParallelGCThreads參數手工設定CMS的線程數量。當CPU資源比較緊張時,受到CMS收集器線程的影響,應用系統的性能在垃圾回收階段可能會非常糟糕。
由于CMS收集器不是獨占式的回收器,在CMS回收過程中,應用程序仍然在不停地工作。在應用程序工作過程中,又會不斷地產生垃圾。這些新生成的垃圾在當前CMS回收過程中是無法清除的。同時,因為應用程序沒有中斷,故在CMS回收過程中,還應該確保應用程序有足夠的內存可用。因此,CMS收集器不會等待堆內存飽和時才進行垃圾回收,而是當堆內存使用率達到某一閥值時,便開始進行回收,以確保應用程序在CMS工作過程中,依然有足夠的內存空間支持應用程序運行。
這個回收閥值可以使用-XX:CMSInitiatingOccupancyFraction來指定,默認是68。即,當老年代的空間使用率達到68%時,會執行一次CMS回收。如果應用程序的內存使用率增長很快,在CMS的執行過程中,已經出現了內存不足的情況,此時,CMS回收就會失敗,JVM將啟動老年代串行收集器進行垃圾回收。如果這樣,應用程序將完全中斷,直到垃圾收集完成,這時,應用程序的停頓時間可能很長。
-XX:+UseCMSCompactAtFullCollection開關可以使CMS在垃圾收集完成后,進行一次內存碎片整理。內存碎片的整理不是并發進行的。-XX:CMSFullGCsBeforeCompaction參數可以用于設定進行多少次CMS回收后,進行一次內存壓縮。
綜上,CMS收集器是一個關注停頓的垃圾收集器。同時CMS收集器在部分工作流程中,可以與用戶程序同時運行,從而降低應用程序的停頓時間。 ###5.3.11 G1收集器### G1收集器的目標是作為一款服務端的垃圾收集器,因此,它在吞吐量和停頓控制上,預期要優于CMS收集器。G1收集器是基于標記—壓縮算法的。因此,它不產生空間碎片,也沒有必要在收集完成后,進行一次獨占式的碎片整理工作。G1收集器還可以進行非常精確的停頓控制。它可以讓開發人員指定在長度為M的時間段中,垃圾回收時間不超過N。
使用以下參數可以啟用G1回收器:-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC,設置G1回收器的目標停頓時間:-XX:MaxGCPauseMillis=50 -XX:GCPauseIntervalMillis=200,表示指定在200ms內,停頓時間不能超過50ms,這兩個參數是G1回收器的目標,G1回收器并不保證能執行它們。 ###5.3.12 Stop the World### 在JVM垃圾回收時,應用系統會產生一定的停頓。尤其在獨占式的垃圾回收器中,整個應用程序會被停止,直到垃圾回收的完成。這種現象稱為Stop the World。慎重選擇回收器并對其進行調優,是相當重要的。 ###5.3.13 GC相關參數總結###
與串行回收器相關的參數 **-XX:+UseSerialGC:**在新生代和老年代使用串行收集器; **-XX:SurvivorRatio:**設置Eden區大小和survivor區大小的比例; **-XX:PretenureSizeThreshold:**設置大對象直接進入老年代的閥值。當對象的大小超過這個值時,將直接在老年代分配; **-XX:MaxTenuringThreshold:**設置對象進入老年代的年齡的最大值。每一次MinorGC后,對象年齡就加1。任何大于這個年齡的對象,一定會進入老年代;
與并行GC相關的參數 **-XX:+UseParNewGC:**在新生代使用并行收集器; **-XX:+UseParallelOldGC:**在老年代使用并行回收收集器; **-XX:ParallelGCThreads:**設置用于垃圾回收的線程數。通常情況下可以和CPU數量相等。但在CPU數量比較多的情況下,設置相對較小的數值也是合理的; **-XX:MaxGCPauseMillis:**設置最大垃圾停頓時間。它的值是一個大于0的整數。收集器在工作時,會調整Java堆大小或者其他一些參數。盡可能地把停頓時間控制在MaxGCPauseMillis以內; **-XX:GCTimeRatio:**設置吞吐量大小,它的值是一個0~100之間的整數。假設GCTimeRatio的值為n,那么系統將花費不超過1/(1+n)的時間用于垃圾收集; **-XX:+UseAdaptiveSizePolicy:**打開自適應GC策略。在這種模式下,新生代的大小,eden和survivor的比例,晉升老年代的對象年齡等參數會被自動調整,以達到在堆大小,吞吐量和停頓時間之間的平衡點;
與CMS回收器相關的參數 **-XX:+UseConcMarkSweepGC:**新生代使用并行收集器,老年代使用CMS+串行收集器; **-XX:ParallelCMSThreads:**設置CMS的線程數量; **-XX:CMSInitiatingOccupancyFraction:**設置CMS收集器在老年代空間被使用多少后觸發。默認為68%; **-XX:+UseCMSCompactAtFullCollection:**設置CMS收集器在完成垃圾收集后是否要進行一次內存碎片的整理; **-XX:CMSFullGCsBeforeCompaction:**設置進行多少次CMS垃圾收集后,進行一次內存壓縮; **-XX:+CMSClassUnloadingEnabled:**允許對類元數據進行回收; **-XX:+CMSParallelRemarkEnabled:**啟用并行重標記; **-XX:CMSInitiatingPermOccupancyFraction:**當永久區占用率達到這一百分比時,啟動CMS回收(前提是-XX:+CMSClassUnloadingEnabled開啟); **-XX:UseCMSInitiatingOccupancyOnly:**表示只在到達閥值的時候,才進行CMS回收; **-XX:+CMSIncrementalMode:**使用增量模式,比較適合單CPU;
與G1回收器相關的參數 **-XX:+UseG1GC:**使用G1回收器; **-XX:MaxGCPauseMillis:**設置最大垃圾收集停頓時間; **-XX:GCPauseIntervalMillis:**設置停頓間隔時間;
其他參數 **-XX:+DisableExplicitGC:**禁用顯示GC(System.gc());
##5.4 常用調優案例和方法## ###5.4.1 將新對象預留在新生代### 在JVM參數調優中,可以為應用程序分配一個合理的新生代空間, 以最大限度避免新對象直接進入老年代的情況。
###5.4.2 大對象進入老年代### 在大部分情況下,將對象分配在新生代是合理的。但是,對于大對象,這種做法確實值得商榷的。因為大對象出現在新生代可能擾亂新生代GC,并破壞新生代原有的對象結構。因為嘗試在新生代分配大對象,很可能導致空間不足,為了有足夠的連續空間容納大對象,JVM不得不將新生代中的年輕對象挪到老年代。因為大對象占用空間多,所以,可能需要移動大量小的年輕對象進入老年代。這對GC來說是相當不利的。
基于以上原因, 通過參數-XX:PretenureSizeThreshold設置大對象進入老年代的大小閥值(字節),可以將大對象直接分配到老年代。尤其是短命的大對象,對于垃圾回收是一場災難。 ###5.4.3 設置對象進入老年代的年齡### 通過參數-XX:MaxTenuringThreshold,設置進入老年代的對象的年齡的閥值。默認值是15。這不意味著新對象非要達到這個年齡才能進入老年代。事實上,對象實際進入老年代的年齡是虛擬機在運行時根據內存使用情況動態計算的,這個參數指定的是閥值年齡的最大值。即,實際晉升老年代年齡等于動態計算所得的年齡與-XX:MaxTenuringThreshold中較小的哪個。 ###5.4.4 穩定與震蕩的堆大小### 一般來說,穩定的堆大小是對垃圾回收有利的。獲得一個穩定的堆大小的方式是使-Xms和-Xmx的大小一致,即最大堆和最小堆一致。如果這樣設置,系統在運行時,堆大小是恒定的,穩定的堆空間可以減少GC的次數。
但是,一個不穩定的堆也并不是毫無用處。穩定的堆大小雖然可以減少GC次數,但同時也增加了每次GC的時間。讓堆大小在一個區間中震蕩,在系統不需要使用大內存時,壓縮堆空間,使GC應對一個較小的堆,可以加快單次GC的速度?;谶@樣的考慮,JVM提供了兩個參數用于壓縮和擴展堆空間。
**-XX:MinHeapFreeRatio:**設置堆空間最小空閑比例,默認是40。當堆空間的空閑內存小于這個數值時,JVM便會擴展堆空間。
**-XX:MaxHeapFreeRatio:**設置堆空間最大空閑比例,默認是70。當堆空間的空閑內存大于這個數值時,JVM便會壓縮堆空間。
注意:當-Xms和-Xmx相等時,-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio這兩個參數是無效的。
###5.4.5 吞吐量優先案例### 吞吐量優先的方案將會盡可能減少系統的執行垃圾回收的總時間,故可以考慮關注系統吞吐量的并行回收收集器。
**-XX:+UseParallelGC:**設置新生代使用并行回收收集器。這是一個關注吞吐量的收集器,可以盡可能地減少GC時間。 **-XX:+UseParallelOldGC:**設置老年代使用并行回收收集器。 **-XX:ParallelGCThreads:**設置用于垃圾回收的線程數,通常情況下可以和CPU數量相等。但在CPU數量比較多的情況下,設置相對較小的數值也是合理的。 ###5.4.6 使用大頁案例### 使用大的內存分頁可以增強CPU的內存尋址能力,從而提升系統的性能。
**-XX:LargePageSizeInBytes:**設置大頁的大小。如:-XX:LargePageSizeInBytes=256m。 ###5.4.7 降低停頓案例### 首先,考慮的是使用關注系統停頓的CMS回收器,其次,為了減少Full GC次數,應盡可能將對象留在新生代。
**-XX:ParallelGCThreads:**設置用于垃圾回收的線程數,通常情況下可以和CPU數量相等。但在CPU數量比較多的情況下,設置相對較小的數值也是合理的。
**-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次MinorGC依然存活,則進入老年代。 ##5.5 實用JVM參數## ###5.5.1 JIT編譯參數### JVM的JIT編譯器,可以在運行時將字節碼編譯成本地代碼,從而提高函數的執行效率。-XX:CompileThreshold為JIT編譯的閥值,當函數的調用次數超過-XX:CompileThreshold時,JIT就將字節碼編譯成本地機器碼。在client模式下,-XX:CompileThreshold取值是1500;在server模式下,取值是10000;JIT編譯完成后,JVM便會用本地代碼代替原來的字節碼解釋執行。
JIT編譯會花費一定的時間,為了能合理地設置JIT編譯的閥值,可以使用-XX:CITime打印出JIT編譯的耗時,也可以使用-XX:+PrintCompilation打印出JIT編譯的信息。 ###5.5.2 堆快照(堆Dump)### 在性能排查問題中,分析堆快照(Dump)是必不可少的一環。-XX:+HeapDumpOnOutOfMemoryError參數在程序發生OOM時,導出應用程序的當前堆快照。通過參數-XX:HeapDumpPath可以指定堆快照的保存位置。
-Xmx10M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/log/dump.hprof ###5.5.3 錯誤處理### 在系統發生OOM錯誤時,虛擬機在錯誤發生時運行一段第三方腳本。比如:當OOM發生時,重置系統:
-XX:OnOutOfMemoryError=/data/reset.bat ###5.5.4 取得GC信息###
###5.5.5 類和對象跟蹤### JVM還提供了一組參數用于獲取系統運行時加載,卸載類的信息。-XX:+TraceClassLoading參數用于跟蹤類加載情況,-XX:+TraceClassUnloading參數用于跟蹤類卸載情況。
如果需要同時跟蹤類的加載和卸載信息,可以同時打開這兩個開關,也可以使用-verbose:class參數。除了類的跟蹤,JVM還提供了-XX:+PrintClassHistogram開關用于打印運行時實例的信息。當此開關被打開時,當ctrl+break被按下,會輸出系統內類的統計信息。 ###5.5.6 控制GC### -XX:+DisableExplicitGC選項用于禁止顯示的GC操作,即禁止在程序中使用System.gc()觸發的Full GC。對應用程序來說,在絕大多數的情況下,是不需要進行類的回收的。因為回收類的性價比非常低,類元數據一旦被載入,通常會伴隨應用程序整個聲明周期。
如果應用程序不需要回收類,則可以使用-Xnoclassgc參數啟動應用程序,那么在GC的過程中,就不會發生類的回收,進而提升GC的性能。因此,如果嘗試使用-XX:+TraceClassUnloading -Xnoclassgc參數運行程序,將看不到任何輸出,因為系統不會卸載任何類,所以類卸載是無法跟蹤到任何信息的。
另一個應用的GC控制參數是-Xincgc,一旦啟用這個參數,系統便會進行增量式的GC。增量式的GC使用特定算法讓GC線程和應用程序線程交叉執行,從而減小應用程序因GC而產生的停頓時間。 ###5.5.7 使用大頁### 對同樣大小的內存空間,使用大頁后,內存分頁的表項就會減少,從而可以提升CPU從虛擬內存地址映射到物理內存地址的能力。在支持大頁的操作系統中,使用JVM參數讓虛擬機使用大頁,從而提升系統性能: -XX:+UseLargePages:啟用大頁; -XX:LargePageSizeInBytes:指定大頁的大小; ###5.5.8 壓縮指針### 在64位虛擬機上,應用程序所占內存的大小要遠遠超出其32為版本(約1.5倍左右)。這是因為64位系統擁有更寬的尋址空間,與32位系統相比,指針對象的長度進行了翻倍。為了解決這個問題,64位的JVM虛擬機可以使用-XX:+UseCompressedOops參數打開指針壓縮,從一定程度上減少了內存消耗。可以對以下指針進行壓縮:
雖然壓縮指針可以節省內存,但是壓縮和解壓縮指針也會對JVM造成一定的性能損失。 ##5.6 實戰JVM調優## ###5.6.1 Tomcat啟動加速###
###5.6.2 JMeter介紹與使用### JMeter是Apache下基于Java的一款性能測試和壓力測試工具。它基于Java開發,可對HTTP服務器和FTP服務器,甚至是數據庫進行壓力測試。(P293) ###5.6.3 WEB應用調優過程###
綜上所述,JVM調優的主要過程有:確定堆內存大小(-Xmx,-Xms),合理分配新生代和老年代(-XX:NewRatio,-XX:SurvivorRatio),確定永久代大小(-XX:Permsize,-XX:MaxPermSize),選擇垃圾收集,對垃圾收集器進行合理的設置。除此之外,禁用顯示GC(-XX:+DisableExplicitGC),禁用類數據回收(-Xnoclassgc),禁用類校驗(-Xverify:none)等設置。
轉載于:https://my.oschina.net/xianggao/blog/412818
總結
- 上一篇: String ... String 三
- 下一篇: [Oracle整理]CASE-END