做爲一名Java編程者,想要往高級進階,內存管理每每是避不開的環節,而垃圾回收 如下簡稱GC(Garbage Collection)
機制做爲內存管理最重要的一個部分,是咱們必需要掌握的。今天就分享下我對 垃圾回收機制 與 分代回收策略 的理解.java
1. 背景算法
2. 兩種回收機制編程
3. 回收算法緩存
4. 分代回收策略bash
5. 四大引用ide
通常來說,在咱們編程的過程當中是會不斷的往內存中寫入數據的,而這些數據用完了要及時從內存中清理,不然會引起OutOfMemory(內存溢出)
,因此每一個編程者都必須聽從這一原則。據說(我也不懂C語言~)在C語言階段,
垃圾是須要編程者本身手動回收的,而咱們Javaer相對來講就要幸福多了,由於JVM存在GC機制
,也就是說JVM會幫咱們自動清理垃圾,但幸福也是有代價的,由於老是會有些垃圾對象陰差陽錯的避開GC算法
,這一現象也稱之爲內存泄漏
,因此只有掌握了GC機制才能避免寫出內存泄漏的程序。spa
什麼是引用計數呢?打個比方A a = new A()
,代碼中 A 對象被引用 a 所持有,此時引用計數就會 +1 ,若是 a 將引用置爲 null 即a = null
此時對象 A 的引用計數就會變爲 0 ,GC算法檢測到 A 對象引用計數爲 0 就會將其回收。很簡單,但引用計數存在必定弊端線程
場景以下:3d
A a = new A();
B b = new B();
a.next = b;
b.next = a;
a = null;
b = null;
複製代碼
執行完上述代碼後 A 和 B 對象會被回收嗎?看似引用都已經置爲 null ,但實際上 a 和 b 的 next 分別持有對方引用,造成了一種相互持有引用的局面,致使A 和 B 即便成了垃圾對象且不能被回收。有些同窗可能會說,內存泄漏太容易看出來了, a 和 b 置空前將各自的 next 置爲空不就完了。嗯,這樣說沒錯,可是在實際業務中面對龐大的業務邏輯內存泄漏是很難一眼看出的。因此JVM在後來摒棄了引用計數,採用了可達性分析。調試
可達性分析實際上是數學中的一個概念,在JVM中,會將一些特殊的引用做爲 GcRoot ,若是經過 GcRoot 能夠訪達的對象不會被看成垃圾對象。換種方式說就是,一個對象被 GcRoot 直接 或 間接持有,那麼該對象就不會被看成垃圾對象。用一張圖表示大概就是這個樣子:
圖中A、B、C、D可
以被 GcRoot 訪達,因此不會被回收。E、F
不能被 GcRoot 訪達,因此會被標記爲垃圾對象。最典型的是G、H
,雖然說相互引用,但不能被 GcRoot 訪達,因此也會被標記爲垃圾對象。綜上所述: 可達性分析 能夠解決 引用計數 中 對象相互引用 不能被回收的問題。
什麼類型的引用可做爲 GcRoot 呢。 大概有以下四種:
- 棧中局部變量
- 方法區中靜態變量
- 方法區中常量
- 本地方法棧JNI的引用對象
千萬不要把引用和對象兩個概念混淆,對象是實實在在存在於內存中的,而引用只是一個變量/常量並持有對象在內存中的地址指。
下面我來經過一些代碼來驗證幾種 GcRoot
筆者是用Android代碼進行調試,不懂Android的同窗把onCreate
視爲main
方法便可。
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
method();
}
private void method(){
Log.i("test","method start");
A a = new A();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i("test","method end");
}
class A{
@Override
protected void finalize() throws Throwable {
Log.i("test","finalize A");
}
}
}
複製代碼
提示
- 在java中一個對象被回收會調用其
finalize
方法- JVM中垃圾回收是在一個單獨線程進行。爲了更好的驗證效果,在此加2000毫秒延時
打印結果以下:
17:58:57.526 method start
17:58:59.526 method end
17:58:59.591 finalize A
複製代碼
method 方法執行時間是2000毫秒,對象A 在 method 方法結束當即被回收。因此能夠認定棧中局部變量可做爲 GcRoot
public class MyApp extends Application {
private static A a;
@Override
public void onCreate() {
super.onCreate();
Log.i("test","onCreate");
a = new A();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
a = null;
Log.i("test","a = null");
}
}
複製代碼
打印結果以下:
18:12:35.988 a = new A()
18:12:38.028 a = null
18:12:38.096 finalize A
複製代碼
建立一個 A 對象賦值給靜態變量 a , 2000毫秒後將靜態變量 a 置爲空。經過日誌能夠看出對象 A 在靜態變量 a 置空後被當即回收。因此能夠認定靜態變量可做爲 GcRoot
方法區常量與靜態變量驗證過程徹底一致,關於native 驗證過程比較複雜,感興趣的同窗可自行驗證。
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
A a = new A();
B b = new B();
a.b = b;
a = null;
}
class A{
B b;
@Override
protected void finalize() throws Throwable {
Log.i("test","finalize A");
}
}
class B{
@Override
protected void finalize() throws Throwable {
Log.i("test","finalize B");
}
}
}
複製代碼
打印結果以下:
13:14:58.999 finalize A
13:14:58.999 finalize B
複製代碼
經過日誌能夠看出,A、B 兩個對象都被回收。雖然 B 對象被 A 對象中的 b 引用所持有,但成員變量不能被做爲 GcRoot, 因此B 對象不可達,進而會被看成垃圾。
上一小結描述了 GC 機制,但具體實現仍是要靠算法,下面我簡單描述一下幾種常見的 GC算法。
獲取全部的 GcRoot 遍歷內存中全部的對象,若是能夠被 GcRoot 就加個標記,剩下全部的對象都將視爲垃圾被清除。
- 優勢:實現簡單,執行效率高
- 缺點:容易產生 內存碎片(可用內存分佈比較分散),若是須要申請大塊連續內存可能會頻繁觸發 GC
將內存分爲兩塊,每次只是用其中一塊。首先遍歷全部對象,將可用對象複製到另外一塊內存中,此時上一塊內存可視爲全是垃圾,清理後將新內存塊置爲當前可用。如此反覆進行
- 優勢:解決了內存碎片的問題
- 缺點:須要按順序分配內存,可用內存變爲原來的一半。
獲取全部的 GcRoot , GcRoot 開始從遍歷內存中全部的對象,將可用對象壓縮到另外一端,再將垃圾對象清除。實則是犧牲時間複雜度來下降空間複雜度
- 優勢:解決了標記清除的 內存碎片 ,也不須要複製算法中的 內存分塊
- 缺點:仍須要將對象進行移動,執行效率略低。
在JVM中 垃圾回收器 是很繁忙的,若是一個對象存活時間較長,避免重複 建立/回收 給 垃圾回收器 進一步形成負擔,能不能犧牲點內存把它緩存起來? 答案是確定的。JVM制定了 分代回收策略 爲每一個對象設置生命週期 ,堆內存會劃分不一樣的區域,來存儲各生命週期的對象。通常狀況下對象的生命週期有 新生代、老年代、永久代(java 8已廢棄)。
首先來看新生代內存結構示意圖:
按照8:1:1將新生代內存分爲 Eden、SurvivorA、SurvivorB
新生代內存工做流程:
- 當一個對象剛被建立時會放到 Eden 區域,當 Eden 區域即將存滿時作一次垃圾回收,將當前存活的對象複製到 SurvivorA ,隨後將 Eden 清空
- 當Eden 下一次存滿時,再作一次垃圾回收,先將存活對象複製到 SurvivorB ,再把 Eden 和 SurvivorA 全部對象進行回收,
- 當Eden 再一次存滿時,再作一次垃圾回收,將存活對象複製到 SurvivorA,再把 Eden 和 SurvivorB對象進行回收。如此反覆進行大概 15 次,將最終依舊存活的對象放入到老年代區域。
新生代工做流程與 複製算法 應用場景較爲吻合,都是以複製爲核心,因此會採用複製算法。
根據對 上一小節 咱們能夠得知 當一個對象存活時間較久會被存入到 老年代 區域。 老年代 區即將被存滿時會作一次垃圾回收,
因此 老年代 區域特色是存活對象多、垃圾對象少,採用標記壓縮 算法時移動少、也不會產生內存碎片。因此老年代 區域能夠選用 標記壓縮 算法進一步提高效率。
在咱們開發程序的過程當中,避免不了會建立一些比較大的對象,好比Android中用於承載像素信息的Bitmap
,使用稍有不當就會形成內存泄漏,若是存在大量相似對象對內存影響仍是蠻大的。
爲了儘量避免上述狀況的出現,JVM爲咱們提供了四種對象引用方式:強引用、軟引用、弱引用、虛引用 供咱們選擇,下面我用一張表格來作一下類比
- 假設如下所述對象可被 GcRoot 訪達
引用類型 | 回收時機 |
---|---|
強引用 | 毫不會被回收(默認) |
軟引用 | 內存不足時回收 |
弱引用 | 第一次觸發GC時就會被回收 |
虛引用 | 隨時都會被回收,不存在實際意義 |
參考文獻:《Android 工程師進階 34 講》 第二講
文章從五個方面描述了 GC 機制。