1. 程式人生 > >深入解析Java垃圾回收機制

深入解析Java垃圾回收機制

normal tor 技術分享 統計分析 time method 堆內存 出棧 類結構

  • 引入垃圾回收
  • 哪些內存需要回收?
  • 引用計數法
  • 可達性分析
  • 如何回收
  • Marking 標記
  • Normal Deletion 清除
  • Deletion with Compacting 壓縮
  • 為什麽需要分代收集?
  • JVM的分代
  • 新生代
  • 老年代
  • 永久代
  • 分代垃圾收集過程詳述

引入垃圾回收

程序計數器、 虛擬機棧、 本地方法棧3個區域隨線程而生,隨線程而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。 每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的(盡管在運行期會由JIT編譯器
進行一些優化,但在本章基於概念模型的討論中,大體上可以認為是編譯期可知的),因此這幾個區域的內存分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因為方法結束或者線程結束時,內存自然就跟隨著回收了。 而Java堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處於運行期間時才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關註的是這部分內存-----《深入理解Java虛擬機》

自動垃圾回收機制就是尋找Java堆中的對象,並對對象進行分類判別,尋找出正在使用的對象和已經不會使用的對象,然後把那些不會使用的對象從堆上清除。
自動垃圾回收機制就是要解決三個問題:

  • 哪些內存需要回收?
  • 什麽時候回收?
  • 如何回收?

哪些內存需要回收?

引用計數法

對於第一個問題,也就是判斷是否還需要使用,最簡單的方法就是通過目前是否有引用指向這個對象,如果沒有就說明這個對象不會再被使用了,如果有就說明這個對象可能還會繼續被使用,這種通過引用是否存在的方法就叫做引用計數法,但這個方法存在一個問題就是無法解決對象循環引用的問題,因此又出現了可達性分析的方法來判斷對象是否可以被會回收。

可達性分析

這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
在Java語言中,可作為GC Roots的對象包括下面幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中類靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(即一般說的Native方法)引用的對象。

如何回收

垃圾收集器通常會幫我們在後臺自動進行垃圾回收。關於具體的回收過程只要有以下這些步驟

  • Step 1: Marking 標記

第一步就是標記,也就是垃圾收集器會找出那些需要回收的對象所在的內存和不需要回收的對象所在的內存,並把它們標記出來,簡單的說,也就是先找出垃圾在哪

技術分享圖片

所有堆中的對象都會被掃描一遍,以此來確定回收的對象,所以這通常會是一個相對比較耗時的過程

  • Step 2: Normal Deletion
    垃圾收集器會清除掉上一步標記出來的那些需要回收的對象區域

技術分享圖片

存在的問題就是碎片問題:
標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程
序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

  • Step 2a: Deletion with Compacting 壓縮
    由於簡單的清除可能會存在碎片的問題,所以又出現了壓縮清除的方法,也就是先清除需要回收的對象,然後再對內存進行壓縮操作,將內存分成可用和不可用兩大部分

技術分享圖片

為什麽需要分代收集?

就像前文所述,標記對象和壓縮內存的過程在JVM中是不高效的,分配的對象越多,垃圾收集的時間就越長。但是,經過一些經驗型性的統計分析表明,一個程序中大部分對象都是短命的!

下圖就是一個類似的統計數據,縱坐標表示分配對象所占用的內存大小,橫坐標表示自分配對象過去的時間

技術分享圖片

從圖中我們看到,大部分對象沒活多久就死了,存活較久的只是少類對象

JVM的分代

為了增大垃圾收集的效率,所以JVM將堆進行分代,分為不同的部分,一般有三部分,新生代,老年代和永久代

技術分享圖片

新生代

所有新new出來的對象都會最先出現在新生代中,當新生代這部分內存滿了之後,就會發起一次垃圾收集事件,這種發生在新生代的垃圾收集稱為Minor collections。這種收集通常比較快,因為新生代的大部分對象都是需要回收的,那些暫時無法回收的就會被移動到老年代。

Stop the World事件-所有minor garbage collections都是Stop the World事件,也就是意味著所有的應用線程都需要停止,直到垃圾回收的操作全部完成。類似於
“你媽媽在給你打掃房間的時候,肯定也會讓你老老實實地在椅子上或者房間外待著,如果她一邊打掃,你一邊亂扔紙屑,這房間還能打掃完?”

老年代

老年代用來存儲那些存活時間較長的對象。一般來說,我們會給新生代的對象限定一個存活的時間,當達到這個時間還沒有被收集的時候就會被移動到老年代中。老年代區域的垃圾收集叫做major garbage collection

技術分享圖片

Major garbage collection也是一個Stop the World事件。通常Major garbage collection都相對比較慢,因為老年代的收集包括了對所有對象的收集,也就是同時需要收集新生代和老年代的對象。

永久代

The Permanent generation contains metadata required by the JVM to describe the classes and methods used in the application. The permanent generation is populated by the JVM at runtime based on classes in use by the application. In addition, Java SE library classes and methods may be stored here.

Classes may get collected (unloaded) if the JVM finds they are no longer needed and space may be needed for other classes. The permanent generation is included in a full garbage collection.

技術分享圖片

分代垃圾收集過程詳述

我們已經知道垃圾回收所需要的方法和堆內存的分代,那麽接下來我們就來具體看一下垃圾回收的具體過程

  • 第一步 所有new出來的對象都會最先分配到新生代區域中,兩個survivor區域初始化是為空的

技術分享圖片

  • 第二步,當eden區域滿了之後,就引發一次 minor garbage collection

技術分享圖片

  • 第三步,當在minor garbage collection,存活下來的對象就會被移動到S0survivor區域

技術分享圖片

  • 第四步,然後當eden區域又填滿的時候,又會發生下一次的垃圾回收,存活的對象會被移動到survivor區域而未存活對象會被直接刪除。但是,不同的是,在這次的垃圾回收中,存活對象和之前的survivor中的對象都會被移動到s1中。一旦所有對象都被移動到s1中,那麽s2中的對象就會被清除,仔細觀察圖中的對象,數字表示經歷的垃圾收集的次數。目前我們已經有不同的年齡對象了。

技術分享圖片

  • 第五步,下一次垃圾回收的時候,又會重復上次的步驟,清除需要回收的對象,並且又切換一次survivor區域,所有存活的對象都被移動至s0。eden和s1區域被清除。

技術分享圖片

  • 第六步,重復以上步驟,並記錄對象的年齡,當有對象的年齡到達一定的閾值的時候,就將新生代中的對象移動到老年代中。在本例中,這個閾值為8.

技術分享圖片

  • 第七步,接下來垃圾收集器就會重復以上步驟,不斷的進行對象的清除和年代的移動

技術分享圖片

  • 最後,我們觀察上述過程可以發現,大部分的垃圾收集過程都是在新生代進行的,直到老年代中的內存不夠用了才會發起一次 major GC,會進行標記和整理壓縮。

技術分享圖片

深入解析Java垃圾回收機制