热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

译文G1收集器

原文出处:G1–GarbageFirstG1设计的一个重要目标是设置stop-the-world阶段的持续时长和频率,因为垃圾收集器可预测,

原文出处:G1 – Garbage First

G1设计的一个重要目标是设置stop-the-world阶段的持续时长和频率,因为垃圾收集器可预测,可配置。事实上,G1是一款软实时的收集器,意味着你可以给它设置明确的运行目标。你可以要求stop-the-world阶段不超过 x milliseconds在给定的y milliseconds时长范围之内,比如,在给定的s内不超过5s。G1收集器尽自己最大努力高概率实现目标(但不是必然,它会是硬实时)。

为了实现它,G1建立在一系列的观察上。首先,heap去不是必须Young和Old代分配连续的空间。相反,heap区分成一定数量(代表性的2048)的小的heap区来分配对象。单个的区域可能是Eden区,Survivor区,Old区。所有逻辑的Eden区和Survivor区合称为Young代,所有的Old区组合在一起称为Old代:

图片描述

这允许GC避免一次回收整个heap区,取而代之递增处理问题:每次只有collection set调用region的子集。每个阶段期间所有的Young region被回收,但同样的只包含一部分old region:

图片描述

G1的另一个新特性是并发阶段期间估算每一个region里包含存活数据的数量。这个被用于建立collection set:region包含的垃圾越多,越先被回收。因此名称是:garbage-first 收集器。

为了激活JVM中G1收集器,按照下面的命令执行你的应用:

java -XX:+UseG1GC com.mypackages.MyExecutableClass

疏散(Evacuation)阶段:Fully Young

在应用程序生命周期的开始阶段,在并发阶段执行之前,G1获取不到任何附加信息,因此它的最初功能是full-yong模式。当Young代塞满了,应用线程暂停,Young区的存活数据被复制到Survivor区域,任何空闲区域因此变成Survivor区。

复制对象过程被叫做疏散(Evacuation), 它的工作方式和我们之前看到其他Young收集器几乎是一样的。疏散阶段full logs相当大,因此在第一次full-young 疏散阶段我们略去一些不相关的片段。并发阶段之后我们会解释大量细节。补充一点,由于log记录的全量尺寸,并行阶段和“其他”阶段的细节被抽取成独立的片段:

0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs]1[Parallel Time: 13.9 ms, GC Workers: 8]2…3[Code Root Fixup: 0.0 ms]4[Code Root Purge: 0.0 ms]5[Clear CT: 0.1 ms][Other: 0.4 ms]6…7[Eden: 24.0M(24.0M)->0.0B(13.0M) 8Survivors: 0.0B->3072.0K 9Heap: 24.0M(256.0M)->21.9M(256.0M)]10[Times: user=0.04 sys=0.04, real=0.02 secs] 11

  1. G1阶段清理Young区域。JVM启动之后的134ms阶段开始,通过钟墙时间检测阶段持续了0.0144s。

  2. 表明下列活动被8个并行GC线程实施耗费13.9ms(real time)。

  3. 省略部分,细节在下面的系列片段。

  4. 释放数据结构用于管理并行活动。通常应该是靠近zero。这通常顺序完成。

  5. 清除更多数据结构,通常应该非常快,不是几乎等于0。顺序完成。

  6. 混杂其他活动,它们的很多是并行的。

  7. 细节可以看下面的章节。

  8. 阶段前后的Eden区使用大小和容量大小。

  9. 阶段前后被用于Survivor区的空间。

  10. 阶段前后的heap区总使用大小和容量大小。

  11. GC时间期间,不同类别的时长:

    .user-回收期间GC线程消耗的总的cpu时间。
    .sys-调用系统或等待系统事件的耗费时长。
    .应用程序的停顿的时钟时间。GC期间并发活动时长理论上接近(user time+sys time)GC线程数量消费的时长,这种情况下用了8个线程。注意的是由于一些活动不是并行执行,它会超过一定比率。

大多数重大事件被多个专用GC线程完成。它们的活动在如下面片段的描述:

[Parallel Time: 13.9 ms, GC Workers: 8]1[GC Worker Start (ms)2: Min: 134.0, Avg: 134.1, Max: 134.1, Diff: 0.1][Ext Root Scanning (ms)3: Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 1.2][Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0][Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0][Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0][Code Root Scanning (ms)4: Min: 0.0, Avg: 0.0, Max: 0.2, Diff: 0.2, Sum: 0.2][Object Copy (ms)5: Min: 10.8, Avg: 12.1, Max: 12.6, Diff: 1.9, Sum: 96.5][Termination (ms)6: Min: 0.8, Avg: 1.5, Max: 2.8, Diff: 1.9, Sum: 12.2][Termination Attempts7: Min: 173, Avg: 293.2, Max: 362, Diff: 189, Sum: 2346][GC Worker Other (ms)8: Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]GC Worker Total (ms)9: Min: 13.7, Avg: 13.8, Max: 13.8, Diff: 0.1, Sum: 110.2][GC Worker End (ms)10: Min: 147.8, Avg: 147.8, Max: 147.8, Diff: 0.0]

  1. 表明下列活动被8个并行GC线程实施耗费13.9ms(real time)。

  2. 线程开始活动的合计时间,在阶段的开始时间匹配时间戳。如果Min和Max差别很大,它也许表明太多线程被使用或者JVM里的GC进程CPU时间被机器上其他进程盗用。

  3. 扫描外部(非heap)Root消耗的时间例如clasloader,JNI引用,JVM系统等等。展示消耗时间,“Sum”是cpu时间。

  4. 扫描来自真实code Root的时长:局部变量等等。

  5. 从回收区域复制存活对象花费的时间。

  6. GC线程确定它们到达安全点消耗的时间,没有多余工作完成,然后终止。

  7. 工作线程尝试终止的次数。实际上线程发现有任务需要完成的时候尝试失败,过早去终止。

  8. 其他琐碎的活动不值得在日志里独立片段展示。

  9. 任务线程总共花费的时间。

  10. 任务线程完成工作的时间戳。通常它们因该大值相等,另一方面它也许显示出太多线程无所事事,或者繁复的上下文工作。

此外,Evacuation阶段期间一些混杂活动被执行。我们会讲解它们的一部分在下面的片段。剩余部分随后讲解。

图片描述

  1. 混杂其他的活动,大多数并行执行。

  2. 处理非强引用的时间:清除或者确定不需要清理。

  3. 顺序处理将剩下的非强引用从引用队列中移除出去。

  4. 释放收集集合里面区域花费的时间以便它们适用于下一次分配。

并发标记

从上面章节看出G1借鉴了CMS的许多理念,因此可以方便你充分理解之前的阶段。虽然在一些方式上不尽相同,但是并发标记的目标非常类似。G1并发标记使用STAB(Snapshot-At-The-Beginning)方法,意味着在周期的开始阶段标记所有存活对象,即使在收集期间已经调整。存活对象被允许建立在每个区域(region)活跃性上,以便收集结合快速选择。

这些信息随后被用于执行Old代GC。它可以完全并发执行,一个仅仅包含垃圾的region被标记,或者一个Old region “stop-the-world”evacuation阶段包含垃圾和存活对象。

并发标记开始于heap区已使用空间足够大。默认的,占45%,但是可以被JVM 选项InitiatingHeapOccupancyPercent 改变。类似CMS,G1并发标记有一些小阶段组成,它们中一些完全并发,一些则不得不暂停应用线程。

阶段1:初始标记。这个阶段标记所有从GC Root可达的对象。在CMS里,他需要“stop-the-world”,但是在G1, Evacuation阶段它可以并行执行,因此它的上限事最小的。你可以通过evacuation阶段第一行添加“(initial-mark)”留意下GC日志里的这个阶段:

1.631: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0062656 secs]

阶段2:Root region扫描。这个阶段标记从所谓的root区域可达所有的存活对象,也就是标记周期中间没有必要分配的非空的对象。因为移除标记周期中的填充会导致异常,这个阶段必须在下一个阶段开始之间完成。如果它提前开始,它会提前中止root扫描,然后等待完成。在目前的实现中,root区域在syrvivor区中:它们占据Young代小部分空间,在下一个Evacuation 阶段被禁止回收。

1.362: [GC concurrent-root-region-scan-start]
1.364: [GC concurrent-root-region-scan-end, 0.0028513 secs]

阶段3:并发标记。这个阶段和CMS的阶段非常相似:它简单统计对象,在特别的bitmap中标记可以访问的对象。为了保证STAB语义论,为了达到标记目的,通过可感知应用线程放弃先前的引用G1 GC需要所有的并发线程更新到对象统计模式。

通过使用写屏障(Pre-Write barriers,不要混淆于Post-Write barriers,以及涉及多线程的memory barriers,随后进行分析)。它们的职责是,每当G1并发标记期间你写进一个字段,在所谓的log buffer里面存储之前结果,被并发标记线程处理。

1.364: [GC concurrent-mark-start]
1.645: [GC concurrent-mark-end, 0.2803470 secs]

阶段4:再标记。这个阶段需要“stop-the-world”,类似之前的CMS里面看到,在标记阶段完成。对于G1,对于遗留的部分它短暂的暂停应用线程去阻塞并发更新日志和处理它们,标记并发标记开始的时候没有标记的存活对象。这个阶段执行一些额外的清理工作,例如,引用(查看Evacuation阶段日志)处理,或者卸载的class。

1.645: [GC remark 1.645: [Finalize Marking, 0.0009461 secs] 1.646: [GC ref-proc, 0.0000417 secs] 1.646: [Unloading, 0.0011301 secs], 0.0074056 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]

阶段5:清除。最后的阶段准备即将到来的Evacuation阶段,计算heap区所有的存活对象,通过预期的GC效率排序这些region。它通常执行所有活动的整理工作,为了下一次并发标记迭代维持内部状态。

最后但同样重要的是,包含不再使用的对象的region在这个阶段被回收。这个阶段的一些部分是并发执行的,例如回收空region,大多数活跃性估算,但你在应用线程不干涉的期间通常需要短暂的“stop-the-world”阶段区确定方案。日志“stop-the-world”阶段和下图类似:

1.652: [GC cleanup 1213M->1213M(1885M), 0.0030492 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]

一旦发现只包含垃圾对象的region,日志格式会有些区别,类似于:

1.872: [GC cleanup 1357M->173M(1996M), 0.0015664 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]
1.874: [GC concurrent-cleanup-start]
1.876: [GC concurrent-cleanup-end, 0.0014846 secs]

疏散阶段:混合

并发清除可以空出来整个old代是令人兴奋,但是事实情况是不是每次都这样。并发标记已经成功完成之后,G1会安排一次混合回收,不仅仅是回收young region的垃圾,还把old region的一部分放进collection set中。

一次混合疏散阶段不是经常紧随并发标记阶段结束之后。有一系列的规则和试探影响这个阶段。例如,并发空出来old region中的大块区域是可能的,然而根本没有必要去做。

它们可能因此在并发标记结束和混合evacuation阶段之间简单发生一系列full-young evacuation阶段。

old区准确的对象被添加进collection set,保证其添加顺序,根据一些规则挑选出顺序。这些包含了为了达到应用程序的软实时性能目标。并发标记期间,活跃的和GC 效率的数据被回收,还有一些JVM配置选项。混合回收进程和初期分析的full-young gc一样庞大,但是这次我们会包含remembered set的子集。

Remembered set运行heap不同region使用独立的收集器。例如,当回收区域A,B和C,我们必须知道D和E中任何一个引用它们,去确定它们的活跃性。但是统计整个heap区花费很长时间,销毁整个增量收集,因此最佳化被破坏。很像为了使用其他GC算法独立手机Young region我们使用卡表( Card Table),在G1中我们使用Remembered Sets。

正如上面插图展示那样,每一个region有一个remembered sets列出外部引用指向这个区域。这些被看作额外的GC Roots。注意的是并发标记期间old区域被判断为垃圾的对象,即使外部引用它们会被忽略:那种情况下被当作垃圾的参照图:

图片描述

下一步操作和其他收集器一样:多个并行GC线程计算出哪些对象存活,哪些是垃圾:

图片描述

最后,存活对象被移到Survior区域中,如果有必要则新建。空的orgion被释放出来,用户再次存储对象:

图片描述

为了维护Remembered Sets,应用运行期间,每当写入操作执行的时候触发一个Post-Write Barrier。如果一个引用跨越region,也就是一个region指向另一个region,目标region的 Remembered Set存入相对应的entry中。为了减少write barrier,将card放进Remember Set过程异步执行,突出性能最优化。但是基本上将dirty card信息放进本地缓存的方式存入Write barrier,一个专门GC线程将信息引用region的Remember set中。

混合模式中,对照fully young模式log展示一些有趣的方面:

[Update RS (ms)1: Min: 0.7, Avg: 0.8, Max: 0.9, Diff: 0.2, Sum: 6.1]
[Processed Buffers2: Min: 0, Avg: 2.2, Max: 5, Diff: 5, Sum: 18]
[Scan RS (ms)3: Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.8]
[Clear CT: 0.2 ms]4
[Redirty Cards: 0.1 ms]5

  1. 自并发执行Remember Set以后,我必须确保真正收集开始之前still-buffered cards被执行。如果数量很多,并发GC线程无法负载。他可能是,举例来说,势不可挡的到来的字段修改
    的数量,或者CPU资源不足。

  2. 每一个任务线程操作local buffer的数量。

  3. 从Remember Set中扫描引用的数量。

  4. 清理card table中的card花费的时间。Remember set通过简单移除“dirty”(表示filed被修改)状态进行清理工作。

  5. card table超着ditry card的占用位置花费的时间。GC自己操作通过heap中发生突变被定义为位置占用,例如引用队列。

总结
这个应该建立在充分理解G1如果工作基础之上,这些当然为了简介,需要我们忽略相当多的一些实现细节,像humongous objects的细节。综合考虑,G1是HotSpot中现有的最先进的收集器产品,在G1上,他被HotSpot的工程师无所不用其极地改进,在即将到来的Java 新版本。

正如他我所看到的,G1修正了CMS的广为人知的问题,从阶段可预测到heap碎片。使得应用不再受限于CPU利用率,但是对个别选项十分敏感。G1很可能是对HotSpot用户来说最好的选择,尤其是运行最新版本的Java。然而,这性能升不是毫无代价:G1吞吐量归功于附加的write barrier和更多后台活动线程。如果应用是吞吐量优先或者CPU使用率100%,不关注个别阶段,CMS,甚至Parallel或许是更好的选择。

唯一可行选择合适的GC算法和设置的方式通过尝试和错误,但是我还在下一章节给出一般的参考。

注意的是G1很有可能是java 9默认的GC收集器:http://openjdk.java.net/jeps/248



推荐阅读
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 第四章高阶函数(参数传递、高阶函数、lambda表达式)(python进阶)的讲解和应用
    本文主要讲解了第四章高阶函数(参数传递、高阶函数、lambda表达式)的相关知识,包括函数参数传递机制和赋值机制、引用传递的概念和应用、默认参数的定义和使用等内容。同时介绍了高阶函数和lambda表达式的概念,并给出了一些实例代码进行演示。对于想要进一步提升python编程能力的读者来说,本文将是一个不错的学习资料。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • 浏览器中的异常检测算法及其在深度学习中的应用
    本文介绍了在浏览器中进行异常检测的算法,包括统计学方法和机器学习方法,并探讨了异常检测在深度学习中的应用。异常检测在金融领域的信用卡欺诈、企业安全领域的非法入侵、IT运维中的设备维护时间点预测等方面具有广泛的应用。通过使用TensorFlow.js进行异常检测,可以实现对单变量和多变量异常的检测。统计学方法通过估计数据的分布概率来计算数据点的异常概率,而机器学习方法则通过训练数据来建立异常检测模型。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • 数组的排序:数组本身有Arrays类中的sort()方法,这里写几种常见的排序方法。(1)冒泡排序法publicstaticvoidmain(String[]args ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
  • 本文讨论了读书的目的以及学习算法的重要性,并介绍了两个算法:除法速算和约瑟夫环的数学算法。同时,通过具体的例子和推理,解释了为什么x=x+k序列中的第一个人的位置为k,以及序列2和序列3的关系。通过学习算法,可以提高思维能力和解决问题的能力。 ... [详细]
author-avatar
matt
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有