高吞吐低延迟Java应用的垃圾回收优化html
高性能应用构成了现代网络的支柱。LinkedIn有许多内部高吞吐量服务来知足每秒数千次的用户请求。要优化用户体验,低延迟地响应这些请求很是重要。java
好比说,用户常常用到的一个功能是了解动态信息——不断更新的专业活动和内容的列表。动态信息在LinkedIn随处可见,包括公司页面,学校页面以及最重要的主页。基础动态信息数据平台为咱们的经济图谱(会员,公司,群组等等)中各类实体的更新创建索引,它必须高吞吐低延迟地实现相关的更新。linux
图1 LinkedIn 动态信息git
这些高吞吐低延迟的Java应用转变为产品,开发人员必须确保应用开发周期的每一个阶段一致的性能。肯定优化垃圾回收(Garbage Collection,GC)的设置对达到这些指标很是关键。github
本文章经过一系列步骤来明确需求并优化GC,目标读者是为实现应用的高吞吐低延迟,对使用系统方法优化GC感兴趣的开发人员。文章中的方法来自于LinkedIn构建下一代动态信息数据平台过程。这些方法包括但不局限于如下几点:并发标记清除(Concurrent Mark Sweep,CMS)和G1垃圾回收器的CPU和内存开销,避免长期存活对象引发的持续GC周期,优化GC线程任务分配使性能提高,以及GC停顿时间可预测所需的OS设置。web
优化GC的正确时机?算法
GC运行随着代码级的优化和工做负载而发生变化。所以在一个已实施性能优化的接近完成的代码库上调整GC很是重要。可是在端到端的基本原型上进行初步分析也颇有必要,该原型系统使用存根代码并模拟了可表明产品环境的工做负载。这样能够捕捉该架构延迟和吞吐量的真实边界,进而决定是否纵向或横向扩展。缓存
在下一代动态信息数据平台的原型阶段,几乎实现了全部端到端的功能,而且模拟了当前产品基础架构所服务的查询负载。从中咱们得到了多种用来衡量应用性能的工做负载特征和足够长时间运行状况下的GC特征。性能优化
优化GC的步骤网络
下面是为知足高吞吐,低延迟需求优化GC的整体步骤。也包括在动态信息数据平台原型实施的具体细节。能够看到在ParNew/CMS有最好的性能,但咱们也实验了G1垃圾回收器。
1.理解GC基础知识
理解GC工做机制很是重要,由于须要调整大量的参数。Oracle的Hotspot JVM 内存管理白皮书是开始学习Hotspot JVM GC算法很是好的资料。了解G1垃圾回收器,请查看该论文。
2. 仔细考量GC需求
为下降应用性能的GC开销,能够优化GC的一些特征。吞吐量、延迟等这些GC特征应该长时间测试运行观察,确保特征数据来自于应用程序的处理对象数量发生变化的多个GC周期。
咱们使用Linux OS的Hotspot Java7u51,32GB堆内存,6GB新生代(young generation)和-XX:CMSInitiatingOccupancyFraction值为70(老年代GC触发时其空间占用率)开始实验。设置较大的堆内存用来维持长期存活对象的对象缓存。一旦这个缓存被填充,提高到老年代的对象比例显著降低。
使用初始的GC配置,每三秒发生一次80ms的新生代GC停顿,超过百分之99.9的应用延迟100ms。这样的GC极可能适合于SLA不太严格要求延迟的许多应用。然而,咱们的目标是尽量下降百分之99.9应用的延迟,为此GC优化是必不可少的。
3.理解GC指标
优化以前要先衡量。了解GC日志的详细细节(使用这些选项:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime)能够对该应用的GC特征有整体的把握。
LinkedIn的内部监控和报表系统,inGraphs和Naarad,生成了各类有用的指标可视化图形,好比GC停顿时间百分比,一次停顿最大持续时间,长时间内GC频率。除了Naarad,有不少开源工具好比gclogviewer能够从GC日志建立可视化图形。
在这个阶段,须要肯定GC频率和停顿时长是否影响应用知足延迟性需求的能力。
4.下降GC频率
在分代GC算法中,下降回收频率能够经过:(1)下降对象分配/提高率;(2)增长代空间的大小。
在Hotspot JVM中,新生代GC停顿时间取决于一次垃圾回收后对象的数量,而不是新生代自身的大小。增长新生代大小对于应用性能的影响须要仔细评估:
对于大部分为短时间存活对象的应用,仅仅须要控制前面所说的参数。对于建立长期存活对象的应用,就须要注意,被提高的对象可能很长时间都不能被老年代GC周期回收。若是老年代GC触发阈值(老年代空间占用率百分比)比较低,应用将陷入不断的GC周期。设置高的GC触发阈值可避免这一问题。
因为咱们的应用在堆中维持了长期存活对象的较大缓存,将老年代GC触发阈值设置为-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly。咱们也试图增长新生代大小来减小新生代回收频率,可是并无采用,由于这增长了应用延迟。
5.缩短GC停顿时间
减小新生代大小能够缩短新生代GC停顿时间,由于这样被复制到survivor区域或者被提高的数据更少。可是,正如前面提到的,咱们要观察减小新生代大小和由此致使的GC频率增长对于总体应用吞吐量和延迟的影响。新生代GC停顿时间也依赖于tenuring threshold(提高阈值)和空间大小(见第6步)。
使用CMS尝试最小化堆碎片和与之关联的老年代垃圾回收full GC停顿时间。经过控制对象提高比例和减少-XX:CMSInitiatingOccupancyFraction的值使老年代GC在低阈值时触发。全部选项的细节调整和他们相关的权衡,请查看Web Services的Java 垃圾回收和Java 垃圾回收精粹。
咱们观察到Eden区域的大部分新生代被回收,几乎没有对象在survivor区域死亡,因此咱们将tenuring threshold从8下降到2(使用选项:-XX:MaxTenuringThreshold=2),为的是缩短新生代垃圾回收消耗在数据复制上的时间。
咱们也注意到新生代回收停顿时间随着老年代空间占用率上升而延长。这意味着来自老年代的压力使得对象提高花费更多的时间。为解决这个问题,将总的堆内存大小增长到40GB,减少-XX:CMSInitiatingOccupancyFraction的值到80,更快地开始老年代回收。尽管-XX:CMSInitiatingOccupancyFraction的值减少了,增大堆内存能够避免不断的老年代GC。在本阶段,咱们得到了70ms新生代回收停顿和百分之99.9延迟80ms。
6.优化GC工做线程的任务分配
进一步缩短新生代停顿时间,咱们决定研究优化与GC线程绑定任务的选项。
-XX:ParGCCardsPerStrideChunk 选项控制GC工做线程的任务粒度,能够帮助不使用补丁而得到最佳性能,这个补丁用来优化新生代垃圾回收的卡表扫描时间。有趣的是新生代GC时间随着老年代空间的增长而延长。将这个选项值设为32678,新生代回收停顿时间下降到平均50ms。此时百分之99.9应用延迟60ms。
也有其余选项将任务映射到GC线程,若是OS容许的话,-XX:+BindGCTaskThreadsToCPUs选项绑定GC线程到个别的CPU核。-XX:+UseGCTaskAffinity使用affinity参数将任务分配给GC工做线程。然而,咱们的应用并无从这些选项发现任何益处。实际上,一些调查显示这些选项在Linux系统不起做用[1,2]。
7.了解GC的CPU和内存开销
并发GC一般会增长CPU的使用。咱们观察了运行良好的CMS默认设置,并发GC和G1垃圾回收器共同工做引发的CPU使用增长显著下降了应用的吞吐量和延迟。与CMS相比,G1可能占用了应用更多的内存开销。对于低吞吐量的非计算密集型应用,GC的高CPU使用率可能不须要担忧。
图2 ParNew/CMS和G1的CPU使用百分数%:相对来讲CPU使用率变化明显的节点使用G1
选项-XX:G1RSetUpdatingPauseTimePercent=20
图3 ParNew/CMS和G1每秒服务的请求数:吞吐量较低的节点使用G1
选项-XX:G1RSetUpdatingPauseTimePercent=20
8.为GC优化系统内存和I/O管理
一般来讲,GC停顿发生在(1)低用户时间,高系统时间和高时钟时间和(2)低用户时间,低系统时间和高时钟时间。这意味着基础的进程/OS设置存在问题。状况(1)可能说明Linux从JVM偷页,状况(2)可能说明清除磁盘缓存时Linux启动GC线程,等待I/O时线程陷入内核。
为避免运行时性能损失,启动应用时使用JVM选项-XX:+AlwaysPreTouch访问和清零页面。设置vm.swappiness为零,除非在绝对必要时,OS不会交换页面。
可能你会使用mlock将JVM页pin在内存中,使OS不换出页面。可是,若是系统用尽了全部的内存和交换空间,OS经过kill进程来回收内存。一般状况下,Linux内核会选择高驻留内存占用但尚未长时间运行的进程(OOM状况下killing进程的工做流)。对咱们而言,这个进程颇有可能就是咱们的应用程序。一个服务具有优雅降级(适度退化)的特色会更好,服务忽然故障预示着不太好的可操做性——所以,咱们没有使用mlock而是vm.swappiness避免可能的交换惩罚。
LinkedIn动态信息数据平台的GC优化
对于该平台原型系统,咱们使用Hotspot JVM的两个算法优化垃圾回收:
使用ParNew/CMS,应用每三秒40-60ms的新生代停顿和每小时一个CMS周期。JVM选项以下:
1 2 3 4 5 6 7 8 |
// 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 |
使用这些选项,对于几千次读请求的吞吐量,应用百分之99.9的延迟下降到60ms。
参考:
[1] -XX:+BindGCTaskThreadsToCPUs彷佛在Linux系统上不起做用,由于hotspot/src/os/linux/vm/os_linux.cpp的distribute_processes方法在JDK7或JDK8没有实现。
[2] -XX:+UseGCTaskAffinity选项在JDK7和JDK8的全部平台彷佛都不起做用,由于任务的affinity属性永远被设置为sentinel_worker = (uint) -1。源码见hotspot/src/share/vm/gc_implementation/parallelScavenge/{gcTaskManager.cpp,gcTaskThread.cpp, gcTaskManager.cpp}。
[3] G1存在一些内存泄露的bug,可能Java7u51没有修改。这个bug仅在Java 8修正了。