目录
概要
如何判断对象已死?
引用计数算法
优点
缺点
举例说明
可达性分析
图例说明
GC Roots的对象包括以下几种
可达性分析回收过程
四大引用
回收方法区
方法区的垃圾收集主要回收两部分内容:
1. 废弃的常量
2. 不再使用的类型。
JVM是否要对类型进行回收参数设置
提起垃圾收集(GC),肯定会思考垃圾收集完成的工作:哪些内存需要回收?什么时候回收?如何回收?那么,在回收前如何判断对象已死呢?
在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,要确定这些对象之中哪些还“存活”着,哪些已经“死去”。(“死去”即不可能再被任何途径使用的对象)。
有两种方式:一种是 引用计数算法、一种是可达性分析算法。
该算法判断对象是否存活是这样的:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
// 示例说明,不用执行
public class ReferenceCountGC{
public Object instance = null;
public static void testGC(){
// step 1
ReferenceCountGC gc1 = new ReferenceCountGC();
// step 2
ReferenceCountGC gc2 = new ReferenceCountGC();
// 相互引用
// step 3
gc1.instance = gc2;
// step 4
gc2.instance = gc1;
// step 5
gc1 = null;
// step 6
gc2 = null;
// 假设在这行发生GC,gc1 和 gc2 对象是否会被回收? 不会回收
System.gc();
}
}
分析代码最终结果:
step1: gc1 的引用+1 =1
step2: gc2 的引用+1 =1
step3: gc2 的引用+1 =2
step4: gc1 的引用+1 =2
step5: gc1 的引用-1 =1
step6: gc2 的引用-1 =1
由此看出,gc1和gc2在置为null之后,这两个对象已经不可能再被访问了,但是因为它们互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。虚拟机这么牛气,自然不会用这样的方法来判断对象的存活状态了。 所以继续往下看,就会明白了
启动参数设置:
// 打印GC信息 -XX:+PrintGCDetails
通过一系列的GC Roots的对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。
这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
以上所述,红色代表不可达对象(可回收对象)
GC Roots对象 | 示例 |
---|---|
在虚拟机栈(栈帧中的本地变量表)中引用的对象 | 譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等 |
在方法区中类静态属性引用的对象 | 譬如Java类的引用类型静态变量 |
在方法区中常量引用的对象 | 譬如字符串常量池(String Table)里的引用 |
在本地方法栈中JNI对象 | (即通常所说的Native方法)引用的对象 |
Java虚拟机内部的引用 | 如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemory Error)等,还有系统类加载器 |
所有被同步锁(synchronized关键字)持有的对象 | - |
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等 | - |
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候他们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
1.如果对象A到GC Roots没有引用链,则进行第一次标记;
2.进行筛选,判断此对象是否有必要执行finalize()方法;
3.如果对象A没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,对象A被判定为不可触及的;
4.如果对象A重写了finalize()方法,且还未执行过,那么对象A会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的finalizer线程触发其finalize()方法执行;
注意:这里所说的“执行”是指虚拟机会触发这个方法开始执行,但并不承诺一定会等待它运行结束。这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。
5.finalize()方法是对象逃逸死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次小规模的标记;
6.如果这期间对象A在finalize()方法中与引用链上的任何一个对象建立了联系(譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量),那么在第二次标记时,对象A会被移除“即将回收”集合;
7.如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
注意:一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,所以同样的对象并且重写了finalize(),执行两次,会出现第一次自救成功,第二次就会失败了。
在JDK1.2版本之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
有些人认为方法区(如HotSpot虚拟机种的元空间或者永久代)是没有垃圾收集行为的,《Java虚拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集,事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载),方法区垃圾收集的“性价比”通常也是比较低的:在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。
回收废弃常量与回收Java堆中的对象非常类似。
判断一个常量是否“废弃”。
举例说明:
假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。
判断一个类型是否属于“不再被使用的类”。
需要满足三个条件:
Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。
-verbose:class和-XX:+TraceClass-Loading可以在Product版的虚拟机中使用;
-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。
在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
关于引用计数法与可达性分析就先介绍到这里了。
作者:筱白爱学习!!
欢迎关注转发评论点赞沟通,您的支持是筱白的动力!