zl程序教程

您现在的位置是:首页 >  IT要闻

当前栏目

《深入理解Java虚拟机》读书笔记(四)

2023-02-19 12:20:09 时间

Java中的引用与方法区垃圾回收

可达性分析算法.png

在Java的内存运行时区域的各个部分中:程序计数器、虚拟机栈、本地方法栈3个区域属于线程私有,随线程而生、随线程而灭,因此不需要过多的考虑内存的回收;所以垃圾回收的主要区域就主要集中在Java堆和方法区

方法区由于判定内存回收的苛刻条件以及较低的回收效率,其垃圾回收的实现在Java虚拟机规范中并未做强制要求,由不同的虚拟机实现厂商自主选择(此内存区域的垃圾回收同样至关重要)

最终,垃圾收集器的重点工作区域落在了Java堆

判断对象是否存活

引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一,当引用失效时,计数器值就减一,任何时刻计数器为零的对象就是不可能再被使用的

引用计数算法的缺陷就在于很难解决对象之间相互循环引用的问题

  • 代码示例一
/**
 * 引用计数算法难以解决对象之间循环引用的问题
 * {@link 《深入理解Java虚拟机》第三版 代码清单3-1}
 * VM Args:-XX:+PrintGC 或者 -XX:+PrintGCDetails
 * 代码在JDK1.8下运行
 */
public class ReferenceCountingGC {

    public Object instance = null;

    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        System.gc();
    }
}
  • GC信息
对象互相引用的GC情况.png
  • 代码示例二:
/**
 * 引用计数算法难以解决对象之间循环引用的问题
 * {@link 《深入理解Java虚拟机》第三版 代码清单3-1}
 * VM Args:-XX:+PrintGC 或者 -XX:+PrintGCDetails
 * 代码在JDK1.8下运行
 */
public class ReferenceCountingGC {

    public Object instance = null;

    public static void main(String[] args) {
        ReferenceCountingGC objA = null;
        ReferenceCountingGC objB = null;

        System.gc();
    }
}
  • GC信息
对象互相引用的GC情况(对照).png

将示例一和示例二的GC情况对比来看,虚拟机并没有因为两个对象互相引用就放弃回收,这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象是否存活的

可达性分析算法

可达性分析算法.png

通过一系列称为GC Roots的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索走过的路径称为引用链(Reference Chain),当某个对象到GC Roots没有任何引用链可达时,即判定对象不可能再被使用

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

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

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

Java中的引用

根据对象在实际应用中情况的不同,在JDK1.2版之后,Java对应用的概念进行了扩充,将引用分为以下几种:

  • 强引用(Strongly Reference)

类似“Object obj = new Object()”这类传统意义上引用赋值的引用,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象

  • 软引用(Soft Reference)

描述一些还有用但非必需的对象。被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常

  • 弱引用(Weak Reference)

描述非必需的引用。被弱引用关联着的对象,只能生存到下一次垃圾收集发生位置;当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联着的对象

  • 虚引用(Phantom Reference)

最弱的引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例;为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

代码finalize()方法的起死回生

要真正宣告一个对象死亡,至少要经历两次标记过程

  • 第一次标记:当前对象到GC Roots被判定不可达时,检查当前对象是否覆写了finalize方法且覆写的finalize方法还未执行过,若满足条件,对象将被放入一个名为F-Queue的队列,并在稍后由一条由虚拟机创建的低优先级的Finalizer线程去触发执行每个对象的finalize()方法
  • 第二次标记:收集器将对F-Queue中的对象进行小规模标记,检查其中已经成功通过finalize()方法“自救”(重新与GC Roots建立关联)成功的对象,并将其移出“即将被回收”的集合
  • 代码示例
/**
 * finalize()的起死回生
 * {@link 《深入理解Java虚拟机》第三版}
 * VM Args:-XX:+PrintGCDetails
 * 代码在JDK1.8下运行
 */
public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes, I'm 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线程优先级低,所以暂停当前线程等待一段时间,以让finalize()方法执行
        Thread.sleep(5);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("no, I'm dead !");
        }

        // 任何一个对象的finalize()方法都只会被系统自动调用一次,因此第二次面临回收时,finalize()方法不会再执行
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(5);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("no, I'm dead !");
        }
    }
}

方法区的垃圾回收

由于方法区的可回收判定条件苛刻、回收效率较低,所以Java虚拟机规范并未强制要求方法区必须实现垃圾收集,但这并非意味着在方法区就完全不需要垃圾回收,在Sun公司的Bug列表中就曾有多个因为低版本HotSpot虚拟机对此区域未完全回收导致的内存泄露

方法区的内存回收主要针对两部分内容:废弃的常量、不再使用的类型

废弃常量的判定

常量池中的常量,不再被任何字符串对象引用,即可判定常量已被废弃,可以被清理出常量池并回收内存

不再使用的类型的判定

不再被使用的类型的判定,需要同时满足三个条件:

  • 该类的所有实例都已经被回收,Java堆中不存在该类及其任何派生子类的实例
  • 加载该类的类加载器已经被回收
  • 该类对象的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

同时满足上述三个条件的类型将会被判定为已废弃,但是否被回收,则还取决于具体虚拟机的实现