JVM垃圾回收機制

Java開發有個很基礎的問題,雖然我們平時接觸的不多,但是了解它卻成為Java開發的必備基礎——這就是JVM。
在Java中JVM內置了垃圾回收的機制,以守護進程的形式在后臺自動回收垃圾,它讓開發者無需關注空間的創建和釋放,幫助開發者承擔對象的創建和釋放的工作,極大的減輕了開發的負擔。那是不是我們就不需要了解JVM了,顯然在做一些優化或者深入研究應用性能的時候,JVM還是起了很關鍵的作用的。因此本篇就總結性的描述下垃圾回收相關的知識。

哪些內存需要回收

回收區域主要集中在java堆和方法區。
程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的,因此這幾個區域的內存分配和回收都具備確定性,所以不需要考慮回收,而Java堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處于運行期間時才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關注的是這部分內存。

什么時候回收

  • 對象沒有引用
  • 作用域發生未捕獲異常
  • 程序在作用域正常執行完畢
  • 程序執行了System.exit()
  • 程序發生意外終止(被殺進程等)

如何回收

所謂“垃圾”,就是指所有不再存活的對象。常見的判斷是否存活有兩種方法:引用計數法和可達性分析。

  • 引用計數法
    為每一個創建的對象分配一個引用計數器,用來存儲該對象被引用的個數。當該個數為零,意味著沒有人再使用這個對象,可以認為“對象死亡”。但是,這種方案存在嚴重的問題,就是無法檢測“循環引用”:當兩個對象互相引用,即時它倆都不被外界任何東西引用,它倆的計數都不為零,因此永遠不會被回收。而實際上對于開發者而言,這兩個對象已經完全沒有用處了。
    因此,Java 里沒有采用這樣的方案來判定對象的“存活性”。

  • 可達性分析
    這種方案是目前主流語言里采用的對象存活性判斷方案。基本思路是把所有引用的對象想象成一棵樹,從樹的根結點 GC Roots 出發,持續遍歷找出所有連接的樹枝對象,這些對象則被稱為“可達”對象,或稱“存活”對象。其余的對象則被視為“死亡”的“不可達”對象,或稱“垃圾”。
    參考下圖,object5,object6 和 object7 便是不可達對象,視為“死亡狀態”,應該被垃圾回收器回收。

    可達性分析

    可作為GC root的對象
    我們可以猜測,GC Roots 本身一定是可達的,這樣從它們出發遍歷到的對象才能保證一定可達。那么,Java 里有哪些對象是一定可達呢?主要有以下四種:

    • 虛擬機棧(幀棧中的本地變量表)中引用的對象。
    • 方法區中靜態屬性引用的對象。
    • 方法區中常量引用的對象。
    • 本地方法棧中 JNI 引用的對象。
      這里只要知道有這么幾種類型的 GC Roots,每次垃圾回收器會從這些根結點開始遍歷尋找所有可達節點。

垃圾回收算法

上面已經知道,所有 GC Roots不可達的對象都稱為垃圾,參考下圖,黑色的表示垃圾,灰色表示存活對象,綠色表示空白空間。


image

那么,我們如何來回收這些垃圾呢?

  • Mark-Sweep標記-清除算法
    第一步,所謂“標記”就是利用可達性遍歷堆內存,把“存活”對象和“垃圾”對象進行標記,得到的結果如上圖;
    第二步,既然“垃圾”已經標記好了,那我們再遍歷一遍,把所有“垃圾”對象所占的空間直接清空即可。結果如下:

    Mark-Sweep標記-清除算法

    這便是“標記-清理”方案,簡單方便 ,但是容易產生內存碎片。

  • Mark-Compact標記-整理算法
    既然上面的方法會產生內存碎片,那好,我在清理的時候,把所有 存活 對象扎堆到同一個地方,讓它們待在一起,這樣就沒有內存碎片了。
    結果如下:

    Mark-Compact標記-整理算法

    這兩種方案適合存活對象多,垃圾少的情況,它只需要清理掉少量的垃圾,然后挪動下存活對象就可以了。

  • Copying復制算法
    這種方法比較粗暴,直接把堆內存分成兩部分,一段時間內只允許在其中一塊內存上進行分配,當這塊內存被分配完后,則執行垃圾回收,把所有存活對象全部復制到另一塊內存上,當前內存則直接全部清空。
    參考下圖:

    Copying復制算法

    起初時只使用上面部分的內存,直到內存使用完畢,才進行垃圾回收,把所有存活對象搬到下半部分,并把上半部分進行清空。
    這種做法不容易產生碎片,也簡單粗暴;但是,它意味著你在一段時間內只能使用一部分的內存,超過這部分內存的話就意味著堆內存里頻繁的 復制清空。
    這種方案適合 存活對象少,垃圾多 的情況,這樣在復制時就不需要復制多少對象過去,多數垃圾直接被清空處理。

  • Generational Collection 分代收集
    最后的這種方法是前面幾種的合體,即目前JVM主要采取的一種方法,思想就是把JVM分成不同的區域。每種區域使用不同的垃圾回收方法。

    分代收集

    上面可以看到堆分成三個區域:
    新生代(Young Generation):用于存放新創建的對象,采用復制回收方法,如果在s0和s1之間復制一定次數后,轉移到年老代中。這里的垃圾回收叫做minor GC;
    年老代(Old Generation):這些對象垃圾回收的頻率較低,采用的標記整理方法,這里的垃圾回收叫做major GC。
    永久代(Permanent Generation):存放Java本身的一些數據,當類不再使用時,也會被回收。

    這里可以詳細的說一下新生代復制回收的算法流程:
    在新生代中,分為三個區:Eden, from survivor, to survior。

    • 當觸發minor GC時,會先把Eden中存活的對象復制到to Survivor中;
    • 然后再看from survivor,如果次數達到年老代的標準,就復制到年老代中;如果沒有達到則復制到to - survivor中,如果to survivor滿了,則復制到年老代中。
    • 然后調換from survivor 和 to survivor的名字,保證每次to survivor都是空的等待對象復制到那里的。

垃圾回收器

HotSpot 虛擬機的垃圾收集器
  • 串行收集器 Serial
    這種收集器就是以單線程的方式收集,垃圾回收的時候其他線程也不能工作。

    串行收集器 Serial

  • 并行收集器 Parallel
    以多線程的方式進行收集

    并行收集器 Parallel

  • 并發標記清除收集器 Concurrent Mark Sweep Collector, CMS
    大致的流程為:初始標記--并發標記--重新標記--并發清除

    并發標記清除收集器 Concurrent Mark Sweep Collector, CMS

  • G1收集器 Garbage First Collector
    大致的流程為:初始標記--并發標記--最終標記--篩選回收

    G1收集器 Garbage First Collector

GC什么時候觸發

由于對象進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有兩種類型:Scavenge GC和Full GC。

  • Scavenge GC
    一般情況下,當新對象生成,并且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,并且把尚且存活的對象移動到Survivor區。然后整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這里需要使用速度快、效率高的算法,使Eden去能盡快空閑出來。

  • Full GC
    對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個堆進行回收,所以比Scavenge GC要慢,因此應該盡可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對于Full GC的調節。有如下原因可能導致Full GC:

    • 年老代(Tenured)被寫滿
    • 持久代(Perm)被寫滿
    • System.gc()被顯示調用
    • 上一次GC之后Heap的各域分配策略動態變化

參考鏈接:
http://www.importnew.com/26821.html
https://www.cnblogs.com/xing901022/p/7725961.html
https://www.cnblogs.com/1024Community/p/honery.html
https://blog.csdn.net/sinat_33087001/article/details/77030118

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 哪些內存需要回收 由于程序計數器、虛擬機棧、本地方法棧的生命周期都跟隨線程的生命周期,當線程銷毀了,內存也就回收了...
    Samuel_Tom閱讀 7,478評論 1 9
  • 一、概述: 本文作為大數據基礎的Java部分,上一篇說明了Linux的常用命令,今天學習了Java的高級特性,我們...
    慕久久閱讀 761評論 1 5
  • 1. 概述 在Java內存區域里講了Java的內存運行時數據區域分為如下5個部分 程序計數器(Program Co...
    謝樸歡閱讀 289評論 0 0
  • 1. 前言 網上關于jvm gc的文章有很多,寫這篇文章不是有什么新東西要講,主要原因是工作時也偶爾碰到比如ful...
    aaron1993閱讀 1,262評論 0 0
  • 本文主要淺談JAVA回收機制,讓初學者對這一塊大概有個簡單的認識,同時也記錄下自己學習的成果,溫故而知新。 疑問 ...
    南山羊閱讀 693評論 0 1