深入理解JVM虛擬機 垃圾收集和內存分配

如何確認一個對象已經死了

垃圾收集器在對堆進行回收前,第一件事情就是要確認那些對象已經死了,哪些對象還活着。我們看以下幾種方法

  1. 引用計數算法
    給對象添加一個引用計數器,當有一個地方引用它的時候,計數器+1,當引用失效的時候就-1;0表示已經不能再被使用了,需要被回收。這種效率很高,但是當遇到相互循環依賴的時候就出現問題了,Java虛擬機沒有使用這種方法來管理內存。
    我們來看一個例子
      ReferenceA referenceA=new ReferenceA();
      ReferenceB referenceB=new ReferenceB();

      referenceA.obj=referenceB;
      referenceB.obj=referenceA;

      referenceA=null;
      referenceB=null;

referenceA和referenceB相互依賴,但是他們其實已經沒有用了,如果使用引用計數算法,則無法將他們GC。
2. 可達性分析算法
通過一系列的稱爲“GC Roots”的對象作爲起始點,如果從這些節點向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象沒有引用鏈時,則進行GC。
“GC Roots”:包括 虛擬機棧中引用的對象、方法區中的靜態屬性引用對象、方法去的常量引用對象、本地方法棧JNI引用的對象

引用

引用分爲:強、軟、弱、虛

  • 強引用:代碼中普遍存在的,類型new Object()這類。只要強引用存在,則不會被回收
  • 軟引用(SoftReference):有用但非必須的對象,在系統發生內存溢出異常前,會吧這些對象列入回收範圍,如果回收後內存還是不夠,則拋出內存溢出異常
  • 弱引用(WeakReference):非必須對象,比軟引用更弱,弱引用關聯的對象只能生存到下一次垃圾回收前,無聊是否內存足夠,都會回收
  • 虛引用:最弱的引用,完全可以說沒有,唯一目的是在對象被回收時可以收到一個系統通知

finalize

即使不可達的對象,也不是肥死不可,至少會經歷兩次標記過程。第一次:如果沒有引用鏈,則會被第一次標記,並且進行第一次篩選,篩選條件是是否有必要執行finalize()方法,如果沒覆蓋此方法或者已經被調用過一次這個方法,則就沒必要執行了。
如果有必要執行finalize()方法,則對象會被放入F-Queue對象中,並稍後,低優先級的Finalizer線程執行它,只是觸發,不一定等到他執行完成,防止方法中寫了死循環之類的。之後GC將對對象F-Queue中的對象進行第二次標記,如果finalize()方法拯救了自己,則會被移除回收集合,否則就會被回收。
一下方法拯救了自己

@Override
    protected void finalize() throws Throwable {
        super.finalize();
        //重新把自己引用給變量obj
        obj=this;
    }

建議不要使用finalize()方法,因爲他的運行代價高昂,不確定性大。

垃圾收集算法

  1. 標記-清除算法
    最基礎的收集算法。首先標記需要回收的對象,標記完成統一回收所有被標記的對象。
    缺點:(1)效率不高,(2)清除後會產生大量不連續的內存碎片,導致之後可能會無法找到一個連續的足夠大的內存區域
    這裏寫圖片描述
  2. 複製算法
    爲了解決效率問題,出現了複製算法,它將可用內存按內容劃分大小相等的兩塊,每次只使用其中的一塊。當一塊的內存用完了,就將還存活的對象複製到另一塊上,然後把已經使用的內存空間清理掉。每次堆整個半邊進行回收,不用考慮內存碎片
    現代商業虛擬機使用這種算法來處理新生代的回收。
    缺點:(1)內存縮小爲原來的一半,(2)對象的存活率較高的時候需要進行較多的複製操作,效率低
    這裏寫圖片描述
  3. 標記-整理算法
    前面和標記-清除算法一樣,但是後續不是直接進行回收,而是讓所有存活的對象向一端移動,然後清理掉端邊界的內存。
    這裏寫圖片描述
  4. 分代收集算法
    把Java堆分爲新生代和老年代,根據不同的代的特點,採用最適合的收集算法。
    新生代:有大批對象死去,適合使用複製算法。
    老年代:對象存活高,適合使用標記-清理或者標記-整理算法

HotSpot算法實現

遍歷引用鏈,需要消耗大量時間,GC將會發生長時間的停頓。HotSpot使用一組成爲OopMap的數據結構,在類加載完成時,就把對象內的偏移量上什麼類型的數據計算出來,這樣,GC掃描就可以直接得到這些信息。
在OopMap的協助下,HotSpot就可以快速的萬能充GC Roots枚舉。

垃圾收集器

收集算法是方法論,收集器就是具體實現。

這裏寫圖片描述

  1. Serial收集器(新生代)
    最基本、發展最久的收集器。單線程的收集器。他在工作的時候需要暫停所有的工作線程,直到他回收結束。它現在依然是虛擬機運行在Client模式下的默認新生代收集器。一兩百兆的新生代,停頓時間控制在幾十毫秒最多100多毫秒內。
    優點:簡單、高效。
  2. ParNew收集器(新生代)
    Serial的多線程版本,多條線程收集垃圾。他是許多運行在Server模式下的虛擬機首選的新生代收集器,他可以和CMS收集器配合工作。ParNew收集器在單CPU中沒有Serial好,甚至由於線程開銷,更慢,但是在多CPU中,效率高很多,默認線程數與CPU數量相同,可以使用-XX:ParallelGCThreads來限制線程數量
  3. Parallel Scavenge收集器
    新生代,複製算法收集器,並行多線程收集器。它的特點是達到一個可控的吞吐量,CPU運行用戶代碼的時間與CPU總消耗的時間比。-XX:MaxGCPauseMillis控制最大垃圾收集停頓時間,小了就會頻繁進行回收,一次回收一部分。-XX:GCTimeRatio控制吞吐量的大小,
  4. Parallel Old收集器
    Parallel Scavenge老年代的版本,使用多線程和標記-整理算法
  5. CMS 收集器
    以獲取最短回收停頓時間爲目標的收集器。基於標記-清除算法實現。(1)初始標記,(2)併發標記,(3)重新標記(4)併發清除,
    (1)(2)需要停止所有線程,但是隻是標記一下GC Roots直接關聯的對象,速度很快,(3)(4)可以和用戶程序一起運行,所有總體上來說可以和用戶線程併發執行。CMS是一款優秀的收集器,併發收集、低停頓。
    缺點:(1)對CPU資源非常敏感,(2)無法處理浮動垃圾(3)標記清除產生大量碎片

  6. G1收集器
    G1(Garbage First)收集器是當前收集器技術發展的最前沿成果,在JDK 1.6_Update14中提供了Early Access版本的G1收集器以供試用。在將來JDK 1.7正式發佈的時候,G1收集器很可能會有一個成熟的商用版本隨之發佈。這裏只對G1收集器進行簡單介紹①。
    G1收集器是垃圾收集器理論進一步發展的產物,它與前面的CMS收集器相比有兩個顯著的改進:一是G1收集器是基於“標記-整理”算法實現的收集器,也就是說它不會產生空間碎片,這對於長時間運行的應用系統來說非常重要。二是它可以非常精確地控制停頓,既能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特徵了。
    G1收集器可以實現在基本不犧牲吞吐量的前提下完成低停頓的內存回收,這是由於它能夠極力地避免全區域的垃圾收集,之前的收集器進行收集的範圍都是整個新生代或老年代,而G1將整個Java堆(包括新生代、老年代)劃分爲多個大小固定的獨立區域(Region),並且跟蹤這些區域裏面的垃圾堆積程度,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收垃圾最多的區域(這就是Garbage First名稱的來由)。區域劃分及有優先級的區域回收,保證了G1收集器在有限的時間內可以獲得最高的收集效率。

GC日誌

33.125:[GC[DefNew:3324K->152K(3712K),0.0025925secs]3324K->152K(11904K),0.0031680 secs]

100.667:[FullGC[Tenured:0K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]

最前面的數字“33.125:”和“100.667:”代表了GC發生的時間,這個數字的含義是從Java虛擬機啓動以來經過的秒數。
GC日誌開頭的“[GC”和“[Full GC”說明了這次垃圾收集的停頓類型,而不是用來區分新生代GC還是老年代GC的。如果有“Full”,說明這次GC是發生了Stop-The-World的,例如下面這段新生代收集器ParNew的日誌也會出現“[Full GC”(這一般是因爲出現了分配擔保失敗之類的問題,所以才導致STW)。如果是調用System.gc()方法所觸發的收集,那麼在這裏將顯示“[Full GC(System)”。

[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]

接下來的“[DefNew”、“[Tenured”、“[Perm”表示GC發生的區域,這裏顯示的區域名稱與使用的GC收集是密切相關的,例如上面樣例所使用的Serial收集器中的新生代名爲“Default New Generation”,所以顯示的是“[DefNew”。

如果是ParNew收集器,新生代名稱就會變爲“[ParNew”,意爲“Parallel New Generation”。

如果採用Parallel Scavenge收集器,那它配套的新生代稱爲“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。

後面方括號內部的“3324K->152K(3712K)”含義是“GC前該內存區域已使用容量->GC後該內存區域已使用容量(該內存區域總容量)”。
而在方括號之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC後Java堆已使用容量(Java堆總容量)”。

再往後,“0.0025925 secs”表示該內存區域GC所佔用的時間,單位是秒。

內存分配與回收策略

大多數情況下,對象在新生代Eden區中分配,當Eden沒有足夠空間,虛擬機將發生一次Minor GC,

程序創建3個2MB和1個4MB的byte[],運行時-Xms20M,-Xmx20M,Xmn10M表示Java堆爲20M,不可擴展,新生代分配10M,則老年代爲20M-10M,-XX:SurvivorRatio=8表示新生代Eden區與Survivor區爲8:1(複製算法,每次使用Eden和一個Survivor,複製時候,和這些存活的移動到另一個Survivor中,清除其他的空間)

運行,發生一次Minor GC生生代從6651KB變成148KB,但是總內存佔用不變。
過程:首先在新生代分配6MB,當在分配4MB的時候發現空間不足了(10M*90%)<10MB,因此發生一次Minor GC,發現前面的6MB都需要用的,但是Survivor只有1MB的空間,所以只能移動到老年代中,GC後,4MB進入新生代Eden,6MB進入老年代

大對象最好直接放入老年代,減少移動的可能。-XX:PretenureSizeThreshlod參數,設置大於多少值直接進入老年代。

存活年齡

如果對象在Eden出生並經歷第一次MinorGc後任然存活,並被Survivor容納,將被移動到Survivor中,對象年齡+1,每經歷一次+1,當默認大於15的時候則晉升爲老年代。也可以通過設置-XX:MaxTenuringThreshold設置晉升年齡。

如果Survivor空間中相同年齡對象大小的總和大於Survivor空間的一半,年齡大於或者等於年齡對象的就直接進入老年代。

空間分配擔保

發生Minor GC前,需要檢查老年代可用連續空間十分大於新生代對象總空間,如果成立免責是安全的。如果不成立,則檢查HandlePromotionFailure十分允許擔保失敗,如果可以,則檢查老年代連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,嘗試Minor GC,儘管有風險。如果小於或者HandlePromotionFailure不允許冒險,則進行一次Full GC

Survivor無法容納的對象直接進入老年代。

發佈了15 篇原創文章 · 獲贊 5 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章