GC总结

74 阅读12分钟

总结:

JDK9关掉了Serial与CMS、ParNew与Serial Old。这样ParNew就成为了专门用于CMS的新生代收集器。

Serial Old会用于CMS出问题时的代替,剩下一个串行的收集器组合、ParNew与CMS用于B/S系统(停顿时间小),Parallel Scavenge与Parallel Old用于服务器(追求吞吐量),外加一个G1。

Serial 收集器

单线程跑,单CPU的时候嘎嘎快,没有线程交互的开销,图中为Serial和Serial Old组合的收集器。

JDK1.3之前是新生代收集的唯一选择,Serial目前仍然是HotSpot虚拟机在Client模式下的默认收集器。

客户端给虚拟机管理的内存不大的情况下,收集几十兆至一两百兆新生代,停顿时间控制在最多100毫秒左右。

Serial Old 收集器

如Serial 收集器中的图所示,Serial Old是Serial 收集器的老年代版本。一般用于客户端。

在服务器端,有以下两点用处:在JDK1.5之前与Parallel Scavenge搭配(那阵没有Parallel Old),当CMS并发收集发生 Concurrent Mode Failure 时使用Serial Old。

Parallel Scavenge 收集器

与 ParNew 一样是多线程收集器。追求吞吐量,

高吞吐量可以高效的利用CPU时间,适合在后台计算而不需要太多交互的任务。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的: 新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。

GC参数:GC Ergonomics,打开GC自适应的调节策略,虚拟机自动调节新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数,以提供最合适的停顿时间或者最大的吞吐量。

Parallel Old 收集器

如Parallel Scavenge中的图所示,是 Parallel Scavenge 收集器的老年代版本。

ParNew 收集器

Serial 收集器的多线程版本,默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。JDK1.5(CMS提出)后,ParNew 收集器是激活CMS后的默认新生代收集器。JDK7(G1提出)之前,遗留项目首选的新生代收集器。

CMS 收集器

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。应用于B/S系统。

分为以下四个流程:

  • 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
  • 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
  • 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
  • 并发清除: 不需要停顿。

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。

具有以下缺点:

  • 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
  • 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。JDK5中默认68%,JDK6中默认92%。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure(并发失败),这时虚拟机将临时启用 Serial Old 来替代 CMS。停顿时间会变长。
  • 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。——为了避免设计了两个GC参数(在JDK9开始废弃),一个是在不得不进行Full GC时,开启内存碎片合并整理过程,空间问题解决,停顿时间加长。一个是在进行若干次不整理空间的Full GC后,在进行Full GC之前,进行一次碎片整理,参数设置为0,就是每次进入Full GC之前都整理碎片。

G1 收集器

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离,可以一起回收。JDK8成熟。

JDK9中,G1正式取代Parallel Scavenge和Parallel Old的组合,成为服务端下的默认垃圾收集器。

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。G1要采用内存的10%到20%来维系这项工作。

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

  • 初始标记,仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。依靠Minor GC的时候同步完成,所以没有额外的停顿。
  • 并发标记,从 GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里对象图,找到可回收的对象,但可与用户程序并发执行。还要重新处理一下原始快照(SATB)记录下的并发时有引用变动的对象。
  • 最终标记: 对用户线程做另外一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的STAB记录,但是可并行执行。
  • 筛选回收: 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间。操作设计对象的移动,在G1中选择暂停用户线程,并行执行(ZGC的特性,不暂停用户线程)。

具备如下特点:

  • 空间整合: 整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
  • 可预测的停顿: 能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。

6GB到8GB堆内存之下,CMS更好,反之G1更好。

ZGC 收集器

JDK11发布了ZGC 收集器

  • 停顿时间不超过10ms;
  • 停顿时间不会随着堆的大小,或者活跃对象的大小而增加(对程序吞吐量影响小于15%);
  • 支持8MB~4TB级别的堆(未来支持16TB)。

与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。

  • 并发标记,和G1一样,遍历对象图做可达性分析的阶段,经过初始标记、最终标记的短暂停顿,不同的是,ZGC在指针上而不是对象上进行标记,标记会更新染色指针的Marked 0、Marked 1标志位。
  • 并发预备重分配,通过特定条件选择清理哪些Region,组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取G1中记忆集的维护成本。ZGC的重分配集,只是决定了里面的存活对象会被重新复制到其他的Region进行,里面的Region会被释放。
  • 并发重分配, 重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否 重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障(见下面详解))所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。当某个Region所有存活的对象复制完毕后,只要留着转发表,就可以将这个Region用于新对象了,因为老的对象,可以通过自愈能力,一点点更改。
  • 并发重映射,重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。

ZGC关键技术

ZGC通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理描述如下:并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即着色指针。下面介绍着色指针和读屏障技术细节。

着色指针

ZGC实际仅使用64位地址空间的第041位,而第4245位存储元数据,第47~63位固定为0。

ZGC将对象存活信息存储在42~45位中,这与传统的垃圾回收并将对象存活信息放在对象头中完全不同。

读屏障

读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。需要注意的是,仅“从堆中读取对象引用”才会触发这段代码。

读屏障示例:

Object o = obj.FieldA   // 从堆中读取引用,需要加入屏障
<Load barrier>
Object p = o  // 无需加入屏障,因为不是从堆中读取引用
o.dosomething() // 无需加入屏障,因为不是从堆中读取引用
int i =  obj.FieldB  //无需加入屏障,因为不是对象引用

ZGC中读屏障的代码作用:在对象标记和转移过程中,用于确定对象的引用地址是否满足条件,并作出相应动作。

记忆集——卡表(特殊讲解):

避免将整个老年代或者整个Region加入GC Roots遍历。

在CMS等分代模型上,新生代维护一个记忆集(数组),每一个位置记录着老年代内存的一部分,0代表是脏的,这部分老年代中有一些对象是引用了新生代的,所以young GC时,利用这个记忆集放入GC Root中。

对于G1来说,每一个Region都维护着一个记忆集,它更像是哈希表,key为其他region的起始地址,Value为该region的卡表索引号的集合。同样可以根据这个进行GC。

zhuanlan.zhihu.com/p/444691935

blog.csdn.net/qq_39432354…

这个网址的前面我不苟同,还是要依靠GCRoots的,但是不能把整个老年代同样搜索一遍,所以就只能依靠记忆集的方法,记录需要搜索的老年代。

我怀疑他的意思是,GCRoots属于新生代和老年代啥的,依靠这个记忆集,我可以只遍历GCRoots在脏的老年代中的,不需要把所有老年代的GCRoots进行遍历了。

blog.csdn.net/Hireek/arti…