JVM(十一)垃圾回收概述和垃圾标记阶段的算法

JVM(十一)垃圾回收概述和垃圾标记阶段的算法


1 Java垃圾回收概述

  • 什么是垃圾?

    • 垃圾是在程序运行过程中不被任何指针指向的对象,这个对象就是需要被回收的垃圾
  • 为什么要进行垃圾回收?

    • 如果不及时对内存中的垃圾进行清理,那么这些垃圾对象所占内存空间会一直保存到应用程序结束,被保留的空间无法被其他对象所使用,甚至会导致内存溢出
    • 垃圾回收还可以清除内存里的记录碎片,将堆内存移到堆的另一边,以便将整理的内存分配给新的对象
  • 自动内存管理,无需开发人员手动参与内存的分配与回收,降低内存泄露和内存溢出的风险

  • 将程序员从繁重的内存管理中释放出来,更专心地注重于业务开发

  • 当需要排查各种内存溢出、内存泄露问题的时候,当垃圾回收成为系统达到更高并发量的瓶颈的时候,就必须对这些自动化的技术实施必要的监控和调节

  • 只有JVM内存区域的方法区和堆存在垃圾回收,Java栈、本地方法栈存在栈溢出的情况而没有垃圾回收,程序计数器则既没有垃圾回收也没有溢出的情况

image-20230704152736441

2 垃圾回收的相关算法

3 垃圾标记阶段的算法

  • 在堆中几乎存放着所有的java对象实例,在执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会在执行垃圾回收的时候释放掉其所占有的空间,因此这个过程也被称作垃圾标记阶段
  • 当一个对象不被任何存活的对象继续引用的时候,就可以宣判这个对象死亡了
  • 判断对象存活一般有两种方式:引用计数算法可达性分析算法
3.1 引用计数算法
  • 引用计数算法即是对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况

  • 每当有其他的对象对这个对象进行了引用,则对象的引用计数器就加一,引用失效的时候,就将引用计数器减一;引用计数器减到0的时候就表示对象不可再被使用,从而进行回收

  • 优点

    • 实现简单,垃圾对象易于辨识,判定效率高且回收没有延迟性
  • 缺点

    • 需要单独的引用计数器,增加了存储空间的开销
    • 每次赋值都需要更新计数器,伴随着加法和减法操作,增加了时间开销
    • 引用计数器无法处理循环引用的问题,这一缺陷导致了Java的垃圾回收器中没有使用这一算法

    引用计数器无法处理循环引用问题,是因为循环引用会导致对象的引用计数始终不为0,无用的对象得不到回收从而导致内存泄露问题

    image-20230704162116157
3.2 可达性分析算法

可达性分析算法又称为根搜索算法追踪性垃圾收集,可达性分析算法不仅可以具备实现简单和执行高效的特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄露的发生GC Roots意思是一群活跃的引用的根集合,可达性分析算法的基本实现思路为:

  • 可达性分析算法以根集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
  • 使用可达性分析算法后,内存中存活的对象会被根对象集合直接或者间接连接着,搜索所走过的路径称作引用链(Reference Chain)
  • 可达性分析算法中,只有能被根对象集合直接或者间接连接的对象才是存活对象
  • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以被标记为垃圾对象
image-20230704164945222

在Java中,GC Roots包括以下几类元素:

  • 虚拟机栈中引用的对象,比如:各个线程被调用的方法的参数、局部变量等

  • 本地方法栈内本地方法引用的对象

  • 方法区中静态属性引用的对象,如Java类的引用类型静态变量

  • 方法区中常量引用的对象,如字符串常量池中的引用

  • 被同步锁Synchronized持有的对象(同步监视器)

  • Java虚拟机内部的引用,包括基本数据类型对应的Class对象以及一些常驻的异常对象(如:NullPointerException、OutOfMemoryError)以及系统类加载器

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

  • 除了上面这些固定的GC Roots集合外,根据用户所选用的垃圾收集器以及当前回收的内存区域的不同,如分代收集局部回收的时候,还可以有其他相关联的对象“临时性”地加入

    这是因为如果只针对Java堆中的一块区域进行垃圾回收(如YGC只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,而不是孤立封闭的,所以这个区域的对象有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性

  • 如果使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能够保障一致性的快照中进行,这点不满足的话就无法保证分析结果的准确性

    这点也是导致GC进行时必须Stop The World的一个重要原因,即使是号称几乎不会停顿的CMS收集器,在枚举根节点的时候也是必须要停顿的

”由于Root采用的是栈方式存放变量和指针,所以如果一个指针,它保存了堆内存的对象而自己又不存放于堆内存中,它就是一个Root“

这句话JDK6之后就有问题了,因为JDK6开始静态变量和常量池都是放在堆里面

image-20230704192033350

4 对象的finalization机制

  • finalization机制用于对象销毁之前的自定义处理逻辑当垃圾回收器发现没有引用指向一个对象的时候,即在回收这个方法之前,总会调用这个对象的finalize()方法

  • finalize()方法允许在子类中被重写,常用于对象被回收时进行资源的释放:通常在这个方法中进行一些资源的释放和清理工作,如关闭文件、套接字和数据库连接等

  • 永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用,因为:

    • finalize()方法可能会导致对象的复活
    • finalize()方法的执行时间没有保障,完全由GC线程决定,极端情况下不发生GC则永远不会调用该方法
    • 一个糟糕的finalize()方法会严重影响GC的性能
  • 由于finalize()方法的存在,一个不可达、无法触及的对象有可能在某一条件下复活自己,因此虚拟机中的对象一般处于三种可能的状态:

    • 可触及的:从根节点开始,可以到达这个对象
    • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中被复活
    • 不可触及的:对象的finalize()方法被调用但是没有被复活,因此就会进入不可触及状态

    不可触及的对象不能被复活,因为finalize()方法只能被调用一次

    只有不可触及的对象才可以被回收

  • 详细来说,判断一个对象是否可以回收,至少要经过两次标记过程

    1. 如果对象到根集合GC Roots没有引用链,则进行第一次标记
    2. 进行筛选,判断对象是否有必要执行finalize()方法
      1. 如果对象没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,对象直接被判定为不可触及的
      2. 如果对象重写了finalize()方法,而且还未被执行过,则对象会被插入到F-Queue队列中,然后先进先出,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法进行执行
      3. finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记;如果该对象在此过程中与引用链上的任何一个对象建立了联系,则对象就被移除队列,之后再出现没有引用指向的情况,finalize()方法就不会再次被调用,对象直接会变成不可触及状态——也就是说:finalize()方法只会被调用一次
4.1 对象复活演示实例
public class CanReliveObj {
    public static CanReliveObj obj;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("调用当前方法的finalize()方法");
        // 当前待回收的对象指向了引用链上的对象
        obj = this;
    }

    public static void main(String[] args) throws InterruptedException {
        obj = new CanReliveObj();
        obj = null;
        System.gc();
        System.out.println("第一次gc");
        // finalizer线程优先级远低于主线程,防止其得不到执行
        // 执行finalize方法,对象复活成功
        Thread.sleep(2000);
        if(obj == null) {
            System.out.println("obj is dead.");
        } else {
            System.out.println("obj is still alive.");
        }
        obj = null;
        System.gc();
        // finalize方法只能被执行一次,因此第二次对象自救失败
        if(obj == null) {
            System.out.println("obj is dead.");
        } else {
            System.out.println("obj is still alive.");
        }
    }
}

5 MAT和JProfiler的GC Roots溯源

posted @ 2023-07-12 10:37  Tod4  阅读(31)  评论(0编辑  收藏  举报