深入理解java虚拟机(第三版)读书笔记——第3章 垃圾收集器与内存分配策略——3.2 对象已死?

本文探讨了Java堆和方法区的垃圾回收机制,重点关注引用计数算法与可达性分析的原理和应用,包括强引用、软引用、弱引用和虚引用的区别,以及对象生存与死亡的判定过程。此外,文章还提及了方法区的废弃常量和不再使用类型的回收策略。
摘要由CSDN通过智能技术生成

提示:此系列博客为博主个人读书笔记,其作用是总结书中内容,个人理解整理,方便复习使用。博客内容分为:原书内容总结和个人理解内容。
注:本文原书内容为博主个人提炼总结内容,方便突出要点。



概述

垃圾回收考虑三点:
                      ·哪些内存需要回收?
                      ·什么时候回收?
                      ·如何回收?

对于程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。这几个区域不太需要考虑回收问题,因为当方法结束或者线程结束时,内存自然就跟随着回收了。
【个解】对于以上三个,都为确定性的,极为大体上可以认为是编译期可知的。

而Java堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样。只有在运行期间才是可知的,即为动态的,不确定性的,文中后面讨论的“内存”也这些。

3.2 对象已死?

Java堆进行垃圾回收,首先要判断的就是,对象时候存活?判断对象是否存活,两个算法。引用计数算法 & 可达性分析算法

3.2.1 引用计数算法

简单来说,为对象添加个计数器,每当引用,计数器+1;引用失效,计数器-1。
确实是个不错的算法,简单,高效。但是,不太适合就java,其原因就是,不太好解决对象互相循环依赖。

package com.hzww.food.jvm;

/*** testGC()方法执行后,objA和objB会不会被GC呢? * @author zzm */
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();
    }
}

【个解】设置JVM参数 -XX:+PrintGCDetails。
在这里插入图片描述
由上代码可知,objA引用了objB,objB引用了objA。两个对象再无其他任何引用,实际上两个对象已经无法访问,但是引用计数不能为零。
从日志中也可以看出,虚拟机并没有因为这两个对象互相引用就放弃回收它们,侧面说明,java虚拟机并没有采用引用计数算法。

3.2.2 可达性分析算法

简单总结是:通过一些“GC Roots”的根对象作为起始节点,根据引用关系开始往下搜索,如果某个对象,跟整体没有任何引用,则是这个对象是不能再被使用的。
在这里插入图片描述
对象object 5、object 6、object7虽然互有关联,但是跟GC Roots之间是没有关联的。所以会被认为是要无用对象。

GC Roots根对象的有:

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。

  2. 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

  3. 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。

  4. 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

  6. ·所有被同步锁(synchronized关键字)持有的对象。

  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

除了以上7个以外,还根据 垃圾收集器不同 和 回收区域不同。还可能“临时性”有其他对象加入。为了避免GC Roots包含过多对象而过度膨胀,它们在实现上也做出了各种优化处理。

3.2.3 再谈引用

java引用分为4中,强度依次逐渐减弱
强引用(Strongly Reference)
软引用(Soft Reference)
弱引用(Weak Reference
虚引用(PhantomReference)

强引用:代码中最常见普通引用,即类似“Object obj=new Object()。只要引用关系还在,永不回收。

软引用:在统内存溢出异常前,这些对象列进回收范围之中进行第二次回收,如果还是不足,则抛出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。

弱引用:只要开始垃圾回收,无论是否内存足够,都会回收这些弱引用对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。
【个解】ThreadLocal里面的key就是弱引用的,如果想了解实际应用,可以去看看ThreadLocal源码。

虚引用:无法通过虚引用获取一个对象实例。虚引用唯一目的是,这个对象被回收时可以收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。

3.2.4 生存还是死亡?

如果是被标记成“不可达”对象,并不会直接回收,而且查看对象的finalize()方法是否执行过。
如果对象的finalize()已经执行过,或者对象干脆没有覆盖finalize(),那么就没必要进行后面的步骤,直接等待回收即可。

如果对象finalize()需要执行,那么会把这些对象放进F-Queue的队列中,并由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。
虽然是执行,但是不一会会等待执行完毕,原因是,可能某个finalize()执行缓慢,或者是有死循环等。如果这执行finalize()期间,跟任何“引用链”的对象建立联系(把自己(this关键字)赋值给某个类变量或者对象的成员变量),那么在第二次标记时查询到则不会被回收。

/*** 此代码演示了两点: 、
 * 1.对象可以在被GC时自我拯救。
 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 * @author zzm */
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("no, 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("no, i am dead :(");
        }
    }
}

在这里插入图片描述
【个解】这个不算个解,由上可知,强调一下,finalize()只会执行一次。还有就是,在原文中作者强调了,finalize()方法不建议大家使用,建议大家完全忘记这个方法,原因是:它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。而且是Java刚诞生时,为了c、c++程序员更容易接受做的妥协

3.2.5 回收方法区

之前就聊过,《Java虚拟机规范》没有强制要求回收方法区(如JDK 11时期的ZGC收集器就不支持类卸载)方法区垃圾收集的“性价比”通常也是比较低的(java堆中,尤其是在新生代中,一次垃圾回收通常可以回收70%至99%的内存)。

方法区的垃圾回收分为两部分:废弃的常量和不再使用的类型。

  1. 废弃常量:例如常量池字符串,当前没有任何字符串对象引用它,且虚拟机也没有任何地方引用这个字面量,如果回收判断有必要,则这个常量会被清理出常量池,常量池中其他类(接口)、方法、字段的符号引用也与此类似。
    【个解】1.8的字符串常量池应该在堆中,这里作者说的应该是1.6及之前。

  2. 一个类型是否属于“不再被使用的类”:达成这个条件需要满足下面三个条件。
    1、该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
    2、加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
    【个解】看到JSP的还有点触动,还是在写JSP的时候,还有个疑问,就是不用重启tomcat。后面看个美团的文档是说,每有个新的JSP就重新编译成字节码由类加载器重新加载。我记得好像是这么说的(不确定,可能还是错的。。。几年前了。。。)。。。这玩意我除了自学的时候学过,出来再也没见过。编程最好玩也是最头疼的一点,就是要不断的学习,每几年又是一门新的东西开启了。。。
    3、·该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

【个解】以上三个可以简单说是:一个类的所有对象都没有了,类的加载器也没了,类对象也没了。这个类就允许被回收了

这里要注意下,以上条件满足仅仅是被允许而并不是和对象一样,没有引用了就必然会回收。
HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版[1]的虚拟机支持。

在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力。
【个解】其实在这里我就有疑问,文中开头说过:JDK 11时期的ZGC收集器就不支持类卸载。那么是不是说,JDK11不适合spring的项目。。。

总结

从这篇文章开始,我改变了之前笔记的格式内容。不再强调摘取原文做个解了,开始自己总结解释。可能以后发博客的时间越来越少了,我写博客能写一天。。。我太墨迹了,这时间我能看好多章书了。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值