天天看點

JVM的垃圾回收與記憶體配置設定

   Java是一種記憶體動态配置設定和垃圾回收技術的一種語言,不需要顯示的進行對象記憶體的配置設定,這一切操作都是由JVM來完成的,由于Java是“一切皆對象”的,是以對于記憶體配置設定的優化與速度非常的高效。在Java中一個對象在堆中的配置設定以及滅亡都是由JVM來完成的。JVM負責來垃圾回收與對象配置設定。

一 垃圾回收

   垃圾回收(Garbage Collection,GC),研究這個主要目的就是為了提升JVM的性能,在記憶體洩露中能及時查缺問題所在。對于垃圾回收,GC必須要解決的問題包括三個:

  1)哪些記憶體可以回收?哪些對象可以回收? 這裡主要就是判斷哪些對象的死活

  2)什麼時候回收? 一般就是當記憶體不夠或者設定垃圾收集器的時間間隔

  3)如何回收? 對于已經判斷為死的對象的回收算法以及實作這些算法的垃圾收集器

哪些記憶體和對象可以回收?

   JVM中的五大記憶體區域中,程式計數器、虛拟機棧和本地方法棧都是随着線程而生,随着線程而滅亡。棧中的大小基本上在類結構确定下來的時候就已知了,這三個區域記憶體配置設定與回收都具備确定性,是以當方法結束或者線程結束時候,這三塊的記憶體就随着回收了。

   而Java堆和方法區中,存放了與類有關的資訊以及類的執行個體對象,這些對象隻會在具體運作期間才能建立,而這些對象建立與回收都是動态的,故垃圾回收需要考慮堆和方法區中的回收。

   明确了需要回收哪一塊的記憶體後,就需要再次解決哪些對象可以回收?

   垃圾回收的對象,這裡主要回收的就是那些已經死去(即不再被任何途徑使用的對象)。既然知道要回收這些死的對象,那麼接下來就要确定怎麼來判斷一個對象的死活。

   在Java中使用的就是根搜尋算法(GC Roots Tracing)來判斷對象是否存活,而不是利用引用計數。

   根搜尋算法的基本思想:通過一系列名為“GC Roots”的對象作為起始點,從這些節點向下搜尋,搜尋所經過的路徑稱為引用鍊,當一個對象到GC Roots沒有引用鍊的時候,則可初次判定該對象不可用。圖論中表示就是從GC Roots到這個對象路徑不可達。

   Java中可以作為GC Roots的對象有虛拟機棧中的引用對象,方法區中的類靜态屬性引用的對象,方法區中的常量引用的對象,本地方法區中Native的引用的對象。

垃圾回收的起點?(基本思想的詳解)

   棧是真正進行程式執行地方,是以要擷取哪些對象正在被使用,則需要從Java棧開始。同時,一個棧是與一個線程對應的,是以,如果有多個線程的話,則必須對這些線程對應的所有的棧進行檢查。同時,除了棧外,還有系統運作時的寄存器等,也是存儲程式運作資料的。這樣,以棧或寄存器中的引用為起點,我們可以找到堆中的對象,又從這些對象找到對堆中其他對象的引用,這種引用逐漸擴充,最終以null引用或者基本類型結束,這樣就形成了一顆以Java棧中引用所對應的對象為根節點的一顆對象樹,如果棧中有多個引用,則最終會形成多顆對象樹。在這些對象樹上的對象,都是目前系統運作所需要的對象,不能被垃圾回收。而其他剩餘對象,則可以視為無法被引用到的對象,可以被當做垃圾進行回收。

是以,垃圾回收的起點是一些根對象(java棧, 靜态變量, 寄存器...)

   Java利用根搜尋算法要經曆兩次标記過程來宣告一個對象死活:

   第一次标記并篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆寫finalize()方法,或者finalize()方法已經被虛拟機調用過一次(因該方法隻會被調用一次),虛拟機則判定對象沒有必要執行finalize()。如果有必要執行,将該對象放入F-Queue隊列,稍後由虛拟機自動建立優先級低的Finalizer線程。第二次标記就發生在F-Queue中,如果在Finalizer線程執行時F-Queue中的對象與引用鍊上的對象建立了關聯,第二次标記時會将該對象移出F-Queue,隊列中剩下的經過兩次标記的對象就是可以回收的。

什麼時候回收? 

    在Java中垃圾回收器啟動的時間是不固定的,它根據記憶體的使用量而進行動态的自适應調整,來運作GC,在為記憶體配置設定的過程中就會有GC的産生過程。

如何回收?

确定了哪些對象以及記憶體需要回收後,此時就需要考慮采用什麼樣的政策以及用什麼來具體實作這些政策。

回收的算法:

   垃圾收集算法都是先用“根搜尋算法”來判斷哪些需要回收的,然後進行垃圾的回收處理,常用的垃圾收集算法的基本概況:

标記-清除算法(Mark-Sweep):這種算法主要分為兩個階段,“标記”和“清除”,标記的過程就是采用的“根搜尋算法”,首先标記處所有被引用的對象,在标記階段完成後,周遊整個堆,對于未被被标記的可回收的對象進行統一的回收掉。優點:MS收集器可以在存儲耗盡的時候啟動,實作起來簡單容易。缺點:工作的時候需要使得工作例程挂起等待很長時間,效率不高,會産生大量的不連續的記憶體碎片,就會導緻一些比較大的對象無法找到足夠連續記憶體進而提前出發另一次垃圾收集動作。

複制算法(Copying):這是為了解決效率問題而出現的,主要用于堆中新生代的回收。它将可用記憶體按容量大小劃分為大小相等的兩塊,每次隻使用其中一塊,當這一塊完了後,就将還存活的對象複制到另一塊上面,然後把剛已使用的記憶體空間一次清除掉。優點:提高了回收效率,回收後不會産生不連續的空間,工作開銷正比于存活的對象。缺點:将可用記憶體縮小為原來的一般,當對象存活率較高時候,就要執行較多的複制操作,效率就會降低。

在新生代采用該算法的思路:将該算法用于新生代中,在現在一般并不是将記憶體劃分為等大的兩塊,由于新生代的對象大多是朝生夕死的,在新生代中将記憶體劃分為一個較大Eden空間和兩塊較小的Survivor,每次僅僅使用Eden和其中一個Survivor,當回收時,将Eden和Survivor還存活的對象一次性拷貝到另一塊的Survivor中,最後清理掉Eden和剛才用過的Survivor空間。當要拷貝至Survivor空間不夠容納還存活的對象時候,此時就需要用老年代來進行配置設定擔保,即将這些存活的對象拷貝至老年代中。

标記-整理算法(Mark-Compact):主要用于堆中的老年代回收。它依舊采用“标記-清理”中的“标記”方法,當“标記”階段完成中,它是将對于存活的對象即被引用的對象進行标記,在“整理”階段中,将标記完成的對象進行移動,使之與相鄰的活動對象連續配置設定,進而将所有存活的對象都移向了堆的一端,然後直接清理掉堆端邊界以外的所有記憶體。“标記-整理算法”克服在對象存活率較高中出現頻繁的複制操作,并且解決回收後的記憶體出現不連續的空間,它是“标記清理”和“複制”的有機結合。

分代收集算法(Generational Collecting):目前垃圾收集都是采用這種算法。主要将Java堆分為年輕代和老年代,根據不同的年代采用不同的算法。在新生代中,由于隻有少量的存活對象,此時就使用“複制”算法;在老年代中,由于存活對象比較長沒有額外空間進行配置設定擔保,就使用“标記-整理”或“标記-清理”算法。之是以要進行分代的原因是由于不同的對象的生命周期是不一樣的。是以,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。

  JVM中分代的模型如下:

回收的具體實作:

  垃圾收集器就是具體實作這些垃圾收集算法的。在HotSpot中主要包括如下:

<a target="_blank" href="http://blog.51cto.com/attachment/201306/091129755.gif"></a>

年輕代垃圾收集器:Young generation,在垃圾收集的過程中都會使得使用者線程等待。

    Serial收集器:一種單線程的收集器,采用“複制”收集算法,收集時候會暫停所有的工作線程,直到收集結束,一般虛拟機在Client模式下的預設新生代收集器就是采用這個收集器。優點:與單線程收集器比較簡單高效。對于單個CPU下,由于沒有多個線程的互動開銷,在堆比較小的時候,一般停頓比較短,可以采用。

    ParNew收集器:是Serial的一種多線程收集器,采用“複制”收集算法,它和Serial收集器除了多線程外其餘行為都相同,即在收集的過程中會暫停所有的線程。它是運作在Server模式下的新生代收集器的首選。可以使用-XX:+UseConcMarkSweepGC選項後的預設新生代收集器,或者使用-XX:+UseParNewGC選項來強制使用它。隻能與CMS收集器配合使用。

    Parallel Scavenge收集器:是一種并行(多條垃圾收集線程并行工作,使用者線程依舊等待)的多線程收集器,采用“複制”算法來收集新生代。它所關注的是達到一個控制的吞吐量,就是CPU運作使用者代碼時間與CPU總耗時間的比值,即吞吐量=運作使用者代碼時間/(運作使用者代碼時間+垃圾收集時間)。利用-XX:MaxGCPauseMillis可設定垃圾收集停頓時間,-XX:GCTimeRatio可設定垃圾收集占的總時間,是吞吐量的倒數。如果為19,則允許GC時間為5%(1/(1+19));這種收集器也有自适應調節政策。

老年代垃圾收集器:Tenured generation

Serial Old收集器:是一種單線程收集器,是Serial的老年代版本,使用的是“标記-整理”算法

主要是虛拟機在Client模式下的使用的收集器。它在工作中依舊需要暫停所有的使用者線程。主要用在作為CMS收集器的後備預案使用。

   Parallel Old收集器:是一種多線程收集器,是Parallel Scavenge的老年代版本,使用的是“标記-整理”算法。它在工作中依舊需要暫停所有的使用者線程。一般是結合Parallel Scavenge來一起使用,用于在注重吞吐量和CPU資源敏感的場合。

   CMS收集器(Concurrent Mark Sweep):采用“标記-清除”的算法,目标是擷取最短回收停頓時間的。整個過程分為4個步驟:

   1 初始标記(Stop the world)  2 并發标記

   3 重新标記(Stop the world)  4 并發清除

  初始标記,僅僅标記一下GC Roots能關聯到的對象,速度非常快,需要停止使用者線程。

  并發标記,進行GC Roots Tracing過程,可以與使用者線程一起工作,不需要停止使用者線程。

  重新标記,為了修正并發标記期間,因使用者程式繼續運作而導緻标記産生變動的那一部分對象,這個時間也是很短的,需要停止使用者線程。

  并發清除,可以與使用者線程一起工作,不需要停止使用者線程。

   在CMS中,耗時最長的是并發标記和并發清除,在這兩個過程中收集器線程都可以與使用者一起工作,是以CMS收集器的記憶體回收過程是與使用者線程一起并發地執行。

   缺點:CMS對CPU資源非常敏感,無法處理浮動垃圾,會産生碎片。

G1收集器:采用的是“标記-整理”算法,可以非常精确的控制停頓,可以實作在基本上不犧牲吞吐量的前提下完成低停頓的記憶體回收。

垃圾收集器中的并發與并行:

并行(Parallel):多條垃圾收集線程并行工作,此時使用者線程處于等待停止狀态。

并發(Concurrent):使用者線程與垃圾收集線程同時執行,即使用者程式繼續運作,而垃圾收集程式運作在另一個CPU中。

二 記憶體配置設定

  對象的記憶體配置設定,就是在Java堆上配置設定的,對象主要配置設定在堆中的新生代的Eden區。

  記憶體配置設定的幾個原則:

  對象優先在Eden配置設定:大多數情況下,對于一個新的對象将會首先配置設定在新生代的Eden區,隻有Eden區沒有足夠的空間進行配置設定的時候,虛拟機發起一次Minor GC,可以使用-verbose:gc -XX:+PrintGCDetails來列印記憶體配置設定的狀态。當配置設定的對象無法容納在Eden區的時候,首先會将Eden中存活的對象複制到另一個Survivor中,如果Survivor無法容納這些存活的對象,則隻有通過配置設定擔保機制将這些存活對象提前移動到老年代中,然後将要配置設定的對象配置設定到Eden區中。

  大對象直接進入老年代:大對象就是需要連續的大量記憶體空間,最典型的就是字元串或者數組。可以設定-XX:PretenureSizeThreshold參數,當大于這個值的對象直接會在老年代中配置設定,避免了在Eden區和兩個Survivor之間大量的拷貝。

  長期存活的對象将進入老年代:虛拟機為每個對象定義了對象年齡,當對象在Eden出生經過第一個Minor GC還存活着,并且能被Survivor容納,則移動到Survivor,年齡加1.每次熬過一次Minor GC,對象年齡就會加1.對象晉升到老年代的年齡閥值,可以通過設定

-XX:MaxTenuringThreshold。

  動态對象年齡判定:不一定非得達到年齡閥值才會進入老年代,如果在Survivor空間中相同年齡的所有對象大小的總和大于Survivor空間的一半,則年齡大于或等于該年齡的對象就會直接進入老年代,無需等待MaxTenuringThreshold的閥值年齡。

  空間配置設定擔保:在發生Minor GC時候,虛拟機會檢測之前每次晉升到老年代的平均大小是否大于老年代的剩餘空間大小,如果大于,則直接對于老年代進行一個Full GC。如果小于,則檢視HandlePermotionFailure設定是否允許進行擔保失敗,如果允許,則在新生代進行Minor GC;如果不允許,則在老年代進行Full GC。

注意:Minor GC和Full GC的差別

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

  老年代 GC(Major GC  / Full GC):指發生在老年代的 GC,出現了Major GC,經常會伴随至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集政策裡就有直接進行 Major GC 的政策選擇過程)。MajorGC的速度一般會比Minor GC慢10倍以上。Major GC會觸發整個heap的回收,包括回收young generation。

本文轉自 zhao_xiao_long 51CTO部落格,原文連結:http://blog.51cto.com/computerdragon/1220373