溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JVM內(nèi)存管理深入垃圾收集器與內(nèi)存分配策略的示例分析

發(fā)布時間:2021-10-23 17:06:55 來源:億速云 閱讀:137 作者:柒染 欄目:編程語言

這篇文章給大家介紹JVM內(nèi)存管理深入垃圾收集器與內(nèi)存分配策略的示例分析,內(nèi)容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

Java與C++之間有一堵由內(nèi)存動態(tài)分配和垃圾收集技術(shù)所圍成的高墻,墻外面的人想進去,墻里面的人卻想出來。

概述:

說起垃圾收集(Garbage Collection,下文簡稱GC),大部分人都把這項技術(shù)當(dāng)做Java語言的伴生產(chǎn)物。事實上GC的歷史遠遠比Java來得久遠,在1960年誕生于MIT的Lisp是***門真正使用內(nèi)存動態(tài)分配和垃圾收集技術(shù)的語言。當(dāng)Lisp還在胚胎時期,人們就在思考GC需要完成的3件事情:哪些內(nèi)存需要回收?什么時候回收?怎么樣回收?

經(jīng)過半個世紀(jì)的發(fā)展,目前的內(nèi)存分配策略與垃圾回收技術(shù)已經(jīng)相當(dāng)成熟,一切看起來都進入“自動化”的時代,那為什么我們還要去了解GC和內(nèi)存分配?答案很簡單:當(dāng)需要排查各種內(nèi)存溢出、泄漏問題時,當(dāng)垃圾收集成為系統(tǒng)達到更高并發(fā)量的瓶頸時,我們就需要對這些“自動化”的技術(shù)有必要的監(jiān)控、調(diào)節(jié)手段。

把時間從1960年撥回現(xiàn)在,回到我們熟悉的Java語言。本文***章中介紹了Java內(nèi)存運行時區(qū)域的各個部分,其中程序計數(shù)器、VM棧、本地方法棧三個區(qū)域隨線程而生,隨線程而滅;棧中的幀隨著方法進入、退出而有條不紊的進行著出棧入棧操作;每一個幀中分配多少內(nèi)存基本上是在Class文件生成時就已知的(可能會由JIT動態(tài)晚期編譯進行一些優(yōu)化,但大體上可以認(rèn)為是編譯期可知的),因此這幾個區(qū)域的內(nèi)存分配和回收具備很高的確定性,因此在這幾個區(qū)域不需要過多考慮回收的問題。而Java堆和方法區(qū)(包括運行時常量池)則不一樣,我們必須等到程序?qū)嶋H運行期間才能知道會創(chuàng)建哪些對象,這部分內(nèi)存的分配和回收都是動態(tài)的,我們本文后續(xù)討論中的“內(nèi)存”分配與回收僅僅指這一部分內(nèi)存。

對象已死?

在堆里面存放著Java世界中幾乎所有的對象,在回收前首先要確定這些對象之中哪些還在存活,哪些已經(jīng)“死去”了,即不可能再被任何途徑使用的對象。

引用計數(shù)算法(Reference Counting)

最初的想法,也是很多教科書判斷對象是否存活的算法是這樣的:給對象中添加一個引用計數(shù)器,當(dāng)有一個地方引用它,計數(shù)器加1,當(dāng)引用失效,計數(shù)器減1,任何時刻計數(shù)器為0的對象就是不可能再被使用的。

客觀的說,引用計數(shù)算法實現(xiàn)簡單,判定效率很高,在大部分情況下它都是一個不錯的算法,但引用計數(shù)算法無法解決對象循環(huán)引用的問題。舉個簡單的例子:對象A和B分別有字段b、a,令A(yù).b=B和B.a=A,除此之外這2個對象再無任何引用,那實際上這2個對象已經(jīng)不可能再被訪問,但是引用計數(shù)算法卻無法回收他們。

根搜索算法(GC Roots Tracing)

在實際生產(chǎn)的語言中(Java、C#、甚至包括前面提到的Lisp),都是使用根搜索算法判定對象是否存活。算法基本思路就是通過一系列的稱為“GC Roots”的點作為起始進行向下搜索,當(dāng)一個對象到GC Roots沒有任何引用鏈(Reference Chain)相連,則證明此對象是不可用的。在Java語言中,GC Roots包括:

1.在VM棧(幀中的本地變量)中的引用

2.方法區(qū)中的靜態(tài)引用

3.JNI(即一般說的Native方法)中的引用

生存還是死亡?

判定一個對象死亡,至少經(jīng)歷兩次標(biāo)記過程:如果對象在進行根搜索后,發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會被***次標(biāo)記,并在稍后執(zhí)行他的finalize()方法(如果它有的話)。這里所謂的“執(zhí)行”是指虛擬機會觸發(fā)這個方法,但并不承諾會等待它運行結(jié)束。這點是必須的,否則一個對象在finalize()方法執(zhí)行緩慢,甚至有死循環(huán)什么的將會很容易導(dǎo)致整個系統(tǒng)崩潰。finalize()方法是對象***一次逃脫死亡命運的機會,稍后GC將進行第二次規(guī)模稍小的標(biāo)記,如果在finalize()中對象成功拯救自己(只要重新建立到GC Roots的連接即可,譬如把自己賦值到某個引用上),那在第二次標(biāo)記時它將被移除出“即將回收”的集合,如果對象這時候還沒有逃脫,那基本上它就真的離死不遠了。

需要特別說明的是,這里對finalize()方法的描述可能帶點悲情的藝術(shù)加工,并不代表筆者鼓勵大家去使用這個方法來拯救對象。相反,筆者建議大家盡量避免使用它,這個不是C/C++里面的析構(gòu)函數(shù),它運行代價高昂,不確定性大,無法保證各個對象的調(diào)用順序。需要關(guān)閉外部資源之類的事情,基本上它能做的使用try-finally可以做的更好。

關(guān)于方法區(qū)

方法區(qū)即后文提到的***代,很多人認(rèn)為***代是沒有GC的,在堆中,尤其是在新生代,常規(guī)應(yīng)用進行一次GC可以一般可以回收70%~95%的空間,而***代的GC效率遠小于此。雖然VM Spec不要求,但當(dāng)前生產(chǎn)中的商業(yè)JVM都有實現(xiàn)***代的GC,主要回收兩部分內(nèi)容:廢棄常量與無用類。這兩點回收思想與Java堆中的對象回收很類似,都是搜索是否存在引用,常量的相對很簡單,與對象類似的判定即可。而類的回收則比較苛刻,需要滿足下面3個條件:

1.該類所有的實例都已經(jīng)被GC,也就是JVM中不存在該Class的任何實例。

2.加載該類的ClassLoader已經(jīng)被GC。

3.該類對應(yīng)的java.lang.Class 對象沒有在任何地方被引用,如不能在任何地方通過反射訪問該類的方法。

是否對類進行回收可使用-XX:+ClassUnloading參數(shù)進行控制,還可以使用-verbose:class或者-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載、卸載信息。

在大量使用反射、動態(tài)代理、CGLib等bytecode框架、動態(tài)生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要JVM具備類卸載的支持以保證***代不會溢出。

垃圾收集算法

在這節(jié)里不打算大量討論算法實現(xiàn),只是簡單的介紹一下基本思想以及發(fā)展過程。最基礎(chǔ)的搜集算法是“標(biāo)記-清除算法”(Mark-Sweep),如它的名字一樣,算法分層“標(biāo)記”和“清除”兩個階段,首先標(biāo)記出所有需要回收的對象,然后回收所有需要回收的對象,整個過程其實前一節(jié)講對象標(biāo)記判定的時候已經(jīng)基本介紹完了。說它是最基礎(chǔ)的收集算法原因是后續(xù)的收集算法都是基于這種思路并優(yōu)化其缺點得到的。它的主要缺點有兩個,一是效率問題,標(biāo)記和清理兩個過程效率都不高,二是空間問題,標(biāo)記清理之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致后續(xù)使用中無法找到足夠的連續(xù)內(nèi)存而提前觸發(fā)另一次的垃圾搜集動作。

為了解決效率問題,一種稱為“復(fù)制”(Copying)的搜集算法出現(xiàn),它將可用內(nèi)存劃分為兩塊,每次只使用其中的一塊,當(dāng)半?yún)^(qū)內(nèi)存用完了,僅將還存活的對象復(fù)制到另外一塊上面,然后就把原來整塊內(nèi)存空間一次過清理掉。這樣使得每次內(nèi)存回收都是對整個半?yún)^(qū)的回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動堆頂指針,按順序分配內(nèi)存就可以了,實現(xiàn)簡單,運行高效。只是這種算法的代價是將內(nèi)存縮小為原來的一半,未免太高了一點。

現(xiàn)在的商業(yè)虛擬機中都是用了這一種收集算法來回收新生代,IBM有專門研究表明新生代中的對象98%是朝生夕死的,所以并不需要按照1:1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的eden空間和2塊較少的survivor空間,每次使用eden和其中一塊survivor,當(dāng)回收時將eden和survivor還存活的對象一次過拷貝到另外一塊survivor空間上,然后清理掉eden和用過的survivor。Sun Hotspot虛擬機默認(rèn)eden和survivor的大小比例是8:1,也就是每次只有10%的內(nèi)存是“浪費”的。當(dāng)然,98%的對象可回收只是一般場景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有10%以內(nèi)的對象存活,當(dāng)survivor空間不夠用時,需要依賴其他內(nèi)存(譬如老年代)進行分配擔(dān)保(Handle Promotion)。

復(fù)制收集算法在對象存活率高的時候,效率有所下降。更關(guān)鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔(dān)保用于應(yīng)付半?yún)^(qū)內(nèi)存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。因此人們提出另外一種“標(biāo)記-整理”(Mark-Compact)算法,標(biāo)記過程仍然一樣,但后續(xù)步驟不是進行直接清理,而是令所有存活的對象一端移動,然后直接清理掉這端邊界以外的內(nèi)存。

當(dāng)前商業(yè)虛擬機的垃圾收集都是采用“分代收集”(Generational Collecting)算法,這種算法并沒有什么新的思想出現(xiàn),只是根據(jù)對象不同的存活周期將內(nèi)存劃分為幾塊。一般是把Java堆分作新生代和老年代,這樣就可以根據(jù)各個年代的特點采用最適當(dāng)?shù)氖占惴ǎ┤缧律看蜧C都有大批對象死去,只有少量存活,那就選用復(fù)制算法只需要付出少量存活對象的復(fù)制成本就可以完成收集。

垃圾收集器

垃圾收集器就是收集算法的具體實現(xiàn),不同的虛擬機會提供不同的垃圾收集器。并且提供參數(shù)供用戶根據(jù)自己的應(yīng)用特點和要求組合各個年代所使用的收集器。本文討論的收集器基于Sun Hotspot虛擬機1.6版。

圖1.Sun JVM1.6的垃圾收集器

JVM內(nèi)存管理深入垃圾收集器與內(nèi)存分配策略的示例分析

圖1展示了1.6中提供的6種作用于不同年代的收集器,兩個收集器之間存在連線的話就說明它們可以搭配使用。在介紹著些收集器之前,我們先明確一個觀點:沒有***的收集器,也沒有***的收集器,只有最合適的收集器。

1.Serial收集器

單線程收集器,收集時會暫停所有工作線程(我們將這件事情稱之為Stop The World,下稱STW),使用復(fù)制收集算法,虛擬機運行在Client模式時的默認(rèn)新生代收集器。

2.ParNew收集器

ParNew收集器就是Serial的多線程版本,除了使用多條收集線程外,其余行為包括算法、STW、對象分配規(guī)則、回收策略等都與Serial收集器一摸一樣。對應(yīng)的這種收集器是虛擬機運行在Server模式的默認(rèn)新生代收集器,在單CPU的環(huán)境中,ParNew收集器并不會比Serial收集器有更好的效果。

3.Parallel Scavenge收集器

Parallel Scavenge收集器(下稱PS收集器)也是一個多線程收集器,也是使用復(fù)制算法,但它的對象分配規(guī)則與回收策略都與ParNew收集器有所不同,它是以吞吐量***化(即GC時間占總運行時間最小)為目標(biāo)的收集器實現(xiàn),它允許較長時間的STW換取總吞吐量***化。

4.Serial Old收集器

Serial Old是單線程收集器,使用標(biāo)記-整理算法,是老年代的收集器,上面三種都是使用在新生代收集器。

5.Parallel Old收集器

老年代版本吞吐量優(yōu)先收集器,使用多線程和標(biāo)記-整理算法,JVM 1.6提供,在此之前,新生代使用了PS收集器的話,老年代除Serial Old外別無選擇,因為PS無法與CMS收集器配合工作。

6.CMS(Concurrent Mark Sweep)收集器

CMS是一種以最短停頓時間為目標(biāo)的收集器,使用CMS并不能達到GC效率***(總體GC時間最?。鼙M可能降低GC時服務(wù)的停頓時間,這一點對于實時或者高交互性應(yīng)用(譬如證券交易)來說至關(guān)重要,這類應(yīng)用對于長時間STW一般是不可容忍的。CMS收集器使用的是標(biāo)記-清除算法,也就是說它在運行期間會產(chǎn)生空間碎片,所以虛擬機提供了參數(shù)開啟CMS收集結(jié)束后再進行一次內(nèi)存壓縮。

內(nèi)存分配與回收策略

了解GC其中很重要一點就是了解JVM的內(nèi)存分配策略:即對象在哪里分配和對象什么時候回關(guān)于對象在哪里分配,往大方向講,主要就在堆上分配,但也可能經(jīng)過JIT進行逃逸分析后進行標(biāo)量替換拆散為原子類型在棧上分配,也可能分配在DirectMemory中(詳見本文***章)。往細節(jié)處講,對象主要分配在新生代eden上,也可能會直接老年代中,分配的細節(jié)決定于當(dāng)前使用的垃圾收集器類型與VM相關(guān)參數(shù)設(shè)置。我們可以通過下面代碼來驗證一下Serial收集器(ParNew收集器的規(guī)則與之完全一致)的內(nèi)存分配和回收的策略。讀者看完Serial收集器的分析后,不妨自己根據(jù)JVM參數(shù)文檔寫一些程序去實踐一下其它幾種收集器的分配策略。

清單1:內(nèi)存分配測試代碼

public class YoungGenGC {  private static final int _1MB = 1024 * 1024;  public static void main(String[] args) {   // testAllocation();   testHandlePromotion();   // testPretenureSizeThreshold();   // testTenuringThreshold();   // testTenuringThreshold2();   }  /**   * VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8  */   @SuppressWarnings("unused")   public static void testAllocation() {   byte[] allocation1, allocation2, allocation3, allocation4;   allocation1 = new byte[2 * _1MB];   allocation2 = new byte[2 * _1MB];   allocation3 = new byte[2 * _1MB];   allocation4 = new byte[4 * _1MB]; // 出現(xiàn)一次Minor GC   }  /**   * VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8  * -XX:PretenureSizeThreshold=3145728   */   @SuppressWarnings("unused")   public static void testPretenureSizeThreshold() {   byte[] allocation;   allocation = new byte[4 * _1MB]; //直接分配在老年代中   }  /**   * VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1  * -XX:+PrintTenuringDistribution   */   @SuppressWarnings("unused")   public static void testTenuringThreshold() {   byte[] allocation1, allocation2, allocation3;   allocation1 = new byte[_1MB / 4]; // 什么時候進入老年代決定于XX:MaxTenuringThreshold設(shè)置   allocation2 = new byte[4 * _1MB];   allocation3 = new byte[4 * _1MB];   allocation3 = null;   allocation3 = new byte[4 * _1MB];   }  /**   * VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15  * -XX:+PrintTenuringDistribution   */   @SuppressWarnings("unused")   public static void testTenuringThreshold2() {   byte[] allocation1, allocation2, allocation3, allocation4;   allocation1 = new byte[_1MB / 4]; // allocation1+allocation2大于survivo空間一半   allocation2 = new byte[_1MB / 4];   allocation3 = new byte[4 * _1MB];   allocation4 = new byte[4 * _1MB];   allocation4 = null;   allocation4 = new byt  /**   * VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure  */   @SuppressWarnings("unused")   public static void testHandlePromotion() {   byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;  allocation1 = new byte[2 * _1MB];   allocation2 = new byte[2 * _1MB];   allocation3 = new byte[2 * _1MB];   allocation1 = null;   allocation4 = new byte[2 * _1MB];   allocation5 = new byte[2 * _1MB];   allocation6 = new byte[2 * _1MB];   allocation4 = null;   allocation5 = null;   allocation6 = null;   allocation7 = new byte[2 * _1MB];   }   }

規(guī)則一:通常情況下,對象在eden中分配。當(dāng)eden無法分配時,觸發(fā)一次Minor GC。

執(zhí)行testAllocation()方法后輸出了GC日志以及內(nèi)存分配狀況。-Xms20M -Xmx20M -Xmn10M這3個參數(shù)確定了Java堆大小為20M,不可擴展,其中10M分配給新生代,剩下的10M即為老年代。-XX:SurvivorRatio=8決定了新生代中eden與survivor的空間比例是1:8,從輸出的結(jié)果也清晰的看到“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代總可用空間為9216K(eden+1個survivor)。

我們也注意到在執(zhí)行testAllocation()時出現(xiàn)了一次Minor GC,GC的結(jié)果是新生代6651K變?yōu)?48K,而總占用內(nèi)存則幾乎沒有減少(因為幾乎沒有可回收的對象)。這次GC是發(fā)生的原因是為allocation4分配內(nèi)存的時候,eden已經(jīng)被占用了6M,剩余空間已不足分配allocation4所需的4M內(nèi)存,因此發(fā)生Minor GC。GC期間虛擬機發(fā)現(xiàn)已有的3個2M大小的對象全部無法放入survivor空間(survivor空間只有1M大小),所以直接轉(zhuǎn)移到老年代去。GC后4M的allocation4對象分配在eden中。

清單2:testAllocation()方法輸出結(jié)果

[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  Heap   def new generation total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)  eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)   from space 1024K, 14% used [0x032d0000, 0x032f5370, 0x033d0000)   to space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)   tenured generation total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)  the space 10240K, 60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000)  compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)  No shared spaces configured.

規(guī)則二:配置了PretenureSizeThreshold的情況下,對象大于設(shè)置值將直接在老年代分配。

執(zhí)行testPretenureSizeThreshold()方法后,我們看到eden空間幾乎沒有被使用,而老年代的10M控件被使用了40%,也就是4M的allocation對象直接就分配在老年代中,則是因為PretenureSizeThreshold被設(shè)置為3M,因此超過3M的對象都會直接從老年代分配。

清單3:

Heap   def new generation   total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000)     eden space 8192K,   8% used [0x029d0000, 0x02a77e98, 0x031d0000)     from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)     to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)   tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)      the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)   compacting perm gen  total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000)      the space 12288K,  17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000)   No shared spaces configured.

規(guī)則三:在eden經(jīng)過GC后存活,并且survivor能容納的對象,將移動到survivor空間內(nèi),如果對象在survivor中繼續(xù)熬過若干次回收(默認(rèn)為15次)將會被移動到老年代中。回收次數(shù)由MaxTenuringThreshold設(shè)置。

分別以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15兩種設(shè)置來執(zhí)行testTenuringThreshold(),方法中allocation1對象需要256K內(nèi)存,survivor空間可以容納。當(dāng)MaxTenuringThreshold=1時,allocation1對象在第二次GC發(fā)生時進入老年代,新生代已使用的內(nèi)存GC后非常干凈的變成0KB。而MaxTenuringThreshold=15時,第二次GC發(fā)生后,allocation1對象則還留在新生代survivor空間,這時候新生代仍然有404KB被占用。

清單4:

MaxTenuringThreshold=1 [GC [DefNew   Desired survivor size 524288 bytes, new threshold 1 (max 1)   - age 1: 414664 bytes, 414664 total   : 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]  [GC [DefNew   Desired survivor size 524288 bytes, new threshold 1 (max 1)   : 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  Heap   def new generation total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)  eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)   from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)   to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)   tenured generation total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000)  the space 10240K, 43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000)  compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)  No shared spaces configured.  MaxTenuringThreshold=15   [GC [DefNew   Desired survivor size 524288 bytes, new threshold 15 (max 15)   - age 1: 414664 bytes, 414664 total   : 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  [GC [DefNew   Desired survivor size 524288 bytes, new threshold 15 (max 15)   - age 2: 414520 bytes, 414520 total   : 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  Heap   def new generation total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000)  eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)   from space 1024K, 39% used [0x031d0000, 0x03235338, 0x032d0000)   to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)   tenured generation total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)  the space 10240K, 40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)  compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)  No shared spaces configured.

規(guī)則四:如果在survivor空間中相同年齡所有對象大小的累計值大于survivor空間的一半,大于或等于個年齡的對象就可以直接進入老年代,無需達到MaxTenuringThreshold中要求的年齡。

執(zhí)行testTenuringThreshold2()方法,并將設(shè)置-XX:MaxTenuringThreshold=15,發(fā)現(xiàn)運行結(jié)果中survivor占用仍然為0%,而老年代比預(yù)期增加了6%,也就是說allocation1、allocation2對象都直接進入了老年代,而沒有等待到15歲的臨界年齡。因為這2個對象加起來已經(jīng)到達了512K,并且它們是同年的,滿足同年對象達到survivor空間的一半規(guī)則。我們只要注釋掉其中一個對象new操作,就會發(fā)現(xiàn)另外一個就不會晉升到老年代中去了。

清單5:

[GC [DefNew   Desired survivor size 524288 bytes, new threshold 1 (max 15)   - age   1:     676824 bytes,     676824 total   : 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]   [GC [DefNew   Desired survivor size 524288 bytes, new threshold 15 (max 15)   : 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   Heap   def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)     eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)     from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)     to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)   tenured generation   total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)      the space 10240K,  46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000)   compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)      the space 12288K,  17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000)   No shared spaces configured.

規(guī)則五:在Minor GC觸發(fā)時,會檢測之前每次晉升到老年代的平均大小是否大于老年代的剩余空間,如果大于,改為直接進行一次Full GC,如果小于則查看HandlePromotionFailure設(shè)置看看是否允許擔(dān)保失敗,如果允許,那仍然進行Minor GC,如果不允許,則也要改為進行一次Full GC。

前面提到過,新生代才有復(fù)制收集算法,但為了內(nèi)存利用率,只使用其中一個survivor空間來作為輪換備份,因此當(dāng)出現(xiàn)大量對象在GC后仍然存活的情況(最極端就是GC后所有對象都存活),就需要老年代進行分配擔(dān)保,把survivor無法容納的對象直接放入老年代。與生活中貸款擔(dān)保類似,老年代要進行這樣的擔(dān)保,前提就是老年代本身還有容納這些對象的剩余空間,一共有多少對象在GC之前是無法明確知道的,所以取之前每一次GC晉升到老年代對象容量的平均值與老年代的剩余空間進行比較決定是否進行Full GC來讓老年代騰出更多空間。

取平均值進行比較其實仍然是一種動態(tài)概率的手段,也就是說如果某次Minor GC存活后的對象突增,大大高于平均值的話,依然會導(dǎo)致?lián)J。@樣就只好在失敗后重新進行一次Full GC。雖然擔(dān)保失敗時做的繞的圈子是***的,但大部分情況下都還是會將HandlePromotionFailure打開,避免Full GC過于頻繁。

清單6:

HandlePromotionFailure = false [GC [DefNew: 6651K->148K(9216K), 0.0078936 secs] 6651K->4244K(19456K), 0.0079192 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]  [GC [DefNew: 6378K->6378K(9216K), 0.0000206 secs][Tenured: 4096K->4244K(10240K), 0.0042901 secs] 10474K->4244K(19456K), [Perm : 2104K->2104K(12288K)], 0.0043613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  HandlePromotionFailure = true [GC [DefNew: 6651K->148K(9216K), 0.0054913 secs] 6651K->4244K(19456K), 0.0055327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  [GC [DefNew: 6378K->148K(9216K), 0.0006584 secs] 10474K->4244K(19456K), 0.0006857 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

總  結(jié)

介紹了垃圾收集的算法、6款主要的垃圾收集器,以及通過代碼實例具體介紹了新生代串行收集器對內(nèi)存分配及回收的影響。

GC在很多時候都是系統(tǒng)并發(fā)度的決定性因素,虛擬機之所以提供多種不同的收集器,提供大量的調(diào)節(jié)參數(shù),是因為只有根據(jù)實際應(yīng)用需求、實現(xiàn)方式選擇***的收集方式才能獲取***的性能。沒有固定收集器、參數(shù)組合,也沒有***的調(diào)優(yōu)方法,虛擬機也沒有什么必然的行為。筆者看過一些文章,撇開具體場景去談?wù)摾夏甏_到92%會觸發(fā)Full GC(92%應(yīng)當(dāng)來自CMS收集器觸發(fā)的默認(rèn)臨界點)、98%時間在進行垃圾收集系統(tǒng)會拋出OOM異常(98%應(yīng)該來自parallel收集器收集時間比率的默認(rèn)臨界點)其實意義并不太大。因此學(xué)習(xí)GC如果要到實踐調(diào)優(yōu)階段,必須了解每個具體收集器的行為、優(yōu)勢劣勢、調(diào)節(jié)參數(shù)。

關(guān)于JVM內(nèi)存管理深入垃圾收集器與內(nèi)存分配策略的示例分析就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

jvm
AI