JVM垃圾收集之——怎样判定一个对象是不是垃圾

2022-12-02 10:46:51 浏览数 (1)

文章目录

  • 1引用计数法
  • 2可达性分析
  • 3一个对象真的非死不可吗?
    • 3.1对象的自我救赎
    • 3.2finalize的作用
    • 3.3finalized的问题
    • 3.4finalize的执行过程(生命周期)

学过了JVM的内存模型,了解了JVM将其管理的内存抽象为不同作用的内存工作区域,这个区域是连续,然后分为五个部分,各司其职。 链接: JVM内存模型——运行时数据区的特点和作用

现在,让我们来学习一下JVM中的重头戏,垃圾收集

想要把一个对象当成垃圾回收掉,我们需要知道,不被需要和使用的对象才是垃圾,关键是怎么找到这些不被需要和使用的对象。

这里我们有两个方法可以去判定一个对象是不是垃圾:

1引用计数法

一个对象呢我给它做一个引用计数,假如一个对象目前有三个引用指向,那么给他记录一个引用数为3。接下来如果有一个引用消失了,变成二,再有一个引用消失变成一,最后当引用全部消失这个数变成零,当它变成零的时候,这对象成为了垃圾(Python 就是使用这样的方式)。 总结: 如果一个对象没有引用指向它的时候,或者说引用计数器里面的值为0的时候,表示该对象就是垃圾。 缺陷:当有循环引用的时候,导致无法回收掉本该是垃圾的对象。

那Java是使用的这一种垃圾回收方法吗? 举个栗子:

代码语言:javascript复制
public class ReferenceCountingGC { 
	public Object instance = null; 
	private static final int _1MB = 1024 * 1024; 
	/**
	* 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过 
	* */ 
	private byte[] bigSize = new byte[2 * _1MB]; 
	public static void testGC() { 
		ReferenceCountingGC objA = new ReferenceCountingGC(); 
		ReferenceCountingGC objB = new ReferenceCountingGC(); 
		objA.instance = objB; 
		objB.instance = objA; 
		objA = null;
		objB = null; 
		// 假设在这行发生GC,objA和objB是否能被回收? 
		System.gc(); 
	}
	public static void main(String[] args) { 
		testGC(); 
	} 
}

运行截图:

从上图可以看出,没有进行垃圾回收之前,内存占用11960K。进行垃圾回收之后,内存占用896K。说明对象确实被回收释放了。但如果按照引用计数算法,两个对象之间其实还存在着互相引用,即引用计数器的值为1,也就是说本来不应该被回收,所以这里使用的显然就不是引用计数算法。

2可达性分析

Java是使用一种叫GC Root的算法,是什么意思呢? 从根上的引用去找对象,能够被根节点引用找到的对象都不是垃圾,不用回收,如果是从根节点引用找不到的对象都是垃圾。

通过GC Root的对象,开始向下寻找,看某个对象是否可达

能作为GC Root:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。 JVM标准里给出了以下几种可以当作GC Root的对象: 1.在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。 2.在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。 3.在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。 4.在本地方法栈中JNI(即通常所说的Native方法)引用的对象。 5.Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。 6.所有被同步锁(synchronized关键字)持有的对象。 7.反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

我们研究的一直都是怎么让一个对象去死,但是

3一个对象真的非死不可吗?

3.1对象的自我救赎

即使在可达性分析算法中不可达的对象,并不是”非死不可“,要真正宣告一个对象死亡,至少要经历两次标记过程:

  1. 如果对象在进行可达性分析后发现没有与GCRoots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
  2. 当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过。

虚拟机将这两种情况都视为”没有必要执行“。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。 这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。 finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

3.2finalize的作用

  • finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。
  • finalize()与C 中的析构函数不是对应的。C 中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java中的finalize的调用具有不确定性
  • 不建议用finalize方法完成“非内存资源”的清理工作。

3.3finalized的问题

  • 一些与finalize相关的方法,由于一些致命的缺陷,已经被废弃了,如System.runFinalizersOnExit()方法、Runtime.runFinalizersOnExit()方法
  • System.gc()与System.runFinalization()方法增加了finalize方法执行的机会,但不可盲目依赖它们
  • Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行
  • finalize方法可能会带来性能问题。因为JVM通常在单独的低优先级线程中完成finalize的执行
  • 对象再生问题:finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的
  • finalize方法至多由GC执行一次(用户当然可以手动调用对象的finalize方法,但并不影响GC对finalize的行为)

由于Finalizer线程优先级相较于普通线程优先级要低,而根据Java的抢占式线程调度策略,优先级越低的线程,分配CPU的机会越少,因此当多线程创建重写finalize方法的对象时,Finalizer可能无法及时执行finalize方法,Finalizer线程回收对象的速度小于创建对象的速度时,会造成F-Queue越来越大,JVM内存无法及时释放,造成频繁的Young GC,然后是Full GC,乃至最终的OutOfMemoryError。

3.4finalize的执行过程(生命周期)

首先,大致描述一下finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。 执行代码演示:

代码语言:javascript复制
public class FinalizeEscapeGC { 
	public static FinalizeEscapeGC SAVE_HOOK = null; 
	public void isAlive() { 
		System.out.println("yes, i am still alive :)"); 
	}
	@Override 
	protected void finalize() throws Throwable { 
	super.finalize(); 
	System.out.println("finalize method executed!"); 
	FinalizeEscapeGC.SAVE_HOOK = this; 
	}
	public static void main(String[] args) throws Throwable {
		SAVE_HOOK = new FinalizeEscapeGC(); 
		//对象第一次成功拯救自己 
		SAVE_HOOK = null; 
		System.gc(); 
		// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
		Thread.sleep(500); 
		if (SAVE_HOOK != null) { 
			SAVE_HOOK.isAlive(); 
		} else { 
			System.out.println("once, i am dead :("); 
		}
		// 下面这段代码与上面的完全相同,但是这次自救却失败了 
		SAVE_HOOK = null; 
		System.gc(); 
		// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
		Thread.sleep(500);
		if (SAVE_HOOK != null) { 
			SAVE_HOOK.isAlive(); 
		} else { 
			System.out.println("second, i am dead :("); 
		} 
	} 
}

从结果可以看出,SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集前成功逃脱了。

另外一个值得注意的地方是,代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。

0 人点赞