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中主要包括如下:

年輕代垃圾收集器: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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章