「JVM 内存管理」GC 算法理论与发展过程

从如何判定对象消亡的角度,GC 算法可以划分为引用计数式垃圾收集(Reference Counting GC)和追踪式垃圾收集(Tracing GC),主流 JVM 使用第二类;

1. 分代收集(Generational Collection)理论

收集器应将 Java Heap 划分为不同区域,并将不同年龄(对象熬过 GC 的次数)的对象分配到不同区域存储;由于大多数对象朝生夕灭(IBM 研究表明,新生代中的对象有 98% 熬不过第一轮收集),每次回收只关注如何保留少量存活对象,将之集中存储在指定区域,并以低频率回收这个区域;(这可以兼顾 GC 的时间开销和内存空间利用率);

  • 部分收集(Partial GC),只对 Java Heap 的部分区域进行回收;
    • 新生代收集(Minor GC/Young GC),只对新生代的垃圾进行收集;
    • 老年代收集(Major GC/Old GC),只对老年代垃圾进行收集;(仅 CMS 存在单独的 Old GC 行为);
    • 混合收集(Mixed Old),对新生代和部分老年代的垃圾进行收集;(仅 G1 存在 Mixed GC 行为);
  • 整堆收集(Full GC),对整个 Java Heap 和方法区的垃圾进行收集;

针对不同区域存储对象的存亡特征可以安排不同的 GC 算法(标记复制、标记清除、标记整理);

分代收集存在明显的困难,如对象间存在跨代引用(比如新生代对象被老年代引用,为了找出新生代的存活对象,不得不在固定的 GC Roots 之外额外遍历整个老年代的所有对象,这将给内存回收带来很大的性能负担);

为了解决对象间存在跨代引用的问题,JVM 在新生代上添加了一个全局的数据结构(Remembered Set),用以标记老年代存在跨代引用的内存块,当发生 Minor GC 时,将这些内存块中的对象加入到 GC Roots 中;(这需要额外在对象间引用关系发生改变时维护 Set 数据正确性);

分代收集是一套符合大多数程序实际运行情况的经验法则,它建立在 3 个分代假说之上:

  • 弱分代假说(Weak Generational Hypothesis),绝大多数对象都是朝生夕灭的;
  • 强分代假说(Strong Generational Hypothesis),熬过越多次 GC 过程的对象就越难以消亡;
  • 跨代引用假说(Intergenerational Reference Hypothesis),跨代引用相对同代引用占比极少;

2. 标记清除(Mark Sweep)算法

标记所有需要回收的(存活的)对象,之后统一清除所有非存活对象;最基本也是最早出现的 GC 算法(1960 年 Lisp 之父 John McCarthy 提出);

主要缺点

  • 内存空间碎片化,标记、清除会产生大量不连续的内存碎片,空间碎片太多会导致在分配较大对象时找不到足够大的连续内存而提前触发 GC;
  • 执行效率不稳定,如果 Java Heap 中包含大量需要被回收的对象,就需要进行大量标记和清楚的动作,执行效率随对象数量增长会降低;

3. 标记复制(Semispace Copying)算法

将可用内存一分为二,每次只使用其中的一块,当这一块内存用完,将被标记为存活的对象复制到另外一块内存上,然后把这块内存空间一次清理掉;这对多数对象是可回收的场景是很高效的,且无空间碎片干扰的;内存分配也是可用指针碰撞的(实现简单、运行高效);

主要缺点

  • 空间浪费过大,内存会被一份为二,可用内存缩小为原来的一半;
  • 不适用于多数内存存活的场景,这会产生大量内存间复制的开销;

现代商用 JVM 大多使用复制算法进行新生代的 GC,但在其基础上做了优化(并不是直接将内存一份为二,而是引入了 Eden 区的概念;1989,Andrew Appel,Appel 式回收);

Appel 式回收的做法是,将新生代划分为 Eden 空间和两个 Survivor 空间(默认 8:1:1),每次分配内存只使用 Eden 区和其中一块 Survivor 区,发生 GC 时,将 Eden 和 Survivor 中依旧存活的对象统一复制到另一 Survivor 区,然后直接清除 Eden 区和当前 Survivor 区;

当 Survivor 区不足以容纳一次 Minor GC 后存活的对象时,可以依赖其他内存区域(大多数是老年代)进行分配担保(Handle Promotion,存活对象直接进入老年代);

4. 标记整理(Mark Compact)算法

老年代一般 100% 空间都是可用状态,不会直接使用复制算法;因此 1974 年 Edward Lueders 提出了正对性的 标记整理算法;

标记完存活对象后,将所有存活对象向内存空间的一端移动(不直接清除可回收对象),直到清理完边界以外的内存;(移动式回收算法);

主要缺点

  • 操作较重,老年代 GC 一般有大量对象存活,移动存活对象并更新所有引用这些对象的引用值是比较大的开销;这些移动操作必须全程暂停用户程序(Stop The World);

若不移动存活对象,引发的内存空间碎片问题会更难解决;(如分区空闲分配链表,但这类操作会对内存的访问造成额外负担,直接影响应用程序吞吐量);

不移动对象分配内存会变复杂,但 GC 停顿时间更短;移动对象回收内存会变复杂,但程序吞吐量更高;因为内存分配和访问相比 GC 的频率要高得多,不移动对象虽然 GC 更高效,但整体吞吐可能还是降低的;(Parallel Scavenge 关注吞吐,使用的是标记整理算法;CMS 关注的是延迟,使用的是标记清楚算法,但当空间碎片过多时 CMS 也会临时进行标记整理);


上一篇:「JVM 内存管理」GC 可回收的对象
下一篇:「JVM 内存管理」HotSpot GC 算法细节实现

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三余知行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值