《深入理解Java虛擬機》——垃圾收集器與內存分配策略問題

通過這篇文章你能知道的問題:

1.如何判斷對象是活著還是死去?

2.在Java語言中,可作為GCRoots的對象有哪些?

3.Java中引用的分類

4.對象的自救姿勢是什么?

5.類在什么情況下是無用的?

6.垃圾收集算法有哪些?

7.年輕代,老年代,永久代?

8.HotSpot虛擬機是如何發起內存回收的?

9.垃圾收集器有哪些以及組合方式有哪些?

10.怎么理解GC日志?

11.內存如何分配?

該篇文章的篇幅會有點長,希望大家耐心閱讀下去。

大家都知道內存回收的是無用對象,但是怎么判斷對象是無用還是有用呢?

比較容易想到的是引用計數法。那么什么是引用計數法呢?

引用計數法:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再使用的。

雖然引用計數算法實現簡單且判斷效率高效,但是在主流的Java虛擬機里面沒有選用引用計數算法來管理內存,其中最主要的原因是它很難解決對象間相互循環引用的問題。

既然我們學習的是Java虛擬機相關的內容,那我們就要了解Java虛擬機的實現中是怎么判斷對象是否存活的?

可達性分析算法:在主流的商用程序語言的主流實現中,都是稱通過可達性分析來判斷對象是否存活的。這個算法的基本思路就是通過一系列的稱為"GC Roots"的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。如圖3-1所示:

在Java語言中,可作為GC Roots的對象包括下面幾種:

虛擬機棧(棧幀中的本地變量表)中引用的對象

方法區中類靜態屬性引用的對象

方法區中常量引用的對象

本地方法棧中JNI(即一般說的Native方法)引用的對象

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象的引用鏈是否可達,判斷對象是否存活都與"引用"有關。

在JDK1.2之后,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

強引用就是指在程序代碼之中普遍存在的,類似"Object obj=new Object()"這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

軟引用是用來描述一些還有用但并非必需的對象。對于軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。

弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

虛引用也被稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。

上面介紹了對象存活的判斷以及引用的分類,那么對象如何在被殺死時來個自我救贖呢?

救贖之前我們要了解我們的對象是怎么被殺死的:

在可達性分析算法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程——如果對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”。(那么問題來了,這種情況下的二次標記在哪里?)

如果這個對象被判定為有必要執行finalize()方法,那么這個對象將會放置在一個叫做F-Queue的隊列之中,并在稍后由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。這里所謂的"執行"是指虛擬機會觸發這個方法,但并不承諾會等待它運行結束,這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生了死循環,將很可能會導致F-Queue隊列中其他對象永久處于等待,甚至導致整個內存回收系統崩潰。finalize()方法是對象逃脫死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規模標記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出"即將回收"的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。

[java]view plaincopy

packagecom.general.android.test.oom;

publicclassTestFinalize?{

publicstaticTestFinalize?SAVE_HOOK=null;

publicvoidisAlive(){

System.out.println("yes,?i?am?still?alive");

}

@Override

protectedvoidfinalize()throwsThrowable?{

//?TODO?Auto-generated?method?stub

super.finalize();

System.out.println("finalize?method?executed");

TestFinalize.SAVE_HOOK=this;

}

publicstaticvoidmain(String[]?args)throwsInterruptedException?{

SAVE_HOOK=newTestFinalize();

//對象第一次成功拯救自己

SAVE_HOOK=null;

System.gc();

Thread.sleep(500);

if(SAVE_HOOK!=null){

SAVE_HOOK.isAlive();

}else{

System.out.println("no,I?am?dead");

}

SAVE_HOOK=null;

System.gc();

Thread.sleep(500);

if(SAVE_HOOK!=null){

SAVE_HOOK.isAlive();

}else{

System.out.println("no,I?am?dead");

}

}

}

上面代碼的輸出結果:

finalize method executed

yes, i am still alive

no,I am dead

注意:任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行。

在方法區中類需要同時滿足下面3個條件才能算是“無用的類”:

1.該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例

2.加載該類的ClassLoader已經被回收

3.該類對應的java.ang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

虛擬機可以對滿足上述3個條件的無用類進行回收,這里說的僅僅是“可以”,而并不是和對象一樣,不使用了就必然會回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載和卸載信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機中使用,-XX:+TraceClassUnLoading參數需要FastDebug版的虛擬機支持。

常用的垃圾收集算法有哪些?

常用的垃圾收集算法有:標記-清除算法、復制算法、標記-整理算法、分代收集算法。

標記-清除算法:算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。標記-清除算法主要有兩個不足:(1)一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

復制算法:復制算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小為了原來的一半,未免太高了一點。

現在的商業虛擬機都采用這種收集算法來回收新生代,IBM公司的專門研究表明,新生代中的對象98%是"朝生夕死"的,所以并不需要按照1:1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活的對象一次性地復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1:1,也就是每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),只有10%的內存會被“浪費”。當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴其他內存(這里指老年代)進行分配擔保,也就是說如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。

標記-整理算法:復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。根據老年代的特點,有人提出了另外一種“標記-整理”算法,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。

分代收集算法:當前商業虛擬機的垃圾收集都采用"分代收集"算法,這種算法并沒有什么新的思想,只是根據對象存活周期的不同將內存劃分為幾塊。一般把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最恰當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高,沒有額外空間對它進行分配擔保,就必須使用標記-清理或者標記-整理算法來進行回收。

這里提到了新生代和老年代,那么新生代是什么?老年代又是什么呢?永久代又是何物呢?

新生代

主要是用來存放新生的對象,即剛剛創建的對象。新生代又被劃分為Eden區和Survivor區(包含空間相等的From、To區,沒有先后順序,是復制算法的需要)。大多數情況下,java中新建的對象都是在新生代上分配的,通過復制算法來進行分配內存和垃圾回收。

老年代

主要存放應用程序中生命周期長的內存對象。在新生代中經過多次垃圾回收仍然存活的對象,會被存放到老年代中。老年代通過標記-整理算法來清理無用內存。

永久代

永久代這個概念比較特殊,因為永久代是Hotspot虛擬機特有的概念,是方法區的一種實現,別的JVM都沒有這個東西。但是在jdk1.8中,永久代已經被移除。

在Java 6中,方法區中包含的數據,除了JIT編譯生成的代碼存放在native memory的CodeCache區域,其他都存放在永久代;

在Java 7中,Symbol的存儲從PermGen移動到了native memory,并且把靜態變量從instanceKlass末尾(位于PermGen內)移動到了java.lang.Class對象的末尾(位于普通Java heap內);

在Java 8中,永久代被徹底移除,取而代之的是另一塊與堆不相連的本地內存——元空間(Metaspace),?XX:MaxPermSize 參數失去了意義,取而代之的是-XX:MaxMetaspaceSize。

http://blog.csdn.net/ooppookid/article/details/51477147

https://www.zhihu.com/question/49044988/answer/113961406

HotSpot是如何去發起內存回收的?

前面說過HotSpot虛擬機是通過可達性分析來判斷對象是否存活的。而垃圾收集算法在標記可回收對象時,肯定也是使用了可達性分析來判斷哪些對象是可以被回收。大家都知道可作為GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,現在很多應用僅僅方法區就有數百兆,如果要逐個檢查這里面的引用,那么必然會消耗很多時間。另外,可達性分析對執行時間的敏感還體現在GC停頓上,因為這項分析工作必須在一個能確保一致性的快照中進行——這里"一致性"的意思是指整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不可以出現分析過程中對象引用關系還在不斷變化的情況,該點不滿足的話分析結果準確性就無法得到保證。這點是導致GC進行時必須停頓所有Java執行線程的其中一個重要原因,即使是在號稱不會發生停頓的CMS收集器中,枚舉根節點時也是必須要停頓的。

由于目前的主流Java虛擬機使用的都是準確式GC,所以當執行系統停頓下來后,并不需要一個不漏地檢查完所有執行上下文和全局的引用位置,虛擬機應當是有辦法直接得知哪些地方存放著對象引用。在HotSpot的實現中,是使用一組稱為OopMap的數據結構來達到這個目的,在類加載完成的時候,HotSpot就把對象內什么偏移量上是什么類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這些位置稱為安全點(Safepoint),即程序執行時并非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。Safepoint的選定既不能太少以致于讓GC等待時間太長,也不能過于頻繁以致于過分增大運行時的負荷。所以,安全點的選定基本上是以程序“是否具有讓程序長時間執行的特征”為標準進行選定的。對于安全點,另一個需要考慮的問題是如何在GC發生時讓所有線程(這里不包括執行JNI調用的線程)都“跑”到最近的安全點上再停頓下來。這里有兩種方案可供選擇:搶先式中斷和主動式中斷。

搶先式中斷:不需要線程的執行代碼主動去配合,在GC發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上。但是現在幾乎沒有虛擬機實現采用搶先式中斷來暫停線程從而響應GC事件。

主動式中斷:當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標志,各個線程執行時主動去輪詢這個標志,發現中斷標志為真時就自己中斷掛起。輪詢標志的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。

使用安全點似乎已經完美地解決了如何進入GC的問題,但是實際情況卻并不一定。安全點機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的Safepoint。但是在程序不執行的時候就無法做到這一點,比如線程在休眠或阻塞狀態。對于這種情況,就需要安全區域(Safe Region)來解決。

安全區域是指在一段代碼片段之中,引用關系不會發生變化。在這個區域中的任意地方開始GC都是安全的。我們也可以把Safe Region看做是被擴展了的Safepoint。

在線程執行到Safe Region中的代碼時,首先標識自己已經進入了Safe Region,那樣,當在這段時間里JVM要發起GC時,就不用管標識自己為Safe Region狀態的線程了。在線程要離開SafeRegion時,它要檢查系統是否已經完成了根節點枚舉(或者是整個GC過程),如果完成了,那線程就繼續執行,否則它就必須等待直到收到可以安全離開Safe Region的信號為止。

這個闡述有點長,我來簡要概括一下:在OopMap的協助下,HotSpot快速且準確地完成GC Roots枚舉,并在安全點或者安全區域進行中斷,以保證在根結點枚舉過程中引用關系不發生改變,也就是在標記可回收對象時,內存中的引用關系不再發生變化,這樣的操作才有意義。

垃圾收集器有哪些以及組合方式有哪些?

如果說收集算法是內存回收的方法論,那么垃圾收集器就是內存回收的具體實現。Java虛擬機規范中對垃圾收集器應該如何實現并沒有任何規定,因此不同的廠商、不同的版本的虛擬機所提供的垃圾收集器都可能會有很大的差別,并且一般都會提供參數供用戶自己根據自己的應用特點和要求組合出各個年代所使用的收集器。

這里所討論的收集器是基于JDK1.7Update 14之后的HotSpot虛擬機,上圖展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。虛擬機所處的區域,則表示它是屬于新生代收集器還是老年代收集器。

Serial收集器

Serial收集器是最基本、發展歷史最悠久的收集器,曾經(在JDK1.3.1之前)是虛擬機新生代收集的唯一選擇。大家看名字就會知道,這個收集器是一個單線程的收集器,但它的“單線程”的意義并不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。Serial收集器是虛擬機運行在Client模式下的默認新生代收集器,它有著優于其他收集器的地方:簡單而高效(與其他收集器的單線程比),對于限定單個CPU的環境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。

ParNew收集器

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其余行為包括Serial收集器可用的所有控制參數、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一樣,在實現上,這兩種收集器也共用了相當多的代碼。ParNew收集器是許多運行在Server模式下的虛擬機中首先的新生代收集器,其中有一個與性能無關但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。

在談論垃圾收集器的上下文語境中,并行和并發的概念如下:

并行:指多余垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態。

并發:指用戶線程與垃圾收集線程同時執行(但不一定是并行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行于另一個CPU上。

Parallel Scavenge收集器

Parallel Scavenge收集器是一個新生代收集器,它也是使用復制算法的收集器,又是并行的多線程收集器。Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗,而高吞吐量則可以高效率地利用CPU時間,盡快完成程序的運算任務,主要適合在后臺運算而不需要太多交互的任務。

Parallel Scavenge的一些參數:

MaxGCPauseMillis:控制最大垃圾收集停頓時間,參數允許的值是一個大于0的毫秒數,收集器將盡可能地保證內存回收花費的時間不超過設定值。不過大家不要認為如果把這個參數的值設置得稍小一點就能使得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的。

GCTimeRatio:控制吞吐量的大小,參數的值應當是一個大于0且小于100的整數,也就是垃圾收集時間占總時間的比率,相當于是吞吐量的倒數。如果把此參數設置為N,那允許的最大GC時間就占總時間的(1/(1+N))%。比如把此參數設置為19,那允許的最大GC時間就占總時間的5%(即1/(1+19)).

UseAdaptiveSizePolicy:這是一個開關參數,當這個參數打開之后,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象大小(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC自適應的調節策略。自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別。

Serial Old收集器

Serial Old是Serial 收集器的老年代版本,它同樣是一個單線程收集器,使用"標記-整理"算法。這個收集器的主要意義也是在于給Client模式下的虛擬機使用。如果在Server模式下,那么它主要還有兩大用途:一種用途是在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作為CMS收集器的后備預案,在并發收集發生Concurrent Mode Failure時使用。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法。這個收集器是在JDK1.6中才開始提供的。在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在互聯網網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。從名字(包含"Mark Sweep")上就可以看出,CMS收集器是基于"標記-清除"算法實現的,它的運作過程相對于前面幾種收集器來說更復雜一些,整個過程分為4個步驟,包括:

初始標記(CMS initial mark)

并發標記(CMS concurrent mark)

重新標記(CMS remark)

并發清除(CMS concurrent sweep)

其中,初始標記、重新標記這兩個步驟仍然需要"Stop The World"。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,并發標記階段就是進行GC Roots Tracing 的過程,而重新標記階段則是為了修正并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發標記的時間短。由于整個過程中耗時最長的并發標記和并發清除過程收集器收集線程都可以與用戶線程一起工作,所以,從總體上來說,CMS收集器的內存回收過程是與用戶線程一起并發執行的。

CMS收集器有3個明顯的缺點:

1.CMS收集器對CPU資源非常敏感。其實,面向并發設計的程序都對CPU資源比較敏感。在并發階段,它雖然不會導致用戶線程停頓,但是會因為占用了一部分線程而導致應用程序變慢,總吞吐量會降低。

2.CMS收集器無法處理浮動垃圾,可能出現"Concurrent Mode Failure"失敗而導致另一次Full GC的產生。由于CMS并發清理階段用戶線程還在運行著,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為"浮動垃圾"。

3.空間碎片:CMS是一款基于標記-清除算法實現的收集器,所有會有空間碎片的現象,當空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很大空間剩余,但是

無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。

G1收集器

G1收集器是當今收集器技術發展的最前沿成果之一,G1是一款面向服務端應用的垃圾收集器,HotSpot開發團隊賦予它的使命是未來可以替換掉JDK1.5中發布的CMS收集器。在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存布局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。與其他GC收集器相比,G1具備如下特點:

(1)并行與并發:G1能充分利用多CPU,多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過并發的方式讓Java程序繼續執行。

(2)分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。

(3)空間整合:與CMS的“標記-清理”算法不同,G1從整體來看是基于“標記-整理”算法實現的收集器,從局部(兩個Region之間)上來看是基于“復制”算法實現的,但無論如何,這兩種算法都意味著G1運作期間不會產生內存空間碎片,收集后能提供規整的可用內存。這種特性有利于程序長時間運行,分配大對象時不會因為無法找到連續內存空間而提前觸發下一次GC。

(4)可預測的停頓:這是G1相對于CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java的垃圾收集器的特征了。

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

在G1收集器中,Region之間的對象引用以及其他收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處于不同的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),如果是,便通過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。當進行內存回收時,在GC根節點的枚舉范圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。

如果不計算維護Remembered Set的操作,G1收集器的運作大致可劃分為以下幾個步驟:

初始標記(Initial Marking)

并發標記(Concurrent Marking)

最終標記(Final Marking)

篩選回收(Live Data Counting and Evacuation)

對CMS收集器運作過程熟悉的讀者,一定已經發現G1的前幾個步驟的運作過程和CMS有很多相似之處。初始標記階段僅僅只是標記一下GC Roots能直接關聯到的對象,并且修改TAMS的值,讓下一階段用戶程序并發運行時,能在正確可用的Region中創建新對象,這階段需要停頓線程,但耗時很短。并發標記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序并發執行。而最終標記階段則是為了修正在并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs里面,最終標記階段需要把Remmbered Set Logs的數據合并到Remembered Set中,這階段需要停頓線程,但是可并行執行。最后篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃。

關于垃圾收集器暫時就了解這么多內容,關于可用的組合圖3-5一目了然。

那么如何閱讀GC日志呢?

想要閱讀GC日志,首先必須要開啟GC日志,開啟方式如下圖:

[java]view plaincopy

packagecom.general.garbage;

publicclassTestGCLog?{

privateObject[]?obj;

publicstaticvoidmain(String[]?args)?{

TestGCLog?gcLog=newTestGCLog();

gcLog.createObjects();

gcLog.clear();

System.gc();

}

publicvoidcreateObjects(){

obj=newObject[]{newbyte[1024*1024],newbyte[1024*1024]};

}

publicvoidclear(){

obj=null;

}

}

上面代碼運行之后,打印出的相關的GC信息如下:

[java]view plaincopy

0.104:?[GC?(System.gc())?[PSYoungGen:?3922K->616K(36352K)]?3922K->624K(119808K),0.0035976secs]?[Times:?user=0.00sys=0.00,?real=0.00secs]

0.108:?[Full?GC?(System.gc())?[PSYoungGen:?616K->0K(36352K)]?[ParOldGen:?8K->519K(83456K)]?624K->519K(119808K),?[Metaspace:?2574K->2574K(1056768K)],0.0061018secs]?[Times:?user=0.00sys=0.00,?real=0.01secs]

Heap

PSYoungGen??????total?36352K,?used?312K?[0x00000000d7980000,0x00000000da200000,0x0000000100000000)

eden?space?31232K,1%?used?[0x00000000d7980000,0x00000000d79ce2b8,0x00000000d9800000)

from?space?5120K,0%?used?[0x00000000d9800000,0x00000000d9800000,0x00000000d9d00000)

to???space?5120K,0%?used?[0x00000000d9d00000,0x00000000d9d00000,0x00000000da200000)

ParOldGen???????total?83456K,?used?519K?[0x0000000086c00000,0x000000008bd80000,0x00000000d7980000)

object?space?83456K,0%?used?[0x0000000086c00000,0x0000000086c81ef0,0x000000008bd80000)

Metaspace???????used?2581K,?capacity?4486K,?committed?4864K,?reserved?1056768K

classspace????used?285K,?capacity?386K,?committed?512K,?reserved?1048576K

下面我們看看如何理解這段GC日志,

[GC (System.gc()) [PSYoungGen: 3922K->616K(36352K)] 3922K->624K(119808K), 0.0035976 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

[Full GC (System.gc()) [PSYoungGen: 616K->0K(36352K)] [ParOldGen: 8K->519K(83456K)] 624K->519K(119808K), [Metaspace: 2574K->2574K(1056768K)], 0.0061018 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

GC和Full GC表示垃圾回收的停頓類型

PSYoungGen:表示新生代使用的是Parallel Scavenge垃圾收集器

3922K(gc前新生代所用內存)->616K(gc后新生代所用內存)(36352K(新生代總內存))

3922K(gc前Java堆已使用內存)->624K(gc后Java堆已使用內存)(119808K(Java堆總容量))

0.0035976 secs:表示GC所占用時間。

下面看下兩幅圖,就對GC日志一目了然了。

上面兩幅圖來源于:http://blog.csdn.net/wanglha/article/details/48713217

內存如何分配的?

對象的內存分配,往大方向講,就是在堆上分配(但也可能經過JIT編譯后被拆散為標量類型并間接地棧上分配,這點屬于JVM編譯優化的操作,這個會在后續的博客中說明),對象主要分配在新生代的Eden區上,如果啟動了本地線程分配緩沖,將按線程優先在TLAB上分配。少數情況下也可能會直接分配在老年代中,分配的規則并不是百分之百固定的,其細節取決于當前使用的是哪一種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。

對象優先在Eden分配:大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。

[java]view plaincopy

privatestaticfinalint_1MB=1024*1024;

/*-XX:+PrintGCTimeStamps?-XX:+PrintGCDetails???-Xms20M?-Xmx20M?-Xmn7M?-XX:SurvivorRatio=8*/

publicstaticvoidtestAllocation(){

byte[]?allocation1,allocation2,allocation3,allocation4;

allocation1=newbyte[2*_1MB];

allocation2=newbyte[2*_1MB];

allocation3=newbyte[2*_1MB];//發生一次Minor?GC

allocation4=newbyte[4*_1MB];//最后在allocation3在新生代里,其余4個對象在老年代里

}

0.111:?[GC?(Allocation?Failure)?[PSYoungGen:?4937K->504K(6656K)]?4937K->4672K(19968K),0.0041380secs]?[Times:?user=0.05sys=0.00,?real=0.00secs]

Heap

PSYoungGen??????total?6656K,?used?2613K?[0x00000000ff900000,0x0000000100000000,0x0000000100000000)

eden?space?6144K,34%?used?[0x00000000ff900000,0x00000000ffb0f748,0x00000000fff00000)

from?space?512K,98%?used?[0x00000000fff00000,0x00000000fff7e010,0x00000000fff80000)

to???space?512K,0%?used?[0x00000000fff80000,0x00000000fff80000,0x0000000100000000)

ParOldGen???????total?13312K,?used?8264K?[0x00000000fec00000,0x00000000ff900000,0x00000000ff900000)

object?space?13312K,62%?used?[0x00000000fec00000,0x00000000ff412030,0x00000000ff900000)

Metaspace???????used?2581K,?capacity?4486K,?committed?4864K,?reserved?1056768K

classspace????used?285K,?capacity?386K,?committed?512K,?reserved?1048576K

//當注釋allocation3和4的這兩句代碼時,GC的運行日志如下:所以,這樣就很好理解了上文中為何會在allocation3處發生了一次Minor?GC。

Heap

PSYoungGen??????total?6656K,?used?5061K?[0x00000000ff900000,0x0000000100000000,0x0000000100000000)

eden?space?6144K,82%?used?[0x00000000ff900000,0x00000000ffdf14d0,0x00000000fff00000)

from?space?512K,0%?used?[0x00000000fff80000,0x00000000fff80000,0x0000000100000000)

to???space?512K,0%?used?[0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)

ParOldGen???????total?13312K,?used?0K?[0x00000000fec00000,0x00000000ff900000,0x00000000ff900000)

object?space?13312K,0%?used?[0x00000000fec00000,0x00000000fec00000,0x00000000ff900000)

Metaspace???????used?2580K,?capacity?4486K,?committed?4864K,?reserved?1056768K

classspace????used?285K,?capacity?386K,?committed?512K,?reserved?1048576K

上述代碼運行在新生代總空間為7M的虛擬機里,當分配到第三個對象時就發生了一次GC,相信大家對比上面的兩個GC日志就明白了為什么在會allocation3=new byte[2*_1MB]處發生GC。

大對象直接進入老年代:虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大于這個設置值的對象直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的內存復制。

長期存活的對象將進入老年代:既然虛擬機采用了分代收集的思想來管理內存,那么內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代中。為了做到這點,虛擬機給每個對象定義了一個對象年齡計數器。如果對象在Eden出生并經過第一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動到Survivor空間中,并且對象年齡設為1。對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數-XX:MaxTenuringThreshold設置。

動態對象年齡判定:為了能更好地適應不同程序的內存狀況,虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

空間分配擔保:在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那么會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次Minor GC,盡管這次Minor GC是有風險的;如果小于,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC。

這里有必要說一下Minor GC和Full GC:

新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。

老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略里就有直接進行Major GC的策略選擇過程),Major GC的速度一般會比Minor GC 慢10倍以上。

其實到了這里,第三章的內容差不多已經總結完全了,后面會繼續往下總結,等把相關章節的內容整理完畢,再來對這個系列的文章進行review進而修改,如果覺得不錯,請頂一下哈。

轉載請注明出處:http://blog.csdn.net/android_jiangjun/article/details/78125281

作者:GeneralAndroid

鏈接:www.lxweimin.com/p/bee8e30c8aea?

來源:簡書

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容