JVM(2):垃圾回收
一 如何判斷物件可以回收
通常由兩種方法可以判斷物件是否為垃圾物件:
- 引用計數法
- 可達性分析演算法
1.1 引用計數法:
定義
:給物件中新增一個引用計數器,每當有一個地方引用它時,計數器就加1,當引用失效時,計數器值就減1,任何時刻計數器為0的物件就是不可能再被使用的優點
:實現簡單,效率很高。弊端
:迴圈引用時,兩個物件的計數都為1,導致兩個物件都無法被釋放
1.2 可達性分析演算法
- JVM中的垃圾回收器通過可達性分析來探索所有存活的物件
- 掃描堆中的物件,看能否沿著GC Root物件為起點的引用鏈找到該物件,如果找不到,則表示可以回收
- 可以作為GC Root的物件
虛擬機器棧(棧幀中的本地變量表)中引用的物件。
方法區中類靜態屬性引用的物件
方法區中常量引用的物件
本地方法棧中JNI(即一般說的Native方法)引用的物件
1.3 五種引用
強引用
只有GC Root都不引用該物件時,才會回收強引用物件
如上圖B、C物件都不引用A1物件時,A1物件才會被回收
軟引用
當GC Root指向軟引用物件時,在記憶體不足時,會回收軟引用所引用的物件
如上圖如果B物件不再引用A2物件且記憶體不足時,軟引用所引用的A2物件就會被回收
軟引用的使用
public class Demo1 { public static void main(String[] args) { final int _4M = 4*1024*1024; //使用軟引用物件 list和SoftReference是強引用,而SoftReference和byte陣列則是軟引用 List<SoftReference<byte[]>> list = new ArrayList<>(); SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]); } }
如果在垃圾回收時發現記憶體不足,在回收軟引用所指向的物件時,軟引用本身不會被清理
如果想要清理軟引用,需要使用引用佇列
public class Demo1 { public static void main(String[] args) { final int _4M = 4*1024*1024; //使用引用佇列,用於移除引用為空的軟引用物件 ReferenceQueue<byte[]> queue = new ReferenceQueue<>(); //使用軟引用物件 list和SoftReference是強引用,而SoftReference和byte陣列則是軟引用 List<SoftReference<byte[]>> list = new ArrayList<>(); SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]); //遍歷引用佇列,如果有元素,則移除 Reference<? extends byte[]> poll = queue.poll(); while(poll != null) { //引用佇列不為空,則從集合中移除該元素 list.remove(poll); //移動到引用佇列中的下一個元素 poll = queue.poll(); } } }
大概思路為:檢視引用佇列中有無軟引用,如果有,則將該軟引用從存放它的集合中移除(這裡為一個list集合)
弱引用
只有弱引用引用該物件時,在垃圾回收時,無論記憶體是否充足,都會回收弱引用所引用的物件
如上圖如果B物件不再引用A3物件,則A3物件會被回收
弱引用的使用和軟引用類似,只是將 SoftReference 換為了 WeakReference
弱引用程式碼演示:
package cn.itcast.jvm.t2;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* 演示弱引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_5 {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
// list --> WeakReference --> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
System.out.println("迴圈結束:" + list.size());
}
}
終結器引用
所有的類都繼承自Object類,Object類有一個finalize方法。當某個物件不再被其他的物件所引用時,會先將終結器引用物件放入引用佇列中,然後根據終結器引用物件找到它所引用的物件,然後呼叫該物件的finalize方法。呼叫以後,該物件就可以被垃圾回收了
如上圖,B物件不再引用A4物件。這是終結器物件就會被放入引用佇列中,引用佇列會根據它,找到它所引用的物件。然後呼叫被引用物件的finalize方法。呼叫以後,該物件就可以被垃圾回收了
引用佇列
軟引用和弱引用可以配合引用佇列
在弱引用和虛引用所引用的物件被回收以後,會將這些引用放入引用佇列中,方便一起回收這些軟/弱引用物件
虛引用和終結器引用必須配合引用佇列
虛引用和終結器引用在使用時會關聯一個引用佇列
二 垃圾回收演算法
2.1 標記-清除演算法
定義:標記清除演算法顧名思義,是指在虛擬機器執行垃圾回收的過程中,先採用標記演算法確定可回收物件,然後垃圾收集器根據標識清除相應的內容,給堆記憶體騰出相應的空間
這裡的騰出記憶體空間並不是將記憶體空間的位元組清0,而是記錄下這段記憶體的起始結束地址,下次分配記憶體的時候,會直接覆蓋這段記憶體
缺點:容易產生大量的記憶體碎片,可能無法滿足大物件的記憶體分配,一旦導致無法分配物件,那就會導致jvm啟動gc,一旦啟動gc,我們的應用程式就會暫停,這就導致應用的響應速度變慢
2.2 標記-整理演算法
標記-整理 會將不被GC Root引用的物件回收,清楚其佔用的記憶體空間。然後整理剩餘的物件,可以有效避免因記憶體碎片而導致的問題,但是因為整體需要消耗一定的時間,所以效率較低
2.3 複製演算法
將記憶體分為等大小的兩個區域,FROM和TO(TO中為空)。先將被GC Root引用的物件從FROM放入TO中,再回收不被GC Root引用的物件。然後交換FROM和TO。這樣也可以避免記憶體碎片的問題,但是會佔用雙倍的記憶體空間。
三 分代回收
回收流程
新建立的物件都被放在了新生代的伊甸園中
當伊甸園中的記憶體不足時,就會進行一次垃圾回收,這時的回收叫做 Minor GC
Minor GC 會將伊甸園和倖存區FROM存活的物件先複製到 倖存區 TO中, 並讓其壽命加1,再交換兩個倖存區
再次建立物件,若新生代的伊甸園又滿了,則會再次觸發 Minor GC(會觸發 stop the world, 暫停其他使用者執行緒,只讓垃圾回收執行緒工作),這時不僅會回收伊甸園中的垃圾,還會回收倖存區中的垃圾,再將活躍物件複製到倖存區TO中。回收以後會交換兩個倖存區,並讓倖存區中的物件壽命加1
如果倖存區中的物件的壽命超過某個閾值(最大為15,4bit),就會被放入老年代中
如果新生代老年代中的記憶體都滿了,就會先觸發Minor GC,再觸發Full GC,掃描新生代和老年代中所有不再使用的物件並回收,如果回收結束後,還是放不下,則會丟擲堆記憶體溢位異常。
GC 分析
-
大物件處理策略
當遇到一個較大的物件時,就算新生代的伊甸園為空,也無法容納該物件時,會將該物件直接晉升為老年代 -
執行緒記憶體溢位
某個執行緒的記憶體溢位了而拋異常(out of memory),不會讓其他的執行緒結束執行
這是因為當一個執行緒丟擲OOM異常後,它所佔據的記憶體資源會全部被釋放掉,從而不會影響其他執行緒的執行,程序依然正常