这里介绍的工作是甲骨文、乌普萨拉大学和KTH联合研究项目的一部分。在inside.java这里关注博客系列,阅读更多关于在斯德哥尔摩的Oracle开发办公室进行的JVM研究的内容。
这是关于我在硕士论文中所做的垃圾收集工作的简短描述。这项工作是与甲骨文合作完成的,这给了我一个机会,让我有机会与才华横溢的人一起解决具有挑战性的问题。我想为我在甲骨文的导师Per Lidén和ErikÖsterlund带来一个特别的大喊答题节目。
要在垃圾收集环境中实现快速分配,通常的方法是使用凹凸指针分配。凹凸指针分配使用指向内存中第一个可用字节的指针,该指针随着我们继续分配对象而单调增加。虽然此方案允许快速分配,但它附带一个警告,即空闲内存必须保持连续。为了保持空闲内存的连续性,许多垃圾收集器在内存中移动对象以将其压缩,从而避免碎片,如下图所示。这通常是在将所有活动对象移出页面然后释放的过程中处理的。这允许清除O(活动)对象中的页面,这通常是一个很小的数量(相对而言),因为大多数新创建的对象往往很快就会死亡。
Z垃圾收集器(ZGC)是OpenJDK(可能运行Java应用程序)中新的移动并发垃圾收集器[1,2]。ZGC在不停止应用程序的情况下四处移动对象,以对抗内存碎片,这以跟踪对象移动的形式给应用程序增加了额外的开销,因此指向它们的所有指针最终都可以更新到新的位置。通常,在GC术语中,这称为转发信息。
ZGC使用辅助转发表-针对快速查找进行了优化,但代价是使用额外的内存。此转发表存储在Java堆之外,称为离堆分配。任何堆外分配或在移动旧对象后保留它们都将被视为内存开销,因为JVM而不是实际的Java应用程序严格需要这样做。ZGC遇到的病态情况是,其转发信息的大小可能变得非常大,理论上与堆本身一样大。如果我们根据病理情况来确定应用程序的大小,这将是对资源的浪费,因为内存使用量通常要少得多。这可能会使确定应用程序的内存需求变得困难。
这种内存开销过大的风险不仅是理论上的问题,而且可以在实际程序中观察到。下面的图表描述了Oracle名为BigRamTester的内部基准测试应用程序的内存开销,其中显示了35%的内存开销。该应用程序的源代码可以在本期附件中找到。
为从地址A(起始地址)到B(目的地址)的每个重新定位的对象存储转发信息花费大约128字节(每个起始地址/目的地址为64字节),可以在计算上高效地实现,但是以额外的存储器开销为代价(如上所述)。作为我论文工作的一部分,我们提出了一种新的转发表设计,它将几个零散填充的页面(即,几乎没有活动对象)映射到单个新页面上,从而允许使用From-Address和活跃度信息来计算To-Address。该设计导致压缩转发表,理论上最坏情况下的内存开销为<;3.2%。
在ZGC中,应用程序线程和垃圾收集器线程之间可能会争用重新定位对象。争用导致对象被重新定位到的非确定性地址。新的设计需要确定性地址,以便我们可以在给定某些信息集的情况下计算新地址。假设我们有一个旧页面X,它的对象将被重新定位到新页面Y。如果我们按照从头到尾遍历实时地图时遇到的顺序,以升序将对象复制到Y,我们就实现了确定性地址。
新的设计将页面分成Q个大小的块。一块保存在该块之前的前面的生命物体的数量。要获得以前活着的对象的大小,您将使用关联的块,并扫描实时地图以查找未被块覆盖的地址。这允许高效地计算X,并允许在重新定位所有对象后立即释放旧页面,但代价是要占用一些空间。下面描述了一个将页面划分为块的示例。
每个页面分隔符对应于一个块的实时地图覆盖范围。在本例中,要重新定位的对象位于第三个块(绿色箭头)覆盖的第三页上。要查找地址,我们不必扫描以前的块,因为live bytes字段描述的是所有前面块的活动字节量(红色箭头)。可以在实时地图(黄色箭头)中找到块内绿色对象之前的所有生命对象(及其大小)。
此设计产生了一个简单的逻辑,以便计算新地址,该地址在伪代码中将表示为:
inline uintptr_t ZCompactForwarding::to_address(Uintptr_T From_Address){uintptr_t to_page_start_address=to_page_start_address(From_Address);uintptr_t live_bytes_pre_chunks=live_bytes_Beer_chunks(From_Address)uintptr_t live_bytes_on_chunks=live_bytes_on_chunks(。
该实现的最大内存开销为<;3.2%。我使用DaCapo基准测试套件和SPECjbb2015基准测试来评估设计对执行时间的影响,因为现在必须计算转发地址,而不是查找地址。自然,我们预计性能会有所下降。基准测试的结果显示,新设计的平均性能下降了大约2%,这在统计上是显著的。值得注意的是,DaCapo的许多程序根本没有受到影响。在新设计中,两个DaCapo程序的性能分别提高了5.69%和22.42%。
我还没有在我的列表上实现所有的优化(到目前为止)。但是,根据测量结果,我非常有希望内存占用和可预测开销的减少超过了执行时间的增加。这可能意味着您刚刚读到的工作有望进入OpenJDK。只有时间能证明。