美文网首页
高吞吐、低延迟Java应用的GC优化实践

高吞吐、低延迟Java应用的GC优化实践

作者: 田真的架构人生 | 来源:发表于2019-05-22 22:16 被阅读0次

    原文:https://mp.weixin.qq.com/s/_lXmc7U3bInytb4H1gLeSQ

    降低 GC 频率

    在分代 GC 算法中,降低 GC 频率可以通过:(1) 降低对象分配(新生代)/晋升率(老年代);(2) 增加各代空间的大小,(3)增加Old GC触发阈值

    但是空间并不是越大越好,相反,过大的空间可能会导致GC的停顿时间更长,在 Hotspot JVM 中,Young GC 停顿时间取决于一次垃圾回收后存活下来的对象的数量,而不是 Young Gen 自身的大小。增加 Young Gen 大小对于应用性能的影响需要仔细评估:

    • 如果更多的数据存活而且被复制到 Survivor 区域,或者每次 GC 更多的数据晋升到 Old Gen,增加 Young Gen 大小可能导致更长的 Young GC 停顿。较长的 GC 停顿可能会导致应用程序延迟增加和(或)吞吐量降低。
    • 另一方面,如果每次垃圾回收后存活对象数量不会大幅增加,停顿时间可能不会延长。在这种情况下,降低 GC 频率可能会使整个应用总体延迟降低和(或)吞吐量增加。

    对于长期存活对象的应用,就需要注意,被晋升的对象可能很长时间都不能被 Old GC 周期回收。如果 Old GC 触发阈值(Old Gen 占用率百分比)比较低,应用将陷入持续的 GC 循环中。可以通过设置高的 GC 触发阈值可避免这一问题。

    由于我们的应用在堆中维持了长期存活对象的较大缓存,将 Old GC 触发阈值设置为

    -XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly
    

    来增加触发 Old GC 的阈值。我们也试图增加 Young Gen 大小来减少 Young GC 频率,但是并没有采用,因为这增加了应用的 999 线。

    缩短 GC 停顿时间

    (1)减小Young Gen(2)减小tenuring threshold(3)减小Old GC触发阈值

    减少 Young Gen 大小可以缩短 Young GC 停顿时间,因为这可能导致被复制到 Survivor 区域或者被晋升的数据更少。但是,正如前面提到的,我们要观察减少 Young Gen 大小和由此导致的 GC 频率增加对于整体应用吞吐量和延迟的影响。Young GC 停顿时间也依赖于 tenuring threshold (晋升阈值)和 Old Gen 大小。

    我们观察到Eden区域的大部分YoungGen被回收,几乎没有3-8年龄对象在Survivor空间中死亡,也就是说,对于长期存活的对象,不管经历了多少次GC,仍然存活。所以我们将tenuring threshold从8降低到2(使用选项:-XX:MaxTenuringThreshold=2),以降低YoungGC消耗在数据复制上的时间。

    在使用 CMS GC 时,应将因堆碎片或者由堆碎片导致的 Full GC 的停顿时间降低到最小。通过控制对象晋升比例和减小 -XX:CMSInitiatingOccupancyFraction 的值使 Old GC 在低阈值时触发。但是,减小该值会增加GC的频率,我们同样需要观察调整该值对系统整体吞吐率的影响。

    我们还注意到 Young GC 暂停时间随着 Old Gen 占用率上升而延长。这意味着来自 Old Gen 的压力使得对象晋升花费更多的时间。为解决这个问题,将总的堆内存大小增加到 40GB,减小 -XX:CMSInitiatingOccupancyFraction 的值到 80,更快地开始 Old GC。尽管 -XX:CMSInitiatingOccupancyFraction 的值减小了,增大堆内存可以避免频繁的 Old GC。在此阶段,我们的结果是 Young GC 暂停 70ms,应用的 999 线在 80ms。

    优化GC工作线程的任务分配

    为了进一步降低 Young GC 停顿时间,我们决定研究 GC 线程绑定任务的参数来进行优化。

    -XX:ParGCCardsPerStrideChunk 参数控制 GC 工作线程的任务粒度,可以帮助不使用补丁而获得最佳性能,这个补丁用来优化 Young GC 中的 Card table(卡表)扫描时间。有趣的是,Young GC 时间随着 Old Gen 的增加而延长。将这个选项值设为 32678,Young GC 停顿时间降低到平均 50ms。此时应用的 999 线在 60ms。

    还有一些的参数可以将任务映射到 GC 线程,如果操作系统允许的话,-XX:+BindGCTaskThreadsToCPUs 参数可以绑定 GC 线程到个别的 CPU 核。使用亲缘性 -XX:+UseGCTaskAffinity 参数可以将任务分配给 GC 工作线程。然而,我们的应用并没有从这些选项带来任何好处。实际上,一些调查显示这些选项在 Linux 系统不起作用。

    了解GC的CPU和内存开销

    并发 GC 通常会增加 CPU 使用率。虽然我们观察到 CMS 的默认设置运行良好,但是 G1 收集器的并发 GC 工作会导致 CPU 使用率的增加,显著降低了应用程序的吞吐量和延迟。与 CMS 相比,G1 还增加了内存开销。对于不受 CPU 限制的低吞吐量应用程序,GC 导致的高 CPU 使用率可能不是一个紧迫的问题。

    为GC优化系统内存和I/O管理

    通常来说,GC 停顿有两种特殊情况:(1) 低 user time,高 sys time 和高 real time
    (2) 低 user time,低 sys time 和高 real time。这意味着基础的进程/OS设置存在问题。情况 (1) 可能意味着 JVM 页面被 Linux 窃取;情况 (2) 可能意味着 GC 线程被 Linux 用于磁盘刷新,并卡在内核中等待 I/O。

    另外,为了避免在运行时造成性能损失,我们可以使用 JVM 选项 -XX:+AlwaysPreTouch 在应用程序启动时先访问所有分配给它的内存,让操作系统把内存真正的分配给 JVM。我们还可以将 vm.swappability 设置为0,这样操作系统就不会交换页面到 swap(除非绝对必要)。

    可能你会使用 mlock 将 JVM 页固定到内存中,这样操作系统就不会将它们交换出去。但是,如果系统用尽了所有的内存和交换空间,操作系统将终止一个进程来回收内存。通常情况下,Linux 内核会选择具有高驻留内存占用但运行时间不长的进程进行终止。在我们的例子中,这个进程很有可能就是我们的应用程序。优雅的降级是服务优秀的属性之一,不过服务突然终止的可能性对于可操作性来说并不好 —— 因此,我们不使用 mlock,只是通过 vm.swapability 来尽可能避免交换内存页到 swap 的惩罚。

    最终JVM参数配置:

    // JVM sizing options
    -server -Xms40g -Xmx40g -XX:MaxDirectMemorySize=4096m -XX:PermSize=256m -XX:MaxPermSize=256m
       
    // Young generation options
    -XX:NewSize=6g -XX:MaxNewSize=6g -XX:+UseParNewGC -XX:MaxTenuringThreshold=2 -XX:SurvivorRatio=8 -XX:+UnlockDiagnosticVMOptions -XX:ParGCCardsPerStrideChunk=32768
    
    // Old generation  options
    -XX:+UseConcMarkSweepGC -XX:CMSParallelRemarkEnabled -XX:+ParallelRefProcEnabled -XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly
       
    // Other options
    -XX:+AlwaysPreTouch -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:-OmitStackTraceInFastThrow
    

    相关文章

      网友评论

          本文标题:高吞吐、低延迟Java应用的GC优化实践

          本文链接:https://www.haomeiwen.com/subject/gezlzqtx.html