将G1垃圾回收的内存使用量减少20%(翻译)
长期以来,记忆集一直是G1垃圾收集器的问题。最近,发生了一个有趣的小变化[7],在这一领域产生了相当积极的影响,我想强调一下。
介绍
与其他增量收集器一样,G1垃圾收集器使用已记忆的集合将引用的位置存储到要在垃圾收集期间复制的区域中。毕竟,如果您复制一个对象,则需要将该对象的引用更改为新位置,否则您的应用程序将在下次访问时崩溃。记住的集是存储这些位置的数据结构。
G1在每个区域存储已记住的集合,以实现每个区域的疏散。每次应用程序在将来可能会撤离的G1区域[0]之间安装参考时,都会执行一小段代码,将该位置添加到该参考指向的区域的记忆集中。
问题
那么,这些记忆的集合可以占用多少内存?令人惊讶的是,相当多。
为了演示目的,本文使用BigRamTester [2]进行了精确演示。这个简单的应用程序模拟了一个“内存数据库”,即一个大型哈希表,该表始终以LRU方式添加和删除项。因此,Java堆中到处都是指向这些元素的引用,从而强调记住的集合并创建大型的记住的集合。
下图显示了由Java VM的BigRamTester [2]的Java VM的本机内存跟踪(NMT)工具[1]计算出的“ GC”组件的承诺内存,每秒在20GB Java堆上获取。这导致在最近的技巧中选择了与OpenJDK一起使用的16 MB区域[3]。
确切的选择是
$ java -XX:+UseLargePages -XX:+AlwaysPreTouch -Xmx20g -Xms20g -XX:MaxGCPauseMillis=500 -XX:+AlwaysPreTouch -XX:NativeMemoryTracking=summary BigRamTester 900
BigRAMTester上的当前内存使用情况
除了深蓝色的内存消耗(“基线”)外,该图还显示了一个“底”(绿色),它表示GC组件使用的内存量,而不是记忆集中的内存使用量。总体而言,在这种情况下,G1的记忆集最多占用1.6 GB,大约占Java堆的8%。比G1所需的其余操作更多,约900 MB
哎呀
背景
G1具有两个交替的操作阶段,仅年轻阶段和空间回收阶段。在年轻的阶段,G1逐渐填充了旧一代的Java堆。在某个时候,G1决定是时候收集旧一代的空间了,并开始在后台寻找空闲空间。完成此所谓的标记后,G1选择区域(集合候选集[4])进行最终的空间回收。在此之前,它需要为这些候选区域建立记住的集合,因为G1在仅年轻阶段就没有维护它们(反正也不打算在老一代中回收空间,所以为什么要维护它们?[7 ])。
一旦记住的集完成,G1便进入空间回收阶段,从每个垃圾收集的收集集候选项中收集并回收几个旧世代区域中的空间。当候选收集集中没有更多区域时,空间回收阶段结束;或者在该阶段中的垃圾回收暂停后,剩余回收空间小于当前堆大小的5%[6]。
第二个条件的原因是,候选列表中的最后几个区域往往会花费相对较长的时间来收集。G1对候选进行排序,以提高效率,该指标考虑了一个区域将产生多少空白空间以及它们收集起来有多困难,即基本上包含了多少个已记忆的集合条目。G1收集的空间最多,并且最容易收集区域。因此,此规则会滤除可能导致暂停时间过长的区域,从而牺牲内存的等待时间。
改变
这次应用有关不撤离低效区域的后一种条件的时间可以加以改进:这意味着G1有效地维护了永远不会从中回收空间的区域的记忆集。更糟糕的是,这些记住的集会一直保留到空间回收阶段结束为止。这可能会导致令人惊讶的情况,即在收集这些区域时,尽管保留这些区域的区域数量减少了,但记忆集中的使用量却增加了。在上面的内存使用情况图表中也可以观察到这种效果:在记忆集的第一个第一次高峰之后,记忆使用情况显示了记忆集的建立位置,但是还有一个第二个记忆集甚至比第一个记忆集要高,直到最终消失。
因此,针对此问题的直接解决方案是在候选人选择之后和构建记忆集之前应用该规则。
需要注意的是:这可能会从集合集候选列表中删除所有区域,从而导致该空间回收阶段没有或只有很少的回收进度。这是不希望的,因为G1只是花了所有的标记工作(几乎)没有。因此,为了模拟在规则的最终启发式中已将规则应用到集合的末尾,G1会根据该空间回收阶段中某些混合集合的预期数量,至少保留一定数量的区域。
再次应用此补丁再次运行BigRamTester时,显示以下内存消耗图表(粉红色):
早期修剪的内存使用量对BigRAMTester的影响
记住的设定峰峰值内存消耗减少了250-300 MB-承诺的20%。至少对于这种类型的应用程序:)
实际上可能更多,但这与G1当前如何管理已记忆的集合数据结构有关。这是另一个需要解决的问题。
下次见
汤玛士
PS:此更改是为减少已记起的设置内存消耗而进行的更长时间的工作的一部分。
为了进行比较,下面的图表显示了JDK 10.0.2(青色)中相同基准的GC组件的内存消耗,其中包括上面显示的用于比较的内存。JDK 8和JDK 9至少比这差,但由于记住的NMT记帐设置中的错误[8],很难获得确切的数字。
那早期的JDK呢?
共有 0 条评论