JVM:如何判断对象可以回收

一、引用计数法

引用计数法是指给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时候计数器值为0就表示不会再被任何对象使用。
客观的说,引用计数法(Reference Counting)的实现简单,判断效率也很高,在大部分情况下都是一个不错的算法。但在主流的Java虚拟机里面没有使用引用计数法来管理内存,主要原因是它很难解决对象之间相互循环引用的问题。

public class ReferenceCountingGC {
	public Object instance=null;
	private static final int _1MB=1024*1024;
	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;
		System.gc();
	}
	public static void main(String []args){
		testGC();
	}
}

在这里插入图片描述
从运行结果可以看出,内存被回收了,说明虚拟机并没有这两个对象相互引用就没有回收他们,也从侧面说明了虚拟机不是通过引用计数法来判断对象是否存活的。

二、可达性分析算法

在主流的商用程序语言(Java、C#)的主流实现中,都是通过可达性分析(ReachabilityAnalysis)来判断对象是否存活的。这个算法的基本思路是通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链(就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 本地方法栈中JNI(即Native方法)引用的对象。
  • 在虚拟机内部de引用,如基本数据类型对应的Class对象,一些常驻的异常对象(譬如NullPointException、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册de回调、本地代码缓存等。
    在这里插入图片描述
    除类这些固定的GC Roots集合外,根据用户所选用的垃圾收集器已及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完成GC Roots集合。譬如分代收集器和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集),必须考虑到内存区域是虚拟机自己de实现细节(在用户视角里任何内存区域都是不可见的),更不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。

三、再谈引用

无论是通过引用计数判断对象的引用数量,还是通过可达性分析判断对象的引用链是否可达,判断对象是否存活都与引用相关。在JDK1.2以前,在Java中的引用的定义很传统:如果reference类型的数据中存储的数值是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但也很狭隘,一个对象在这种状态下只有被引用和没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就很无能为力。我们希望能描述这样一类对象:对于内存空间还足够时,能保留在内存中,如果内存空间在垃圾回收后还是很紧张时就可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

在JDK1.2之后,Java对引用的概念做了扩充,将引用分为强引用(StrongReference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(PhantomReference)4种,这4种引用的强度依次减弱。

四、生存还是死亡

即使在可达性分析算法中不可达的对象,也不是“非死不可”的,这时候它们暂时处于缓刑阶段,要真正宣告一个对象死亡,至少要经过两次标记过程:如果对象经过可达性分析后发现没有到GCRoots的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有执行finalize()方法的必要。当对象没有覆盖finalize()方法或者finalize()方法已经被执行过,虚拟机将这两种情况都视为“没有必要执行”。

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

/**
 * 此代码演示了两点
 * 1:对象可以在被GC时自我拯救
 * 2:这种自救的机会只有一次,因为finalize方法只会被系统调用一次
 * @author Administrator
 *
 */
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;
		//因为finalize方法优先级很低,所以暂停0.5秒等待它
		Thread.sleep(500);
		if(null!=SAVE_HOOK){
			SAVE_HOOK.isAlive();
		}
		else{
			System.out.println("no,i am dead :(");
		}
		
		//两段代码完全相同,但这次自救却失败了
		SAVE_HOOK=null;
		//因为finalize方法优先级很低,所以暂停0.5秒等待它
		Thread.sleep(500);
		if(null!=SAVE_HOOK){
			SAVE_HOOK.isAlive();
		}
		else{
			System.out.println("no,i am dead :(");
		}
	}
}

以上代码运行在JDK1.7及JDK1.8两次均没有自救成功,还没发现原因。
finalize方法尽量不用使用,因为它运行代价高昂,不确定性大,无法保证各个对象的调用顺序,并不适合做“关闭外部资源”之类的工作。finalize()能做的所有工作,使用try-finally或其他方式都可以做的更好、更及时,所以最好忘掉Java语言有这个方法的存在。

五、回收方法区

很多人认为方法区(或HotSpot虚拟机的永久代)是没有垃圾收集的,Java虚拟机规范确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是新生代中,常规应用进行一次垃圾回收一般可以回收70%95的空间,而永久代的垃圾收集效率远低于此。

永久代的垃圾回收主要回收两部分内容:废弃常量和无用类。回收废弃常量与回收Java堆上的对象类似。以常量池中的字面量回收为例,假如一个字符串“abc”已经进入了常量池,但是当前系统没有任何一个String对象叫做“abc”,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且有必要的话,这个“abc”常量就会被清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也于此类似。

判断一个常量是否是废弃常量比较简单,而要判断一个类是否是无用的类,条件相对苛刻很多。类需要满足下面3个条件才能算作无用的类:

该类所有的实例已经被回收,也就是Java堆中不存在该类的任何实例。

加载该类的ClassLoader已经被回收。

该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上面3个要求的无用类进行回收,这里说的仅仅是可以,而不是像对象一样,不用了就必然会回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading需要FastDebug版的虚拟机支持。

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

书香水墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值