垃圾回收之垃圾標記算法
對象被判定爲垃圾的標準:
- 該對象沒有被其他對象引用
判定對象是否爲垃圾的算法:
- 引用計數算法,優點:執行效率高,程序執行受影響較小;缺點:無法檢測出循環引用的情況,容易導致內存泄露
- 通過判斷對象的引用數量來決定對象是否可以被回收
- 每個對象實例都有一個引用計數器,被引用則+1,完成引用即引用結束則-1
- 綜上,任何引用計數爲0的對象實例就可以被當作垃圾收集
- 可達性分析算法(Java使用的算法)
- 通過判斷對象的引用鏈是否可達來決定對象是否可以被回收
可達性分析算法遍歷引用鏈如圖:
可以作爲GC Root的對象:
- 虛擬機棧中引用的對象(棧幀中的本地變量表)
- 方法區中的常量引用的對象
- 方法區中的類靜態屬性所引用的對象
- 本地方法棧中JNI(Native)的引用對象
- 活躍線程的引用對象,即線程對象
Java垃圾回收之回收算法
光有垃圾標記算法還不行,JVM還需要有垃圾回收算法來將這些標記爲垃圾的對象給釋放回收掉。主要的回收算法有以下幾種:
1.標記 - 清除算法(Mark and Sweep):
- 標記:從根集合進行掃描,對存活的對象進行標記
- 清除:對堆內存從頭到尾進行線性遍歷,回收不可達對象內存
缺點:由於標記 - 清除不需要進行對象的移動,並且僅對不可達的對象進行處理,因此使用該回收算法之後會產生大量不連續的內存碎片。而內存碎片過多可能會導致以後在程序運行過程中,需要分配內存給較大的對象時,無法找到足夠大的連續內存空間,從而不得不再次觸發垃圾回收工作,若依舊無法分配內存的話就會觸發內存溢出異常。
1.複製算法(Copying):
- 將可用的內存容量按一定比例劃分爲兩塊或多塊,並將其中一塊或兩塊作爲對象面,剩餘的則作爲空閒面
- 對象在對象面上創建,當對象面的內存空間用完時,會將存活的對象從對象面複製到空閒面中,接着清除該對象面中所有的對象
優點:解決內存碎片化問題,順序分配內存,簡單高效。該算法適用於對象存活率低的場景,所以普遍應用在新生代中,因爲新生代裏的對象存活率通常情況下只有10%左右
3.標記 - 整理算法(Compacting):
- 標記:從根集合進行掃描,對存活的對象進行標記
- 整理:移動所有存活的對象,且按照內存地址次序依次排列,然後將末端內存地址以後的內存全部回收
優點:避免了標記 - 清除算法所帶來的內存不連續性問題,以及不需要像複製算法那樣需要設置兩塊內存互換。該算法適用於對象存活率較高的場景,所以普遍應用在老年代中,因爲老年代裏對象存活率較高
4.分代收集算法(Generational Collector):
- 實際是多種垃圾回收算法的組合拳,該算法對堆內存進行進一步的劃分,按照對象生命週期的不同劃分區域以採用不同的垃圾回收算法。目的是提高JVM的回收效率,也是目前JVM使用的回收算法
在JDK7及之前的JVM版本共有三個分代,即新生代、老年代和永久代(注意,永久代不存在於堆中,而是存在於方法區):
而JDK8及以後的版本只有新生代和老年代:
分代收集算法的GC分爲兩種:
- Minor GC,發生在新生代中的垃圾收集工作,採用的是複製算法
- Full GC,當觸發老年代的垃圾回收的時候通常也會伴隨着對新生代堆內存的回收,即對整個堆進行垃圾回收,這便是所謂的FullGC
新生代用於儘可能快速地收集掉那些生命週期短的對象,新生代分爲兩個區:
- Eden區:新創建的對象通常存放在這個區域,如果新創建的對象太大放不進該區域則會放進Survivor區或老年代中
- 兩個Survivor區,該區域又分爲to和from區,至於誰是to區誰是from區則不是固定的,而是會隨着GC互相轉換
對象如何晉升到老年代:
- 新生代裏的對象每經歷一次Minor GC並且存活,其年齡就會+1,當經歷一定Minor GC次數依然存活的對象就會晉升到老年代,默認是15歲,即默認經歷了15次Minor GC依舊存活的對象會被放到老年代
- Survivor區中存放不下的對象會被直接放到老年代
- 新生成的大對象會被直接放到老年代(通過參數:-XX:+PreetenuerSizeThreshold 設置)
常用的調優參數:
- -XX:SurvivorRatio:Eden和Survivor的比值,默認8:1
- -XX:NewRatio:老年代和新生代內存大小的比例
- -XX:MaxTenuringThreshold:對象從新生代晉升到老年代經過GC次數的最大閾值
綜上,老年代用於存放生命週期較長的對象,老年代採用的是標記 - 整理算法。
Full GC和Major GC:
- Major GC通常是跟full GC是等價的,即收集整個GC堆。但因爲HotSpot VM發展了這麼多年,外界對各種名詞的解讀已經完全混亂了,當有人說“major GC”的時候一定要問清楚他想要指的是上面的full GC還是僅僅針對老年代的 GC
- Full GC比Minor GC慢,但執行頻率低
觸發Full GC的條件:
- 老年代空間不足
- 永久代空間不足(JDK7及之前的版本)
- 使用CMS GC時出現promotion failed,concurrent mode failure
- Minor GC晉升到老年代的平均大小大於老年代的剩餘空間
- 在代碼裏調用System.gc(),該方法只能作爲提醒,具體是否觸發Full GC還得看JVM
- 使用RMI來進行RPC或管理的JDK應用,每小時執行1次Full GC
注:promotion failed是在進行Minor GC時,survivor space放不下、對象只能放入老年代,而此時老年代也放不下造成的;concurrent mode failure是在執行CMS GC的過程中同時有對象要放入老年代,而此時老年代空間不足造成的。
Java垃圾回收之新生代垃圾收集器
在瞭解垃圾收集器之前,我們需要知道一個概念“Stop-the-World”:
- 該單詞的含義即:JVM由於要執行GC而停止了應用程序的執行
- 並且任何一個GC算法中都會發生
- 實際上多數GC優化就是通過減少Stop-the-World發生的時間來提高程序性能
除此之外,我們需要知道什麼是Safepoint:
- 分析過程中對象引用關係不會發生變化的點,即安全點(Safepoint)。因爲JVM不能隨隨便便就發生Stop-the-World,而是到一個相對安全的點纔會發生
- 產生Safepoint的地方:方法調用、循環跳轉、異常跳轉等等
- 安全點數量得適中
JVM的運行模式:
- Server:啓動速度較慢,但是啓動完成進入穩定期之後的運行速度比Client快,因爲Server模式採用的是重量級的JVM,有比Client模式更多的優化
- Client:啓動速度較快,採用的是輕量級的JVM
各垃圾收集器之間的聯繫,即可以搭配使用關係:
Serial收集器(啓動參數:-XX:+UseSerialGC,採用複製算法):
- 單線程收集,進行垃圾收集時,必須暫停所有工作線程
- 簡單高效,是Client模式下默認的新生代收集器
ParNew收集器(啓動參數:-XX:+UseParNewGC,採用複製算法):
- 除了是採用多線程進行垃圾回收外,其餘行爲、特點與Serial收集器一樣;是Server模式下首選的年輕代垃圾收集器
- 單核執行效率不如Serial,需要在多核環境下執行纔有優勢
- 該收集器默認開啓的垃圾收集線程數與CPU核心數量相同
Parallel Scavenge收集器(啓動參數:-XX:+UseParallelGC,採用複製算法):
- 該收集器也是多線程的,只不過比起之前所介紹的收集器關注用戶線程停頓時間,該收集器更關注系統的吞吐量
- 吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)
- 同樣在多核環境下執行纔有優勢,Server模式下默認的新生代收集器
Java垃圾回收之老年代垃圾收集器
Serial Old收集器(啓動參數:-XX:+UseSerialOldGC,採用標記 - 整理算法):
- 該收集器是Serial收集器的老年代版本,也是單線程收集,進行垃圾收集時,必須暫停所有工作線程
- 簡單高效,Client模式下默認的老年代收集器
Parallel Old收集器(啓動參數:-XX:+UseParallelOldGC,採用標記 - 整理算法):
- 該收集器是Parallel Scavenge收集器的老年代版本,也是多線程收集,同樣是吞吐量優先
- 通常配合新生代的Parallel Scavenge收集器使用
CMS收集器(啓動參數:-XX:+UseConcMarkSweepGC,採用標記 - 清除算法):
- 多線程收集,停頓時間優先,通常配合新生代的ParNew收集器使用
CMS收集器收集流程:
- 初始標記:會觸發stop-the-world,但是停頓時間很短
- 併發標記:併發追溯標記,程序不會停頓
- 併發預清理:查找執行併發標記階段從新生代晉升到老年代的對象
- 重新標記:暫停虛擬機,掃描CMS堆中的剩餘對象
- 併發清理:清理垃圾對象,程序不會停頓
- 併發重置:重置CMS收集器的數據結構
CMS收集器圖示:
G1收集器(啓動參數:-XX:+UseG1GC,採用複製 + 標記 - 整理算法):
- 該收集同時用於新生代和老年代,該收集器的目標在於替換掉CMS,並且採取了某些不同的方式跨越了新生代和老年代的邊界
- 將整個Java堆內存劃分成多個大小相等的Region,即新生代和老年代不再是物理隔離的了
- G1收集器全稱爲Garbage First,該收集器的特點如下:
- 並行和併發
- 分代收集
- 空間整合
- 可預測的停頓
Java垃圾回收之常見面試題
1.Object的finalize()方法的作用是否與C++的析構函數作用相同:
- 與C++的析構函數不同,析構函數調用確定,而finalize()方法是不確定的,因爲finalize()方法在對象被GC回收時調用
- 將未被引用的對象放置於F-Queue隊列
- 該方法執行隨時可能會被終止
- 它的設計目的是保證對象在被垃圾收集前完成特定資源的回收或給予對象最後一次重生機會等
示例代碼:
package com.example.demo.gc;
/**
* @author 01
* @date 2019-07-18
**/
public class Finalization {
public static Finalization finalization;
@Override
protected void finalize() throws Throwable {
System.out.println("finalize");
finalization = this;
}
public static void main(String[] args) {
Finalization f = new Finalization();
System.out.println("First print: " + f);
f = null;
System.gc();
System.out.println("Second print: " + f);
System.out.println(f.finalization);
}
}
執行結果:
從執行結果可以看到,Finalization對象被GC回收時finalize()方法會被調用,finalize()方法裏將當前對象this賦值給了靜態屬性finalization實現了對象的“重生”,所以在GC之後依舊能打印到該對象的地址信息
注:finalize是個不太可控的方法因此並不常用,並且在JDK9+版本被標註爲過時方法
2.Java中的強引用,軟引用,弱引用及虛引用有什麼用:
- 強引用(Strong reference):
所謂強引用,就是我們最常見的普通對象引用,只要還有強引用指向一個對象,就能表明對象還“活着”,垃圾收集器不會碰這種對象。對於一個普通的對象,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值爲 null,就是可以被垃圾收集的了,當然具體回收時機還是要看垃圾收集策略。總結:
- 最普遍的引用,例:
Object obj = new Object();
- JVM寧可拋出OutOfMemoryError終止程序也不會回收具有強引用的對象
- 通過將對象設置爲null來弱化引用,使其被回收
- 最普遍的引用,例:
- 軟引用(Soft reference):
是一種相對強引用弱化一些的引用,可以讓對象豁免一些垃圾收集,只有當 JVM 認爲內存不足時,纔會去試圖回收軟引用指向的對象。JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。軟引用通常用來實現內存敏感的緩存,如果還有空閒內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。總結:
- 對象處在有用但非必須的狀態
- 只有當內存空間不足時,GC會回收該引用的對象內存
- 軟引用通常用來實現內存敏感的高速緩存
- 可以配合引用對象使用(ReferenceQueue)
- 弱引用(Weak reference):
弱引用並不能使對象豁免垃圾收集,僅僅是提供一種訪問在弱引用狀態下對象的途徑。這就可以用來構建一種沒有特定約束的關係,比如,維護一種非強制性的映射關係,如果試圖獲取時對象還在,就使用它,否則重現實例化。它同樣是很多緩存實現的選擇。總結:
- 用於描述非必須的對象,比軟引用更弱一些
- 發生GC時就會被回收掉,不過被回收的概率也不大,因爲GC線程優先級比較低
- 適用於引用偶爾被使用且不影響垃圾收集的對象
- 可以配合引用對象使用(ReferenceQueue)
- 虛引用(Phantom reference),也被稱爲幻象引用:
對於虛引用,你不能通過它訪問對象。虛引用僅僅是提供了一種確保對象被 finalize 以後,做某些事情的機制,比如,通常用來做所謂的 Post-Mortem 清理機制,例如 Java 平臺自身 Cleaner 機制等,也有人利用幻象引用監控對象的創建和銷燬。總結:
- 不會決定對象的生命週期
- 虛引用的對象任何時候都可能被垃圾收集器回收,就像是沒有引用的對象一樣
- 虛引用通常用來跟蹤對象被垃圾收集器回收的活動,起哨兵作用
- 與軟引用和弱引用不同的是,該引用必須與引用對列(ReferenceQueue)聯合使用
軟引用代碼示例:
// 強引用
String str = new String("abc");
// 轉換爲軟引用
SoftReference<String> softReference = new SoftReference<>(str);
弱引用代碼示例:
String str = new String("abc");
// 弱引用
WeakReference<String> weakReference = new WeakReference<>(str);
虛引用代碼示例:
String str = new String("abc");
// 引用隊列
ReferenceQueue<String> queue = new ReferenceQueue<>();
// 轉換爲虛引用
PhantomReference<String> phantomReference = new PhantomReference<>(str, queue);
// GC在回收一個對象時,如果發現該對象存在虛引用,那麼在回收之前會先將該對象的虛引用添加到與該對象關聯的引用隊列中;程序代碼可以通過判斷引用隊列是否已加入虛引用來得知被引用的對象是否已經被回收
引用隊列(ReferenceQueue):
- ReferenceQueue無實際的存儲結構,其存儲邏輯依賴於內部節點之間的關係來表達
- 存儲關聯的且被GC後的軟引用,弱引用以及虛引用
引用強度關係:
- 強引用 > 軟引用 > 弱引用 > 虛引用
下面流程圖簡單總結了對象生命週期和不同可達性狀態,以及不同狀態可能的改變關係:
上圖的具體狀態,實際是 Java 定義的不同可達性級別(reachability level),在之前也說過判斷對象可達性,是 JVM 垃圾收集器決定如何處理對象的一部分考慮。可達性具體含義如下:
- 強可達(Strongly Reachable),就是當一個對象可以有一個或多個線程可以不通過各種引用訪問到的情況。比如,我們新創建一個對象,那麼創建它的線程對它就是強可達。
- 軟可達(Softly Reachable),就是當我們只能通過軟引用才能訪問到對象的狀態。
- 弱可達(Weakly Reachable),類似前面提到的,就是無法通過強引用或者軟引用訪問,只能通過弱引用訪問時的狀態。這是十分臨近 finalize 狀態的時機,當弱引用被清除的時候,就符合 finalize 的條件了。
- 幻象可達(Phantom Reachable),上面流程圖已經很直觀了,就是沒有強、軟、弱引用關聯,並且 finalize 過了,只有幻象引用指向這個對象的時候。
- 當然,還有一個最後的狀態,就是不可達(unreachable),意味着對象可以被清除了。
各引用包裝類的繼承關係圖:
下面我們來用一個例子演示引用包裝對象及引用隊列的使用,首先定義一個普通的類,並且實現finalize方法以便我們在測試時可以看到該對象是否被GC回收了:
package com.example.demo.gc;
/**
* @author 01
* @date 2019-07-18
**/
public class NormalObject {
public String name;
public NormalObject(String name) {
this.name = name;
}
@Override
protected void finalize() throws Throwable {
System.out.println("finalizing obj: " + name);
}
}
然後定義一個WeakReference的子類,目的是擴展name屬性,以便我們在測試時能夠得知是哪個對象的引用對象:
package com.example.demo.gc;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
/**
* @author 01
* @date 2019-07-18
**/
public class NormalObjectWeakReference extends WeakReference<NormalObject> {
public String name;
public NormalObjectWeakReference(NormalObject referent, ReferenceQueue<NormalObject> queue) {
super(referent, queue);
this.name = referent.name;
}
@Override
protected void finalize() throws Throwable {
System.out.println("finalizing NormalObjectWeakReference: " + name);
}
}
最後編寫一個測試類:
package com.example.demo.gc;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* @author 01
* @date 2019-07-18
**/
public class ReferenceQueueTests {
// 引用隊列
private static ReferenceQueue<NormalObject> queue = new ReferenceQueue<>();
/**
* 檢查引用隊列裏有沒有引用對象,有的話則打印相關信息
*/
private static void checkQueue() {
Reference<? extends NormalObject> reference;
while ((reference = queue.poll()) != null) {
// 存在於引用隊列中的引用對象
System.out.println("In Queue: " + ((NormalObjectWeakReference) (reference)).name);
// 獲取引用的對象實例
System.out.println("reference object: " + reference.get());
}
}
public static void main(String[] args) throws InterruptedException {
List<WeakReference<NormalObject>> weakReferenceList = new ArrayList<>();
// 創建引用對象
for (int i = 0; i < 3; i++) {
NormalObject normalObject = new NormalObject("Weak-" + i);
NormalObjectWeakReference reference = new NormalObjectWeakReference(normalObject, queue);
weakReferenceList.add(reference);
System.out.println("Create weak: " + reference);
}
System.out.println("\nbefore gc ------");
checkQueue();
System.out.println("\ngc ing... ------");
System.gc();
// 讓線程休眠一會,以保gc能夠正常執行完畢
Thread.sleep(1000);
System.out.println("\nafter gc ------");
checkQueue();
}
}
運行結果:
可以看到在GC執行之前調用checkQueue方法沒有打印任何信息,因爲此時引用隊列中沒有任何引用對象。而當GC執行之後,引用隊列中就被添加了與之相關聯的引用對象,所以就能夠打印出引用對象的相關信息
GC相關參考文章: