JVM垃圾收集简介
前言
Java
相比于C/C++
最大的不同是不用手动进行管理内存, 这一特性得益于Java
的自动回收内存机制.
这一回收动作称为Garbage Collection
垃圾收集, 也就是GC
. 由此延伸出了垃圾收集算法, 垃圾收集器等.
如何判断一个对象可以回收
我们判断一个对象obj
是否可以回收, 一般是通过判断是否有其他对象对这个obj
还持有引用. 如果没有, 这个obj
就是可以回收的对象. 使用到的算法, 就是垃圾收集算法.
垃圾收集算法主要有两种
- 引用计数算法
- 可达性分析算法
引用计数算法(不使用)
最简单高效的方法, 是在对象中添加一个计数器, 比如一个count
字段, 有对象持有对它的引用则加一, 有对象失去对它的引用则减一.
只要计数器为0
则说明没有其他对象引用它. 那么就可以对它进行垃圾回收.
但是目前主流的Java
虚拟机都没有使用引用计数算法. 这个算法看似简单的背后, 要配合大量额外处理才能保证正常工作.
比如对象间相互循环引用的问题, 就不能简单使用引用计数算法来解决.
1 | class MyGC { |
按引用计数算法的逻辑, 对象a
和b
各自的引用计数器的值都是1
, 不能被回收.
但是外界已经没有对这两个对象的引用了, 按正常逻辑来说, 应该是可以被回收的.
可达性分析算法
所以, 主流的JVM
还是使用可达性分析算法来判断对象是否可以被垃圾回收的. 涉及到图论相关知识.
基本思路就是通过一系列被称为GC Roots
的跟对象作为起始节点, 从这些节点根据引用关系向下搜索, 搜索走过的路径称为引用链.
如果某个对象没有经过这个引用链, 那么就说明此对象可以被回收.
即使是循环引用的两个对象DE
, 它们都没有经过引用链, 也可以被回收.
常见垃圾收集算法
说到垃圾收集, 就不得不说到分代收集这个概念.JVM
中大部分的对象都是朝生夕灭的, 当一个对象经历了多次GC
还没有被回收, 就说明它是一个很难被回收的对象. 根据对象经过GC
的次数, 将堆划分为新生代和老年代, 以此来使用不同的垃圾收集算法。
但是也有些收集器是不按分代收集的, 比如G1
收集器是按一块一块的区域收集的.
标记-清除算法
标记-清除算法是最早出现的最基础的垃圾收集算法.
它主要是通过可达性分析算法, 标记出所有需要回收的对象, 然后做垃圾回收.
但它有两个缺点
- 执行效率不稳定, 对象越多, 标记清除花费的时间越多
- 内存碎片化问题, 下次分配大内存对象的时候找不到足够连续大的内存空间导致提前进行垃圾回收
标记-复制算法
标记-清除算法会出现内存碎片化的问题, 要解决内存碎片化的问题, 就需要对内存碎片进行整理. 标记-复制算法用了巧妙的方式, 用复制的方法避过了整理.
原理是将新生代内存空间划分为一个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 + CMS
是JDK9
之前官方推荐的服务端模式下的收集器解决方案.Parallel Scavenge + Parallel Old
是吞吐量优先的组合, 应用在处理器资源比较稀缺的场合.G1
是超大堆的首选, 遇到超大堆就直接选G1
.
CMS 垃圾收集器
CMS
垃圾收集器是在老年代使用标记清除算法进行垃圾回收的, 一种以获取最短回收停顿时间为目标的收集器.
回收步骤如下
- 初始标记, 只标记
GC Roots
的直接关联对象, 会发生Stop the world
- 并发标记, 和用户线程并发执行, 用来遍历整个对象图
- 重新标记, 修正因为用户线程运行导致的引用变动, 也会发生
Stop the world
- 并发清除, 和用户线程并发执行, 因为不用去移动存活对象.
ParNew + CMS
是JDK9
之前官方推荐的服务端模式下的收集器解决方案.
G1 垃圾收集器
Garbage First
简称G1
, 不再使用分代收集策略, 是一个面向全堆收集的垃圾收集器.
将全堆内存划分为一块一块的Region
区域, 然后根据每块Region
区域的价值(有点类似背包问题), 进行垃圾回收, 从而做到GC
时间可控制.
用于在超大内存下, 替换ParNew + CMS
组合的收集器方案.