【深入理解Java虚拟机】读书笔记——垃圾收集器与内存分配策略

垃圾收集 (GC)

垃圾收集(Garbage Collection,GC),它的任务是解决以下 3 件问题:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

其中第一个问题很好回答,在 Java 中,GC 主要发生在 Java 堆和方法区中,对于后两个问题,我们将在之后的内容中进行讨论,并介绍 HotSpot 的 7 个垃圾收集器。

判断对象的生死

什么时候回收对象?当然是这个对象再也不会被用到的时候回收。所以要想解决 “什么时候回收?” 这个问题,我们要先能判断一个对象什么时候什么时候真正的 “死” 掉了,判断对象是否可用主要有以下两种方法。

判断对象是否可用的算法

引用计数算法

  • 算法描述:
    • 给对象添加一个引用计数器;
    • 每有一个地方引用它,计数器加 1;
    • 引用失效时,计数器减 1;
    • 计数器值为 0 的对象不再可用。
  • 缺点:
    • 很难解决循环引用的问题。即 objA.instance = objB; objB.instance = objA;,objA 和 objB 都不会再被访问后,它们仍然相互引用着对方,所以它们的引用计数器不为 0,将永远不能被判为不可用。
package com.jvm;

/**
 * @author :jhys
 * @date :Created in 2021/2/13 19:34
 * @Description :
 */
public class TestGC {

    public Object obj = null;

    private static final int _1MB = 1024 * 1024;
    // 每个对象中包含2M的成员,方便观察
    private byte[] bigSize = new byte[2 * _1MB];
    public static void main(String[] args) {
        TestGC objA = new TestGC();
        TestGC objB = new TestGC();
        objA.obj = objB.obj;
        objB.obj = objA.obj;

        //取消对对象的引用
        objA = null;
        objB = null;
        // 是否进行垃圾回收
        System.gc();
    }
}

首先,我们将 System.gc() 注释掉,也就是我们在默认情况下,不去触发垃圾回收。并在运行的时候,添加VM参数 -XX:+PrintGCDetails。我们观察输出结果;

可以看到,这个时候,占用的空间为10M左右。

如果我们取消注释,也就是主动去调用垃圾回收器,那么运行结果为:

占用空间为2M左右。

可以看出来,Java 的垃圾回收,并非采用我们上面介绍的引用计数方式。

可达性分析算法(主流)

  • 算法描述:
    • 从 "GC Root" 对象作为起点开始向下搜索,走过的路径称为引用链(Reference Chain);
    • 从 "GC Root" 开始,不可达的对象被判为不可用。
  • Java 中可作为 “GC Root” 的对象:
    • 栈中(本地变量表中的reference)
      • 虚拟机栈中,栈帧中的本地变量表引用的对象;
      • 本地方法栈中,JNI 引用的对象(native方法);
    • 方法区中
      • 类的静态属性引用的对象;
      • 常量引用的对象;

即便如此,一个对象也不是一旦被判为不可达,就立即死去的,宣告一个的死亡需要经过两次标记过程。

可达性算法,还有一系列的别名:根搜索算法,追踪性垃圾收集,GC Root。

之后,看到原理,其实这些别名都是描述原理的。

首先,我们选取一些对象,这些对象是存活的,也被称为 GC Roots,然后根据这些对象的引用关系,凡是直接或者间接跟 GC Roots 相关联的对象,都是存活的。就像图中的连通性判断一样。

这个算法的想法不难。难的是,如何确定 GC Roots。

我们考虑,我们什么时候需要用到对象?(我们需要对象的时候,肯定需要这个对象是存活的)

  • 栈中保存着,我们当前或者之后需要运行的方法及相关参数,所以,栈上所引用的堆中对象肯定是存活的。
  • 类中的一些属性,比如,静态属性,因为它不依赖于具体的类
  • 一些常用的对象,以免清理后,又要重复加载,比如常用的异常对象,基本数据类型对应的 Class 对象。

除此之外,还有很多零零碎碎的。

在堆结构周围的一些结构,其中引用的对象可以作为GC Roots

具体 GC Roots 可以概括为:

  • 虚拟机栈上(确切的说,是栈帧上的本地变量表)所引用的对象

  • 本地方法栈引用的对象

  • 方法区中的静态属性,常量引用

  • Java 虚拟机的内部引用,常用数据类型的 Class 对象,常驻的异常对象,系统类加载器

  • 所有被同步锁持有的对象

除此之外,还有一些临时的 GC Roots 可以加入进来。这里涉及到新生代老年代。

比如老年代中的对象一般都存活时间比较久,也就是大概率是活着的对象,也可临时作为 GC Roots。

可达性算法的一些细节

前面说了可达性算法,我们根据 GC Roots 来进行标记对象的死活。

但是,被判定为不可达的对象,并不立刻死亡。它仍然有次机会进行自救。

这个自救的机会,是需要重写 finalize()进行自救。

也就是可达性算法的逻辑大致是这样的:

  • 第一次进行标记,凡是不可达 GC Roots 的对象,都暂时判定为死亡,只是暂时
  • 检查暂时被判定为死亡对象,检查是否有重写 finalize()方法,如果有,则触发,对象可以在里面完成自救。

如果没有自救成功 或者 没有重写 finalize()方法,则宣告这个对象的死亡。

除此之外,这个对象中的 finalize()方法,只能被调用一次,一生只有一次自救机会。

四种引用类型

JDK 1.2 后,Java 中才有了后 3 种引用的实现。

  • 强引用: 像 Object obj = new Object() 这种,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用: 用来引用还存在但非必须的对象。对于软引用对象,在 OOM 前,虚拟机会把这些对象列入回收范围中进行第二次回收,如果这次回收后,内存还是不够用,就 OOM。实现类:SoftReference
  • 弱引用: 被弱引用引用的对象只能生存到下一次垃圾收集前,一旦发生垃圾收集,被弱引用所引用的对象就会被清掉。实现类:WeakReference
  • 虚引用: 幽灵引用,对对象没有半毛钱影响,甚至不能用来取得一个对象的实例。它唯一的用途就是:当被一个虚引用引用的对象被回收时,系统会收到这个对象被回收了的通知。实现类:PhantomReference

宣告对象死亡的两次标记过程

  • 当发现对象不可达后,该对象被第一次标记,并进行是否有必要执行 finalize() 方法的判断;
    • 不需要执行:对象没有覆盖 finalize() 方法,或者 finalize() 方法已被执行过(finalize() 只被执行一次);
    • 需要执行:将该对象放置在一个队列中,稍后由一个虚拟机自动创建的低优先级线程执行。
  • finalize() 方法是对象逃脱死亡的最后一次机会,不过虚拟机不保证等待 finalize() 方法执行结束,也就是说,虚拟机只触发 finalize() 方法的执行,如果这个方法要执行超久,那么虚拟机并不等待它执行结束,所以最好不要用这个方法。
  • finalize() 方法能做的,try-finally 都能做,所以忘了这个方法吧!

方法区的回收

永久代的 GC 主要回收:废弃常量 和 无用的类

  • 废弃常量:例如一个字符串 "abc",当没有任何引用指向 "abc" 时,它就是废弃常量了。
  • 无用的类:同时满足以下 3 个条件的类。
    • 该类的所有实例已被回收,Java 堆中不存在该类的任何实例;
    • 加载该类的 Classloader 已被回收;
    • 该类的 Class 对象没有被任何地方引用,即无法在任何地方通过反射访问该类的方法。

垃圾收集算法

基础:标记 - 清除算法

  • 算法描述:
    • 先标记出所有需要回收的对象(图中深色区域);
    • 标记完后,统一回收所有被标记对象(留下狗啃似的可用内存区域……)。
  • 不足:
    • 效率问题:标记和清理两个过程的效率都不高。
    • 空间碎片问题:标记清除后会产生大量不连续的内存碎片,导致以后为较大的对象分配内存时找不到足够的连续内存,会提前触发另一次 GC。

标记清除GC算法.png

解决效率问题:复制算法

  • 算法描述:

    • 将可用内存分为大小相等的两块,每次只使用其中一块;
    • 当一块内存用完时,将这块内存上还存活的对象复制到另一块内存上去,将这一块内存全部清理掉。
  • 不足: 可用内存缩小为原来的一半,适合GC过后只有少量对象存活的新生代。

  • 节省内存的方法:

    • 新生代中的对象 98% 都是朝生夕死的,所以不需要按照 1:1 的比例对内存进行划分;
    • 把内存划分为:
      • 1 块比较大的 Eden 区;
      • 2 块较小的 Survivor 区;
    • 每次使用 Eden 区和 1 块 Survivor 区;
    • 回收时,将以上 2 部分区域中的存活对象复制到另一块 Survivor 区中,然后将以上两部分区域清空;
    • JVM 参数设置:-XX:SurvivorRatio=8 表示 Eden 区大小 / 1 块 Survivor 区大小 = 8

复制GC算法.png

解决空间碎片问题:标记 - 整理算法

  • 算法描述:
    • 标记方法与 “标记 - 清除算法” 一样;
    • 标记完后,将所有存活对象向一端移动,然后直接清理掉边界以外的内存。
  • 不足: 存在效率问题,适合老年代。

标记整理GC算法.png

进化:分代收集算法

  • 新生代: GC 过后只有少量对象存活 —— 复制算法
  • 老年代: GC 过后对象存活率高 —— 标记 - 整理算法

 参考资料:

https://github.com/TangBean/understanding-the-jvm/blob/master/Ch1-Java%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E6%9C%BA%E5%88%B6/02-%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86(GC).md#%E8%A7%A3%E5%86%B3%E7%A9%BA%E9%97%B4%E7%A2%8E%E7%89%87%E9%97%AE%E9%A2%98%E6%A0%87%E8%AE%B0---%E6%95%B4%E7%90%86%E7%AE%97%E6%B3%95

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值