【JVM】 常用GC算法简介

在对Java内存区域进行回收时, 我们会涉及到不同的算法, 这些算法或多或少都有自身的优缺点, 正因如此, 它们有着自己的用武之地(适合的内存区), 能在适合自己的场景下发挥长处.

可回收判断方法


在介绍回收算法前, 我们需要了解一下, 如何进行对象可回收的判断, 常见的有两种方法:

1 引用计数:

这个方法简单高效, 但是很难解决对象循环引用的问题

2 引用树 (可达性分析) :

主流商用语言的主流实现中, 都是通过可达性分析(Reachability Analysis)实现的, 也有人称为引用树.
这个方法以GC Roots为起点, 当一个对象不能通过起点到达, 则说明次对象是可回收的.

GC Roots包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象

当然, Java 引用再细说还分为强引用 软引用 弱引用 虚引用, 他们有特定的回收规则, 这里不再赘述.

mark-sweep 标记-清除算法


首先介绍标记-清除算法, 顾名思义, 该算法分为两个阶段:

  • 标记
    标记出需要回收的对象

  • 清除
    在标记完成后, 统一回收所有被标记的对象

用图来表示会直观一点, 我们首先定义以下图例:

算法工作过程如下:

缺点

从这个回收过程看, 我们也可以发现有以下缺点:

  • 内存碎片问题
    大量不连续的内存碎片, 如果碎片过多, 在需要分配大对象时, 无法满足连续的内存需求, 会导致提前触发GC.

copying 复制算法


为了解决低存活率内存区的效率问题, 出现了一种"复制"算法.
该算法将内存分为大小相同的两部分, 只使用其中的一块, 当其内存快用完时, 将存活对象复制到另外一块, 并将使用内存切换到另外另外一块, 同时原来的整个内存块进行回收. 这种方法不用考虑内存碎片问题, 简单高效(只需移动堆顶指针, 按序分配内存).

算法工作过程如下:

缺点

内存缩小到了原来的一半.

应用场景

新生代的回收:

因为新生代的对象98%都是朝生夕死(根据IBM的研究), 比如Hotspot虚拟机, 有一个大的eden内存区和两个survivor区, 运行时使用eden和其中一块survivor, 默认8:1的eden:survivor比例.
即是有10%的内存会被用作第二块survivor内存. 当然, 如果survivor空间不足时, 会有其他内存如老年代进行担保, 而且survivor也并不需要很大内存.

标记整理算法


针对老年代, mark-compact (标记整理) 算法是比较适合的.
标记过程和标记-清除算法一样, 不过回收的时候, 是会让存活对象往一端移动, 再清理掉存货对象边界外的内存.

算法工作过程如下:

分代收集算法


该算法主要思想是, 根据对象存活周期和其他特点的不同, 去选择适当的收集算法, 这个策略也在前面提到过了.
比如新生代存活比例小, 选择复制算法; 而老年代的存活比例高, 则采用"标记-清除"或"标记-整理"; 分代收集也算是顶层的指导策略了.

参考


<深入理解Java虚拟机 JVM高级特性和最佳实践>.周志明