栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

虚拟机:垃圾收集器如何判断对象的死活(HotSpot)

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

虚拟机:垃圾收集器如何判断对象的死活(HotSpot)

后续我会陆陆续续更新虚拟机的源码,原理,和介绍。大家如果觉得对自己有用就点个关注吧。

声明

本文大部分内容摘自于《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》 — 周志明
并加上一些我自己的理解,和查阅的资料

对象的死活

 首先说一下什么是对象的死活,这面的死活当然是一种抽象的概念。

  • 死:表示那些最后需要虚拟机回收的对象。
  • 活:反之表示那些不需要虚拟机回收的对象。
引用计数算法

 第一种方式就是“引用计数算法”,就是在对象中添加一个引用计数器,每当被其他地方引用一次计数器便+1,如果有一天这个计数器的值变为0以后,那么就代表这个对象“死了”(可以被回收了)。
 这种方式虽然原理简单并且还很高效但它不是完美的,想使用这种实现方式去做判断,就还需要很多额外的计算和操作,才能保证功能的正确工作,举个栗子——看代码

public class JvmTest {

    public Object instance = null;

    private static final int _1MB = 1024*1024;

    
    private byte[] bigSize = new byte[2 * _1MB];

    public static void testGC(){
        JvmTest jvmTest1 = new JvmTest();
        JvmTest jvmTest2 = new JvmTest();

        jvmTest1.instance = jvmTest2;
        jvmTest2.instance = jvmTest1;

        jvmTest1 = null;
        jvmTest2 = null;

        //假设这行发生了GC
        System.gc();
    }
}

 这种方法,显然如果没有其他操作,jvmTest1和jvmTest2他们永远相互引用,所以计数器就都不会是0。

可达性分析算法

 这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

 这样就可以解决上面提到的循环依赖的问题,如果A和B循环依赖,当A不再与GC Roots集存在引用,B也不再与GC Roots集存在引用,那么及时A和B相互引用,垃圾回收也会将他们列为处理对象。

GC Roots

 在Java体系里面,固定可作为GC Roots的对象包括一下几种

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

 除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入。

 一个问题需要注意下,如果我们按照内存区域局部回收,比如我们只针对Java堆中某一块区域进行回收,那么我们我就要考虑,回收区域的对象完全可能被其他区域所引用,或者引用其他区域的对象,所以就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。

对象可以死去的标准

 如何清楚哪些对象可以死去。

  1. 一审:可达性分析算法中判定为不可达对象。目前只是初审,要真正判处一个对象的死刑,还需要进行两次标记的过程。
  2. 二审:一审标识为不可达对象以后,就会筛选对象中覆盖了finalize()方法的对象,并判断finalize()方法是否有必要执行,不必要执行的条件——finalize()方法已经被虚拟机调用过一次(finalize()方法只会把被虚拟机调用一次)。最后这些有必要执行finalize()的对象将成功进入三审。至于没有进入三审的对象将会被判处死刑。
  3. 三审:通过二审之后,这些有必要执行finalize()的对象将会被放在一个名为F-Queue的队列中,并在稍后有一条虚拟机自动建立的、低优先级的Finalizer线程去执行它们的finalize()方法。执行完成之后整个三审就结束了,如果在这个过程中对象没有自救,那么三审过后,对象就会被判处死刑。

 自救方法:只需要在finalize()方法中将自己与引用链上的任何一个对象建立起关联即可。比如:把自己(this)赋值到某个变量或者某个对象的成员变量上。

 值得注意的是finalize()方法不保证会执行完成,因为一旦某个对象的finalize()方法运行的很慢,甚至出现了死循环,那么就有可能导致F-Queue队列中的其他对象永久处于等待,甚至导致内存回收的子系统崩溃。

代码示例
   
public class JvmTest {


    public static JvmTest 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!");
        JvmTest.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new JvmTest();

        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低, 暂停0.5秒, 以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }

        // 下面这段代码与上面的完全相同, 但是这次自救却失败了:因为finalize只能被执行一次
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低, 暂停0.5秒, 以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}

运行结果:

finalize method executed!
yes, i am still alive :)
no, i am dead :(

 仔细看代码不难看出这里面两段一模一样的代码,但是出现了不一样的运行结果,这就说明,finalize()方法只会被虚拟机调用一次,也就是对象只有一次可以自我拯救的机会。所以他的第二次自救失败了。

尽量不要使用finalize()方法

 官方也声明这一点,因为这个方法的执行开销是比较大的,他的存在为了给C、C++的朋友们一点安慰,他可不像析构函数那么好用,所以能不用就不用。

回收方法区

本段将直接使用原文内容,因为我觉着这一段都很重要。
摘自:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》 — 周志明

 有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,《Java虚拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集,事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载),方法区垃圾收集的“性价比”通常也是比较低的:在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。
 方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。回收废弃常量与回收Java堆中的对象非常类似。举个常量池中字面量回收的例子,假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。
 判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
     Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。
     在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/334250.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号