1. 程式人生 > 其它 >JVM(2):垃圾回收

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異常後,它所佔據的記憶體資源會全部被釋放掉,從而不會影響其他執行緒的執行,程序依然正常