前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM垃圾收集之——怎样判定一个对象是不是垃圾

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

作者头像
向着百万年薪努力的小赵
发布2022-12-02 10:46:51
3070
发布2022-12-02 10:46:51
举报
文章被收录于专栏:小赵的Java学习

文章目录

学过了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()方法不会被再次执行,因此第二段代码的自救行动失败了。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-07-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 1引用计数法
  • 2可达性分析
  • 3一个对象真的非死不可吗?
    • 3.1对象的自我救赎
      • 3.2finalize的作用
        • 3.3finalized的问题
          • 3.4finalize的执行过程(生命周期)
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档