hotspot jvm_HotSpot JVM中的垃圾回收

上个月 ,我们研究了经典的垃圾回收技术,包括引用计数,复制,标记清除和标记紧凑。 这些方法中的每一种在某些情况下都有优点和缺点。 例如,当大部分对象为垃圾时,复制效果很好,但对于许多寿命很长的对象,复制效果不佳(重复复制它们)。 相反,mark-compact对于寿命长的对象(只复制一次)效果很好,但是对于许多寿命短的对象却效果不佳。 1.2及更高版本JVM所使用的技术称为世代垃圾收集 ,将这两种技术结合在一起可以兼得两者的优势,此外,它还提供了非常低的对象分配开销。

老物件,年轻物件

在任何应用程序堆中,有些对象在创建后不久就会变为垃圾,有些对象会生存很长时间,然后变为垃圾,而另一些对象则可以在程序的整个运行过程中保持活动状态。 经验研究表明,对于大多数面向对象的语言(包括Java语言),绝大多数对象(多达98%,具体取决于您对对象青年的度量标准)都死于年轻。 一个人可以测量一个对象的年龄(以时钟为单位),自分配对象以来内存管理子系统分配的总字节数,或自分配对象以来的垃圾回收数。 但是,无论如何衡量,这些研究都显示出相同的东西-大多数物体都死于年轻。 大多数物体会早逝的事实对于选择收藏家具有重要意义。 特别是,当大多数对象死于年轻时,复制收集器的性能都很好,因为复制收集器根本不会访问死掉的对象。 他们只是将活动对象复制到另一个堆区域,然后一次回收整个剩余空间。

在第一个收藏中幸存下来的物体中,很大一部分将成为长期或永久的。 各种垃圾收集策略的执行情况会有所不同,具体取决于短期对象和长期对象的组合。 当大多数对象年轻时,复制收集器就可以很好地工作,因为年轻时死亡的对象根本不需要复制。 但是,复制收集器无法很好地处理长期存在的对象,因此反复将它们从一个半空间来回复制到另一个半空间。 相反,标记紧凑的收集器对长寿命的对象非常有效,因为长寿命的对象往往会堆积在堆的底部,因此不需要再次复制。 但是,标记清除和标记紧凑的收集器在检查死对象上花费了更多的精力,因为它们必须在清除阶段检查堆中的每个对象。

世代收藏

分代收集器将堆分为多代。 对象是在年轻一代中创建的,并且满足某些提升条件(例如在一定数量的收藏中幸存下来)的对象随后被提升到下一代。 世代收集器可以自由地对不同世代使用不同的收集策略,并分别对世代进行垃圾收集。

小型收藏

分代收集的优点之一是,通过不立即收集所有世代,可以缩短垃圾收集的暂停时间。 当分配器无法满足分配请求时,它首先触发次要集合 ,该次要集合仅收集最年轻的一代。 由于年轻一代中的许多对象已经死亡,并且复制收集器根本不需要检查死对象,因此较小的收集暂停可能非常短,并且通常可以回收大量的堆空间。 如果次要集合释放了足够的堆空间,则用户程序可以立即恢复。 如果没有释放足够的堆空间,它将继续收集更高的代数,直到回收了足够的内存为止。 (如果垃圾收集器在完成完整的收集后无法回收足够的内存,则它将扩展堆或抛出OutOfMemoryError 。)

代际参考

跟踪垃圾收集器(例如复制,标记扫描和标记压缩)都从根集开始扫描,遍历对象之间的引用,直到访问了所有活动对象。

世代跟踪收集器从根集开始,但不遍历导致较老一代对象的引用,这减小了要跟踪的对象图的大小。 但这会带来一个问题-如果较老的对象引用较年轻的对象,而该对象无法通过根的其他任何引用链访问,该怎么办?

为了解决此问题,分代收集器必须显式跟踪从较旧的对象到较年轻的对象的引用,并将这些旧到年轻的引用添加到次要集合的根集中。 有两种方法可以创建从旧对象到年轻对象的引用。 可以将旧对象中包含的引用之一修改为引用年轻对象,或者将引用其他年轻对象的年轻对象提升为较老的一代。

追踪代际参考

无论旧的引用是通过升级还是指针修改创建的,垃圾收集器在要执行次要收集时都需要具有一套全面的旧引用。 做到这一点的一种方法是追溯老一代,但这显然有大量的开销。 更好的方法是线性扫描旧一代,以查找对年轻对象的引用。 这种方法比跟踪更快,并且具有更好的局部性,但是仍然是一项艰巨的工作。

增变器和垃圾收集器可以一起工作,以维护创建时所有旧的引用的完整列表。 将对象提升为较早的一代时,垃圾收集器可以记录由于提升而创建的所有旧的引用,这仅保留了对指针修改所创建的代间引用的跟踪。

垃圾收集器可以跟踪通过多种方式修改现有对象中保存的引用而产生的旧引用。 它可以以与在引用计数收集器中维护引用计数相同的方式跟踪它们(编译器可以生成围绕指针分配的其他指令),或者可以在旧堆上使用虚拟内存保护来捕获对较旧对象的写入。 另一种可能更有效的虚拟内存方法是使用旧堆中的页面修改脏位来标识块,以扫描包含旧指针的对象。

稍微聪明一点,就可以避免跟踪每个指针修改并检查它是否跨越代边界的开销。 例如,不必将存储跟踪到局部或静态变量,因为它们已经是根集的一部分。 也有可能避免在简单初始化新创建对象的字段的构造函数中跟踪指针存储(所谓的初始化存储 ),因为(几乎)所有对象都分配在年轻代中。 无论如何,运行时必须维护从旧对象到年轻对象的显式引用集,并在收集年轻代时将这些引用添加到根集中。

在图1中,箭头表示堆中对象之间的引用。 红色箭头表示旧参考,必须将其添加到次要集合的根集中。 蓝色箭头表示从根集或从年轻一代到旧对象的引用,仅收集年轻一代时无需跟踪。

代际参考

卡标

Sun JDK使用称为卡标记的算法的优化变体来识别对保留在旧对象字段中的指针的修改。 用这种方法,堆被分成一组卡 ,每个卡通常小于一个内存页。 JVM维护一个卡映射,其中一个位(在某些实现中为字节)对应于堆中的每个卡。 每次修改堆中对象的指针字段时,都会在该卡的卡映射中设置相应的位。 在垃圾回收时,将检查与旧一代卡相关联的标记位,并在脏卡上扫描包含年轻一代引用的对象。 然后清除标记位。 卡片标记有几个成本–卡片映射的额外空间,在每个指针存储上要做的其他工作以及在垃圾回收时要做的其他工作。 卡标记算法可以为每个非初始化堆指针存储添加最少两到三个机器指令,并且需要在较小的收集时间扫描脏卡上的任何对象。

JDK 1.4.1默认收集器

默认情况下,1.4.1 JDK将堆分为两部分,年轻的一代和老的一代。 (实际上,还有第三部分,永久空间,用于存储已加载的类和方法对象。)使用复制,将年轻一代划分为一个创建空间(通常称为Eden )和两个幸存者半空间。集电极。

老一代使用标记紧凑的收集器。 对象经过复制一定次数后,便从年轻一代升级为老一代。 一个较小的集合会将有生命的物体从伊甸园和一个幸存者半空间复制到另一个幸存者空间,从而有可能将某些对象推广到老一辈。 一个重要的收藏将收集年轻人和老年人。 System.gc()方法始终会触发一个主集合,这是您尽量少使用System.gc()的原因之一System.gc()如果有的话System.gc() ,因为主集合比次要集合要花费更长的时间。 无法以编程方式触发次要收藏。

其他收集选项

除了默认使用的复制和标记紧凑收集器之外,1.4.1 JDK还包含其他四种垃圾收集算法,每种算法都适合不同的目的。 JDK 1.4.1包括一个增量收集器(自JDK 1.2开始就存在)和三个新的收集器,用于在多处理器系统上进行更有效的收集-并行复制收集器,并行清除收集器和并发标记清除收集器。 这些新的收集器解决了垃圾收集器成为多处理器系统上的可伸缩性瓶颈的问题。 图2显示了有关何时选择备用收集选项的一些准则。

垃圾收集选项

增量收集

从1.2开始,增量收集选项已成为JDK的一部分。 增量收集以吞吐量为代价减少了垃圾收集暂停,仅在较短的收集暂停非常重要的情况下才需要使用增量收集,例如近实时系统。

JDK用于增量收集的算法,即Train算法,在老一辈和年轻一代之间创建了堆的新部分。 这些堆部分被划分为“火车”,每个火车被划分为一系列“汽车”。 每辆车可以分开收集。 实际上,每辆火车车厢都是一个独立的世代,这意味着不仅要跟踪旧到年轻的引用,而且还必须跟踪从旧火车到年轻火车的参考以及从旧车到年轻汽车的参考。 这为转换器和垃圾收集器增加了很多额外的工作,但允许暂停时间短得多。

并行和并行收集器

JDK 1.4.1中的新收集器均旨在解决多处理器系统上的垃圾收集问题。 由于大多数垃圾收集算法会在一段时间内停止运行,因此单线程垃圾收集器可能很快成为可伸缩性瓶颈,因为除了一个处理器之外,所有处理器均处于空闲状态,而垃圾收集器已挂起了用户程序线程。 其中两个新的收集器旨在减少收集暂停时间-并行复制收集器和并发标记清除收集器。 另一个是并行清除收集器,旨在提高大堆上的吞吐量。

-XX:+UseParNewGC JVM选项启用的并行复制收集器是一个年轻的复制收集器,它将垃圾收集的工作分配到与CPU一样多的线程中。 由-XX:+UseConcMarkSweepGC选项启用的并发标记扫描收集器是一种老式的标记扫描收集器,它在初始标记阶段(随后再次在短暂的重新标记阶段)短暂停止了运行允许用户程序恢复,而垃圾回收器线程与用户程序同时执行。 并行复制收集器和并发标记清除收集器基本上是默认复制和mark-compact收集器的并发版本。 由-XX:+UseParallelGC启用的并行清除收集器是一种年轻的收集器,已针对多处理器系统上的很大(千兆字节和更大)堆进行了优化。

选择算法

有六种算法可供选择,您可能想知道要使用哪种算法。 图2提供了一些指导,将收集器分为单线程和并发,以及低暂停和高吞吐量。 根据您对应用程序和部署环境的了解,这足以选择适当的算法。 对于许多应用程序,默认收集器可以正常工作-因此,如果您不存在性能问题,则没有必要增加更多复杂性。 但是,如果您的应用程序部署在多处理器系统上或使用非常大的堆,则可以通过更改收集器选项来提高性能。

调整垃圾收集器

JDK 1.4.1还包括许多用于优化垃圾回收的选项。 您可以花费大量时间来调整这些选项并评估其效果,因此在尝试调整垃圾回收器之前,通过确保首先对应用程序进行了彻底的概要分析和优化,您可能会在调整投资上获得更好的回报。

从调整垃圾收集开始的第一个地方是检查详细的GC输出。 这将为您提供有关垃圾收集操作的频率,时间和持续时间的信息。 垃圾收集调整的最简单形式是简单地扩展最大堆大小( -Xmx )。 随着堆的增长,复制收集变得更加有效,因此通过扩展堆,可以减少每个对象的收集成本。 除了增加最大堆大小外,还可以使用-XX:NewRatio选项来增加分配给年轻代的空间比例。 您还可以使用-Xmn选项明确指定年轻代的大小。 请参阅相关主题的一些文章,提供更详细的垃圾收集调整建议。

摘要

随着JVM的发展,默认垃圾收集器变得越来越好。 与早期JDK使用的mark-sweep-compact收集器相比,JDK 1.2和更高版本使用的分代收集器提供了更好的分配和收集性能。 1.4.1 JDK通过为多处理器系统和非常大的堆添加新的多线程收集选项,进一步提高了垃圾收集的效率。

下个月,我们将通过观察有关垃圾收集的一些性能提示和神话来结束对垃圾收集的探索,包括对象分配的实际成本,显式空值的成本和收益以及最终确定的成本。


翻译自: https://www.ibm.com/developerworks/java/library/j-jtp11253/index.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值