Java的GC垃圾回收機制

什麼是GC垃圾回收

垃圾回收(Garbage Collection)是Java虛擬機(JVM)垃圾回收器提供的一種用於在空閒時間不定時回收無任何對象引用的對象佔據的內存空間的一種機制。

注意:垃圾回收回收的是無任何引用的對象佔據的內存空間而不是對象本身。換言之,垃圾回收只會負責釋放那些對象佔有的內存。

分析

引用:如果Reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表着一個引用。(引用都有哪些?對垃圾回收又有什麼影響呢?
垃圾:無任何對象引用的對象。(怎麼通過算法找到這些垃圾呢?
回收:清理垃圾”垃圾“佔用的內存空間而非對象本身(通過怎樣的算法實現垃圾回收呢?
發生地點:一般發生在堆內存空間中,因爲大部分對象都存儲在堆內存空間中(堆內存爲了配合垃圾回收進行了不同區域的劃分,各個區域有什麼不同呢?
發生時間:程序空間不定時回收(回收的執行機制是什麼?是否可以通過顯式調用函數方式來確定回收過程

這些都是我們需要解決的問題。

Java中對象引用

  1. 強引用(Strong Reference):如”Object obj = new Object()“,這類引用是Java程序中最普遍的。只要強引用還存在,垃圾回收機制就永遠不會回收掉被引用的對象。
  2. 軟引用(Soft Reference):它用來描述一些可能還有用,但並非必須的對象。在系統內存不夠用的時候,這類引用關聯的對象將會被垃圾收集器回收。JDK1.2之後提供了SoftReference類來實現軟引用。
  3. 弱引用(Weak Reference):它也是用來描述非必須的對象的,但它的引用強度比軟引用更加弱一些,被弱引用關聯的對象只能生存到下一次垃圾回收發生之前。在垃圾回收開始時,無論內存空間是否足夠,都會回收只被弱引用關聯的對象。在JDK1.2之後,提供了WeakReference來實現弱引用。
  4. 虛引用(Phantom Reference):最弱的一種引用關係,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的是希望能在這個對象被收集器回收時收到一個系統通知。JDK1.2之後提供了PhantomReference類來實現虛引用。

判斷對象是否是需要回收的垃圾算法

引用計數算法(Reference Counting Collector)

堆中每個對象(不是引用)都有一個計數器。當一個對象被創建並初始化賦值後,該變量計數設置爲1。每當每一個地方引用,計數器值就會加1(a=b, b b被引用,則b的引用計數加一)。當引用失效時(一個對象的某個引用超過生命週期(出作用域範圍)或者被設置爲一個新值時),計數器值就減一,任何引用計數爲0的對象可以被當作垃圾收集。

優點:引用計數收集器執行簡單,判定效率高,交織在程序中運行。對程序不被長時間打斷的環境比較有利(OC的內存管理使用該算法)

缺點:難以檢測出對象之間的循環引用。同時,引用計數器增加了程序執行的開銷,所以Java語言並沒有選擇這種算法進行垃圾回收。

根搜索算法(Tracing Collector)

首先了解一個概念:根集(Root Set)
所謂根集(Root Set)就是正在執行的Java程序可以訪問的引用變量(注意:不是對象)的集合(包括局部變量,參數,類變量),程序可以使用引用變量訪問對象屬性和調用對象的方法。

這種算法的基本思路:

  1. 通過一系列名爲”GC Roots“的對象作爲起點,尋找對應的引用節點。
  2. 找到這些引用節點後,從這些節點開始向下繼續尋找它們的引用節點
  3. 重複第二的步驟
  4. 搜索所走過的路徑成爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,證明此對象不可用。

Java和C#都是採用根搜索算法來判定對象是否存活的。
在這裏插入圖片描述

垃圾回收器將某些特殊的對象定義爲GC Roots根對象。

  1. 虛擬機棧中引用對象(棧幀中本地變量表)
  2. 方法區中常量引用對象
  3. 方法區中類靜態屬性引用的對象
  4. 本地方法棧中JNI(Native方法)引用對象
  5. 活躍線程

接下來,垃圾回收器會對內存中的整個對象圖進行遍歷,它先從GC根對象開始,然後是根對象引用其他對象,比如實例變量。回收器將訪問到所有的對象都標記爲存活。

存活對象在上圖中都標記爲藍色。當標記階段完成了之後,所有存活的對象都已經被標記完了,其他的那些(上圖中灰色的那些)也就是GC根對象不可達的對象,也就是說你的應用不會再用到它了,這些就是垃圾對象,回收器將會再接下來的階段清除他們。

關於標記階段有幾個關鍵點是值得注意的

  1. 開始進行標記前,需要先暫停應用線程,否則如果對象圖一直在變化的話是無法真正去遍歷它的。暫停應用線程以便JVM可以盡情地收拾家務地情況稱之爲安全點(Safe Point),這裏會觸發一次Stop The World(STW)暫停。觸發安全點地原因有許多,但最常見地就是垃圾回收了
  2. 暫停時間地長短並不取決於堆內對象地多少也不是堆地大小,而是存活對象地多少。因此,調高堆地大小並不會影響到標記時間地長短。
  3. 在根蒐集算法中,要真正宣告一個對象死亡,至少需要兩個過程:

如果對象在進行根搜索後發現沒有於GC Roots相連接地引用鏈,那它會被第一次標記並且進行一次篩選。篩選條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或finalize()方法已經被虛擬機調用過,虛擬機將這種情況都視爲沒有必要執行。

如果該對象被判定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲F-Queue隊列中,並在稍後由一條由虛擬機自動建立地,低優先級地finalize線程去執行finalize()方法。finalize()方法是對象逃脫死亡命運地最後一次機會(因爲一個對象地finalize()方法最多隻會被系統調用一次),稍後GC將對F-Queue中地對象進行小規模地標記,如果要在finalize()方法上拯救自己,只要在finalize()方法中讓該對象重新引用一個對象即可。而如果這時還沒有關聯到任何鏈上引用,那麼它就會被回收掉。

  1. 實際上GC判斷對象是否可達看到是強引用。
  2. 當標記階段完成後,GC開始進入下一個階段,刪除不可達對象。
    JVM體系結構圖

垃圾回收發生地點和垃圾回收的算法

垃圾回收發生地點

在JVM中存在垃圾回收主要是堆空間中,因爲大部分對象都是存儲在堆空間當中。
Java內存空間除了對空間還有其他部分:

  1. 棧:每個線程執行每個方法的時候都會在棧中申請一個棧幀,每個棧幀包括局部變量和操作數棧,用於存放此方法調用過程中的臨時變量、參數和中間結果。
  2. 本地方法棧:用於支持native方法的執行,存儲了每個native方法調用的狀態
  3. 方法區:存放了要加載的類信息、靜態變量,final類型的常量、屬性和方法信息,JVM用持久代(PermanetGeneration)來存放方法區,可通過設置-XX:PermSize-XX:PermSize來指定最小值和最大值
    在堆空間中主要分爲下面幾個部分(jdk1.8之後永久代被稱爲元空間)
    堆空間分佈圖

年輕代(Young Generation,採用複製算法進行GC)

幾乎所有的新生成的對象都放在了年輕代。新生代內存按照8:1:1的比例分爲了Eden區和兩個Survivor(Survivor0,Survivor1)區。大部分對象在Eden區生成。當新的對象生成,Eden空間申請失敗(因爲空間不足等),則會發起一次GC(Scavenge GC),回收後將Eden存活的對象移動到Survivor0區,然後清空Eden區,持續這個步驟,當EdenSurvivor0區都滿了進行GC將這兩個區域任然存活的對象採用複製算法移動到另外一個Survivor1區域中,清空EdenSurvivor0。此時Survivor0是空的,我們的jvm會將Survivor0Survivor1區域互換,始終保持Survivor1是一個空閒的空間,如此往復。
Survivor1不足以存放EdenSurvivor0存活下來的對象,就會直接將其放入老年代。
當對象在Survivor區躲過了一次GC的話,其年齡便會加1,默認情況下,如果對象年齡到達15歲就會移動到老年代。
如果老年代也滿了,那麼會觸發Full GC,也就是新生代,老年代都進行回收。
新生代的大小也可以由-Xmn來控制,也可以用-XX:SurvivorRatio來控制EdenSurvivor比例
-XX:MaxTenuringThreshold — 設置對象在新生代中存活的次數
在這裏插入圖片描述

老年代(Old Generation, 採用標記清除和標記整理算法)

在年輕代中經歷N次GC後任然存活的對象,就會被放到老年代。因此,可以認爲老年代都是一些生命週期比較長的對象。內存也比新生代大很多(大概是1:2),當老年代內存滿的時候出發Major GC也就是Full GCFull GC發生的頻率較低,老年代存活時間較長,存活率較高。一般來說大對象會被直接分配到老年代。所謂大對象是指需要大量連續存儲空間的對象,最常見的就是這種大數組。

byte[] array = new byte[4 * 1024 * 1024];

這種一般直接分配到老年代,當然這種分配也不是固定的。

永久代(Permanent Generation)

用於存放靜態文件(class類、方法)和常量。永久代對垃圾回收沒有顯著的影響,但是有些應用可能動態生成或者調用一些class,例如Hibernate等,這種時候需要設置一個比較大的永久代來存放運行過程中新增的類。對永久代的回收主要是:廢棄的常量和無用的類。

永久代空間在Java SE8特性中已經被移除。取而代之的是元空間(MetaSpace)。因此不會再出現“java.lang.OutOfMemoryError: PermGen error”錯誤。

堆內存分配策略明確以下三點

  • 對象優先分配到Eden
  • 大對象直接進入老年代
  • 長期存活的對象直接進入老年代

垃圾回收機制說明

  • 新生代GC(Minor GC/Scavenge GC):發生在新生代的垃圾收集動作。因爲Java對象大多都具有朝生夕滅的特點,因此Minor GC非常頻繁(不一定等Eden滿才觸發),一般回收速度也比較快。在新生代中,每次垃圾收集都會發現大量對象死去,只有少量存活,因此可以使用複製算法
  • 老年代GC(Major GC/Full GC):發生在老年代的垃圾回收動作。Major GC,經常會伴隨着至少一次Minor GC。由於老年代中對象生命週期較長,因此Major GC並不頻繁,一般都是等老年代滿了後才進行一次Full GC,而且其速度一般會比Minor GC慢上10倍以上,另外,如果分配了Direct Memory,在老年代中進行Full GC時會順便清理掉Direct Memory中廢棄的對象。而老年代中因爲對象存活率高、沒有額外的空間對它進行分配擔保,就必須使用標記-清除或者標記整理算法。

垃圾回收算法

複製算法

Minor GC會把Eden中的所有活的對象都移到Survivor區域中,如果Survivor區中放不下,那麼剩下的活的對象就被移到Old generation中,也即一旦收集後,Eden是就變成空的了。
當對象在 Eden ( 包括一個 Survivor 區域,這裏假設是 from 區域 ) 出生後,在經過一次 Minor GC後,如果對象還存活,並且能夠被另外一塊 Survivor區域所容納( 上面已經假設爲 from 區域,這裏應爲 to 區域,即 to 區域有足夠的內存空間來存儲 Edenfrom 區域中存活的對象 ),則使用複製算法將這些仍然還存活的對象複製到另外一塊 Survivor 區域 ( 即 to 區域 ) 中,然後清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),並且將這些對象的年齡設置爲1,以後對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,通過-XX:MaxTenuringThreshold 來設定參數),這些對象就會成爲老年代。

-XX:MaxTenuringThreshold — 設置對象在新生代中存活的次數

複製算法的缺點
  • 很明顯,它要浪費出一部分內存空間
  • 如果對象的存活率很高,我們可以舉一個很極端的例子,假設是100%存活,那麼我們需要將所有對象都複製一遍,並將所有的引用地址重置一遍,複製這個工作需要一定的時間。所以複製算法必須在對象存活率低的地方使用

標記清除算法

用通俗的話解釋一下標記清除算法,就是當程序運行期間,若可以使用的內存被耗盡的時候,GC線程就會被觸發並將程序暫停,隨後將要回收的對象標記一遍,最終統一回收這些對象,完成標記清理工作接下來便讓應用程序恢復運行。

主要進行兩項工作,第一項則是標記,第二項則是清除。

標記清除算法缺點
  • 首先,它的缺點就是效率比較低(遞歸與全堆對象遍歷),而且在進行GC的時候,需要停止應用程序,這會導致用戶體驗非常差勁
  • 其次,主要的缺點則是這種方式清理出來的空閒內存是不連續的,這點不難理解,我們的死亡對象都是隨即的出現在內存的各個角落的,現在把它們清除之後,內存的佈局自然會亂七八糟。而爲了應付這一點,JVM就不得不維持一個內存的空閒列表,這又是一種開銷。而且在分配數組對象的時候,尋找連續的內存空間會不太好找。
    在這裏插入圖片描述

標記整理算法

在整理壓縮階段,不再對標記的對像做回收,而是通過所有存活對像都向一端移動,然後直接清除邊界以外的內存。
可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閒列表顯然少了許多開銷。

標記/整理算法不僅可以彌補標記/清除算法當中,內存區域分散的缺點,也消除了複製算法當中,內存減半的高額代價

標記整理算法缺點

標記/整理算法唯一的缺點就是效率也不高,不僅要標記所有存活對象,還要整理所有存活對象的引用地址。
從效率上來說,標記/整理算法要低於複製算法。
在這裏插入圖片描述

垃圾回收執行時間和注意事項

GC分爲Minor GCFull GC
Minor GC:發生在Eden區的垃圾回收
Full GC:對整個堆進行整理,包括新生代,老年代,永久代。Full GC因爲要對整個堆回收,所以比Minor GC要慢,因此儘可能減少Full GC次數,在對JVM調優的過程中,很大部分就是對於Full GC的調節。

有如下原因可能導致Full GC:

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

與垃圾回收時間有關的兩個函數

System.gc();

命令行參數監視垃圾收集器的運行:
使用System.gc()可以不管JVM使用哪一種垃圾回收算法,都可以請求Java的垃圾回收。在命令行中有一個參數-verbosegc可以查看Java使用的堆內存的情況,他的格式是:
java -verbosegc classfile
需要注意的是,調用System.gc()也僅僅是一個建議。jvm接收這個請求後,並不是立即做垃圾回收,而只是對幾個垃圾回收算法做了加權,使垃圾回收更加容易發生,或提早發生,或回收較多。

finalize()

在JVM垃圾回收器收集一個對象之前,一般要求程序調用適當的方法釋放資源。但沒有明確釋放資源的情況下,Java提供了缺省機制來終止該對象以釋放資源這個方法就是finalize()

protected void finalize() throws Throwable

finalize()方法返回之後,對象消失,垃圾收集開始執行,throws Throwable表示它可以拋出任何類型的異常。
當 finalize() 方法被調用時,JVM 會釋放該線程上的所有同步鎖。

觸發GC主條件

  • 當程序空閒時,即沒有應用程序運行時,GC會被調用。因此GC在優先級最低的線程中進行,所以當應用忙時GC線程不會被調用,但以下情況例外:
  • Java堆內存不足時,GC會被調用。當應用程序在運行,並在運行過程中創建對象,這時內存不足,jvm就會強制調用GC線程,以便回收內存用於新的分配。如果一次GC不能滿足需求,jvm會嘗試進行第二次GC,如果任然不能滿足需求,則會報出outOfMemory的錯誤,程序停止。
  • 在編譯過程中作爲一種優化技術,Java編譯器能選擇性賦值null,從而標記可以回收。

減少GC開銷措施

  • 不要顯示調用System,gc()
    此函數建議JVM進行GC,雖然知識建議而非一定,但很多情況下它會觸發GC,從而增加GC頻率,也增加了間歇性暫停。
  • 儘量減少臨時變量的使用
    臨時變量在跳出函數調用後,會成爲垃圾,少用臨時變量就相當於減少垃圾的產生
  • 對象不用時最好顯示賦值爲null
    一般而言,爲Null的對象都會被作爲垃圾處理,所以將不用的對象顯式地設爲Null,有利於GC收集器判定垃圾,從而提高了GC的效率。
  • 儘量使用StringBuffer,而不用String來累加字符串
    由於String是固定長的字符串對象,累加String對象時,並非在一個String對象中擴增,而是重新創建新的String對象,如Str5=Str1+Str2+Str3+Str4,這條語句執行過程中會產生多個垃圾對象,因爲對次作“+”操作時都必須創建新的String對象,但這些過渡對象對系統來說是沒有實際意義的,只會增加更多的垃圾。避免這種情況可以改用StringBuffer來累加字符串,因StringBuffer是可變長的,它在原有基礎上進行擴增,不會產生中間對象。
  • 能用基本數據類型就不要用包裝類型
    基本類型變量佔用的內存資源比相應對象佔用的少得多,如果沒有必要,最好使用基本變量。
  • 儘量少用靜態對象變量

在這裏插入圖片描述

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar

詳細信息可以參考這篇博文Java虛擬機

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