JVM垃圾收集简介

前言

Java相比于C/C++最大的不同是不用手动进行管理内存, 这一特性得益于Java的自动回收内存机制.
这一回收动作称为Garbage Collection垃圾收集, 也就是GC. 由此延伸出了垃圾收集算法, 垃圾收集器等.

如何判断一个对象可以回收

我们判断一个对象obj是否可以回收, 一般是通过判断是否有其他对象对这个obj还持有引用. 如果没有, 这个obj就是可以回收的对象. 使用到的算法, 就是垃圾收集算法.

垃圾收集算法主要有两种

  1. 引用计数算法
  2. 可达性分析算法

引用计数算法(不使用)

最简单高效的方法, 是在对象中添加一个计数器, 比如一个count字段, 有对象持有对它的引用则加一, 有对象失去对它的引用则减一.
只要计数器为0则说明没有其他对象引用它. 那么就可以对它进行垃圾回收.

但是目前主流的Java虚拟机都没有使用引用计数算法. 这个算法看似简单的背后, 要配合大量额外处理才能保证正常工作.
比如对象间相互循环引用的问题, 就不能简单使用引用计数算法来解决.

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyGC {
public static void main(String[] args){
A a = new A();
B b = new B();
a.val = b;
b.val = a;

a = null;
b = null;

System.gc(); // a 和 b 不能被回收
}
}

按引用计数算法的逻辑, 对象ab各自的引用计数器的值都是1, 不能被回收.
但是外界已经没有对这两个对象的引用了, 按正常逻辑来说, 应该是可以被回收的.

可达性分析算法

所以, 主流的JVM还是使用可达性分析算法来判断对象是否可以被垃圾回收的. 涉及到图论相关知识.
基本思路就是通过一系列被称为GC Roots的跟对象作为起始节点, 从这些节点根据引用关系向下搜索, 搜索走过的路径称为引用链.
如果某个对象没有经过这个引用链, 那么就说明此对象可以被回收.
即使是循环引用的两个对象DE, 它们都没有经过引用链, 也可以被回收.
可达性分析算法

常见垃圾收集算法

说到垃圾收集, 就不得不说到分代收集这个概念.
JVM中大部分的对象都是朝生夕灭的, 当一个对象经历了多次GC还没有被回收, 就说明它是一个很难被回收的对象. 根据对象经过GC的次数, 将堆划分为新生代和老年代, 以此来使用不同的垃圾收集算法。

但是也有些收集器是不按分代收集的, 比如G1收集器是按一块一块的区域收集的.

标记-清除算法

标记-清除算法是最早出现的最基础的垃圾收集算法.
它主要是通过可达性分析算法, 标记出所有需要回收的对象, 然后做垃圾回收.
但它有两个缺点

  1. 执行效率不稳定, 对象越多, 标记清除花费的时间越多
  2. 内存碎片化问题, 下次分配大内存对象的时候找不到足够连续大的内存空间导致提前进行垃圾回收

标记-复制算法

标记-清除算法会出现内存碎片化的问题, 要解决内存碎片化的问题, 就需要对内存碎片进行整理. 标记-复制算法用了巧妙的方式, 用复制的方法避过了整理.
原理是将新生代内存空间划分为一个Eden区和两个Survivor区, 默认比例是8:1:1,
每次GC的时候, 会把Eden区和其中一块Survivor区做垃圾回收, 存活的对象复制到另一块未使用的Survivor区. 每次存活下来的对象年龄会加一, 到达一定年龄, 就复制到老年代去.

标记-整理算法

标记-清除算法如果每次GC时存活的对象越多, 进行复制的成本也越高, 那就还不如直接进行内存整理了.

垃圾收集器

从上面的垃圾收集算法, 可以知道, 根据不同分代的垃圾特性, 需要使用不同的垃圾收集算法, 也需要使用不同垃圾收集器.

下面简单列了一些收集器的特点

垃圾收集器 新生代 老年代 并行 垃圾收集算法 特点
Serial 单线程 复制 单核下效率最高
ParNew 多线程 复制 Serial多线程版本, 和CMS是官配
Parallel Scavenge 多线程 复制 吞吐量优先
Serial Old 单线程 复制 Serial老年代版本
Parallel Old 多线程 整理 Parallel Scavenge老年代版本
CMS 并发多线程 清除 尽量少停顿时间
G1 并发多线程 分区回收 大内存无脑上

各个分代间的垃圾收集器搭配

老年代/年轻代 Serial ParNew Parallel Scavenge
Serial Old
Parallel Old
CMS

Serial + Serial Old是低配置服务端的解决方案.
ParNew + CMSJDK9之前官方推荐的服务端模式下的收集器解决方案.
Parallel Scavenge + Parallel Old是吞吐量优先的组合, 应用在处理器资源比较稀缺的场合.
G1是超大堆的首选, 遇到超大堆就直接选G1.

CMS 垃圾收集器

CMS垃圾收集器是在老年代使用标记清除算法进行垃圾回收的, 一种以获取最短回收停顿时间为目标的收集器.

回收步骤如下

  1. 初始标记, 只标记GC Roots的直接关联对象, 会发生Stop the world
  2. 并发标记, 和用户线程并发执行, 用来遍历整个对象图
  3. 重新标记, 修正因为用户线程运行导致的引用变动, 也会发生Stop the world
  4. 并发清除, 和用户线程并发执行, 因为不用去移动存活对象.

ParNew + CMSJDK9之前官方推荐的服务端模式下的收集器解决方案.

G1 垃圾收集器

Garbage First简称G1, 不再使用分代收集策略, 是一个面向全堆收集的垃圾收集器.
将全堆内存划分为一块一块的Region区域, 然后根据每块Region区域的价值(有点类似背包问题), 进行垃圾回收, 从而做到GC时间可控制.

用于在超大内存下, 替换ParNew + CMS组合的收集器方案.