Java垃圾收集器與內存分配策略

正文


一. 對象是否死亡

進行垃圾收集首先需要的就是判斷對象是否死亡, 也就是是否可以清除收集, 下面將講解判斷對象是否死亡的方法

1.1 引用計數算法

1.1.1 原理

給對象添加一個引用計數, 每當有一個地方引用它, 計數器值就加1; 當引用失效時, 計數器值就減1; 任何時刻計數器值為0的對象就是不可能再被使用的

1.1.2 優缺點

優點: 實現簡單, 效率高

缺點: 無法解決循環引用的問題, 如下;

主流Java虛擬機中沒有使用引用計數來管理內存的

public class ReferenceCountingGC {
public Object instance = null;

private final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB]; // 佔用內存, 以便GC日誌中看清楚是否被回收過
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}

1.2 可達性分析算法

1.2.1 原理

通過一系列成為GC Root的對象作為起始點, 從這些節點開始往下搜索, 搜索走過的路徑稱為引用鏈, 當一個對象到GC Root沒有任何引用鏈相連時, 則證明此對象是不可引用的; 如下圖(圖片來自)

Java垃圾收集器與內存分配策略

可作為GC Root的對象:

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

使用可達性分析算法時, 至少需要經過兩次標記過程才能宣告一個對象真正死亡: 如果對象在進行可達性分析後發現沒有與GC Root相連接的引用鏈, 那它會被第一次標記並且進行一次篩選, 篩選的條件是此對象是否有必要執行finalize()方法, 當對象沒有覆蓋finalize()方法時, 或者finalize()方法已經被虛擬機調用過, 虛擬機將這兩種情況都視為沒有必要執行(也就是說finalize()方法最多執行一次, 這點需要注意~); 如果這個對象被判定為有必要執行finalize()方法, 那麼這個對象將會放置在一個叫做F-Queue的隊列中, 並在稍後由一個由虛擬機自動建立, 低優先級的Finalizer線程去執行它, 但並不承諾會等待finalize()方法運行結束(這裡也需要注意, 這是為了防止finalize()中出現耗時操作甚至死循環); 在finalize()中對象可以使自己再次加入引用鏈中, 逃脫GC; 但是不建議使用該方法來逃脫GC

public class FinalizeEscapeGC {
public static FinalizeEscapeGC finalizeEscapeGC = null;
int count = 0;
public void isAlive() {
System.out.println("Yes, I am alive :) ");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed");
FinalizeEscapeGC.finalizeEscapeGC = this; // 再次進入引用鏈
// 經過驗證, 虛擬機不會等待finalize()執行完畢
// while (true) {
// System.out.println(count++);
// }
}
public static void main(String[] args) throws InterruptedException {
finalizeEscapeGC = new FinalizeEscapeGC();
finalizeEscapeGC = null;
System.gc();
Thread.sleep(500);
if (finalizeEscapeGC == null) {
System.out.println("No, I am dead :(");
} else {
finalizeEscapeGC.isAlive();
}
finalizeEscapeGC = null;
System.gc();
Thread.sleep(500);
if (finalizeEscapeGC == null) {
System.out.println("No, I am dead :(");
} else {
finalizeEscapeGC.isAlive();
}
}
}

上述程序運行結果為: 可以看出, finalize()確實被調用了, 而且只被調用了一次

finalize method executed
Yes, I am alive :)
No, I am dead :(

1.3 再談引用

JDK1.2以前, Java引用很傳統: 如果reference類型的數據中存儲的數值代表的是另一塊內存的起始地址, 就稱這塊內存代表著一個引用

但是實際我們希望的是: 當內存空間還足夠時, 能夠保留在內存中; 當內存空間在進行垃圾收集後還是非常緊張, 則可以拋棄某些對象

JDK1.2之後, 擴充了引用概念:

: 以下引用強度遞減

  1. 強引用: 類似Object obj = new Object()的引用; 這類引用永遠不會被垃圾收集器回收
  2. 軟引用: 有用但非必需的對象; 在系統將要發生內存溢出之前, 將回收這部分對象, 如果回收之後內存仍然不夠, 才會內存溢出
  3. 弱引用: 無論當前內存是否足夠都會回收掉只被弱引用關聯的對象
  4. 虛引用: 一個對象是否有虛引用的存在, 完全不會對其生存時間構成影響, 也無法通過一個虛引用來取得對象實例; 為一個對象設置虛引用關聯的唯一目的是能在這個對象被收集器回收之前收到一個系統通知

1.4 方法區回收

GC的主要區域是堆, 但是方法區也會進行GC; 方法區的GC主要是廢棄常量和無用的類

廢棄常量好理解, 和堆中對象回收一樣, 沒有引用即回收

無用類需要滿足條件:

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

二. 垃圾收集算法

2.1 標記-清除算法(Mark-Sweep)

2.1.1 原理

先標記, 後回收咯

如下圖:

Java垃圾收集器與內存分配策略

2.1.2 優缺點

  1. 標記和清除的效率都不高
  2. 產生大量不連續的內存碎片

2.2 複製算法

多用於新生代

2.2.1 原理

將內存分成相等的兩塊, 每次只使用其中一塊, 當這一塊用完了, 就將還存活的對象複製到另一塊, 然後把使用過的內存空間一次清掉

如下圖:

Java垃圾收集器與內存分配策略

2.2.2 優缺點

  1. 效率高, 無碎片
  2. 每次可使用內存減半

2.3 標記-整理算法

多用於老年代

2.3.1 原理

讓所有存活對象都向一端移動, 然後清理掉端邊界以外的內存

如下圖:

Java垃圾收集器與內存分配策略

2.4 分代收集算法

2.4.1 分代

分代的依據是: 不同對象的生命週期不同; 因此不同對象可以採用不同的回收方式, 以便提高回收效率; 因為如果每次都對整個堆進行回收遍歷的話, 對於生命週期較長的對象而言, 實際上有很多次回收都是不必要和沒用的

分代:

Java垃圾收集器與內存分配策略

分為年輕代(Young Generation), 老年代(Old Generation)和永久代(Permanent Generation); 永久代主要存放的是Java類的類信息, 與垃圾收集要收集的Java對象關係不大; 年輕代和老年代的劃分是對垃圾收集影響比較大的

2.4.2 原理

新生代採用複製算法, 有老年代作空間擔保; 老年代中因為對象存活率高, 沒有額外空間對它進行分配擔保, 就必須使用標記-清理或者標記-整理算法


三. 內存分配與回收策略

  1. 對象優先在Eden分配, 當Eden沒有足夠空間時, 將發起一次Minor GC(Minor GC指發生在新生代的GC, 較為頻繁, 但是速度也快; Major GC指的是發生在老年代的GC; Full GC指的是發生在整個堆空間的GC, 包括老年代和新生代)
  2. 大對象直接進入老年代, 這是為了防止在新生代的Eden和兩個Survivor區之間發生大量的內存複製
  3. 長期存活的對象直接進入老年代; 虛擬機給每個對象定義一個年齡計數器, 如果對象在Eden區出生並經過第一Minor GC後仍然存活, 並且能被Survivor區容納, 將被移到Survivor區中, 並且對象年齡設置為1, 對象在Survivor區中沒度過一次Minor GC, 年齡就增1, 當其年齡達到一定程度(默認15歲), 則移入老年代
  4. 動態對象年齡判定, 如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半, 年齡大於或等於該年齡的對象就可以直接進入老年代, 而不必等到年齡達到閾值
  5. 空間分配擔保: 因為新生代採用複製算法, 可能會出現複製空間不足的情況, 老年代可以為新生代作內存空間擔保

四. Others

這裡提一些其他的要點~

  1. GC時會發生Stop the World, 由此可以聯想, 也有可能由於GC造成的Android白屏~
  2. 安全點和安全區: 引用關係不變; GC只有在程序執行達到安全點或者安全區的時候才能進行; 如何讓所有線程都跑到最近的安全點? 有兩種方法: 搶先式中斷(GC時, 首先把所有線程全部中斷, 如果發現有線程不在安全點上, 則恢復該線程, 讓它跑到安全點)和主動式中斷(GC時, 不直接對線程中斷, 僅僅簡單地設置一個標誌, 各個線程執行時主動去輪詢該標誌, 發現該標誌為真時就自己中斷掛起)(多采用主動式中斷)

注:對Java微服務、分佈式、高併發、高可用、大型互聯網架構技術、面試經驗交流感興趣的。可以關注我的頭條號,我會在微頭條不定期的發放免費的資料,歡迎分享,歡迎評論,歡迎轉發!


分享到:


相關文章: