垃圾收集算法与垃圾收集器
对象存活判断
- 引用计数:一个对象被引用了,计数+1,否则就-1,如果计数为0,意味着没有被引用可以回收。
但是无法解决对象相互循环引用的问题。用快慢指针不就好了吗(笑 - 可达性分析:从GC
Roots开始向下搜索引用对象,如果一个对象没有被搜索到的话,为不可达对象,意味着没有被引用可以回收。
GC Roots包括:- 方法区的常量/静态对象
- 虚拟机栈的对象
垃圾收集算法
标记-清除算法
先标记死亡的对象,标记完成后回收死亡对象的空间。
会产生空间碎片,如果要分配大对象而又没有足够大的空间,就又会触发GC以期清除出足够大的空间。
标记-整理算法
先标记存活的对象,然后将存活对象移到内存的一端,清理边界以外的内存。
能整理出大片的连续空间。如果对象多是长命不动的,需要被整理的对象就会相对较少,因此适用于老年代。
复制算法
- 将内存划为大小一样的两半。新对象分配在其中一半,需要GC时,把存活对象复制到另外一半内存,清空本来的一半内存。
虽然内存划一半能确保存活对象都能放下,但是就是浪费了一半的内存。由于需要将存活对象来回复制,所以适用于新生代。 - 升级版复制算法是将内存划分为两个幸存区和一个eden区,比例1:1:8。
新对象在eden区分配,需要GC时,把存活对象复制到其中一个幸存区,清空eden区和另一个幸存区。
由于幸存区未必放得下全部存活对象,因此会使用老年代的内存来担保。
垃圾收集器
概念
- 并行(Parallel):多条垃圾收集线程并行,用户线程依然STW
- 并发(Concurrent):用户线程与垃圾收集线程同时执行(不一定在并发,要看CPU调度,反正用户线程没被STW)
- 吞吐量:用户代码时间 /(用户代码时间 + 垃圾收集时间),所以看重总体占比,而非每次垃圾收集时间长短
- 新生代GC(Minor GC):新生代垃圾收集,比较频繁,速度较快
- 老年代GC(Major GC / Full GC):Full GC指新生代和老年代都GC,但很多时候老年代的GC是由新生代GC触发的
新生代 | 老年代 | |
---|---|---|
串行回收 | Serial | Serial Old |
并行回收 | ParNew,Parallel Scavenge | Parallel Old |
并发回收 | G1 | CMS,G1 |
Serial与Serial Old
STW,单线程,新生代使用复制算法,老年代使用标记-整理算法。
适用于桌面应用,内存不大,STW时间短。单核服务器,单线程效率高。

ParNew
ParNew是Serial的多线程版本,新生代收集器。
在Server模式下,是除了Serial,唯一能和CMS配合的收集器。
Parallel Scavenge与Parallel Old
STW,多线程,新生代使用复制算法,老年代使用标记-整理算法。
关注吞吐量,适合于后台交互不多的服务。

CMS收集器
CMS(Concurrent Mark Sweep)是老年代收集器,使用标记-清除算法。
以最短停顿时间为目标,所以适用交互比较多的Server。
CMS收集器工作分为四个步骤:
1. 初始标记:STW,只是标记一下GC Roots能直接关联到的对象,速度很快
2. 并发标记:进行可达性分析,耗时长
3.
重新标记:STW,修正在并发标记期间,用户线程继续运行所变动的对象的标记
4. 并发清除:使用标记-清除算法进行清除

耗时主要在并发标记和并发清除,有点低停顿,并发收集。缺点是:
1. 使用标记-清除算法会导致内存碎片。
不过可以通过虚拟机参数配置在Full
GC之前进行碎片整理,也可以设置多少次Full GC进行一次碎片整理。
2. 并发标记期间无法标记用户线程产生的新垃圾,需要下次GC再清理。
并且不能等到内存满了再GC,不然在GC时就没有新内存给用户线程申请了。
如果真的在GC时没有内存了,会使用Serial Old做备案。
3. 并发能力依赖于CPU资源,如果CPU紧张,性能会比较差。
G1收集器
G1内存划分
G1将内存划分为多个等大小的区域(Region),区域最小1M,最大32M,需是2的幂次方,默认会把堆进行2048等分。
这些区域可能是Eden,Survivor、Humongous区和未使用区域。逻辑上映射为eden区、幸存区、老年代。
其中,Humongous用于存放大于50%区域大小的对象。如果一个H区还装不下,G1回去寻找连续的H区,为此可能会使用Full
GC。
Remembered Set
由于G1将堆划分为多个区域,对象可能会引用多个其他区域的对象,这导致在可达性分析时需要扫描整个堆。
为此G1在每个区域里维护了一个Remembered
Set,当发现对引用进行写操作时,虚拟机会加一个写屏障,在写之前检查新写入的引用的对象,是否有引用其他区域的对象。
如果有则记录在Remembered Set里。这样子在可达性分析时只需要在Remembered
Set上分析即可,避免对全堆扫描。
G1回收步骤
- 初始标记:STW,只是标记一下GC Roots能直接关联到的对象,速度很快
- 并发标记:进行可达性分析,耗时长
- 最终标记:STW,修正在并发标记期间,用户线程继续运行所变动的对象的标记。
虚拟机会将这段时间里对象的变化记录在Remembered Set Logs里,此时会把Remembered Set Logs的变化合并到Remembered Set中 - 筛选回收:STW(老年代使用标记-整理算法),对各个区域的回收价值和成本进行排序,根据用户指定的停顿时间指定回收计划。
通过控制回收的区域数量来控制停顿时间。
- G1使用复制算法和标记-整理算法,不会产生内存碎片。
- G1依然有分代,但是通过把内存划分为多个区域的设计,使得G1能同时管理整个堆。
- G1支持并发回收,并且通过并行回收,利用多核CPU来缩短停顿时间。
- G1通过维护每个区域的回收空间大小与回收成本的经验值,使G1能在有限时间里达到最大的回收效率。(G1,Garbage-First名字的由来)
G1适用于多CPU,大内存(>=4G)的服务端上。
参考文章