JVM系列(四) - 垃圾收集算法和垃圾收集器

内容导读

  1. 垃圾收集算法
  2. 垃圾收集器
  3. 三色标记算法
  4. 读写屏障
  5. 记忆集与卡表
  6. 安全点

一. 垃圾收集算法

复制算法

简单来说, 就是将一块内存均分成A,B两部分. 假设当前正在使用A部分,新创建的对象都会放入A. 当A放不了, 会将存活的对象复制到B部分.清除其余的作为垃圾对象,再有新对象被创建,会放入B部分,直到B满了,重复上面的操作。
优点: 效率高
缺点: 内存使用率不高, 因为每次只能使用其中一半的内存


复制算法.png

标记清理算法

与复制算法不同的是, 算法分为两部分: 标记 + 清理, 标记出存活对象, 然后对垃圾对象进行回收.但该算法有明显的缺点: 效率问题内存碎片, 当对象特别多时, 效率不高; 清理完毕后, 会产生大量不连续的内存碎片空间,

标记清除.png

标记整理算法

该算法分为两部分: 标记整理, 标记存活对象, 清除垃圾对象, 然后将存活对象直接移动到垃圾对象所在的内存空间, 最后再清理掉没有被覆盖的垃圾对象.

标记整理.png

分代算法

上面的算法都是理论, 而分代算法是对以上理论的实现.当前JVM都是采用分代算法. 比如在Eden区采用复制算法, 在Survivor区采用标记清除算法. 在老年代采用标记整理算法.

二. 垃圾收集器

垃圾收集器.png

中间的虚线分隔年轻代和老年代,虚线上面是年轻代,虚线下面是老年代。
实线部分表示两款垃圾收集器可以配合使用.注意, CMS和Serial Old 中间有一根红线, 是要避免出现的情况.

Serial

在JVM早期, 受硬件设备限制, JVM使用的是Serial垃圾收集器, 从名字就可以看出, 这是一款单线程垃圾收集器.也就是说, 在进行垃圾收集的时候, 应用啥都做不了. 这一现象也就是著名的"STW"现象(Stop The World)。在深入Java虚拟机一书中,有一个比较形象的例子。在打扫卫生时,一边丢垃圾,一边扫垃圾,肯定没有先关上门专心扫垃圾快。Serial垃圾收集器简单高效
Serial垃圾收集器一共有两款:SerialSerialOld, 分别对象年轻代和老年代的垃圾收集器

-XX:+UserSerialGC 
-XX:+UserSerialOldGC
image.png

Parallel

Parallel是一款并发垃圾收集器,就是Serial的多线程版本。Parallel关注的是吞吐量。Parallel同样有年轻代和老年代两个垃圾收集器。

-XX:+UseParallelGC
-XX:+UseParallelOldGC
image.png

ParNew

Parallel垃圾收集器有个缺点, 不能和CMS配合使用, 因此JVM又提供了一款并发的垃圾收集器-ParNew


image.png

CMS

CMS(Concurrent Mark Sweep)是一款以最短时间停顿为目标的垃圾收集器。它是一款真正意义上的并发垃圾收集器。CMS采用的标记-清除算法,主要分为五步:

image.png

  • 初始标记
    先STW,然后从GC Root开始标记,找到GC Root后就不在继续往下找了,可以理解为STW,标记GC Root。
  • 并发标记
    在这一阶段,从GC Root的直接关联对象开始标记,这一过程相对比较耗时。因此用户线程和垃圾回收线程并发执行。因为是并发执行,会出现漏标误标的情况
    发生。对于误标的情况,会在下次GC进行回收,这类垃圾叫做浮动垃圾;而对于漏标的情况,会造成很严重的后果,因此,JVM采用三色标记法来解决这一问题。
  • 重新标记
    这一阶段主要是为了修正并发期间因用户线程并发执行而导致标记发生变化的那部分对象的记录。这个阶段消耗的时间比初始标记时间长,但远远小于并发标记阶段。这一段主要采用三色标记算法增量更新算法做重新标记
  • 并发清理
    对未标记的区域进行清理, 这一阶段产生的新对象会被标记成黑色。不做任何处理
  • 并发重置
    清理结束后,要清除对象的标记

主要优点:并发收集,低停顿
但是也有几个明显的缺点:
1)无法处理浮动垃圾
2)对CPU敏感,因为是和用户线程并发执行,所以会和用户线程抢占资源
3)无法处理浮动垃圾,只能放到下一次GC时回收
4)如果垃圾收集失败,会进入“concurrent mode failure”,此时会STW,并采用SerialOld垃圾收集器进行回收
5)CMS采用标记清除算法,会产生大量内存碎片,但是
-XX:+UseCompactAtFullCollection配置,可以配置几次GC后对内存进行压缩

核心参数

-XX:+UseConcMarkSweepGC: 启动cms
-XX:ConcGCThreads: 并发的GC线程数
-XX:+UseCompactAtFullCollection: 经过几次full gc后进行内存压缩, 默认是0, 每次都会压缩
-XX:CMSInitiatingOccupancyFraction: 当老年代使用比例达到该值后,触发Full GC
-XX:+UseCMSInitiatingOccupancyOnly: 只使用CMSInnitiatingOccupancyFraction设定的值, 没设定的话,JVM仅在第一次使用设定值, 后续会自动调整
-XX:+CMSScavengeBeforeRemark: 在CMS GC启动前先触发下Minor GC, 目的是减少老年代对年轻代对象的引用
-XX:+CMSParallellnitialMarkEnabled: 初始标记阶段多线程执行
-XX:+CMSParallelRemarkEnabled: 并发标记阶段多线程执行

G1

ZGC

三色标记法

三色标记法主要是将对象分为三种颜色: 黑色, 灰色白色
黑色: 表示从GC Root开始全部标记完成的对象
灰色: 表示还未完全标记完的对象
白色: 表示还没被扫描到的对象, 或者是不可达的对象(经过可达性分析, 没有被引用的对象).

多标 - 浮动垃圾

在初始标记的过程中, 一开始是非垃圾对象, 后来在并发标记的过程, 由于垃圾回收线程和用户线程并发处理, 可能这个对象已经不再被引用, 这种对象应该回收却没有回收的对象被称为浮动垃圾, 这种对象影响比较小, 可以在下次GC时判断是否需要回收. 还有就是在并发标记和并发清理过程中新建的对象, 都统一标称黑色。

漏标 - 读写屏障

在初始标记的过程中是垃圾对象, 但是在并发标记的过程中, 可能有被重新引用, 因此在重置标记阶段会做一次重新标记,主要解决漏标的情况,一般有增量更新和原始快照两种方式。而cms采用增量更新的方式。
增量更新(Incremental Update)
增量更新是当黑色对象引用白色对象时,将引用关系保存下来,在并发标记结束后,以黑色对象为GC Root,重新扫描

原始快照(Snapshot At The Beginning,SATB)
原始快照是灰色对象在删除白色对象的引用时,将引用关系保存下来,在并发标记结束后,以灰色对象作为GC Root重新扫描,这样可以扫描到白色对象,将白色对象改为黑色,在下次GC时回收

写屏障

增量更新和原始快照都是通过写屏障实现的。
写屏障就是在赋值语句前后做一些操作,类似AOP中的around。

读屏障

读屏障是在读取的时候记录下对象

oop oop_field_load(oop* field) {
  pre_load_barrier(field); // 读屏障‐读取前操作
  return *field;
}

void pre_load_barrier(oop* field) {
  oop old_value = *field;
  remark_set.add(old_value); // 记录读取到的对象
}

记忆集与卡表

在新生代做GC Root扫描时,可能出现跨代的情况,即新生代的对象引用了老年代的对象。如果要扫描整个老年代,效率太低。因此,JVM采用记忆集的方式解决跨代引用的问题。

  • 记忆集
    在新生代引入记忆集(Remember Set)的数据结构,记录非收集区对收集区的指针集合。在垃圾收集时,只需要判断非收集区是否存在指向收集区的指针。
    hotspot采用卡表(Card Table)的方式实现记忆集,卡表由卡页(Card Page)组成。将老年代划分成一个个卡页,每个卡页512字节。当出现跨代引用时,将对应卡页的状态改为dirty。这样,原本需要扫描整个老年代变成只需要扫描对应的卡页即可。卡表记录某个卡页的dirty状态,而这一操作是通过写屏障实现的。

安全点

安全点指的是代码中的一些安全位置,当代码运行到这些位置时状态是确定的,这样JVM就可以进行一些安全操作。
安全位置主要有以下几种

  • 方法返回前
  • 调用某个方法之后
  • 抛出异常的位置
  • 循环的末尾

主要目的就是避免程序长时间无法进入safepoint,比如JVM在做GC之前要等所有的应用线程进入到安全点后VM线程才能分派GC任务 ,如果有线程一直没有进入到安全点,就会导致GC时JVM停顿时间延长

安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集 过程的安全点。但是,程序“不执行”的时候呢?

所谓的程序不执行就是没有分配处理器时间,典型的 场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。

对于 这种情况,就必须引入安全区域(Safe Region)来解决。

安全区域

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的 阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以 离开安全区域的信号为止。

你可能感兴趣的:(JVM系列(四) - 垃圾收集算法和垃圾收集器)