郉臺網站建設百度關鍵詞搜索引擎排名優(yōu)化
目錄
垃圾判斷算法(你是不是垃圾?)
引用計數法
可達性算法
對象的引用
強引用
軟引用
弱引用
虛引用
對象的自我救贖
垃圾回收算法--分代
標記清除算法
復制算法
標記整理法
垃圾處理器
垃圾判斷算法(你是不是垃圾?)
引用計數法
最容易想到的一種方式是引用計數法,啥叫引用計數法,簡單地說,就是對象被引用一次,在它的對象頭上加一次引用次數,如果沒有被引用(引用次數為 0),則此對象可回收
String ref = new String("Java");
以上代碼 ref 引用了右側定義的對象,所以引用次數是 1
?
如果在上述代碼后面添加一個 ref = null,則由于對象沒被引用,引用次數置為 0,由于不被任何變量引用,此時即被回收,動圖如下
看起來用引用計數確實沒啥問題了,不過它無法解決一個主要的問題:循環(huán)引用!啥叫循環(huán)引用
public class TestRC { TestRC instance; public TestRC(String name) { } public static void main(String[] args) { // 第一步 TestRC a = new TestRC("a"); TestRC b = new TestRC("b"); // 第二步 a.instance = b; b.instance = a; // 第三步 a = null; b = null; }
}
按步驟一步步畫圖
到了第三步,雖然 a,b 都被置為 null 了,但是由于之前它們指向的對象互相指向了對方(引用計數都為 1),所以無法回收,也正是由于無法解決循環(huán)引用的問題,所以現代虛擬機都不用引用計數法來判斷對象是否應該被回收。
可達性算法
現代虛擬機基本都是采用這種算法來判斷對象是否存活,可達性算法的原理是以一系列叫做 GC Root 的對象為起點出發(fā),引出它們指向的下一個節(jié)點,再以下個節(jié)點為起點,引出此節(jié)點指向的下一個結點。(這樣通過 GC Root 串成的一條線就叫引用鏈),直到所有的結點都遍歷完畢, 如果相關對象不在任意一個以 GC Root 為起點的引用鏈中,則這些對象會被判斷為「垃圾」, 會被 GC 回收。
如圖示,如果用可達性算法即可解決上述循環(huán)引用的問題,因為從GC Root 出發(fā)沒有到達 a,b, 所以 a,b 可回收
a, b 對象可回收,就一定會被回收嗎? 并不是,對象的 finalize 方法給了對象一次垂死掙扎的機會,當對象不可達(可回收)時,當發(fā)生 GC 時,會先判斷對象是否執(zhí)行了 finalize 方法,如果未執(zhí)行,則會先執(zhí)行 finalize 方法,我們可以在此方法里將當前對象與 GC Roots 關聯,這樣執(zhí)行 finalize 方法之后,GC 會再次判斷對象是否可達,如果不可達,則會被回收,如果可達,則不回收!
注意: finalize 方法只會被執(zhí)行一次,如果第一次執(zhí)行 finalize 方法此對象變成了可達確實不會回收,但如果對象再次被 GC,則會忽略 finalize 方法,對象會被回收!這一點切記!
雖然可達性分析算法解決了循環(huán)依賴的問題,但是也產生了許多新的問題。進行可達性分析(搜索)的過程是需要時間的,而此時程序也是在并行運行著,不斷產生新的對象,丟棄無用的對象。整個內存的狀態(tài)是在變化的,所以目前主流的垃圾回收算法大多都要進行一種操作 Stop-The-World,翻譯過來即停止世界,即暫停所有的用戶線程,避免內存堆的狀態(tài)發(fā)生變化。
那么這些 GC Roots 到底是什么東西呢,哪些對象可以作為 GC Root 呢,有以下幾類
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中 JNI(即一般說的 Native 方法)引用的對象
虛擬機棧中引用的對象
如下代碼所示,a 是棧幀中的本地變量,當 a = null 時,由于此時 a 充當了 GC Root 的作用,a 與原來指向的實例 new Test() 斷開了連接,所以對象會被回收。
public class Test {public static void main(String[] args) {Test a = new Test(); a = null; }
}
方法區(qū)中類靜態(tài)屬性引用的對象
如下代碼所示,當棧幀中的本地變量 a = null 時,由于 a 原來指向的對象與 GC Root (變量 a) 斷開了連接,所以 a 原來指向的對象會被回收,而由于我們給 s 賦值了變量的引用,s 在此時是類靜態(tài)屬性引用,充當了 GC Root 的作用,它指向的對象依然存活!
public class Test {public static Test s; public static void main(String[] args) {Test a = new Test(); Test.s = new Test(); a = null; }
}
方法區(qū)中常量引用的對象
如下代碼所示,常量 s 指向的對象并不會因為 a 指向的對象被回收而回收
public class Test {public static final Test s = new Test(); public static void main(String[] args) { Test a = new Test(); a = null; }
}
?本地方法棧中 JNI(即一般說的 Native 方法)引用的對象
方法調用中的時候,是作為 GC Root ,如果方法調用完成了,這些 JNI 引用對象是可以被回收的,如果不想要這些 JNI 引用對象被回收,可以設置為全局變量。
如果想讓某些局部引用在從 C 函數返回后不被 JVM 回收,則可以借助 JNI 函數 NewGlobalRef
,將該局部引用轉換為全局引用。被全局引用的對象,不會被 JVM 回收,只能通過 JNI 函數 DeleteGlobalRef
消除全局引用后,才可以被回收。
這是簡單給不清楚本地方法為何物的童鞋簡單解釋一下:所謂本地方法就是一個 java 調用非 java 代碼的接口,該方法并非 Java 實現的,可能由 C 或 Python 等其他語言實現的, Java 通過 JNI 來調用本地方法, 而本地方法是以庫文件的形式存放的(在 WINDOWS 平臺上是 DLL 文件形式,在 UNIX 機器上是 SO 文件形式)。
當調用 Java 方法時,虛擬機會創(chuàng)建一個棧楨并壓入 Java 棧,而當它調用的是本地方法時,虛擬機會保持 Java 棧不變,不會在 Java 棧禎中壓入新的禎,虛擬機只是簡單地動態(tài)連接并直接調用指定的本地方法。
JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {... // 緩存String的class jclass jc = (*env)->FindClass(env, STRING_PATH);}
?
如上代碼所示,當 java 調用以上本地方法時,jc 會被本地方法棧壓入棧中, jc 就是我們說的本地方法棧中 JNI 的對象引用,因此只會在此本地方法執(zhí)行完成后才會被釋放。一旦從 C 函數中返回至 Java 方法中,那么局部引用將會失效,JVM 在整個 Tracing 過程中就不再考慮這些局部引用,也就是說,一段時間后,局部引用占用的內存將會被回收。
對象的引用
無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象是否引用鏈可達,判定對象是否存活都和“引用”離不開關系。
在JDK 1.2版之前,Java里面的引用是很傳統(tǒng)的定義:如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱該reference數據是代表某塊內存、某個對象的引用。這種定義并沒有什么不對,只是現在看來有些過于狹隘了,一個對象在這種定義下只有“被引用”或者“未被引用”兩種狀態(tài),對于描述一些“食之無味,棄之可惜”的對象就顯得無能為力。譬如我們希望能描述一類對象:當內存空間還足夠時,能保留在內存之中,如果內存空間在進行垃圾收集后仍然非常緊張,那就可以拋棄這些對象——很多系統(tǒng)的緩存功能都符合這樣的應用場景。
在JDK 1.2版之后,Java對引用的概念進行了擴充,將引用分為強引用(Strongly Re-ference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。
并不是說只要是和 GC Roots 有一條聯系(Reference Chain),對象就是存活的,它還與對象引用級別有關。
強引用
屬于普通常見的那種,也就是我們 new 出來的對象,無論任何情況下,只要強引用關系還存在,垃圾收集器就永遠不會回收掉被引用的對象。
軟引用
只被軟引用關聯著的對象,在系統(tǒng)將要發(fā)生內存溢出異常前,會把這些對象列進回收范圍之中進行第二次回收,如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。在JDK 1.2版之后提供了SoftReference類來實現軟引用。
弱引用
弱引用也是用來描述那些非必須對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發(fā)生為止(不太對,如果是 Young GC 不一定會回收,如果是 Full GC 則一定會回收,參考下面例子)。當垃圾收集器開始工作,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK 1.2版之后提供了WeakReference類來實現弱引用。
虛引用
虛引用也稱為“幽靈引用”或者“幻影引用”,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的只是為了能在這個對象被收集器回收時收到一個系統(tǒng)通知。在JDK 1.2版之后提供了PhantomReference類來實現虛引用。
栗子說明一下
添加配置JVM參數 -XX:+PrintGC -XX:+PrintGCDateStamps -Xmx5m -Xms5m
public class test {static class MyOb {private int i = 1;// 覆蓋finalize()方法@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("====== obj [" + this.toString() + "] is gc =======");}}// 消耗大量內存public static void drainMemory() {String[] array = new String[1024 * 10];for(int i = 0; i < 1024 * 10; i++) {for(int j = 'a'; j <= 'e'; j++) {array[i] += (char)j;}}}
}
首先先寫一個 覆蓋 finalize 方法的對象,進行 GC 的時候會進入,另外有個消耗大量內存的方法。
public static void main(String[] args) {MyOb ob = new MyOb();drainMemory();}
強引用的,我們看看輸出
2020-08-28T16:33:56.546+0800: [GC (Allocation Failure) 1024K->616K(5632K), 0.0011773 secs]
2020-08-28T16:33:56.578+0800: [GC (Allocation Failure) 1633K->827K(5632K), 0.0006751 secs]
2020-08-28T16:33:56.591+0800: [GC (Allocation Failure) 1851K->1049K(5632K), 0.0022424 secs]
2020-08-28T16:33:56.594+0800: [GC (Allocation Failure) 2073K->1273K(5632K), 0.0010249 secs]
2020-08-28T16:33:56.596+0800: [GC (Allocation Failure) 2297K->1521K(5632K), 0.0011336 secs]
明顯內存不足進行 GC 的時候,判斷到 ob 可達的,是不會進行回收的。
然后看看軟引用
public static void main(String[] args) {SoftReference<MyOb> ob = new SoftReference<>(new MyOb());drainMemory();}
2020-08-28T16:37:38.730+0800: [GC (Allocation Failure) 1024K->584K(5632K), 0.0013387 secs]
2020-08-28T16:37:38.764+0800: [GC (Allocation Failure) 1601K->842K(5632K), 0.0007544 secs]
2020-08-28T16:37:38.784+0800: [GC (Allocation Failure) 1866K->1066K(5632K), 0.0011751 secs]
2020-08-28T16:37:38.787+0800: [GC (Allocation Failure) 2090K->1314K(5632K), 0.0010511 secs]
2020-08-28T16:37:38.789+0800: [GC (Allocation Failure) 2338K->1554K(5632K), 0.0009488 secs]
同樣在沒有內存溢出時候,他和強引用是沒有區(qū)別的
我們把消耗內存的算法改一下,讓它產生內存溢出
// 消耗大量內存public static void drainMemory() {String[] array = new String[1024 * 1000];for(int i = 0; i < 1024 * 10; i++) {for(int j = 'a'; j <= 'b'; j++) {array[i] += (char)j;}}}
再重新跑一下
2020-08-28T16:39:38.310+0800: [GC (Allocation Failure) 1024K->624K(5632K), 0.0012934 secs]
2020-08-28T16:39:38.351+0800: [GC (Allocation Failure) 1641K->884K(5632K), 0.0011559 secs]
2020-08-28T16:39:38.365+0800: [GC (Allocation Failure) 1407K->934K(5632K), 0.0011081 secs]
2020-08-28T16:39:38.366+0800: [GC (Allocation Failure) 934K->942K(5632K), 0.0007891 secs]
2020-08-28T16:39:38.367+0800: [Full GC (Allocation Failure) 942K->706K(5632K), 0.0035185 secs]
2020-08-28T16:39:38.371+0800: [GC (Allocation Failure) 706K->706K(5632K), 0.0002779 secs]
2020-08-28T16:39:38.371+0800: [Full GC (Allocation Failure) 706K->690K(5632K), 0.0051638 secs]
====== obj [com.example.demo.conf.test$MyOb@51fe5ed6] is gc =======
Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat com.example.demo.conf.test.drainMemory(test.java:33)at com.example.demo.conf.test.main(test.java:27)
?可以看到再拋出 OOM 之前,會先清理掉這個對象(強引用 OOM 前如果還是可達的,也不會清理掉,可以自己試一下)
然后看弱引用
public static void main(String[] args) {ReferenceQueue<MyOb> queue = new ReferenceQueue<>();//創(chuàng)建一個引用給定對象并在給定隊列中注冊的新的弱引用。WeakReference<MyOb> ob = new WeakReference<>(new MyOb() , queue);drainMemory();Thread.sleep(1000);System.out.println("ob = " + ob.get());System.out.println("queue poll = " + queue.poll());}
記得把之前的調OOM的內存消耗部分修改回來
得到的結果有多種情況
2020-08-28T17:38:16.625+0800: [GC (Allocation Failure) 1024K->600K(5632K), 0.0008772 secs]
2020-08-28T17:38:16.657+0800: [GC (Allocation Failure) 1624K->839K(5632K), 0.0007530 secs]
2020-08-28T17:38:16.671+0800: [GC (Allocation Failure) 1863K->1009K(5632K), 0.0005883 secs]
====== obj [com.example.demo.conf.test$MyOb@51fe5ed6] is gc =======
2020-08-28T17:38:16.672+0800: [GC (Allocation Failure) 2033K->1201K(5632K), 0.0006779 secs]
2020-08-28T17:38:16.674+0800: [GC (Allocation Failure) 2225K->1353K(5632K), 0.0005505 secs]
2020-08-28T17:38:16.675+0800: [GC (Allocation Failure) 2377K->1529K(5632K), 0.0010291 secs]
ob = null
queue poll = java.lang.ref.WeakReference@270421f5
2020-08-28T17:39:50.249+0800: [GC (Allocation Failure) 1024K->592K(5632K), 0.0007809 secs]
2020-08-28T17:39:50.283+0800: [GC (Allocation Failure) 1610K->843K(5632K), 0.0029328 secs]
2020-08-28T17:39:50.304+0800: [GC (Allocation Failure) 1867K->1045K(5632K), 0.0008099 secs]
2020-08-28T17:39:50.306+0800: [GC (Allocation Failure) 2069K->1157K(5632K), 0.0012691 secs]
2020-08-28T17:39:50.308+0800: [GC (Allocation Failure) 2181K->1341K(5632K), 0.0009482 secs]
2020-08-28T17:39:50.313+0800: [GC (Allocation Failure) 2365K->1485K(5632K), 0.0014565 secs]
ob = com.example.demo.conf.test$MyOb@270421f5
queue poll = null
可以發(fā)現,Young GC 是不一定會回收,由系統(tǒng)來判斷回收的時機。
然后我們改一下代碼,改用 System.gc() 觸發(fā) FULL GC 看看有什么不同
public static void main(String[] args) {ReferenceQueue<MyOb> queue = new ReferenceQueue<>();//創(chuàng)建一個引用給定對象并在給定隊列中注冊的新的弱引用。WeakReference<MyOb> ob = new WeakReference<>(new MyOb() , queue);System.gc();System.out.println("ob = " + ob.get());System.out.println("queue poll = " + queue.poll());}
2020-08-28T18:25:22.967+0800: [GC (Allocation Failure) 1024K->632K(5632K), 0.0014284 secs]
2020-08-28T18:25:23.001+0800: [GC (Allocation Failure) 1650K->846K(5632K), 0.0008219 secs]
2020-08-28T18:25:23.016+0800: [GC (System.gc()) 1370K->886K(5632K), 0.0005198 secs]
2020-08-28T18:25:23.017+0800: [Full GC (System.gc()) 886K->707K(5632K), 0.0045606 secs]
====== obj [com.example.demo.conf.test$MyOb@51fe5ed6] is gc =======
ob = null
queue poll = null
可以發(fā)現 GC 后肯定會取消對象的引用
注意:上面的控制臺順序不一定和真實的gc順序一樣,關鍵得看 ob.get() 是否為空去判斷
對象的自我救贖
即使在可達性分析算法中判定為不可達的對象,也不是“非死不可”的,這時候它們暫時還處于“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析后發(fā)現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記,隨后進行一次篩選,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,那么虛擬機在發(fā)生GC的時候,只需要判斷對象可達 GC Roots 即可判定是否可回收。
如果這個對象被判定為確有必要執(zhí)行 finalize() 方法,那么該對象將會被放置在一個名為 F-Queue 的隊列之中,并在稍后由一條由虛擬機自動建立的、低調度優(yōu)先級的 Finalizer 線程去執(zhí)行它們的 finalize() 方法。這里所說的“執(zhí)行”是指虛擬機會觸發(fā)這個方法開始運行,但并不承諾一定會等待它運行結束。這樣做的原因是,如果某個對象的 finalize() 方法執(zhí)行緩慢,或者更極端地發(fā)生了死循環(huán),將很可能導致 F-Queue 隊列中的其他對象永久處于等待,甚至導致整個內存回收子系統(tǒng)的崩潰。
finalize() 方法是對象逃脫死亡命運的最后一次機會,稍后收集器將對 F-Queue 中的對象進行第二次小規(guī)模的標記,如果對象要在 finalize() 中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的要被回收了。從代碼清單3-2中我們可以看到一個對象的 finalize() 被執(zhí)行,但是它仍然可以存活。
并不鼓勵大家使用這個方法來拯救對象。相反,筆者建議大家盡量避免使用它,因為它并不能等同于C和C++語言中的析構函數,而是Java剛誕生時為了使傳統(tǒng)C、C++程序員更容易接受Java所做出的一項妥協。它的運行代價高昂,不確定性大,無法保證各個對象的調用順序,如今已被官方明確聲明為不推薦使用的語法。有些教材中描述它適合做“關閉外部資源”之類的清理性工作,這完全是對finalize()方法用途的一種自我安慰。finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及時,所以筆者建議大家完全可以忘掉Java語言里面的這個方法。
垃圾回收算法--分代
標記清除算法
步驟很簡單
- 先根據可達性算法標記出相應的可回收對象(圖中黃色部分)
- 對可回收的對象進行回收
優(yōu)點是不需要額外空間
缺點是兩次掃描耗時嚴重,會產生內存碎片
復制算法
把堆等分成兩塊區(qū)域, A 和 B,區(qū)域 A 負責分配對象,區(qū)域 B 不分配, 對區(qū)域 A 使用以上所說的標記法把存活的對象標記出來,然后把區(qū)域 A 中存活的對象都復制到區(qū)域 B(存活對象都依次緊鄰排列)最后把 A 區(qū)對象全部清理掉釋放出空間,這樣就解決了內存碎片的問題了。
不過復制算法的缺點很明顯,比如給堆分配了 500M 內存,結果只有 250M 可用,空間平白無故減少了一半!這肯定是不能接受的!另外每次回收也要把存活對象移動到另一半,效率低下(我們可以想想刪除數組元素再把非刪除的元素往一端移,效率顯然堪憂)
標記整理法
前面兩步和標記清除法一樣,不同的是它在標記清除法的基礎上添加了一個整理的過程 ,即將所有的存活對象都往一端移動, 緊鄰排列(如圖示),再清理掉另一端的所有區(qū)域,這樣的話就解決了內存碎片的問題。
但是缺點也很明顯:每進一次垃圾清除都要頻繁地移動存活的對象,效率十分低下。
垃圾處理器
名稱 | 負責區(qū)域 | 使用算法 | 描述 |
Serial | young | 復制算法 | 單線程,Client默認新生代處理器 |
ParNew | young | 復制算法 | 多線程,Serial的多線程版 |
Parallel Scavenge | young | 復制算法 | 多線程,注重停頓時間和吞吐量 |
Serial Old | old | 標記整理 | Client默認老年代處理器,Serial的老年版本,CMS的后備方案 |
Parallel Old | old | 標記整理 | 多線程,注重停頓時間和吞吐量 |
CMS | old | 標記清除 | 整個過程分為4個步驟:
|
G1 | young/old | 整體標記整理, 局部復制 | 多線程,內存分區(qū)(各分區(qū)大小相同,同一分區(qū)邏輯角色相同,都是新生代等,回收以分區(qū)為單位,復制到另一個分區(qū)),首先回收垃圾最多的分區(qū),低停頓,使用暫停預測模型。 |