Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的「高牆」,牆外面的人想進去,牆裏面的人卻想出來。html
提及垃圾收集(Garbage Collection,GC),大部分人都把這項技術當作Java語言的伴生產物。事實上,GC的歷史比Java久遠,1960年誕生於MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當Lisp還在胚胎時期時,人們就在思考GC須要完成的3件事情:java
通過半個多世紀的發展,目前內存的動態分配與內存回收技術已經至關成熟,一切看起來都進入了「自動化」時代,那爲何咱們還要去了解GC和內存分配呢?答案很簡單:當須要排查各類內存溢出、內存泄漏問題時,當垃圾收集成爲系統達到更高併發量的瓶頸時,咱們就須要對這些「自動化」的技術實施必要的監控和調節。程序員
把時間從半個多世紀之前撥回到如今,回到咱們熟悉的Java語言。第2章介紹了Java內存運行時區域的各個部分,其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅;棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操做。每個棧幀中分配多少內存基本上是在類結構肯定下來時就已知的(儘管在運行期會由JIT編譯器進行一些優化,但在本章基於概念模型的討論中,大致上能夠認爲是編譯期可知的),所以這幾個區域的內存分配和回收都具有肯定性,在這幾個區域內就不須要過多考慮回收的問
題,由於方法結束或者線程結束時,內存天然就跟隨着回收了。而Java堆和方法區則不同,一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期間時才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,垃圾收集器所關注的是這部份內存,本章後續討論中的「內存」分配與回收也僅指這一部份內存。面試
在堆裏面存放着Java世界中幾乎全部的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要肯定這些對象之中哪些還「存活」着,哪些已經「死去」(即不可能再被任何途徑使用的對象)。算法
不少教科書判斷對象是否存活的算法是這樣的:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。做者面試過不少的應屆生和一些有多年工做經驗的開發人員,他們對於這個問題給予的都是這個答案。數據庫
客觀地說,引用計數算法(Reference Counting)的實現簡單,斷定效率也很高,在大部分狀況下它都是一個不錯的算法,也有一些比較著名的應用案例,例如微軟公司的COM(Component Object Model)技術、使用ActionScript 3的FlashPlayer、Python語言和在遊戲腳本領域被普遍應用的Squirrel中都使用了引用計數算法進行內存管理。可是,至少主流的Java虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的緣由是它很難解決對象之間相互循環引用的問題。編程
舉個簡單的例子,請看代碼清單3-1中的testGC()方法:對象objA和objB都有字段instance,賦值令objA.instance=objB及objB.instance=objA,除此以外,這兩個對象再無任何引用,實際上這兩個對象已經不可能再被訪問,可是它們由於互相引用着對方,致使它們的引用計數都不爲0,因而引用計數算法沒法通知GC收集器回收它們。數組
代碼清單3-1 引用計數算法的缺陷緩存
/** * testGC()方法執行後,objA和objB會不會被GC呢? * @author zzm */ public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 這個成員屬性的惟一意義就是佔點內存,以便在能在GC日誌中看清楚是否有回收過 */ private byte[] bigSize = new byte[2 * _1MB]; public static void testGC() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; // 假設在這行發生GC,objA和objB是否能被回收? System.gc(); } }
運行結果:安全
[F u l l G C(S y s t e m)[T e n u r e d:0 K->2 1 0 K(1 0 2 4 0 K),0.0 1 4 9 1 4 2 s e c s]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs] Heap def new generation total 9216K,used 82K[0x00000000055e0000,0x0000000005fe0000,0x0000000005fe0000) Eden space 8192K,1%used[0x00000000055e00000x00000000055f4850,0x0000000005de0000) from space 1024K,0%used[0x0000000005de0000,0x0000000005de0000,0x0000000005ee0000) to space 1024K,0%used[0x0000000005ee0000,0x0000000005ee0000,0x0000000005fe0000) tenured generation total 10240K,used 210K[0x0000000005fe0000,0x00000000069e0000,0x00000000069e0000) the space 10240K,2%used[0x0000000005fe0000,0x0000000006014a18,0x0000000006014c00,0x00000000069e0000) compacting perm gen total 21248K,used 3016K[0x00000000069e0000,0x0000000007ea0000,0x000000000bde0000) the space 21248K,14%used[0x00000000069e0000,0x0000000006cd2398,0x0000000006cd2400,0x0000000007ea0000) No shared spaces configured.
從運行結果中能夠清楚看到,GC日誌中包含「4603K->210K」,意味着虛擬機並無由於這兩個對象互相引用就不回收它們,這也從側面說明虛擬機並非經過引用計數算法來判斷對象是否存活的。
在主流的商用程序語言(Java、C#,甚至包括前面提到的古老的Lisp)的主流實現中,都是稱經過可達性分析(Reachability Analysis)來斷定對象是否存活的。這個算法的基本思路就是經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來講,就是從GC Roots到這個對象不可達)時,則證實此對象是不可用的。如圖3-1所示,對象object 五、object 六、object 7雖然互相有關聯,可是它們到GC Roots是不可達的,因此它們將會被斷定爲是可回收的對象。
在Java語言中,可做爲GC Roots的對象包括下面幾種:
不管是經過引用計數算法判斷對象的引用數量,仍是經過可達性分析算法判斷對象的引用鏈是否可達,斷定對象是否存活都與「引用」有關。在JDK 1.2之前,Java中的引用的定義很傳統:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。這種定義很純粹,可是太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,對於如何描述一些「食之無味,棄之惋惜」的對象就顯得無能爲力。咱們但願能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;若是內存空間在進行垃圾收集後仍是很是緊張,則能夠拋棄這些對象。不少系統的緩存功能都符合這樣的應用場景。
在JDK 1.2以後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong
Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(PhantomReference)4種,這4種引用強度依次逐漸減弱。
強引用就是指在程序代碼之中廣泛存在的,相似「Object obj=new Object()」這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
軟引用是用來描述一些還有用但並不是必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。在JDK 1.2以後,提供了SoftReference類來實現軟引用。
弱引用也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK 1.2以後,提供了WeakReference類來實現弱引用。
虛引用也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2以後,提供了PhantomReference類來實現虛引用。
即便在可達性分析算法中不可達的對象,也並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」。
若是這個對象被斷定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫作F-Queue的隊列之中,並在稍後由一個由虛擬機自動創建的、低優先級的Finalizer線程去執行它。這裏所謂的「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣作的緣由是,若是一個對象在finalize()方法中執行緩慢,或者發生了死循環(更極端的狀況),將極可能會致使F-Queue隊列中其餘對象永久處於等待,甚至致使整個內存回收系統崩潰。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是對象要在finalize()中成功拯救本身——只要從新與引用鏈上的任何一個對象創建關聯便可,譬如把本身(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出「即將回收」的集合;若是對象這時候尚未逃脫,那基本上它就真的被回收了。從代碼清單3-2中咱們能夠看到一個對象的finalize()被
執行,可是它仍然能夠存活。
代碼清單3-2 一次對象自我拯救的演示
/** * 此代碼演示了兩點: * 1.對象能夠在被GC時自我拯救。 * 2.這種自救的機會只有一次,由於一個對象的finalize()方法最多隻會被系統自動調用一次 * @author zzm */ public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("yes, i am still alive :)"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize mehtod executed!"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws Throwable { SAVE_HOOK = new FinalizeEscapeGC(); //對象第一次成功拯救本身 SAVE_HOOK = null; System.gc(); // 由於Finalizer方法優先級很低,暫停0.5秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead :("); } // 下面這段代碼與上面的徹底相同,可是此次自救卻失敗了 SAVE_HOOK = null; System.gc(); // 由於Finalizer方法優先級很低,暫停0.5秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead :("); } } }
運行結果:
finalize mehtod executed !
yes,i am still alive : )
no,i am dead : (
從代碼清單3-2的運行結果能夠看出,SAVE_HOOK對象的finalize()方法確實被GC收集器觸發過,而且在被收集前成功逃脫了。
另一個值得注意的地方是,代碼中有兩段徹底同樣的代碼片斷,執行結果倒是一次逃脫成功,一次失敗,這是由於任何一個對象的finalize()方法都只會被系統自動調用一次,若是對象面臨下一次回收,它的finalize()方法不會被再次執行,所以第二段代碼的自救行動失敗了。
須要特別說明的是,上面關於對象死亡時finalize()方法的描述可能帶有悲情的藝術色彩,筆者並不鼓勵你們使用這種方法來拯救對象。相反,筆者建議你們儘可能避免使用它,由於它不是C/C++中的析構函數,而是Java剛誕生時爲了使C/C++程序員更容易接受它所作出的一個妥協。它的運行代價高昂,不肯定性大,沒法保證各個對象的調用順序。有些教材中描述它適合作「關閉外部資源」之類的工做,這徹底是對這個方法用途的一種自我安慰。finalize()能作的全部工做,使用try-finally或者其餘方式均可以作得更好、更及時,因此筆者建議你們徹底能夠忘掉Java語言中有這個方法的存在。
不少人認爲方法區(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的,Java虛擬機規範中確實說過能夠不要求虛擬機在方法區實現垃圾收集,並且在方法區中進行垃圾收集的「性價比」通常比較低:在堆中,尤爲是在新生代中,常規應用進行一次垃圾收集通常能夠回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。
永久代的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的對象很是相似。以常量池中字面量的回收爲例,假如一個字符串「abc」已經進入了常量池中,可是當前系統沒有任何一個String對象是叫作「abc」的,換句話說,就是沒有任何String對象引用常量池中的「abc」常量,也沒有其餘地方引用了這個字面量,若是這時發生內存回收,並且必要的話,這個「abc」常量就會被系統清理出常量池。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。
斷定一個常量是不是「廢棄常量」比較簡單,而要斷定一個類是不是「無用的類」的條件則相對苛刻許多。類須要同時知足下面3個條件才能算是「無用的類」:
虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣,不使用了就必然會回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還能夠使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載和卸載信息,其中-verbose:class和-XX:+TraceClassLoading能夠在Product版的虛擬機中使用,-XX:+TraceClassUnLoading參數須要FastDebug版的虛擬機支持。
因爲垃圾收集算法的實現涉及大量的程序細節,並且各個平臺的虛擬機操做內存的方法又各不相同,所以本節不打算過多地討論算法的實現,只是介紹幾種算法的思想及其發展過程。
最基礎的收集算法是「標記-清除」(Mark-Sweep)算法,如同它的名字同樣,算法分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象,它的標記過程其實在前一節講述對象標記斷定時已經介紹過了。之因此說它是最基礎的收集算法,是由於後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另外一個是
空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。標記—清除算法的執行過程如圖3-2所示。
爲了解決效率問題,一種稱爲「複製」(Copying)的收集算法出現了,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲了原來的一半,未免過高了一點。複製算法的執行過程如圖3-3所示。
如今的商業虛擬機都採用這種收集算法來回收新生代,IBM公司的專門研究代表,新生代中的對象98%是「朝生夕死」的,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是
8:1,也就是每次新生代中可用內存空間爲整個新生代容量的90%(80%+10%),只有10%的內存會被「浪費」。固然,98%的對象可回收只是通常場景下的數據,咱們沒有辦法保證每次回收都只有很少於10%的對象存活,當Survivor空間不夠用時,須要依賴其餘內存(這裏指老年代)進行分配擔保(Handle Promotion)。
內存的分配擔保就比如咱們去銀行借款,若是咱們信譽很好,在98%的狀況下都能按時償還,因而銀行可能會默認咱們下一次也能按時按量地償還貸款,只須要有一個擔保人能保證若是我不能還款時,能夠從他的帳戶扣錢,那銀行就認爲沒有風險了。內存的分配擔保也同樣,若是另一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接經過分配擔保機制進入老年代。關於對新生代進行分配擔保的內容,在本章稍後在講解垃圾收集器執行規則時還會再詳細講解。
複製收集算法在對象存活率較高時就要進行較多的複製操做,效率將會變低。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保,以應對被使用的內存中全部對象都100%存活的極端狀況,因此在老年代通常不能直接選用這種算法。
根據老年代的特色,有人提出了另一種「標記-整理」(Mark-Compact)算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存,「標記-整理」算法的示意圖如圖3-4所示。
當前商業虛擬機的垃圾收集都採用「分代收集」(Generational Collection)算法,這種算法並無什麼新的思想,只是根據對象存活週期的不一樣將內存劃分爲幾塊。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記—清理」或者「標記—整理」算法來進行回收。
3.2節和3.3節從理論上介紹了對象存活斷定算法和垃圾收集算法,而在HotSpot虛擬機上實現這些算法時,必須對算法的執行效率有嚴格的考量,才能保證虛擬機高效運行。
從可達性分析中從GC Roots節點找引用鏈這個操做爲例,可做爲GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,如今不少應用僅僅方法區就有數百兆,若是要逐個檢查這裏面的引用,那麼必然會消耗不少時間。
另外,可達性分析對執行時間的敏感還體如今GC停頓上,由於這項分析工做必須在一個能確保一致性的快照中進行——這裏「一致性」的意思是指在整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不能夠出現分析過程當中對象引用關係還在不斷變化的狀況,該點不知足的話分析結果準確性就沒法獲得保證。這點是致使GC進行時必須停頓全部Java執行線程(Sun將這件事情稱爲「Stop The World」)的其中一個重要緣由,即便是在號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也是必需要停頓的。
因爲目前的主流Java虛擬機使用的都是準確式GC(這個概念在第1章介紹Exact VM對Classic VM的改進時講過),因此當執行系統停頓下來後,並不須要一個不漏地檢查完全部執行上下文和全局的引用位置,虛擬機應當是有辦法直接得知哪些地方存放着對象引用。在HotSpot的實現中,是使用一組稱爲OopMap的數據結構來達到這個目的的,在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描時就能夠直接得知這些信息了。下面的代碼清單3-3是HotSpot Client VM生成的一段String.hashCode()方法的本地代碼,能夠看到在0x026eb7a9處的call指令有OopMap記錄,它指明瞭EBX寄存器和棧中偏移量爲16的內存區域中各有一個普通對象指針(Ordinary Object Pointer)的引用,有效範圍爲從call指令開始直到0x026eb730(指令流的起始位置)+142(OopMap記錄的偏移量)=0x026eb7be,即hlt指令爲止。
代碼清單3-3 String.hashCode()方法編譯後的本地代碼
[Verified Entry Point] 0x026eb730:mov%eax,-0x8000(%esp) …… ;ImplicitNullCheckStub slow case 0x026eb7a9:call 0x026e83e0 ;OopMap{ebx=Oop[16]=Oop off=142} ;*caload ;-java.lang.String:hashCode@48(line 1489) ;{runtime_call} 0x026eb7ae:push$0x83c5c18 ;{external_word} 0x026eb7b3:call 0x026eb7b8 0x026eb7b8:pusha 0x026eb7b9:call 0x0822bec0;{runtime_call} 0x026eb7be:hlt
在OopMap的協助下,HotSpot能夠快速且準確地完成GC Roots枚舉,但一個很現實的問題隨之而來:可能致使引用關係變化,或者說OopMap內容變化的指令很是多,若是爲每一條指令都生成對應的OopMap,那將會須要大量的額外空間,這樣GC的空間成本將會變得很高。
實際上,HotSpot也的確沒有爲每條指令都生成OopMap,前面已經提到,只是在「特定的位置」記錄了這些信息,這些位置稱爲安全點(Safepoint),即程序執行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點時才能暫停。Safepoint的選定既不能太少以至於讓GC等待時間太長,也不能過於頻繁以至於過度增大運行時的負荷。因此,安全點的選定基
本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的——由於每條指令執行的時間都很是短暫,程序不太可能由於指令流長度太長這個緣由而過長時間運行,「長時間執行」的最明顯特徵就是指令序列複用,例如方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生Safepoint。
對於Sefepoint,另外一個須要考慮的問題是如何在GC發生時讓全部線程(這裏不包括執行JNI調用的線程)都「跑」到最近的安全點上再停頓下來。這裏有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension),其中搶先式中斷不須要線程的執行代碼主動去配合,在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它「跑」到安全點上。如今幾乎沒有虛擬機實現採用搶先式中斷來暫停線程從而響應GC事件。
而主動式中斷的思想是當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。下面代碼清單3-4中的test指令是HotSpot生成的輪詢指令,當須要暫停線程時,虛擬機把0x160100的內存頁設置爲不可讀,線程執行到test指令時就會產生一個自陷異常信號,在預先註冊的異常處理器中暫停線程實現等待,這樣一條彙編指令便完成安全點輪詢和觸發線程中斷。
代碼清單3-4 輪詢指令
0x01b6d627:call 0x01b2b210;OopMap{[60]=Oop off=460} ;*invokeinterface size ;-Client1:main@113(line 23) ;{virtual_call} 0x01b6d62c:nop ;OopMap{[60]=Oop off=461} ;*if_icmplt ;-Client1:main@118(line 23) 0x01b6d62d:test%eax,0x160100;{poll} 0x01b6d633:mov 0x50(%esp),%esi 0x01b6d637:cmp%eax,%esi
使用Safepoint彷佛已經完美地解決了如何進入GC的問題,但實際狀況卻並不必定。Safepoint機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的Safepoint。可是,程序「不執行」的時候呢?所謂的程序不執行就是沒有分配CPU時間,典型的例子就是線程處於Sleep狀態或者Blocked狀態,這時候線程沒法響應JVM的中斷請求,「走」到安全的地方去中斷掛起,JVM也顯然不太可能等待線程從新被分配CPU時間。對於這種狀況,就須要安全區域(Safe Region)來解決。
安全區域是指在一段代碼片斷之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。咱們也能夠把Safe Region看作是被擴展了的Safepoint。
在線程執行到Safe Region中的代碼時,首先標識本身已經進入了Safe Region,那樣,當在這段時間裏JVM要發起GC時,就不用管標識本身爲Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者是整個GC過程),若是完成了,那線程就繼續執行,不然它就必須等待直到收到能夠安全離開Safe Region的信號爲止。
到此,筆者簡要地介紹了HotSpot虛擬機如何去發起內存回收的問題,可是虛擬機如何具體地進行內存回收動做仍然未涉及,由於內存回收如何進行是由虛擬機所採用的GC收集器決定的,而一般虛擬機中每每不止有一種GC收集器。下面繼續來看HotSpot中有哪些GC收集器。
若是說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。Java虛擬機規範中對垃圾收集器應該如何實現並無任何規定,所以不一樣的廠商、不一樣版本的虛擬機所提供的垃圾收集器均可能會有很大差異,而且通常都會提供參數供用戶根據本身的應用特色和要求組合出各個年代所使用的收集器。這裏討論的收集器基於JDK 1.7 Update 14以後的HotSpot虛擬機(在這個版本中正式提供了商用的G1收集器,以前G1仍處於實驗狀態),這個虛擬機包含的全部收集器如圖3-5所示。
圖3-5展現了7種做用於不一樣分代的收集器,若是兩個收集器之間存在連線,就說明它們能夠搭配使用。虛擬機所處的區域,則表示它是屬於新生代收集器仍是老年代收集器。接下來筆者將逐一介紹這些收集器的特性、基本原理和使用場景,並重點分析CMS和G1這兩款相對複雜的收集器,瞭解它們的部分運做細節。
在介紹這些收集器各自的特性以前,咱們先來明確一個觀點:雖然咱們是在對各個收集器進行比較,但並不是爲了挑選出一個最好的收集器。由於直到如今爲止尚未最好的收集器出現,更加沒有萬能的收集器,因此咱們選擇的只是對具體應用最合適的收集器。這點不須要多加解釋就能證實:若是有一種放之四海皆準、任何場景下都適用的完美收集器存在,那HotSpot虛擬機就不必實現那麼多不一樣的收集器了。
Serial收集器是最基本、發展歷史最悠久的收集器,曾經(在JDK 1.3.1以前)是虛擬機新生代收集的惟一選擇。你們看名字就會知道,這個收集器是一個單線程的收集器,但它的「單線程」的意義並不只僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工做,更重要的是在它進行垃圾收集時,必須暫停其餘全部的工做線程,直到它收集結束。「Stop The World」這個名字也許聽起來很酷,但這項工做其實是由虛擬機在後臺自動發起和自動完成的,在用戶不可見的狀況下把用戶正常工做的線程所有停掉,這對不少應用來講都是難以接受的。讀者不妨試想一下,要是你的計算機每運行一個小時就會暫停響應5分鐘,你會有什麼樣的心情?圖3-6示意了Serial/Serial Old收集器的運行過程。
對於「Stop The World」帶給用戶的不良體驗,虛擬機的設計者們表示徹底理解,但也表示很是委屈:「你媽媽在給你打掃房間的時候,確定也會讓你老老實實地在椅子上或者房間外待着,若是她一邊打掃,你一邊亂扔紙屑,這房間還能打掃完?」這確實是一個合情合理的矛盾,雖然垃圾收集這項工做聽起來和打掃房間屬於一個性質的,但實際上確定還要比打掃房間複雜得多啊!
從JDK 1.3開始,一直到如今最新的JDK 1.7,HotSpot虛擬機開發團隊爲消除或者減小工做線程因內存回收而致使停頓的努力一直在進行着,從Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)乃至GC收集器的最前沿成果Garbage First(G1)收集器,咱們看到了一個個愈來愈優秀(也愈來愈複雜)的收集器的出現,用戶線程的停頓時間在不斷縮短,可是仍然沒有辦法徹底消除(這裏暫不包括RTSJ中的收集器)。尋找更優秀的垃圾收集器的工做仍在繼續!
寫到這裏,筆者彷佛已經把Serial收集器描述成一個「老而無用、食之無味棄之惋惜」的雞肋了,但實際上到如今爲止,它依然是虛擬機運行在Client模式下的默認新生代收集器。它也有着優於其餘收集器的地方:簡單而高效(與其餘收集器的單線程比),對於限定單個CPU的環境來講,Serial收集器因爲沒有線程交互的開銷,專心作垃圾收集天然能夠得到最高的單線程收集效率。在用戶的桌面應用場景中,分配給虛擬機管理的內存通常來講不會很大,收集幾十兆甚至一兩百兆的新生代(僅僅是新生代使用的內存,桌面應用基本上不會再大了),停頓時間徹底能夠控制在幾十毫秒最多一百多毫秒之內,只要不是頻繁發生,這點停頓是能夠接受的。因此,Serial收集器對於運行在Client模式下的虛擬機來講是一個很好的選擇。
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集以外,其他行爲包括Serial收集器可用的全部控制參數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器徹底同樣,在實現上,這兩種收集器也共用了至關多的代碼。ParNew收集器的工做過程如圖3-7所示。
ParNew收集器除了多線程收集以外,其餘與Serial收集器相比並無太多創新之處,但它倒是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的緣由是,除了Serial收集器外,目前只有它能與CMS收集器配合工做。在JDK 1.5時期,HotSpot推出了一款在強交互應用中幾乎可認爲有劃時代意義的垃圾收集器——CMS收集器(Concurrent Mark Sweep,本節稍後將詳細介紹這款收集器),這款收集器是HotSpot虛
擬機中第一款真正意義上的併發(Concurrent)收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工做,用前面那個例子的話來講,就是作到了在你的媽媽打掃房間的時候你還能一邊往地上扔紙屑。
不幸的是,CMS做爲老年代的收集器,卻沒法與JDK 1.4.0中已經存在的新生代收集器Parallel Scavenge配合工做,因此在JDK 1.5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個。ParNew收集器也是使用-XX:+UseConcMarkSweepGC選項後的默認新生代收集器,也能夠使用-XX:+UseParNewGC選項來強制指定它。
ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至因爲存在線程交互的開銷,該收集器在經過超線程技術實現的兩個CPU的環境中都不能百分之百地保證能夠超越Serial收集器。固然,隨着能夠使用的CPU的數量的增長,它對於GC時系統資源的有效利用仍是頗有好處的。它默認開啓的收集線程數與CPU的數量相同,在CPU很是多(譬如32個,如今CPU動輒就4核加超線程,服務器超過32個邏輯CPU的狀況愈來愈多了)的環境下,能夠使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
注意 從ParNew收集器開始,後面還會接觸到幾款併發和並行的收集器。在你們可能產生疑惑以前,有必要先解釋兩個名詞:併發和並行。這兩個名詞都是併發編程中的概念,在談論垃圾收集器的上下文語境中,它們能夠解釋以下。
Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器……看上去和ParNew都同樣,那它有什麼特別之處呢?
Parallel Scavenge收集器的特色是它的關注點與其餘收集器不一樣,CMS等收集器的關注點是儘量地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗,而高吞吐量則能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。
Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。
MaxGCPauseMillis參數容許的值是一個大於0的毫秒數,收集器將盡量地保證內存回收花費的時間不超過設定值。不過你們不要認爲若是把這個參數的值設置得稍小一點就能使得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集300MB新生代確定比收集500MB快吧,這也直接致使垃圾收集發生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,如今變成5秒收集一次、每次停頓70毫秒。停頓時間的確在降低,但吞吐量也降下來了。
GCTimeRatio參數的值應當是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,至關因而吞吐量的倒數。若是把此參數設置爲19,那容許的最大GC時間就佔總時間的5%(即1/(1+19)),默認值爲99,就是容許最大1%(即1/(1+99))的垃圾收集時間。
因爲與吞吐量關係密切,Parallel Scavenge收集器也常常稱爲「吞吐量優先」收集器。除上述兩個參數以外,Parallel Scavenge收集器還有一個參數-XX:+UseAdaptiveSizePolicy值得關注。這是一個開關參數,當這個參數打開以後,就不須要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱爲GC自適應的調節策略(GC Ergonomics)。若是讀者對於收集器運做原來不太瞭解,手工優化存在困難的時候,使用Parallel Scavenge收集器配合自適應調節策略,把內存管理的調優任務交給虛擬機去完成將是一個不錯的選擇。只須要把基本的內存數據設置好(如-Xmx設置最大堆),而後使用MaxGCPauseMillis參數(更關注最大停頓時間)或GCTimeRatio(更關注吞吐量)參數給虛擬機設立一個優化目標,那具體細節參數的調節工做就由虛擬機完成了。自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別。
Serial Old是Serial收集器的老年代版本,它一樣是一個單線程收集器,使用「標記-整理」算法。這個收集器的主要意義也是在於給Client模式下的虛擬機使用。若是在Server模式下,那麼它主要還有兩大用途:一種用途是在JDK 1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另外一種用途就是做爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。這兩點都將在後面的內容中詳細講解。Serial Old收集器的工做過程如圖3-8所示。
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和「標記-整理」算法。這個收集器是在JDK 1.6中才開始提供的,在此以前,新生代的Parallel Scavenge收集器一直處於比較尷尬的狀態。緣由是,若是新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外別無選擇(還記得上面說過Parallel Scavenge收集器沒法與CMS收集器配合工做嗎?)。因爲老年代Serial Old收集器在服務端應用性能上的「拖累」,使用了Parallel Scavenge收集器也未必能在總體應用上得到吞吐量最大化的效果,因爲單線程的老年代收集中沒法充分利用服務器多CPU的處理能力,在老年代很大並且硬件比較高級的環境中,這種組合的吞吐量甚至還不必定有ParNew加CMS的組合「給力」。
直到Parallel Old收集器出現後,「吞吐量優先」收集器終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工做過程如圖3-9所示。
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤爲重視服務的響應速度,但願系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就很是符合這類應用的需求。
從名字(包含「Mark Sweep」)上就能夠看出,CMS收集器是基於「標記—清除」算法實現的,它的運做過程相對於前面幾種收集器來講更復雜一些,整個過程分爲4個步驟,包括:
其中,初始標記、從新標記這兩個步驟仍然須要「Stop The World」。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是進行GC RootsTracing的過程,而從新標記階段則是爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但遠比並發標記的時間短。
因爲整個過程當中耗時最長的併發標記和併發清除過程收集器線程均可以與用戶線程一塊兒工做,因此,從整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。經過圖3-10能夠比較清楚地看到CMS收集器的運做步驟中併發和須要停頓的時間。
CMS是一款優秀的收集器,它的主要優勢在名字上已經體現出來了:併發收集、低停頓,Sun公司的一些官方文檔中也稱之爲併發低停頓收集器(Concurrent Low Pause Collector)。可是CMS還遠達不到完美的程度,它有如下3個明顯的缺點:
G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一,早在JDK 1.7剛剛確立項目目標,Sun公司給出的JDK 1.7 RoadMap裏面,它就被視爲JDK 1.7中HotSpot虛擬機的一個重要進化特徵。從JDK 6u14中開始就有Early Access版本的G1收集器供開發人員實驗、試用,由此開始G1收集器的「Experimental」狀態持續了數年時間,直至JDK 7u4,Sun公司才認爲它達到足夠成熟的商用程度,移除了「Experimental」的標識。
G1是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是(在比較長期的)將來能夠替換掉JDK 1.5中發佈的CMS收集器。與其餘GC收集器相比,G1具有以下特色。
並行與併發:G1能充分利用多CPU、多核環境下的硬件優點,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間,部分其餘收集器本來須要停頓Java線程執行的GC動做,G1收集器仍然能夠經過併發的方式讓Java程序繼續執行。
分代收集:與其餘收集器同樣,分代概念在G1中依然得以保留。雖然G1能夠不須要其餘收集器配合就能獨立管理整個GC堆,但它可以採用不一樣的方式去處理新建立的對象和已經存活了一段時間、熬過屢次GC的舊對象以獲取更好的收集效果。
空間整合:與CMS的「標記—清理」算法不一樣,G1從總體來看是基於「標記—整理」算法實現的收集器,從局部(兩個Region之間)上來看是基於「複製」算法實現的,但不管如何,這兩種算法都意味着G1運做期間不會產生內存空間碎片,收集後能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會由於沒法找到連續內存空間而提早觸發下一次GC。
可預測的停頓:這是G1相對於CMS的另外一大優點,下降停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已是實時Java(RTSJ)的垃圾收集器的特徵了。
在G1以前的其餘收集器進行收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。使用G1收集器時,Java堆的內存佈局就與其餘收集器有很大差異,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分Region(不須要連續)的集合。
G1收集器之因此能創建可預測的停頓時間模型,是由於它能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大Region(這也就是Garbage-First名稱的來由)。這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內能夠獲取儘量高的收集效率。
G1把內存「化整爲零」的思路,理解起來彷佛很容易,但其中的實現細節卻遠遠沒有想象中那樣簡單,不然也不會從2004年Sun實驗室發表第一篇G1的論文開始直到今天(將近10年時間)纔開發出G1的商用版。筆者以一個細節爲例:把Java堆分爲多個Region後,垃圾收集是否就真的能以Region爲單位進行了?聽起來瓜熟蒂落,再仔細想一想就很容易發現問題所在:Region不多是孤立的。一個對象分配在某個Region中,它並不是只能被本Region中的其
他對象引用,而是能夠與整個Java堆任意的對象發生引用關係。那在作可達性斷定肯定對象是否存活的時候,豈不是還得掃描整個Java堆才能保證準確性?這個問題其實並不是在G1中才有,只是在G1中更加突出而已。在之前的分代收集中,新生代的規模通常都比老年代要小許多,新生代的收集也比老年代要頻繁許多,那回收新生代中的對象時也面臨相同的問題,若是回收新生代時也不得不一樣時掃描老年代的話,那麼Minor GC的效率可能降低很多。
在G1收集器中,Region之間的對象引用以及其餘收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每一個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描也不會有遺漏。
若是不計算維護Remembered Set的操做,G1收集器的運做大體可劃分爲如下幾個步驟:
對CMS收集器運做過程熟悉的讀者,必定已經發現G1的前幾個步驟的運做過程和CMS有不少類似之處。初始標記階段僅僅只是標記一下GC Roots能直接關聯到的對象,而且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的Region中建立新對象,這階段須要停頓線程,但耗時很短。併發標記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序併發執行。而最終標記階段則是爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線Remembered Set Logs裏面,最終標記階段須要把Remembered Set Logs的數據合併到Remembered Set中,這階段須要停頓線程,可是可並行執行。最後在篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃,從Sun公司透露出來的信息來看,這個階段其實也能夠作到與用戶程序一塊兒併發執行,可是由於只回收一部分Region,時間是用戶可控制的,並且停頓用戶線程將大幅提升收集效率。經過圖3-11能夠比較清楚地看到G1收集器的運做步驟中併發和須要停頓的階段。
因爲目前G1成熟版本的發佈時間還很短,G1收集器幾乎能夠說尚未通過實際應用的考驗,網絡上關於G1收集器的性能測試也很是貧乏,到目前爲止,筆者尚未搜索到有關的生產環境下的性能測試報告。強調「生產環境下的測試報告」是由於對於垃圾收集器來講,僅僅經過簡單的Java代碼寫個Microbenchmark程序來建立、移除Java對象,再用-XX:+PrintGCDetails等參數來查看GC日誌是很難作到準確衡量其性能的。所以,關於G1收集器的性能部分,筆者引用了Sun實驗室的論文《Garbage-First Garbage Collection》中的一段測試數據。
Sun給出的Benchmark的執行硬件爲Sun V880服務器(8×750MHz UltraSPARC III CPU、32G內存、Solaris 10操做系統)。執行軟件有兩個,分別爲SPECjbb(模擬商業數據庫應用,堆中存活對象約爲165MB,結果反映吐量和最長事務處理時間)和telco(模擬電話應答服務應用,堆中存活對象約爲100MB,結果反映系統能支持的最大吞吐量)。爲了便於對比,還收集了一組使用ParNew+CMS收集器的測試數據。全部測試都配置爲與CPU數量相同的8條GC線程。
在反應停頓時間的軟實時目標(Soft Real-Time Goal)測試中,橫向是兩個測試軟件的時間片斷配置,單位是毫秒,以(X/Y)的形式表示,表明在Y毫秒內最大容許GC時間爲X毫秒(對於CMS收集器,沒法直接指定這個目標,經過調整分代大小的方式大體模擬)。縱向是兩個軟件在對應配置和不一樣的Java堆容量下的測試結果,V%、avgV%和wV%分別表明的含義以下。
V%:表示測試過程當中,軟實時目標失敗的機率,軟實時目標失敗即某個時間片斷中實際GC時間超過了容許的最大GC時間。
avgV%:表示在全部實際GC時間超標的時間片斷裏,實際GC時間超過最大GC時間的平均百分比(實際GC時間減去容許最大GC時間,再除以總時間片斷)。
wV%:表示在測試結果最差的時間片斷裏,實際GC時間佔用執行時間的百分比。
測試結果見表3-1。
從表3-1所示的結果可見,對於telco來講,軟實時目標失敗的機率控制在0.5%~0.7%之間,SPECjbb就要差一些,但也控制在2%~5%之間,機率隨着(X/Y)的比值減少而增長。另外一方面,失敗時超出容許GC時間的比值隨着總時間片斷增長而變小(分母變大了),在(100/200)、512MB的配置下,G1收集器出現了某些時間片斷下100%時間在進行GC的最壞狀況。而相比之下,CMS收集器的測試結果就要差不少,3種Java堆容量下都出現100%時間進行GC的狀況。
在吞吐量測試中,測試數據取3次SPECjbb和15次telco的平均結果如圖3-12所示。在SPECjbb的應用下,各類配置下的G1收集器表現出了一致的行爲,吞吐量看起來只與容許最大GC時間成正比關係,而在telco的應用中,不一樣配置對吞吐量的影響則顯得很微弱。與CMS收集器的吞吐量對比能夠看到,在SPECjbb測試中,在堆容量超過768MB時,CMS收集器有5%~10%的優點,而在telco測試中,CMS的優點則要小一些,只有3%~4%左右。
在更大規模的生產環境下,筆者引用一段在StackOverflow.com上看到的經驗與讀者分享:「我在一個真實的、較大規模的應用程序中使用過G1:大約分配有60~70GB內存,存活對象大約在20~50GB之間。服務器運行Linux操做系統,JDK版本爲6u22。G1與PS/PS Old相比,最大的好處是停頓時間更加可控、可預測,若是我在PS中設置一個很低的最大容許GC時間,譬如指望50毫秒內完成GC(-XX:MaxGCPauseMillis=50),但在65GB的Java堆下有可能獲得的直接結果是一次長達30秒至2分鐘的漫長的Stop-The-World過程;而G1與CMS相比,雖然它們都立足於低停頓時間,CMS仍然是我如今的選擇,可是隨着Oracle對G1的持續改進,我相信G1會是最終的勝利者。若是你如今採用的收集器沒有出現問題,那就沒有任何理由如今去選擇G1,若是你的應用追求低停頓,那G1如今已經能夠做爲一個可嘗試的選擇,若是你的應用追求吞吐量,那G1並不會爲你帶來什麼特別的好處」。
閱讀GC日誌是處理Java虛擬機內存問題的基礎技能,它只是一些人爲肯定的規則,沒有太多技術含量。在本書的第1版中沒有專門講解如何閱讀分析GC日誌,爲此做者收到許多讀者來信,反映對此感到困惑,所以專門增長本節內容來說解如何理解GC日誌。
每一種收集器的日誌形式都是由它們自身的實現所決定的,換而言之,每一個收集器的日誌格式均可以不同。但虛擬機設計者爲了方便用戶閱讀,將各個收集器的日誌都維持必定的共性,例如如下兩段典型的GC日誌:
33.125:[GC[DefNew:3324K->152K(3712K),0.0025925 secs]3324K->152K(11904K),0.0031680 secs] 100.667:[FullGC[Tenured:0K->210K(10240K),0.0 149142secs]4603K->210K(19456K),[Perm:2999K-> 2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]
最前面的數字「33.125:」和「100.667:」表明了GC發生的時間,這個數字的含義是從Java虛擬機啓動以來通過的秒數。
GC日誌開頭的「[GC」和「[Full GC」說明了此次垃圾收集的停頓類型,而不是用來區分新生代GC仍是老年代GC的。若是有「Full」,說明此次GC是發生了Stop-The-World的,例以下面這段新生代收集器ParNew的日誌也會出現「[Full GC」(這通常是由於出現了分配擔保失敗之類的問題,因此才致使STW)。若是是調用System.gc()方法所觸發的收集,那麼在這裏將顯示「[Full GC(System)」。
[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]
接下來的「[DefNew」、「[Tenured」、「[Perm」表示GC發生的區域,這裏顯示的區域名稱與使用的GC收集器是密切相關的,例如上面樣例所使用的Serial收集器中的新生代名爲「Default New Generation」,因此顯示的是「[DefNew」。若是是ParNew收集器,新生代名稱就會變爲「[ParNew」,意爲「Parallel New Generation」。若是採用Parallel Scavenge收集器,那它配套的新生代稱爲「PSYoungGen」,老年代和永久代同理,名稱也是由收集器決定的。
後面方括號內部的「3324K->152K(3712K)」含義是「GC前該內存區域已使用容量->GC後該內存區域已使用容量(該內存區域總容量)」。而在方括號以外的「3324K->152K(11904K)」表示「GC前Java堆已使用容量->GC後Java堆已使用容量(Java堆總容量)」。
再日後,「0.0025925 secs」表示該內存區域GC所佔用的時間,單位是秒。有的收集器會給出更具體的時間數據,如「[Times:user=0.01 sys=0.00,real=0.02 secs]」,這裏面的user、sys和real與Linux的time命令所輸出的時間含義一致,分別表明用戶態消耗的CPU時間、內核態消耗的CPU事件和操做從開始到結束所通過的牆鍾時間(Wall Clock Time)。CPU時間與牆鍾時間的區別是,牆鍾時間包括各類非運算的等待耗時,例如等待磁盤I/O、等待線程阻塞,而CPU時間不包括這些耗時,但當系統有多CPU或者多核的話,多線程操做會疊加這些CPU時間,因此讀者看到user或sys時間超過real時間是徹底正常的。
JDK 1.7中的各類垃圾收集器到此已所有介紹完畢,在描述過程當中提到了不少虛擬機非穩定的運行參數,在表3-2中整理了這些參數供讀者實踐時參考。
Java技術體系中所提倡的自動內存管理最終能夠歸結爲自動化地解決了兩個問題:給對象分配內存以及回收分配給對象的內存。關於回收內存這一點,咱們已經使用了大量篇幅去介紹虛擬機中的垃圾收集器體系以及運做原理,如今咱們再一塊兒來探討一下給對象分配內存的那點事兒。
對象的內存分配,往大方向講,就是在堆上分配(但也可能通過JIT編譯後被拆散爲標量類型並間接地棧上分配),對象主要分配在新生代的Eden區上,若是啓動了本地線程分配緩衝,將按線程優先在TLAB上分配。少數狀況下也可能會直接分配在老年代中,分配的規則並非百分之百固定的,其細節取決於當前使用的是哪種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。
接下來咱們將會講解幾條最廣泛的內存分配規則,並經過代碼去驗證這些規則。本節下面的代碼在測試時使用Client模式虛擬機運行,沒有手工指定收集器組合,換句話說,驗證的是在使用Serial/Serial Old收集器下(ParNew/Serial Old收集器組合的規則也基本一致)的內存分配和回收的策略。讀者不妨根據本身項目中使用的收集器寫一些程序去驗證一下使用其餘幾種收集器的內存分配策略。
大多數狀況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。
虛擬機提供了-XX:+PrintGCDetails這個收集器日誌參數,告訴虛擬機在發生垃圾收集行爲時打印內存回收日誌,而且在進程退出的時候輸出當前的內存各區域分配狀況。在實際應用中,內存回收日誌通常是打印到文件後經過日誌工具進行分析,不過本實驗的日誌並很少,直接閱讀就能看得很清楚。
代碼清單3-5的testAllocation()方法中,嘗試分配3個2MB大小和1個4MB大小的對象,在運行時經過-Xms20M、-Xmx20M、-Xmn10M這3個參數限制了Java堆大小爲20MB,不可擴展,其中10MB分配給新生代,剩下的10MB分配給老年代。-XX:SurvivorRatio=8決定了新生代中Eden區與一個Survivor區的空間比例是8:1,從輸出的結果也能夠清晰地看到「eden space 8192K、from space 1024K、to space 1024K」的信息,新生代總可用空間爲9216KB(Eden區+1個Survivor區的總容量)。
執行testAllocation()中分配allocation4對象的語句時會發生一次Minor GC,此次GC的結果是新生代6651KB變爲148KB,而總內存佔用量則幾乎沒有減小(由於allocation一、allocation二、allocation3三個對象都是存活的,虛擬機幾乎沒有找到可回收的對象)。此次GC發生的緣由是給allocation4分配內存的時候,發現Eden已經被佔用了6MB,剩餘空間已不足以分配allocation4所需的4MB內存,所以發生Minor GC。GC期間虛擬機又發現已有的3個2MB大小的對象所有沒法放入Survivor空間(Survivor空間只有1MB大小),因此只好經過分配擔保機制提早轉移到老年代去。
此次GC結束後,4MB的allocation4對象順利分配在Eden中,所以程序執行完的結果是Eden佔用4MB(被allocation4佔用),Survivor空閒,老年代被佔用6MB(被allocation一、allocation二、allocation3佔用)。經過GC日誌能夠證明這一點。
注意 做者屢次提到的Minor GC和Full GC有什麼不同嗎?
代碼清單3-5 新生代Minor GC
private static final int _1MB = 1024 * 1024; /** * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 */ 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]; // 出現一次Minor GC }
運行結果:
[GC[DefMew: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 ,5Uused[0x029d0000 ,0x02de4828 ,0x031d0000 ) from space 1024K ,14Sused[0x032d0000 ,0x032f5370 ,0x033d0000 ) to space 1024K ,0%used[0x03ldO000 ,0x031d0000 ,0x032d0000 ) tenured generation total 1024OK,used 6144K[0x033d0000 ,0x03dd0000 ,0x03dd0000 ) the space 1024OK,60lused[0x033d0000,0x039d0030,0x039d0200,0x03dd0000) compacting perm gen total 12288K,used 2114K[0x03dd0000 ,0x049d0000 ,0x07dd0000 ) the space 12288K ,17lused[0x03dd0000 ,0x03fe0998 ,0x03fe0a00 ,0x049d0000 ) Mo shared spaces configured.
所謂的大對象是指,須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組(筆者列出的例子中的byte[]數組就是典型的大對象)。大對象對虛擬機的內存分配來講就是一個壞消息(替Java虛擬機抱怨一句,比遇到一個大對象更加壞的消息就是遇到一羣「朝生夕滅」的「短命大對象」,寫程序的時候應當避免),常常出現大對象容易致使內存還有很多空間時就提早觸發垃圾收集以獲取足夠的連續空間來「安置」它們。
虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接在老年代分配。這樣作的目的是避免在Eden區及兩個Survivor區之間發生大量的內存複製(複習一下:新生代採用複製算法收集內存)。
執行代碼清單3-6中的testPretenureSizeThreshold()方法後,咱們看到Eden空間幾乎沒有被使用,而老年代的10MB空間被使用了40%,也就是4MB的allocation對象直接就分配在老年代中,這是由於PretenureSizeThreshold被設置爲3MB(就是3145728,這個參數不能像-Xmx之類的參數同樣直接寫3MB),所以超過3MB的對象都會直接在老年代進行分配。注意PretenureSizeThreshold參數只對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認識這個參數,Parallel Scavenge收集器通常並不須要設置。若是遇到必須使用此參數的場合,能夠考慮ParNew加CMS的收集器組合。
代碼清單3-6 大對象直接進入老年代
private static final int _1MB = 1024 * 1024; /** * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 * -XX:PretenureSizeThreshold=3145728 */ public static void testPretenureSizeThreshold() { byte[] allocation; allocation = new byte[4 * _1MB]; //直接分配在老年代中 }
運行結果:
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.
既然虛擬機採用了分代收集的思想來管理內存,那麼內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代中。爲了作到這點,虛擬機給每一個對象定義了一個對象年齡(Age)計數器。若是對象在Eden出生並通過第一次Minor GC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,而且對象年齡設爲1。對象在Survivor區中每「熬過」一次Minor GC,年齡就增長1歲,當它的年齡增長到必定程度(默認爲15歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,能夠經過參數-XX:MaxTenuringThreshold設置。
讀者能夠試試分別以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15兩種設置來執行代碼清單3-7中的testTenuringThreshold()方法,此方法中的allocation1對象須要256KB內存,Survivor空間能夠容納。當MaxTenuringThreshold=1時,allocation1對象在第二次GC發生時進入老年代,新生代已使用的內存GC後很是乾淨地變成0KB。而MaxTenuringThreshold=15時,第二次GC發生後,allocation1對象則還留在新生代Survivor空間,這時新生代仍然有404KB被佔用。
代碼清單3-7 長期存活的對象進入老年代
private static final int _1MB = 1024 * 1024; /** * VM參數:-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設置 allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; allocation3 = null; allocation3 = new byte[4 * _1MB]; }
以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.
爲了能更好地適應不一樣程序的內存情況,虛擬機並非永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
執行代碼清單3-8中的testTenuringThreshold2()方法,並設置-XX:
MaxTenuringThreshold=15,會發現運行結果中Survivor的空間佔用仍然爲0%,而老年代比預期增長了6%,也就是說,allocation一、allocation2對象都直接進入了老年代,而沒有等到15歲的臨界年齡。由於這兩個對象加起來已經到達了512KB,而且它們是同年的,知足同年對象達到Survivor空間的一半規則。咱們只要註釋掉其中一個對象new操做,就會發現另一個就不會晉升到老年代中去了。
代碼清單3-8 動態對象年齡斷定
private static final int _1MB = 1024 * 1024; /** * VM參數:-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 byte[4 * _1MB]; }
運行結果:
[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.
在發生Minor GC以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼Minor GC能夠確保是安全的。若是不成立,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次Minor GC,儘管此次Minor GC是有風險的;若是小於,或者HandlePromotionFailure設置不容許冒險,那這時也要改成進行一次Full GC。
下面解釋一下「冒險」是冒了什麼風險,前面提到過,新生代使用複製收集算法,但爲了內存利用率,只使用其中一個Survivor空間來做爲輪換備份,所以當出現大量對象在Minor GC後仍然存活的狀況(最極端的狀況就是內存回收後新生代中全部對象都存活),就須要老年代進行分配擔保,把Survivor沒法容納的對象直接進入老年代。與生活中的貸款擔保相似,老年代要進行這樣的擔保,前提是老年代自己還有容納這些對象的剩餘空間,一共有多少對象會活下來在實際完成內存回收以前是沒法明確知道的,因此只好取以前每一次回收晉升到老年代對象容量的平均大小值做爲經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。
取平均值進行比較其實仍然是一種動態機率的手段,也就是說,若是某次Minor GC存活後的對象突增,遠遠高於平均值的話,依然會致使擔保失敗(Handle Promotion Failure)。若是出現了HandlePromotionFailure失敗,那就只好在失敗後從新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分狀況下都仍是會將HandlePromotionFailure開關打開,避免Full GC過於頻繁,參見代碼清單3-9,請讀者在JDK 6 Update 24以前的版本中運行測試。
代碼清單3-9 空間分配擔保
private static final int _1MB = 1024 * 1024; /** * VM參數:-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]; }
以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] [G C[D e f N e w:6 3 7 8 K->6 3 7 8 K(9 2 1 6 K),0.0 0 0 0 2 0 6 s e c s][T e n u r e d: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]
在JDK 6 Update 24以後,這個測試結果會有差別,HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,觀察OpenJDK中的源碼變化(見代碼清單3-10),雖然源碼中還定義了HandlePromotionFailure參數,可是在代碼中已經不會再使用它。JDK 6 Update 24以後的規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。
代碼清單3-10 HotSpot中空間分配檢查的代碼片斷
bool TenuredGeneration:promotion_attempt_is_safe(size_tmax_promotion_in_bytes)const{ //老年代最大可用的連續空間 size_t available=max_contiguous_available(); //每次晉升到老年代的平均大小 size_t av_promo=(size_t)gc_stats()->avg_promoted()->padded_average(); //老年代可用空間是否大於平均晉升大小,或者老年代可用空間是否大於當此GC時新生代全部對象容量 bool res=(available>=av_promo)||(available>= max_promotion_in_bytes); return res; }
本章介紹了垃圾收集的算法、幾款JDK 1.7中提供的垃圾收集器特色以及運做原理。經過代碼實例驗證了Java虛擬機中自動內存分配及回收的主要規則。
內存回收與垃圾收集器在不少時候都是影響系統性能、併發能力的主要因素之一,虛擬機之因此提供多種不一樣的收集器以及提供大量的調節參數,是由於只有根據實際應用需求、實現方式選擇最優的收集方式才能獲取最高的性能。沒有固定收集器、參數組合,也沒有最優的調優方法,虛擬機也就沒有什麼必然的內存回收行爲。所以,學習虛擬機內存知識,若是要到實踐調優階段,那麼必須瞭解每一個具體收集器的行爲、優點和劣勢、調節參數。在接下來的兩章中,做者將會介紹內存分析的工具和調優的一些具體案例。