积极的垃圾回收器策略

2022-09-01 05:26:19

我正在运行一个应用程序,该应用程序创建并忘记了大量对象,长期存在对象的数量确实增长缓慢,但与短期对象相比,这很少。这是一个具有高可用性要求的桌面应用程序,需要每天 24 小时打开。大部分工作都是在单个线程上完成的,这个线程将只使用它能得到的所有CPU。

过去,我们在重负载下看到过以下情况:随着垃圾回收器收集的内存量小于新分配的内存量,已用堆大小缓慢增长并最终接近指定的最大堆,已用堆空间缓慢上升。此时,垃圾回收器将大量启动并开始使用大量资源来防止超过最大堆大小。这会减慢应用程序的速度(很容易慢10倍),在这一点上,大多数时候GC会在几分钟后成功清理垃圾,或者失败并抛出一个,它们都不是真正可以接受的。OutOfMemoryException

使用的硬件是四核处理器,至少有4GB内存运行64位Linux,如果需要,我们可以使用所有这些。目前,该应用程序大量使用单个内核,该内核的大部分时间都在运行单个内核/线程。其他内核大多处于空闲状态,可用于垃圾回收。

我有一种感觉,垃圾收集器应该在早期阶段更积极地收集,远远早于它耗尽内存之前。我们的应用程序没有任何吞吐量问题,低暂停时间要求比吞吐量重要一些,但远不如不接近最大堆大小重要。如果单个繁忙线程仅以当前速度的 75% 运行,则是可以接受的,只要这意味着垃圾回收器可以跟上创建的速度。因此,简而言之,性能的稳步下降比我们现在看到的突然下降要好。

我已经彻底阅读了Java SE 6 HotSpot[tm] 虚拟机垃圾回收调整,这意味着我很好地理解了这些选项,但是我仍然发现很难选择正确的设置,因为我的要求与论文中讨论的内容略有不同。

目前,我正在使用带有选项的ParallelGC。这比时间比的默认设置好一点,但我有一种感觉,GC可以通过该设置运行得更多。-XX:GCTimeRatio=4

对于监控,我主要使用jconsole和jvisualvm。

我想知道您对上述情况建议哪些垃圾回收选项。另外,我可以查看哪些GC调试输出以更好地了解瓶颈。

编辑:我知道一个非常好的选择是创建更少的垃圾,这是我们真正考虑的事情,但是我想知道我们如何通过GC调整来解决这个问题,因为这是我们可以更容易地完成的事情,并且比更改大量源代码更快地推出。此外,我还运行了不同的内存分析器,并且我了解垃圾的使用方式,并且我知道它由可以收集的对象组成。

我正在使用:

java version "1.6.0_27-ea"
Java(TM) SE Runtime Environment (build 1.6.0_27-ea-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.2-b03, mixed mode)

使用 JVM 参数:

-Xmx1024M and -XX:GCTimeRatio=4 

编辑回复 Matts 的评论:大多数内存(和cpu)用于构造表示当前情况的对象。随着情况的快速变化,其中一些将被立即丢弃,而另一些则如果一段时间内没有更新,则会有一个中等寿命。


答案 1

您没有提到您正在运行的JVM的哪个版本,这是至关重要的信息。你也没有提到应用程序倾向于运行多长时间(例如,它是一个工作日的长度吗?一周?更少?)

其他几点

  1. 如果你不断地将对象泄漏到终身制中,因为你的分配速度比你的年轻一代可以被扫荡的速度快,那么你的后代就被错误地调整了。您需要对应用程序的行为进行一些适当的分析,以便能够正确调整它们的大小,您可以使用visualgc来实现此目的。
  2. 吞吐量收集器设计为接受单个大暂停,而不是许多较小的暂停,其优点是它是一个压缩收集器,并且可实现更高的总吞吐量
  3. CMS的存在是为了服务于频谱的另一端,即更多的暂停要小得多,但总吞吐量却要低得多。缺点是它没有压缩,因此碎片可能是一个问题。6u26 中的碎片问题已得到改进,因此,如果您不在该版本中,则可能是升级时间。请注意,您所说的“渗入终身制”效应会加剧碎片问题,并且,如果时间推移,这将导致升级失败(也称为计划外的完整gc和关联的STW暂停)。我之前已经写过关于这个问题的答案
    1. 如果您运行的是具有>4GB RAM和足够多的JVM的64位JVM,请确保您只是在浪费空间,因为64位JVM占用的空间是32位JVM的约1.5倍,如果没有它(如果不是,请升级以访问更多RAM)-XX:+UseCompressedOops

您可能还想阅读我写的关于这个主题的另一个答案,该答案涉及适当地调整您的幸存者空间和伊甸园的大小。基本上,你想要实现的是;

  • 伊甸园足够大,不会太频繁地收集
  • 幸存者空间的大小与紧张阈值相匹配
  • 设置一个延长阈值,以尽可能确保只有真正长寿的物体才能进入终身职位

因此,假设你有一个6G堆,你可以做一些类似5G伊甸园+16M幸存者空间+1的张力阈值1。

基本过程是

  1. 分配到伊甸园
  2. 伊甸园填满
  3. 活体被扫入幸存者空间
  4. 从幸存者空间进入的活体要么复制到空间,要么被提升为终身制(取决于守恒阈值和可用空间,以及它们从1复制到另一个的次数)
  5. 留在伊甸园里的任何东西都被扫走了

因此,给定适合应用程序分配配置文件大小的空间,完全可以配置系统,使其能够很好地处理负载。对此有一些警告;

  1. 您需要一些长时间运行的测试才能正确执行此操作(例如,可能需要数天才能遇到CMS碎片问题)
  2. 你需要做几次每次测试才能得到好的结果
  3. 您需要在GC配置中一次更改1件事
  4. 您需要能够向应用程序提供合理可重复的工作负载,否则将很难客观地比较来自不同测试运行的结果
  5. 如果工作负载是不可预测的并且具有巨大的峰值/低谷,这将变得非常难以可靠地完成

第1-3点意味着这可能需要很长时间才能正确。另一方面,你可以让它足够好v很快,这取决于你有多肛门!

最后,与 Peter Lawrey 的观点相呼应,如果你对对象分配非常严格,你可以节省很多麻烦(尽管引入了一些其他麻烦)。


答案 2

已经推出的G1GC算法已经稳定下来,它做得很好。您只需指定要在应用程序中使用的最大暂停时间。JVM将为您处理所有其他事情。Java 1.7

主要参数:

-XX:+UseG1GC -XX:MaxGCPauseMillis=1000 

还有更多参数需要配置。如果您使用的是 4 GB RAM,请将区域大小配置为 4 GB/2048 块,大约为 2 MB

-XX:G1HeapRegionSize=2  

如果您有 8 核 CPU,请微调另外两个参数

-XX:ParallelGCThreads=4 -XX:ConcGCThreads=2 

除了这些参数之外,将其他参数值保留为默认值,例如

-XX:TargetSurvivorRatio等。

查看 Oracle 网站,了解有关 的更多详细信息。G1GC

-XX:G1HeapRegionSize=n

设置 G1 区域的大小。该值将是 2 的幂,范围从 1MB 到 32MB。目标是根据最小 Java 堆大小拥有大约 2048 个区域。

 -XX:MaxGCPauseMillis=200

为所需的最大暂停时间设置目标值。默认值为 200 毫秒。指定的值不适应堆大小。

-XX:ParallelGCThreads=n

设置 STW 工作线程的值。将 n 的值设置为逻辑处理器数。n 的值与逻辑处理器的数量相同,最大值为 8。

如果存在八个以上的逻辑处理器,则将 n 的值设置为逻辑处理器的大约 5/8。这在大多数情况下都有效,但较大的 SPARC 系统除外,其中 n 的值大约是逻辑处理器的 5/16。

-XX:ConcGCThreads=n

来自预言机的建议

在评估和微调 G1 GC 时,请记住以下建议:

  1. 年轻一代大小:避免使用选项或任何其他相关选项(如 .)显式设置年轻一代大小。.-Xmn-XX:NewRatioFixing the size of the young generation overrides the target pause-time goal

  2. 暂停时间目标:在评估或优化任何垃圾回收时,始终存在延迟与吞吐量之间的权衡。G1 GC 是一个增量垃圾回收器,具有统一的暂停,但也会增加应用程序线程的开销。.The throughput goal for the G1 GC is 90 percent application time and 10 percent garbage collection time

最近,我用G1GC算法替换了CMS,用于4 GB堆,年轻一代和老一代的划分几乎相等。我设置了时间,结果真棒。MaxGCPause


推荐