垃圾收集器(一)

本文介绍了JVM内存管理的五个区域,并重点探讨了Java堆与方法区的垃圾回收策略。文中详细解释了引用计数算法和可达性分析算法的工作原理及应用场景,同时还讨论了对象如何在finalize()方法中自救的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

近期学习了JVM垃圾收集器与内存分配策略,现将部分学习笔记记录如下,以供今后使用。

前言

为了能使Java应用程序正常运行,JVM将内存数据分为程序计数器、虚拟机栈、本地方法栈、Java堆和方法区5个区域。其中程序计数器、虚拟机栈、本地方法栈3个区域是线程私有的,其生命随线程而生,随线程而亡;栈中的栈帧随着方法的进入和退出而执行着入栈和出栈操作,每个栈帧中分配的内存基本上在类结构确定时就已知了,因此这几个区域的内存分配与回收都是确定的,在这几个区域就不需要过多的考虑内存回收问题。而Java堆与方法区则完全不同,一个接口的多个实现类需要的内存可能不同,我们只有在程序运行期间才能知道创建哪些对象,需要分配多少内存,所以这部分内存的分配与回收是动态的,垃圾收集器所关注的就是这部分内存。
既然垃圾收集器关注的是Java堆与方法区的内存回收问题,那么最重要的一个问题就是:哪些内存需要回收?
下面介绍解决这个问题的常用算法。

引用计数算法(Reference Counting)

引用计数算法原理:给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不能被使用的对象。
引用计数算法实现简单,判断效率很高;但JVM没有使用该算法管理内存,主要原因是它很难解决对象间的循环引用问题。

public class ReferenceCountingGC {
    public Object instance;

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC(); // objACount = 1
        ReferenceCountingGC objB = new ReferenceCountingGC(); // objBCount = 1
        objA.instance = objB; // objBCount = 2
        objB.instance = objA; // objACount = 2

        objA = null; // objACount = 1
        objB = null; // objBCount = 1

        // 此时进行垃圾回收,objA与objB的引用计数器都不为0,这两个对象不会被回收
        System.gc();
    }
}

可达性分析算法

可达性分析算法的原理:通过一系列的称为“GC Roots”的对象作为起点,从这个借口开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则此对象不可用。
Java语言中,可作为 GC Roots 的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象;
- 方法区中类的静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(Native方法)引用的对象。

生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是非死不可的,一个对象在真正死亡之前,至少要经历两次标记过程。
如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那么它将被第一次标记并进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已被虚拟机调用过,则虚拟机认为没有必要执行finalize()方法。
如果该对象被判定为有必要执行finalize()方法,那么该对象将会被放置在F-Queue队列中,并在稍后由低优先级的Finalizer线程(一个由虚拟机自动建立的线程)去执行它。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次标记,如果对象在finalize()方法中成功拯救自己(只需要重新与引用链上的任何对象建立关联即可,如把自己this复制给某个类变量或对象的成员变量),那么在第二次标记时它将被移除“即将回收”的集合;否则,这个对象就会被回收了。
任何一个对象的finalize()方法都只会被系统自动调用一次,因此对象也只有在第一次被调用finalize()方法时拯救自己一次,后续再被回收,它的finalize()方法不会被再次执行,也就无法自救了。

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC instance;

    public void isAlive() {
        System.out.println("Yes, i'm still alive!");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize() executed!");
        FinalizeEscapeGC.instance = this;
    }

    public static void main(String[] args) throws InterruptedException {
        FinalizeEscapeGC.instance = new FinalizeEscapeGC();

        // 第一次回收时对象拯救自己
        FinalizeEscapeGC.instance = null;
        System.gc();
        // 等待1s,让gc执行
        Thread.sleep(1000);
        if (FinalizeEscapeGC.instance != null) {
            FinalizeEscapeGC.instance.isAlive();
        } else {
            System.out.println("Oh, i'm dead!");
        }

        // 第二次拯救自己是否能成功呢?看运行结果
        FinalizeEscapeGC.instance = null;
        System.gc();
        Thread.sleep(1000);
        if (FinalizeEscapeGC.instance != null) {
            FinalizeEscapeGC.instance.isAlive();
        } else {
            System.out.println("Oh, i'm dead!");
        }
    }
}

运行结果:
finalize() executed!
Yes, i'm still alive!
Oh, i'm dead!

从运行结果看,对象第一次被回收时拯救自己成功了,但第二次被回收时拯救自己失败了。

回收方法区

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
如果没有任何对象引用常量池中的常量,对于针对字符串常量也需要没有其他地方引用这个字面量,那么发生内存回收时,这些常量有可能会被系统清理出常量池。
类需要同时满足以下3个条件才能算是无用的类:
- 该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例;
- 加载该类的类加载器已经被回收;
- 该类对象的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

JVM提供以下参数,可以查看类加载与卸载信息:
-XX:+TraceClassLoading 查看类加载信息。
-XX:+TraceClassUnLoading 查看类卸载信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值