垃圾收集算法与垃圾收集器

垃圾收集算法与垃圾收集器

对象存活判断

  • 引用计数:一个对象被引用了,计数+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时间短。单核服务器,单线程效率高。

Serial与Serial Old

ParNew

ParNew是Serial的多线程版本,新生代收集器。
在Server模式下,是除了Serial,唯一能和CMS配合的收集器。

Parallel Scavenge与Parallel Old

STW,多线程,新生代使用复制算法,老年代使用标记-整理算法。
关注吞吐量,适合于后台交互不多的服务。

Parallel Scavenge与Parallel Old

CMS收集器

CMS(Concurrent Mark Sweep)是老年代收集器,使用标记-清除算法。
以最短停顿时间为目标,所以适用交互比较多的Server。

CMS收集器工作分为四个步骤:
1. 初始标记:STW,只是标记一下GC Roots能直接关联到的对象,速度很快
2. 并发标记:进行可达性分析,耗时长
3. 重新标记:STW,修正在并发标记期间,用户线程继续运行所变动的对象的标记
4. 并发清除:使用标记-清除算法进行清除

CMS收集器

耗时主要在并发标记和并发清除,有点低停顿,并发收集。缺点是:
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回收步骤

  1. 初始标记:STW,只是标记一下GC Roots能直接关联到的对象,速度很快
  2. 并发标记:进行可达性分析,耗时长
  3. 最终标记:STW,修正在并发标记期间,用户线程继续运行所变动的对象的标记。
    虚拟机会将这段时间里对象的变化记录在Remembered Set Logs里,此时会把Remembered Set Logs的变化合并到Remembered Set中
  4. 筛选回收:STW(老年代使用标记-整理算法),对各个区域的回收价值和成本进行排序,根据用户指定的停顿时间指定回收计划。
    通过控制回收的区域数量来控制停顿时间。

  • G1使用复制算法和标记-整理算法,不会产生内存碎片。
  • G1依然有分代,但是通过把内存划分为多个区域的设计,使得G1能同时管理整个堆。
  • G1支持并发回收,并且通过并行回收,利用多核CPU来缩短停顿时间。
  • G1通过维护每个区域的回收空间大小与回收成本的经验值,使G1能在有限时间里达到最大的回收效率。(G1,Garbage-First名字的由来)

G1适用于多CPU,大内存(>=4G)的服务端上。

参考文章

jvm系列(三):GC算法 垃圾收集器

深入理解JVM(3)——7种垃圾收集器

一文了解JVM全部垃圾回收器,从Serial到ZGC

7种 JVM 垃圾收集器特点、优劣势及使用场景(多图)

深入剖析JVM:G1收集器+回收流程+推荐用例


垃圾收集算法与垃圾收集器
https://cellargalaxy.github.io/posts/java/16.垃圾收集算法与垃圾收集器/
作者
cellargalaxy
发布于
2020年5月15日
许可协议