Java虛擬機(二)—— 垃圾回收

如何判定對象爲垃圾對象:

1、引用記數法

在對象中添加引用的標識,每對對象增加一個引用,引用標識進行+1操作,減少一個引用,標識-1,當標識爲0的時候,說明對象不存在引用可以被回收;

在這裏插入圖片描述
缺點:無法處理對象間相互引用的問題;

當我們把上圖的x1指針置爲空的時候,那就沒有指針指向這些對象了,也就是這些對象沒人用了,應該作爲垃圾被回收,但是因爲這幾個對象相互引用,導致它們的引用標識都不爲0,所以這幾個對象不會被標記爲垃圾對象;
在這裏插入圖片描述

2、可達性分析

判斷對象是否存活,將堆中對象想象成一棵樹,從樹根(GC root)開始遍歷所有的對象,能到達的稱爲可用對象,不能到達的稱之爲垃圾;
在這裏插入圖片描述
GC root(樹根)一定是可達的,主要有:

  • 虛擬機棧中引用的對象一定是可達的;
  • 本地方法棧中的JNI引用的對象;
  • 方法區中靜態屬性引用的對象;
  • 方法區中常量引用的對象;

這個判定方法就能解決引用計數法的問題了,只要從GC root節點出發,不可到達的對象就是垃圾對象,所以一般在虛擬機中判定對象是否是垃圾對象用的都是這種方法;
在這裏插入圖片描述

如何回收垃圾對象

回收策略

堆中的對象以及棧中的引用如下:
在這裏插入圖片描述

1、標記—清除

用可達性分析,先標記、再清除;

標記:
在這裏插入圖片描述
清除:

在這裏插入圖片描述

缺點:
①標記—清除之後會出現斷斷續續的空閒空間(內存碎片),空間無法高效利用,
②並且先標記後清除需要對堆空間前後兩次遍歷,效率不高;

2、複製算法

一開始,將堆空間內存分成兩個部分A、B,只使用其中一部分,對象都是在A中進行分配的,B是空的,然後對A進行可達性分析,將可用的對象拷貝到另一個空間B去,再把A中的對象全部擦除,然後下一次分配空間是在B中分配,垃圾回收也是在B中進行,相當於是A、B換着來的,這樣就把標記—清除算法的兩個問題都解決了;

可達性分析,回收前:
在這裏插入圖片描述
回收後:
在這裏插入圖片描述

缺點:
①只使用一半堆空間,浪費一半的空間,
②如果對應的A那半部分出現極端情況(A全都是或大部分都是生命週期比較長的對象),那就需要全部拷貝);

3、標記—整理

先標記、再整理,先可達性分析,標記對象是否可用,然後將可用對象向一端移動,這樣垃圾回收之後的堆空間的剩餘空間是連續的;

回收前:
在這裏插入圖片描述
回收後:
在這裏插入圖片描述
缺點:
效率也不高,不僅要標記存活對象,還要整理所有存活對象的引用地址,在效率上不如複製算法;

可以這麼理解,標記—整理算法也是把堆分了兩個部分X、Y,把其中一部分X用來存放可用對象,把剩下的那一部分Y的可用對象全部放過來,把X的垃圾對象放出去,這就是標記—整理算法;

4、分代回收算法

這個算法把堆分爲新生代和老年代:
在這裏插入圖片描述

新生代:朝生夕滅(也沒這麼久),存活時間短,老年代:經過多次minor GC依舊存在,存活時間比較長;

分代回收是對上面三種算法的通用:

  • 在新生代中每次垃圾回收都發現有大量的對象死去,只有少量存活,因此採用複製算法回收新生代,只需要付出少量對象的複製成本就可以完成收集;
  • 而老年代中對象的存活率高,不適合採用複製算法,而且如果老年代採用複製算法,它是沒有額外的空間進行分配擔保的,因此必須使用標記/清除算法或者標記/整理算法來進行回收。

總結一下就是,分代收集算法的原理是採用複製算法來收集新生代,採用標記—清除算法或者標記—整理算法收集老年代。

垃圾回收器

如果說垃圾收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。

下圖展示了7種作用於不同分代的收集器:

  • 其中用於回收新生代的收集器包括Serial、PraNew、Parallel Scavenge;
  • 回收老年代的收集器包括SerialOld、Parallel Old、CMS,
  • 還有用於回收整個Java堆的G1收集器。

在這裏插入圖片描述

1、Serial、Serial Old(單線程垃圾收集器)

Serial、Serial Old收集垃圾的方式都是下圖那樣的,只不過Serial使用複製算法收集的是新生代、Serial Old使用標記整理算法收集的是老年代

這個收集算法就是當需要垃圾回收的時候,工作線程全部暫停,由一個單線程的垃圾回收線程回收垃圾對象,回收完成之後,工作線程繼續工作,垃圾回收線程暫停,如此往復

在這裏插入圖片描述
優點:標記和清理都是單線程,優點是簡單高效;

這個收集器最基本,發展最悠久,單線程(整體性能低,單個看的話效率高),適合分配內存比較小的、收集起來速度比較快的場景,比如桌面應用;

2、ParNew(Serial的多線程版本)、Parallel Scavenge、Parallel Old

ParNew新生代收集器,實際上是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現,它也是採用複製算法收集新生代唯一可以和CMS合作收集垃圾的垃圾收集器

Parallel Scavenge和ParNew一樣都是使用複製算法的新生代多線程收集器,不同點在於最初設計的時候他們的關注點不同,Parallel Scavenge的關注點在於達到可控制的吞吐量(CPU運行用戶代碼的時間與CPU消耗的總時間的比值,吞吐量=執行用戶代碼消耗的時間/執行用戶代碼的時間+垃圾回收所佔用的時間);

Parallel Old收集器 (標記-整理算法): 老年代並行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本

在這裏插入圖片描述

以上兩個收集器都是併發執行的,也就是不論是單線程(Serial、Serial Old)還是多線程(ParNew),收集垃圾的時候是要暫停工作線程的,而工作線程執行的時候垃圾回收線程是不能執行的,它們的收集垃圾的線程和工作線程是不能同時執行的下面的垃圾回收器收集垃圾的線程和工作線程是可以並行執行的;

3、CMS收集器

使用標記—清除算法,老年代並行收集器,以獲取最短回收停頓時間爲目標的收集器,具有高併發、低停頓的特點,追求最短GC回收停頓時間。

工作過程:

  1. 初始標記:標記GC root能直接關聯的對象(比如上面可達性分析裏面的對象1),這個非常快;
  2. 併發標記:就是接着標記從GC root直接關聯的對象繼續往下走能到達的對象;
  3. 重新標記:爲了修正併發標記期間因用戶程序基於運作而導致產生變動的那部分對象,也就是對併發標記做一個修正;
  4. 併發清理:把垃圾對象清理掉;

在這裏插入圖片描述
可以看到,CMS也不是完全的並行執行,但是它實現了垃圾回收過程中最耗時最基本的操作併發標記、垃圾清理的並行執行,這樣:

  • 吞吐量提高了很多,高併發;
  • 低停頓,

但這樣同時:

  • 佔用了大量的CPU資源;
  • 無法處理浮動垃圾(這樣並行執行的時候就像邊打掃邊扔垃圾一樣,對於打掃過的地方,後面再扔的垃圾那就得等下一次打掃了);
  • 出現Concurrent Mode Failure錯誤,這是因爲清理垃圾的線程和工作線程並行執行,那麼對於這些工作線程創建的新的對象我們得預留一塊空間,這塊空間留大了浪費資源,留小了不夠用就會發生這個錯誤;
  • 產生空間碎片,標記—清除算法導致的;

4、G1收集器

最牛逼的垃圾回收器,老年代和新生代它都可以用,它的優勢:

  • 並行和併發
  • 分代收集
  • 空間整合(標記—整理算法實現的)
  • 可預測的停頓(能指定停頓不超過某一時間)

與CMS比較:吞吐量並不比CMS好多少,但在減少停頓方面G1比CMS強很多;

步驟:

  1. 初始標記
  2. 併發標記
  3. 最終標記
  4. 篩選回收

G1收集器可以在幾乎不犧牲吞吐量的前提下完成低停頓的內存回收,這是由於它能夠極力避免全區域的垃圾收集,之前的收集器進行收集的範圍都是整個新生代或老年代,而G1將整個Java堆(包括新生代、老年代)劃分爲多個大小固定的獨立區域(Region),並且跟蹤這些區域裏面的垃圾堆積程度,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收垃圾最多的區域(這就是Garbage First名稱的由來)。區域劃分、有優先級的區域回收,保證了G1收集器在有限的時間內可以獲得最高的收集效率。

在這裏插入圖片描述

垃圾回收器總結

總結一下垃圾回收器吧:

垃圾回收器 採用的回收算法 線程數 回收的區域 備註
Serial 複製算法 單線程 新生代
Serial Old 標記—整理算法 單線程 老年代 Serial收集器的老年代版本
ParNew 複製算法 多線程 新生代 Serial收集器的多線程版本
Parallel Scavenge 複製算法 多線程 新生代 追求高吞吐量
Parallel Old 標記—整理算法 多線程 老年代 Parallel Scavenge的老年代版本
CMS 標記—清除算法 多線程 老年代 最求最短GC停頓時間
G1 標記—整理算法 多線程 整個堆 高效

內存分配策略

Java技術體系中所提倡的自動內存管理最終可以歸結爲自動化地解決了兩個問題:

  • 給對象分配內存 ;
  • 回收分配給對象的內存。

一般而言,對象主要分配在新生代的Eden區上,如果啓動了本地線程分配緩存(TLAB,下面會說),將按線程優先在TLAB上分配。少數情況下也可能直接分配在老年代中。總的來說,內存分配規則並不是一層不變的,其細節取決於當前使用的是哪一種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。

我們知道,垃圾回收策略(選擇哪種垃圾回收算法)決定了我們的堆是否規整,而堆是否規整決定了我們給新對象分配內存的策略(規整的話用指針碰撞,就是一邊是空閒的空間,一邊是存放對象的空間,中間拿一個指針隔開,增加對象的時候移動指針即可,而堆不規整的話使用空閒列表,就是記錄空閒的位置,每次創建出一個新的對象都往空閒的、足夠的地方補就是了),而不同的內存分配策略又決定了線程安全性問題(兩種分配策略都有線程安全性問題),那麼對於線程安全的處理一般有兩種方法:

  1. 線程同步,就是給資源上鎖,這樣相當於加了synchronized關鍵字,對於訪問此處的資源的線程,只能一個一個來,給串行化了,低效;
  2. 本地線程分配緩存(TLAB),就是事先給每個線程一塊空間,每個線程創建的對象都放在自己對應的空間裏面;

在這裏插入圖片描述

下面就來說說JVM的內存分配策略吧:

  1. 對象優先在Eden分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次MinorGC。現在的商業虛擬機一般都採用複製算法來回收新生代,將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。 當進行垃圾回收時,將Eden和Survivor中還存活的對象一次性地複製到另外一塊Survivor空間上,最後處理掉Eden和剛纔的Survivor空間。(HotSpot虛擬機默認Eden和Survivor的大小比例是8:1)當Survivor空間不夠用時,需要依賴老年代進行分配擔保。

  2. 大對象直接進入老年代。所謂的大對象是指,需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。

  3. 長期存活的對象將進入老年代。當對象在新生代中經歷過一定次數(默認爲15)的Minor GC後,就會被晉升到老年代中。

  4. 動態對象年齡判定。爲了更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

需要注意的是,Java的垃圾回收機制是Java虛擬機提供的能力,用於在空閒時間以不定時的方式動態回收無任何引用的對象佔據的內存空間。也就是說,垃圾收集器回收的是無任何引用的對象佔據的內存空間而不是對象本身。

方法區的回收

方法區的內存回收目標主要是針對 常量池的回收對類型的卸載。回收廢棄常量與回收Java堆中的對象非常類似。以常量池中字面量的回收爲例,假如一個字符串“abc”已經進入了常量池中,但是當前系統沒有任何一個String對象是叫做“abc”的,換句話說是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果在這時候發生內存回收,而且必要的話,這個“abc”常量就會被系統“請”出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。

判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是“無用的類”:

  • 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例;
  • 加載該類的ClassLoader已經被回收;
  • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

虛擬機可以對滿足上述3個條件的無用類進行回收(卸載),這裏說的僅僅是“可以”,而不是和對象一樣,不使用了就必然會回收。特別地,在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成JSP和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

推薦一篇好的博主博文→入口

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