导航:首页 > 源码编译 > jvmcms标记源码

jvmcms标记源码

发布时间:2023-01-22 04:42:06

① G1从入门到放弃(一)

最近在看关于G1垃圾收集的文章,看了很多国内与国外的资料,本文对G1的这些资料进行了整理。这篇合适JVM垃圾回收有一定基础的同学,作为G1入门可以看一下,如果要死磕G1实现的内容细节。大家可以找 R大 。 个人认为R大是目前国内JVM领域研究的先驱了,当然R大也是不建议大家去看JVM的源码的。 为啥别读HotSpot VM的源码
G1系列第一篇文章会介绍G1的理论知识,不会做JVM源码的深入分析。第二篇准备介绍G1实践中的日志分析。

G1(Garbadge First Collector)作为一款JVM最新的垃圾收集器,可以解决CMS中Concurrent Mode Failed问题,尽量缩短处理超大堆的停顿,在G1进行垃圾回收的时候完成内存压缩,降低内存碎片的生成。G1在堆内存比较大的时候表现出比较高吞吐量和短暂的停顿时间,而且已成为java 9的默认收集器。未来替代CMS只是时间的问题。

G1的内存结构和传统的内存空间划分有比较的不同。G1将内存划分成了多个大小相等的Region(默认是512K),Region逻辑上连续,物理内存地址不连续。同时每个Region被标记成E、S、O、H,分别表示Eden、Survivor、Old、Humongous。其中E、S属于年轻代,O与H属于老年代。
示意图如下:

H表示Humongous。从字面上就可以理解表示大的对象(下面简称H对象)。 当分配的对象大于等于Region大小的一半 的时候就会被认为是巨型对象。H对象默认分配在老年代,可以防止GC的时候大对象的内存拷贝。通过如果发现堆内存容不下H对象的时候,会触发一次GC操作。

在进行Young GC的时候,Young区的对象可能还存在Old区的引用, 这就是跨代引用的问题。为了解决Young GC的时候,扫描整个老年代,G1引入了 Card Table 和 Remember Set 的概念,基本思想就是用空间换时间。这两个数据结构是专门用来处理Old区到Young区的引用。Young区到Old区的引用则不需要单独处理,因为Young区中的对象本身变化比较大,没必要浪费空间去记录下来。

下图展示的是 RSet 与 Card 的关系。每个 Region 被分成了多个 Card ,其中绿色部分的 Card 表示该 Card 中有对象引用了其他 Card 中的对象,这种引用关系用蓝色实线表示。 RSet 其实是一个HashTable,Key是Region的起始地址,Value是 Card Table (字节数组),字节数组下标表示 Card 的空间地址,当该地址空间被引用的时候会被标记为 dirty_card 。

关于RSet结构的维护,可以参考这篇 文章 ,这里不做过多的深入。

SATB的全称(Snapshot At The Beginning)字面意思是开始GC前存活对象的一个快照。SATB的作用是保证在并发标记阶段的正确性。如何理解这句话?
首先要介绍三色标记算法

在GC扫描C之前的颜色如下:

在并发标记阶段,应用线程改变了这种引用关系

得到如下结果。

在重新标记阶段扫描结果如下

这种情况下C会被当做垃圾进行回收。Snapshot的存活对象原来是A、B、C,现在变成A、B了,Snapshot的完整遭到破坏了,显然这个做法是不合理。
G1采用的是 pre-write barrier 解决这个问题。简单说就是在并发标记阶段,当引用关系发生变化的时候,通过 pre-write barrier 函数会把这种这种变化记录并保存在一个队列里,在JVM源码中这个队列叫 satb_mark_queue 。在remark阶段会扫描这个队列,通过这种方式,旧的引用所指向的对象就会被标记上,其子孙也会被递归标记上,这样就不会漏标记任何对象,snapshot的完整性也就得到了保证。

这里引用R大对SATB的解释:

SATB的方式记录活对象,也就是那一时刻对象snapshot, 但是在之后这里面的对象可能会变成垃圾, 叫做浮动垃圾(floating garbage),这种对象只能等到下一次收集回收掉。在GC过程中新分配的对象都当做是活的,其他不可达的对象就是死的。
如何知道哪些对象是GC开始之后新分配的呢?
在Region中通过top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS来记录新配的对象。示意图如下:

每个region记录着两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象就是新分配的,因而被视为隐式marked。 这里引用R大的解释。

其中top是该region的当前分配指针,[bottom, top)是当前该region已用(used)的部分,[top, end)是尚未使用的可分配空间(unused)。
(1): [bottom, prevTAMS): 这部分里的对象存活信息可以通过prevBitmap来得知
(2): [prevTAMS, nextTAMS): 这部分里的对象在第n-1轮concurrent marking是隐式存活的
(3): [nextTAMS, top): 这部分里的对象在第n轮concurrent marking是隐式存活的

Young GC 回收的是所有年轻代的Region。 当E区不能再分配新的对象时就会触发 。E区的对象会移动到S区,当S区空间不够的时候,E区的对象会直接晋升到O区,同时S区的数据移动到新的S区,如果S区的部分对象到达一定年龄,会晋升到O区。
Yung GC过程示意图如下:

Mixed GC 翻译过来叫混合回收。之所以叫混合是因为回收所有的年轻代的Region+部分老年代的Region。
1、为什么是老年代的 部分 Region?
2、什么时候触发Mixed GC?
这两个问题其实可以一并回答。回收 部分 老年代是参数 -XX:MaxGCPauseMillis ,用来指定一个G1收集过程目标停顿时间,默认值200ms,当然这只是一个期望值。G1的强大之处在于他有一个停顿预测模型(Pause Prediction Model),他会有选择的挑选 部分 Region,去尽量满足停顿时间,关于G1的这个模型是如何建立的,这里不做深究。
Mixed GC的触发也是由一些参数控制。比如 XX: 表示老年代占整个堆大小的百分比,默认值是45%,达到该阈值就会触发一次Mixed GC。

Mixed GC主要可以分为两个阶段:
1、全局并发标记(global concurrent marking)
全局并发标记又可以进一步细分成下面几个步骤:

2、拷贝存活对象(Evacuation)
Evacuation阶段是全暂停的。它负责把一部分region里的活对象拷贝到空region里去(并行拷贝),然后回收原本的region的空间。Evacuation阶段可以自由选择任意多个region来独立收集构成收集集合(collection set,简称CSet),CSet集合中Region的选定依赖于上文中提到的 停顿预测模型 ,该阶段并不evacuate所有有活对象的region,只选择收益高的少量region来evacuate,这种暂停的开销就可以(在一定范围内)可控。

Mixed GC的清理过程示意图如下:

G1的垃圾回收过程是和应用程序并发执行的,当Mixed GC的速度赶不上应用程序申请内存的速度的时候,Mixed G1就会降级到Full GC,使用的是Serial GC。Full GC会导致长时间的STW,应该要尽量避免。
导致G1 Full GC的原因可能有两个:

PS: 本文主要参考的国内文章:
java Hotspot G1 GC的一些关键技术
Garbage First G1收集器 理解和原理分析
G1: One Garbage Collector To Rule Them All
请教G1算法的原理
深入理解 Java G1 垃圾收集器
Getting Started with the G1 Garbage Collector !

② cms如何查找标签的所在的源码

好像只有 米拓这个企业CMS能实现了
不过 他确实如楼主所说 是个伪开源系统
而且最恶劣的是 会收集你的信息反馈给官方
这个 很要命 谁都不希望涉及到自己的一些机密隐私信息被偷偷收集
虽然有破解版的 可是始终让人用的不放心
连官方都安插有后门 何况是比官方还不靠谱的第三方破解组织呢~

③ JVM之ParNew和CMS日志分析

今天这篇文章主要是对生产环境中(Java7)常用的两种垃圾收集器(ParNew:年轻代,CMS:老年代)从日志信息上进行分析,做一下总结,这样当我们在排查相应的问题时,看到 GC 的日志信息,不会再那么陌生,能清楚地知道这些日志是什么意思,GC 线程当前处在哪个阶段,正在做什么事情等。

ParNew 收集器

ParNew 收集器是年轻代常用的垃圾收集器,它采用的是复制算法,youngGC 时一个典型的日志信息如下所示:

依次分析一下上面日志信息的含义:

2018-04-12T13:48:26.134+0800:Mirror GC 发生的时间;

15578.050:GC 开始时,相对 JVM 启动的相对时间,单位时秒,这里是4h+;

ParNew:收集器名称,这里是 ParNew 收集器,它使用的是并行的 mark- 算法,GC 过程也会 Stop the World;

3412467K->59681K:收集前后年轻代的使用情况,这里是 3.25G->58.28M;

3774912K:整个年轻代的容量,这里是 3.6G;

0.0971990 secs:Duration for the collection w/o final cleanup.

9702786K->6354533K:收集前后整个堆的使用情况,这里是 9.25G->6.06G;

24746432K:整个堆的容量,这里是 23.6G;

0.0974940 secs:ParNew 收集器标记和复制年轻代活着的对象所花费的时间(包括和老年代通信的开销、对象晋升到老年代开销、垃圾收集周期结束一些最后的清理对象等的花销);

对于 [Times: user=0.95 sys=0.00, real=0.09 secs],这里面涉及到三种时间类型,含义如下:

user:GC 线程在垃圾收集期间所使用的 CPU 总时间;

sys:系统调用或者等待系统事件花费的时间;

real:应用被暂停的时钟时间,由于 GC 线程是多线程的,导致了 real 小于 (user+real),如果是 gc 线程是单线程的话,real 是接近于 (user+real) 时间。

CMS 收集器

CMS 收集器是老年代经常使用的收集器,它采用的是标记-清楚算法,应用程序在发生一次 Full GC 时,典型的 GC 日志信息如下:

CMS Full GC 拆分开来,涉及的阶段比较多,下面分别来介绍各个阶段的情况。

阶段1:Initial Mark

这个是 CMS 两次 stop-the-wolrd 事件的其中一次,这个阶段的目标是:标记那些直接被 GC root 引用或者被年轻代存活对象所引用的所有对象,标记后示例如下所示

上述例子对应的日志信息为:

逐行介绍上面日志的含义:

2018-04-12T13:48:26.233+0800: 15578.148:GC 开始的时间,以及相对于 JVM 启动的相对时间(单位是秒,这里大概是4.33h),与前面 ParNew 类似,下面的分析中就直接跳过这个了;

CMS-initial-mark:初始标记阶段,它会收集所有 GC Roots 以及其直接引用的对象;

6294851K:当前老年代使用的容量,这里是 6G;

(20971520K):老年代可用的最大容量,这里是 20G;

6354687K:整个堆目前使用的容量,这里是 6.06G;

(24746432K):堆可用的容量,这里是 23.6G;

0.0466580 secs:这个阶段的持续时间;

[Times: user=0.04 sys=0.00, real=0.04 secs]:与前面的类似,这里是相应 user、system and real 的时间统计。

阶段2:并发标记

在这个阶段 Garbage Collector 会遍历老年代,然后标记所有存活的对象,它会根据上个阶段找到的 GC Roots 遍历查找。并发标记阶段,它会与用户的应用程序并发运行。并不是老年代所有的存活对象都会被标记,因为在标记期间用户的程序可能会改变一些引用,如下图所示

在上面的图中,与阶段1的图进行对比,就会发现有一个对象的引用已经发生了变化,这个阶段相应的日志信息如下:

这里详细对上面的日志解释,如下所示:

CMS-concurrent-mark:并发收集阶段,这个阶段会遍历老年代,并标记所有存活的对象;

0.138/0.138 secs:这个阶段的持续时间与时钟时间;

[Times: user=1.01 sys=0.21, real=0.14 secs]:如前面所示,但是这部的时间,其实意义不大,因为它是从并发标记的开始时间开始计算,这期间因为是并发进行,不仅仅包含 GC 线程的工作。

阶段3:Concurrent Preclean

Concurrent Preclean:这也是一个并发阶段,与应用的线程并发运行,并不会 stop 应用的线程。在并发运行的过程中,一些对象的引用可能会发生变化,但是这种情况发生时,JVM 会将包含这个对象的区域(Card)标记为 Dirty,这也就是 Card Marking。如下图所示

在pre-clean阶段,那些能够从 Dirty 对象到达的对象也会被标记,这个标记做完之后,dirty card 标记就会被清除了,如下

这个阶段相应的日志信息如下:

其含义为:

CMS-concurrent-preclean:Concurrent Preclean 阶段,对在前面并发标记阶段中引用发生变化的对象进行标记;

0.056/0.057 secs:这个阶段的持续时间与时钟时间;

[Times: user=0.20 sys=0.12, real=0.06 secs]:同并发标记阶段中的含义。

阶段4:Concurrent Abortable Preclean

这也是一个并发阶段,但是同样不会影响影响用户的应用线程,这个阶段是为了尽量承担 STW(stop-the-world)中最终标记阶段的工作。这个阶段持续时间依赖于很多的因素,由于这个阶段是在重复做很多相同的工作,直接满足一些条件(比如:重复迭代的次数、完成的工作量或者时钟时间等)。这个阶段的日志信息如下:

CMS-concurrent-abortable-preclean:Concurrent Abortable Preclean 阶段;

3.506/3.514 secs:这个阶段的持续时间与时钟时间,本质上,这里的 gc 线程会在 STW 之前做更多的工作,通常会持续 5s 左右;

[Times: user=11.93 sys=6.77, real=3.51 secs]:同前面。

阶段5:Final Remark

这是第二个 STW 阶段,也是 CMS 中的最后一个,这个阶段的目标是标记所有老年代所有的存活对象,由于之前的阶段是并发执行的,gc 线程可能跟不上应用程序的变化,为了完成标记老年代所有存活对象的目标,STW 就非常有必要了。

通常 CMS 的 Final Remark 阶段会在年轻代尽可能干净的时候运行,目的是为了减少连续 STW 发生的可能性(年轻代存活对象过多的话,也会导致老年代涉及的存活对象会很多)。这个阶段会比前面的几个阶段更复杂一些,相关日志如下:

对上面的日志进行分析:

YG occupancy: 1805641 K (3774912 K):年轻代当前占用量及容量,这里分别是 1.71G 和 3.6G;

ParNew:...:触发了一次 young GC,这里触发的原因是为了减少年轻代的存活对象,尽量使年轻代更干净一些;

[Rescan (parallel) , 0.0429390 secs]:这个 Rescan 是当应用暂停的情况下完成对所有存活对象的标记,这个阶段是并行处理的,这里花费了 0.0429390s;

[weak refs processing, 0.0027800 secs]:第一个子阶段,它的工作是处理弱引用;

[class unloading, 0.0033120 secs]:第二个子阶段,它的工作是:unloading the unused classes;

[scrub symbol table, 0.0016780 secs] ... [scrub string table, 0.0004780 secs]:最后一个子阶段,它的目的是:cleaning up symbol and string tables which hold class-level metadata and internalized string respectively,时钟的暂停也包含在这里;

6299829K(20971520K):这个阶段之后,老年代的使用量与总量,这里分别是 6G 和 20G;

6348225K(24746432K):这个阶段之后,堆的使用量与总量(包括年轻代,年轻代在前面发生过 GC),这里分别是 6.05G 和 23.6G;

0.1365130 secs:这个阶段的持续时间;

[Times: user=1.24 sys=0.00, real=0.14 secs]:对应的时间信息。

经历过这五个阶段之后,老年代所有存活的对象都被标记过了,现在可以通过清除算法去清理那些老年代不再使用的对象。

阶段6:Concurrent Sweep

这里不需要 STW,它是与用户的应用程序并发运行,这个阶段是:清除那些不再使用的对象,回收它们的占用空间为将来使用。如下图所示

这个阶段对应的日志信息如下(这中间又发生了一次 Young GC):

分别介绍一下:

CMS-concurrent-sweep:这个阶段主要是清除那些没有被标记的对象,回收它们的占用空间;

8.193/8.284 secs:这个阶段的持续时间与时钟时间;

[Times: user=30.34 sys=16.44, real=8.28 secs]:同前面;

阶段7:Concurrent Reset.

这个阶段也是并发执行的,它会重设 CMS 内部的数据结构,为下次的 GC 做准备,对应的日志信息如下:

日志详情分别如下:

CMS-concurrent-reset:这个阶段的开始,目的如前面所述;

0.044/0.044 secs:这个阶段的持续时间与时钟时间;

[Times: user=0.15 sys=0.10, real=0.04 secs]:同前面。

总结

CMS 通过将大量工作分散到并发处理阶段来在减少 STW 时间,在这块做得非常优秀,但是 CMS 也有一些其他的问题:

CMS 收集器无法处理浮动垃圾( Floating Garbage),可能出现 “Concurrnet Mode Failure” 失败而导致另一次 Full GC 的产生,可能引发串行 Full GC;

空间碎片,导致无法分配大对象,CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认就是开启的),用于在 CMS 收集器顶不住要进行 Full GC 时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长;

对于堆比较大的应用上,GC 的时间难以预估。

CMS 的一些缺陷也是 G1 收集器兴起的原因。

欢迎工作一到五年的Java工程师朋友们加入Java程序员开发: 854393687

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

④ JVM 垃圾回收( CMS 和 G1 )篇

GC Roots 对象的包括如下几种:

当 GC 线程进行并发操作时,应用程序可能会进行新增对象、删除对象、变更对象引用等一系列操作。这种条件下可能会出现活动对象的漏标的情况,

比如下面场景:

为了解决这个问题,还需要额外的操作,这个操作就是 write barrier。

在使用 Write bariier 之后同样的情景就不会出现活动对象被遗漏的情况了.

YGC 时为了标记活动标记对象除了 tracing GC ROOTS 之外,老年代里也可能会引用新生代对象。
所以正常来说还要扫描一次老年代,如果是扫描整个老年代这将会随着堆的增大变得越来越慢,特别是现在内存都越来越大了。所以为了提升性能就引入卡表。

卡表提升性能的原理 :逻辑上把老年代内存分成一个个大小相等的卡片,然后对每个卡片准备一个与其对应的标记位,并将这些位集中起管理就好像一个表格 (mark table) 一样,当改写对象引用是从老年代指向新生代时,在老年代对应的卡片标记位上设置标志位即可,通常这样的卡片我们称之为 dirty card。

这项操作可以通过上面的提到的 write barrier 来实现,这样就算对象跨多张卡片也不会有什么问题。 卡表通常是用 byte 数组实现的,byte 的值只能取 [0,1] 这两种。所以 btye [i] = 1 就表示第 i + 1 卡片所在内存上有指向新生代引用的老年代对象,这时只要 tracing 这个卡片上的对象即可。背后思想就是典型以空间换时间的思路!

G1 将整个堆划分为一个个大小相等的小块(每一块称为一个 region),每一块的内存是连续的。和分代算法一样,G1 中每个块也会充当 Eden、Survivor、Old 三种角色,但是它们不是固定的,这使得内存使用更加地灵活。

G1 收集器主要包括了以下 4 种操作:

全局并发标记过程分为五个阶段

(1) 初始标记

(2) Root Region Scanning 根区域扫描

(3) Concurrent Marking 并发标记

(4) Remark 最终标记

(5) Cleanup 清除

主要完成了垃圾定位的工作,定位出了哪些分区是垃圾最多的。

并发周期结束后是混合垃圾回收周期,不仅进行年轻代垃圾收集,而且回收之前标记出来的老年代的垃圾最多的部分区块。

MIXED GC 周期会持续进行,直到几乎所有的被标记出来的分区(垃圾占比大的分区)都得到回收,然后恢复到常规的年轻代垃圾收集,最终再次启动并发周期。

下面我们来介绍特殊情况,那就是会导致 Full GC 的情况,也是我们需要极力避免的:

把 Ruser 和 Rcon 合并一下,形成一个新的,完整的可到达对象关系 Rfinal,交给 GC 程序。

CMS 和 G1 都采取一种方式 Write barrier+log,3 个步骤:

Rslog 的作用就是记录用户程序对对象关系的修改;
用户程序的修改只能有 2 种:

总结: 宁可放过,下一次处理,也不错杀

参考: CMS 收集器原理理解与分析
参考: G1 收集器原理理解与分析

⑤ 三色标记法与垃圾回收器(CMS、G1)

JVM中的CMS、G1垃圾回收器所使用垃圾回收算法即为三色标记法。

三色标记法将对象的颜色分为了黑、灰、白,三种颜色。

存在问题:

浮动垃圾:并发标记的过程中,若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,此时,此对象不是白色的不会被清除,重新标记也不能从GC Root中去找到,所以成为了浮动垃圾,这种情况对系统的影响不大,留给下一次GC进行处理即可。
对象漏标问题(需要的对象被回收):并发标记的过程中,一个业务线程将一个未被扫描过的白色对象断开引用成为垃圾(删除引用),同时黑色对象引用了该对象(增加引用)(这两部可以不分先后顺序);因为黑色对象的含义为其属性都已经被标记过了,重新标记也不会从黑色对象中去找,导致该对象被程序所需要,却又要被GC回收,此问题会导致系统出现问题,而CMS与G1,两种回收器在使用三色标记法时,都采取了一些措施来应对这些问题,CMS对增加引用环节进行处理(Increment Update),G1则对删除引用环节进行处理(SATB)。

在JVM虚拟机中有两种常见垃圾回收器使用了该算法:

CMS(Concurrent Mark Sweep)

CMS,是非常有名的JVM垃圾回收器,它起到了承上启下的作用,开启了并发回收的篇章。
但是CMS由于许多小问题,现在基本已经被淘汰。

增量更新(Increment Update)
在应对漏标问题时,CMS使用了Increment Update方法来做:
在一个未被标记的对象(白色对象)被重新引用后,==引用它的对象==,若为黑色则要变成灰色,在下次二次标记时让GC线程继续标记它的属性对象。
但是就算时这样,其仍然是存在漏标的问题:

在一个灰色对象正在被一个GC线程回收时,当它已经被标记过的属性指向了一个白色对象(垃圾)
而这个对象的属性对象本身还未全部标记结束,则为灰色不变
而这个GC线程在标记完最后一个属性后,认为已经将所有的属性标记结束了,将这个灰色对象标记为黑色,被重新引用的白色对象,无法被标记

补充,CMS除了这个缺陷外,仍然存在两个个较为致命的缺陷:

解决方案:使用Mark-Sweep-Compact算法,减少垃圾碎片

当JVM认为内存不够了,再使用CMS进行并发清理内存可能会发生OOM的问题,而不得不进行Serial Old GC,Serial Old是单线程垃圾回收,效率低

解决方案:降低触发CMS GC的阈值,让浮动垃圾不那么容易占满老年代

G1(Garbage First)

从G1垃圾回收器开始,G1的物理内存不再分代,而是由一块一块的Region组成;逻辑分代仍然存在。

前置知识 — Card Table(多种垃圾回收器均具备)

由于在进行YoungGC时,我们在进行对一个对象是否被引用的过程,需要扫描整个Old区,所以JVM设计了CardTable,将Old区分为一个一个Card,一个Card有多个对象;如果一个Card中的对象有引用指向Young区,则将其标记为Dirty Card,下次需要进行YoungGC时,只需要去扫描Dirty Card即可。

Card Table 在底层数据结构以 Bit Map实现。

CSet(Collection Set)

SATB(Snapshot At The Beginning)
在应对漏标问题时,CMS使用了SATB方法来做:

因为SATB在重新标记环节只需要去重新扫描那些被推到堆栈中的引用,并配合Rset来判断当前对象是否被引用来进行回收;

并且在最后G1并不会选择回收所有垃圾对象,而是根据Region的垃圾多少来判断与预估回收价值(指回收的垃圾与回收的STW时间的一个预估值),将一个或者多个Region放到CSet中,最后将这些Region中的存活对象压缩并复制到新的Region中,清空原来的Region。

问题:G1会不会进行Full GC?
会,当内存满了的时候就会进行Full GC;且JDK10之前的Full GC,为单线程的,所以使用G1需要避免Full GC的产生。
解决方案:

加大内存;
提高CPU性能,加快GC回收速度,而对象增加速度赶不上回收速度,则Full GC可以避免;
降低进行Mixed GC触发的阈值,让Mixed GC提早发生(默认45%)

G1的第一篇paper(附录1)发表于2004年,在2012年才在jdk1.7u4中可用。oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。为何oracle要极力推荐G1呢,G1有哪些优点?

首先,G1的设计原则就是简单可行的性能调优

开发人员仅仅需要声明以下参数即可:

其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。

其次,G1将新生代,老年代的物理空间划分取消了。

这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。

取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。

在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

PS:在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。

对象分配策略

说起大对象的分配,我们不得不谈谈对象的分配策略。它分为3个阶段:

对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。

最后,G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。下面我们将分别介绍一下这2种模式。

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

这时,我们需要考虑一个问题,如果仅仅GC 新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。

在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。

但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。

需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。

Young GC 阶段:

Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。

它的GC步骤分2步:

全局并发标记(global concurrent marking)
拷贝存活对象(evacuation)
在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)。 global concurrent marking的执行过程是怎样的呢?

在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:

初始标记(initial mark,STW)
在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
根区域扫描(root region scan)
G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
并发标记(Concurrent Marking)
G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
最终标记(Remark,STW)
该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
清除垃圾(Cleanup,STW)
在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。

提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。

根对象被置为黑色,子对象被置为灰色。

继续由灰色遍历,将已扫描了子对象的对象置为黑色。

遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。

这看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题

我们看下面一种情况,当垃圾收集器扫描到下面情况时:

这时候应用程序执行了以下操作:

这样,对象的状态图变成如下情形:

这时候垃圾收集器再标记扫描的时候就会下图成这样:

很显然,此时C是白色,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?有如下2中可行的方式:

在插入的时候记录对象
在删除的时候记录对象
刚好这对应CMS和G1的2种不同实现方式:

在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。

在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:

这样,G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:

混合式GC也是采用的复制的清理策略,当GC完成后,会重新释放空间。

至此,混合式GC告一段落了。下一小节我们讲进入调优实践。

MaxGCPauseMillis调优

前面介绍过使用GC的最基本的参数:

前面2个参数都好理解,后面这个MaxGCPauseMillis参数该怎么配置呢?这个参数从字面的意思上看,就是允许的GC最大的暂停时间。G1尽量确保每次GC暂停的时间都在设置的MaxGCPauseMillis范围内。 那G1是如何做到最大暂停时间的呢?这涉及到另一个概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的区域集合。

Young GC:选定所有新生代里的region。通过控制新生代的region个数来控制young GC的开销。
Mixed GC:选定所有新生代里的region,外加根据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。
在理解了这些后,我们再设置最大暂停时间就好办了。 首先,我们能容忍的最大暂停时间是有一个限度的,我们需要在这个限度范围内设置。但是应该设置的值是多少呢?我们需要在吞吐量跟MaxGCPauseMillis之间做一个平衡。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间。

其他调优参数

避免使用以下参数:

避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。

触发Full GC

在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。那么发生Full GC的情况有哪些呢?

并发模式失败
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。

晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:

巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。

由于篇幅有限,G1还有很多调优实践,在此就不一一列出了,大家在平常的实践中可以慢慢探索。最后,期待java 9能正式发布,默认使用G1为垃圾收集器的java性能会不会又提高呢?

G1处理和传统的垃圾收集策略是不同的,关键的因素是它将所有的内存进行了子区域的划分。

总结

G1是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的GC暂停目标,就能得到不错的性能;同时,我们也看到G1对内存空间的浪费较高,但通过**首先收集尽可能多的垃圾(Garbage First)的设计原则,可以及时发现过期对象,从而让内存占用处于合理的水平。

参考链接:
https://juejin.cn/post/6859931488352370702
https://blog.csdn.net/qq_39276448/article/details/104470796

⑥ JVM CMS和G1执行过程比较

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。由于大部分 Java 应用主要集中在互联网网站以及基于浏览器的 B/S 系统的服务端,这类应用通常会较为关注服务的响应速度,希望系统的停顿时间尽可能少,CMS 收集器就非常符合这类应用的需求

从名字可以知道,CMS 收集器是基于标记 - 清除算法实现的,它的运作过程分为四个步骤:

由于整个过程中耗时最长的是并发标记和并发清除阶段,而这两个阶段都可以和用户线程并发执行,所以从总体上看,CMS 收集器内存回收过程是与用户线程一起并发执行的

CMS 收集器的主要优点就是:并发收集、低停顿,因此也称 CMS 收集器为并发低停顿收集器。但 CMS 还远未达到完美的程度,它至少有以下四个明显的缺点:

Garbage First(G1)收集器是一款主要面向服务端应用的垃圾收集器,开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。HotSpot 开发团队对 G1 收集器的期望就是能在将来替代 CMS 收集器,所以在 JDK9 发布之日,G1 便宣告取代 Parallel Scavenge 加 Parallel Old 组合,成为服务端模式下的默认垃圾收集器,而 CMS 则沦为不推荐使用

在过去,包括 CMS 在内,垃圾收集的范围要么是整个新生代,要么是整个老年代,再要么是整个 Java 堆。而 G1 可以面向堆内存任何部分来组成回收集(Collection Set,一般简称 CSet)进行回收,衡量标准是哪块内存中垃圾数量最多,回收收益最大,这就是 G1 收集器的 Mixed GC 模式

虽然 G1 也是基于分代收集理论设计,但其对内存布局与其他收集器有明显差异。G1 把连续的 Java 堆划分成多个大小相等的独立区域(Region),每一个 Region 可以根据需要扮演新生代的 Eden 空间、Survivor 空间、老年代空间等等。收集器能对扮演不同角色的 Region 采用不同的策略处理

Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。只要该对象大小超过一半的 Region 的容量即可判定为大对象。而对于那些超过整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的大多数行为都把 Humongous Region 作为老年代的一部分来看待

停顿时间模型的意思是能够支持指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过 M 毫秒这么一个目标。G1 收集器作为 CMS 收集器的替代者,自然可以实现这个目标

G1 之所以能建立起可预测的停顿时间模型,是因为它将 Region 作为单词回收的最小单元,即每次收集到的内存空间都是 Region 大小的整数倍,这样可以有计划地避免进行全区域的垃圾收集。G1 收集器还可以跟踪每个 Region 的垃圾堆积的“价值”大小,即回收所获得的空间大小以及所需时间,并在后台维护一个优先级列表,每次根据用户设置的允许收集停顿时间(使用 -XX:MaxGCPauseMillis 指定),优先处理回收价值最大的 Region。这种使用 Region 划分内存空间,以及具有优先级的区域回收方式,保证了 G1 收集器在有限的时间内获取尽可能高的收集效率

G1 收集器的设计理念看似无太多惊人之处,其实有很多关键的细节问题需要解决:

G1 收集器的运作过程大致可划分为以下四个步骤:

G1 和 CMS 都非常关注停顿时间控制,毫无疑问,可以由用户指定期望的停顿时间是 G1 收集器的一大杀手锏。G1 收集器经常被拿来和 CMS 收集器比较,从长远来看,G1 收集器肯定是会取代 CMS 收集器的

除了更先进的设计理念,单从传统的算法理论来看,G1 从整体来看是基于标记 - 整理算法实现,而从局部来看(两个 Region 之间)又是基于标记 - 复制算法实现,这意味着 G1 不会产生内存碎片。但 G1 并非全方面碾压 CMS,G1 由于其复杂的内部细节实现,使得垃圾收集时的内存占用和程序运行时的额外执行负载都要比 CMS 高。使用哪款收集器,往往要针对具体场景才能做定量比较,目前在小内存应用上 CMS 的表现大概率会优于 G1,而在大内存应用上 G1 则占有优势,这个平衡点通常在 6GB ~ 8GB 之间。当然,随着 HotSpot 开发者对 G1 的持续优化,最终胜利的天平必定回向 G1 倾斜

⑦ 垃圾收集器-CMS、三色标记、记忆集

    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

    从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

初始标记:

    暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快

并发标记:

    并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。

重新标记:

    重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。

并发清理:

    开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。

并发重置:

    重置本次GC过程中的标记数据。

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面几个明显的缺点:

1.对CPU资源敏感(会和服务抢资源);

2.无法处理浮动垃圾( 在并发标记和并发清理阶段又产生垃圾 ,这种浮动垃圾只能等到下一次gc再清理了);

3.它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是 在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收

CMS的相关核心参数

1.-XX:+UseConcMarkSweepGC:启用cms

2.-XX:ConcGCThreads:并发的GC线程数

3.-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)

4.-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次

5.-XX:: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)

6.-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整

7.-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段

8.-XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW

9.-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;

    在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。这里引入“三色标记”来给大家解释下,把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色:

黑色:

    表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。

灰色:

    表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。

白色:

    表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。

标记过程:

初始时,所有对象都在 【白色集合】中;

将GC Roots 直接引用到的对象 挪到 【灰色集合】中;

从灰色集合中获取对象:

3.1. 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;

3.2. 将本对象 挪到 【黑色集合】里面。

重复步骤3,直至【灰色集合】为空时结束。

结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收

多标-浮动垃圾

    在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过 (被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动 垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。

    另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分 对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。

漏标-读写屏障

漏标只有 同时满足 以下两个条件时才会发生:

     条件一:灰色对象 断开了 白色对象的引用;即灰色对象 原来成员变量的引用 发生了变化。

    条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。

漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案:  增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB)  。

增量更新 就是当黑色对象 插入新的指向 白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。

原始快照 就是当灰色对象要 删除指向 白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)

以上 无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。

写屏障实现原始快照(SATB): 当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B原来成员变量的引用对象D记录下来:

写屏障实现增量更新: 当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D 记录下来:

记忆集

当我们进行young gc时,我们的 gc roots除了常见的栈引用、静态变量、常量、锁对象、class对象 这些常见的之外,如果 老年代有对象引用了我们的新生代对象 ,那么老年代的对象也应该加入gc roots的范围中,但是如果每次进行young gc我们都需要扫描一次老年代的话,那我们进行垃圾回收的代价实在是太大了,因此我们引入了一种叫做记忆集的抽象数据结构来记录这种引用关系。

什么是记忆集?

    记忆集是一种用于记录从非收集区域指向收集区域的指针集合的数据结构。

    如果我们不考虑效率和成本问题,我们可以用一个数组存储所有有指针指向新生代的老年代对象。但是如果这样的话我们维护成本就很好,打个比方,假如所有的老年代对象都有指针指向了新生代,那么我们需要维护整个老年代大小的记忆集,毫无疑问这种方法是不可取的。因此我们引入了卡表的数据结构

什么是卡表?

    记忆集是我们针对于跨代引用问题提出的思想,而卡表则是针对于该种思想的具体实现。(可以理解为记忆集是结构,卡表是实现类)

    在hotspot虚拟机中,卡表是一个字节数组,数组的每一项对应着内存中的某一块连续地址的区域,如果该区域中有引用指向了待回收区域的对象,卡表数组对应的元素将被置为1,没有则置为0;

G1的记忆集

上述的 卡表机制基本上适用于CMS垃圾回收器 ,因为CMS垃圾回收器只需要在young gc时维护老年代对新生代的引用即可,但是G1垃圾回收器不一样,因为G1垃圾回收器是基于分区模型的,所以每一个Region需要知道有哪些region的引用指向了它,并且这些region是不是本次垃圾回收区域的一部分。因此G1垃圾回收器不能简单的只维护一个卡表(卡表只能简单的知道某块内存区域有没有引用收集区域的对象,但是不能知道到底是谁引用了自己),所以在 G1垃圾回收器的记忆集的实现实际上是基于哈希表的 ,key代表的是其他region的起始地址,value是一集合,里面存放了对应区域的卡表的索引,因此G1的region能够通过记忆集知道,当前是哪个region有引用指向了它,并且能知道是哪块区域存在指针指向。

但是大家应该能注意到, 每个region都维护一个记忆集,内存占用量肯定很大,这也就是为什么G1垃圾回收器比传统的其他垃圾回收器要有更高的内存占用 。据统计G1至少要耗费大约10%-20%的Java堆空间来维护收集器的工作。

参考:

https://blog.csdn.net/xc1989xc/article/details/107466313

https://blog.csdn.net/shangshanzixu/article/details/113918994

⑧ JVM垃圾回收的“三色标记算法”实现,内容太干

三色标记法是一种垃圾回收法,它可以让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的。JVM中的 CMS、G1垃圾回收器 所使用垃圾回收算法即为三色标记法。

三色标记法将对象的颜色分为了黑、灰、白,三种颜色。

白色 :该对象没有被标记过。(对象垃圾)

灰色 :该对象已经被标记过了,但该对象下的属性没有全被标记完。(GC需要从此对象中去寻找垃圾)

黑色 :该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对象)

从我们main方法的根对象(JVM中称为GC Root)开始沿着他们的对象向下查找,用黑灰白的规则,标记出所有跟GC Root相连接的对象,扫描一遍结束后,一般需要进行一次短暂的STW(Stop The World),再次进行扫描,此时因为黑色对象的属性都也已经被标记过了,所以只需找出灰色对象并顺着继续往下标记(且因为大部分的标记工作已经在第一次并发的时候发生了,所以灰色对象数量会很少,标记时间也会短很多), 此时程序继续执行,GC线程扫描所有的内存,找出扫描之后依旧被标记为白色的对象(垃圾),清除。

具体流程:

在JVM虚拟机中有两种常见垃圾回收器使用了该算法:CMS(Concurrent Mark Sweep)、G1(Garbage First) ,为了解决三色标记法对对象漏标问题各自有各自的法:

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求(但是实际由于某些问题,很少有使用CMS作为主要垃圾回收器的)。

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:1)初始标记(CMS initial mark) 2)并发标记(CMS concurrent mark) 3)重新标记(CMS remark) 4)并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快;

并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;

重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;

最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

在应对漏标问题时,CMS使用了增量更新(Increment Update)方法来做:

在一个未被标记的对象(白色对象)被重新引用后, 引用它的对象若为黑色则要变成灰色,在下次二次标记时让GC线程继续标记它的属性对象

但是就算是这样,其仍然是存在漏标的问题:

G1(Garbage First)物理内存不再分代,而是由一块一块的Region组成,但是逻辑分代仍然存在。G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待,如图所示

Card Table(多种垃圾回收器均具备)

RSet(Remembered Set)

是辅助GC过程的一种结构,典型的空间换时间工具,和Card Table有些类似。

后面说到的CSet(Collection Set)也是辅助GC的,它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。

在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可。逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。

而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。每个Region中都有一个RSet,记录其他Region到本Region的引用信息;使得垃圾回收器不需要扫描整个堆找到谁引用当前分区中的对象,只需要扫描RSet即可。

CSet(Collection Set)

一组可被回收的分区Region的集合, 是多个对象的集合内存区域。

新生代与老年代的比例

5% - 60%,一般不使用手工指定,因为这是G1预测停顿时间的基准,这地方简要说明一下,G1可以指定一个预期的停顿时间,然后G1会根据你设定的时间来动态调整年轻代的比例,例如时间长,就将年轻代比例调小,让YGC尽早行。

SATB(Snapshot At The Beginning), 在应对漏标问题时,G1使用了SATB方法来做,具体流程:

因为SATB在重新标记环节只需要去重新扫描那些被推到堆栈中的引用,并配合Rset来判断当前对象是否被引用来进行回收;

并且在最后G1并不会选择回收所有垃圾对象,而是根据Region的垃圾多少来判断与预估回收价值(指回收的垃圾与回收的STW时间的一个预估值),将一个或者多个Region放到CSet中,最后将这些Region中的存活对象压缩并复制到新的Region中,清空原来的Region。

会,当内存满了的时候就会进行Full GC;且JDK10之前的Full GC,为单线程的,所以使用G1需要避免Full GC的产生。

解决方案:

阅读全文

与jvmcms标记源码相关的资料

热点内容
xlsx转换pdf 浏览:94
3dmax挤出命令英语 浏览:903
靶心率的定义和算法 浏览:513
3d模术师app哪里下载 浏览:474
php中文api文档 浏览:458
安卓设计怎么加入输入框 浏览:185
主根服务器什么时候开始 浏览:738
奇门遁甲完整版pdf 浏览:901
app软件怎么用的 浏览:802
电子书pdf购买 浏览:193
浪潮服务器如何做系统 浏览:111
冒险岛img格式加密 浏览:596
我的世界手游如何复制命令 浏览:659
天刀自动弹琴脚本源码 浏览:970
打开其它app微信怎么收不到 浏览:447
安卓游戏耳机怎么戴 浏览:18
不越狱怎么去除app广告 浏览:178
ipadminipdf阅读 浏览:506
文件夹无限制压缩会不会降低内存 浏览:412
荣耀怎样创建文件夹 浏览:631