高性能应用构成了现代网络的支柱。LinkedIn有许多内部高吞吐量服务来满足每秒数千次的用户请求。要优化用户体验,低延迟地响应这些请求非常重要。

比如说,用户经常用到的一个功能是了解动态信息——不断更新的专业活动和内容的列表。动态信息在LinkedIn随处可见,包括公司页面,学校页面以及最重要的主页。基础动态信息数据平台为我们的经济图谱(会员,公司,群组等等)中各种实体的更新建立索引,它必须高吞吐低延迟地实现相关的更新。


图1 LinkedIn 动态信息

这些高吞吐低延迟的Java应用转变为产品,开发人员必须确保应用开发周期的每个阶段一致的性能。确定优化垃圾回收(Garbage Collection,GC)的设置对达到这些指标非常关键。

本文章通过一系列步骤来明确需求并优化GC,目标读者是为实现应用的高吞吐低延迟,对使用系统方法优化GC感兴趣的开发人员。文章中的方法来自于LinkedIn构建下一代动态信息数据平台过程。这些方法包括但不局限于以下几点:并发标记清除(Concurrent Mark Sweep,CMS)和G1垃圾回收器的CPU和内存开销,避免长期存活对象引起的持续GC周期,优化GC线程任务分配使性能提升,以及GC停顿时间可预测所需的OS设置。

优化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周期。

  • Stop-the-world回收器回收垃圾时会暂停应用线程。停顿的时长和频率不应该对应用遵守SLA产生不利的影响。
  • 并发GC算法与应用线程竞争CPU周期。这个开销不应该影响应用吞吐量。
  • 不压缩GC算法会引起堆碎片化,导致full GC长时间Stop-the-world停顿。
  • 垃圾回收工作需要占用内存。一些GC算法产生更高的内存占用。如果应用程序需要较大的堆空间,要确保GC的内存开销不能太大。
  • 清晰地了解GC日志和常用的JVM参数对简单调整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的内部监控和报表系统,inGraphsNaarad,生成了各种有用的指标可视化图形,比如GC停顿时间百分比,一次停顿最大持续时间,长时间内GC频率。除了Naarad,有很多开源工具比如gclogviewer可以从GC日志创建可视化图形。

在这个阶段,需要确定GC频率和停顿时长是否影响应用满足延迟性需求的能力。

4.降低GC频率

在分代GC算法中,降低回收频率可以通过:(1)降低对象分配/提升率;(2)增加代空间的大小。

在Hotspot JVM中,新生代GC停顿时间取决于一次垃圾回收后对象的数量,而不是新生代自身的大小。增加新生代大小对于应用性能的影响需要仔细评估:

  • 如果更多的数据存活而且被复制到survivor区域,或者每次垃圾回收更多的数据提升到老年代,增加新生代大小可能导致更长的新生代GC停顿。
  • 另一方面,如果每次垃圾回收后存活对象数量不会大幅增加,停顿时间可能不会延长。在这种情况下,减少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时线程陷入内核。在这些情况下如何设置参数可以参考该PPT

为避免运行时性能损失,启动应用时使用JVM选项-XX:+AlwaysPreTouch访问和清零页面。设置vm.swappiness为零,除非在绝对必要时,OS不会交换页面。

可能你会使用mlock将JVM页pin在内存中,使OS不换出页面。但是,如果系统用尽了所有的内存和交换空间,OS通过kill进程来回收内存。通常情况下,Linux内核会选择高驻留内存占用但还没有长时间运行的进程(OOM情况下killing进程的工作流)。对我们而言,这个进程很有可能就是我们的应用程序。一个服务具备优雅降级(适度退化)的特点会更好,服务突然故障预示着不太好的可操作性——因此,我们没有使用mlock而是vm.swappiness避免可能的交换惩罚。

LinkedIn动态信息数据平台的GC优化

对于该平台原型系统,我们使用Hotspot JVM的两个算法优化垃圾回收:

  • 新生代垃圾回收使用ParNew,老年代垃圾回收使用CMS。
  • 新生代和老年代使用G1。G1用来解决堆大小为6GB或者更大时存在的低于0.5秒稳定的、可预测停顿时间的问题。在我们用G1实验过程中,尽管调整了各种参数,但没有得到像ParNew/CMS一样的GC性能或停顿时间的可预测值。我们查询了使用G1发生内存泄漏相关的一个bug[3],但还不能确定根本原因。

使用ParNew/CMS,应用每三秒40-60ms的新生代停顿和每小时一个CMS周期。JVM选项如下:

Xml代码  收藏代码
  1. // JVM sizing options  
  2. -server -Xms40g -Xmx40g -XX:MaxDirectMemorySize=4096m -XX:PermSize=256m -XX:MaxPermSize=256m     
  3. // Young generation options  
  4. -XX:NewSize=6g -XX:MaxNewSize=6g -XX:+UseParNewGC -XX:MaxTenuringThreshold=-XX:SurvivorRatio=8 -XX:+UnlockDiagnosticVMOptions -XX:ParGCCardsPerStrideChunk=32768  
  5. // Old generation  options  
  6. -XX:+UseConcMarkSweepGC -XX:CMSParallelRemarkEnabled -XX:+ParallelRefProcEnabled -XX:+CMSClassUnloadingEnabled  -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly     
  7. // Other options  
  8. -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.cppdistribute_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修正了。

目前服务器配置如下:

Xml代码  收藏代码
  1. -server -Xms1536m -Xmx1536m -XX:MaxDirectMemorySize=128m -XX:PermSize=256m -Xss256k -XX:MaxPermSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:+UseParNewGC -XX:MaxTenuringThreshold=-XX:SurvivorRatio=8 -XX:+UnlockDiagnosticVMOptions -XX:+UseConcMarkSweepGC -XX:+ParallelRefProcEnabled -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:CMSMaxAbortablePrecleanTime=500 -XX:+UseCompressedOops -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=-XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+UseCMSInitiatingOccupancyOnly -XX:+AlwaysPreTouch -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:-OmitStackTraceInFastThrow -verbose:gc -Xloggc:/log/gc/amazon-gc.log  

 

 

 另外付几个参考配置:

http://developer.51cto.com/art/201201/312020.htm

http://blog.csdn.net/madun/article/details/7913043

 http://www.tuicool.com/articles/6Vj63qy

http://www.tuicool.com/articles/zQbmae

http://www.open-open.com/lib/view/open1399988244301.html

http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html

http://blog.sae.sina.com.cn/archives/4141

堆设置
-Xmx3550m:设置JVM最大堆内存 为3550M。 
-Xms3550m:设置JVM初始堆内存 为3550M。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。 
-Xss128k: 设置每个线程的栈 大小。JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K。应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能 生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。 
-Xmn2g:设置堆内存年轻代 大小为2G。整个堆内存大小 = 年轻代大小 + 年老代大小 + 持久代大小 。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。 
-XX:PermSize=256M:设置堆内存持久代 初始值为256M。(貌似是Eclipse等IDE的初始化参数) 
-XX:MaxNewSize=size:新生成的对象能占用内存的最大值。 
-XX:MaxPermSize=512M:设置持久代最大值为512M。 
-XX:NewRatio=4:设置堆内存年轻代(包括Eden和两个Survivor区)与堆内存年老代的比值(除去持久代) 。设置为4,则年轻代所占与年老代所占的比值为1:4。 
-XX:SurvivorRatio=4: 设置堆内存年轻代中Eden区与Survivor区大小的比值 。设置为4,则两个Survivor区(JVM堆内存年轻代中默认有2个Survivor区)与一个Eden区的比值为2:4,一个Survivor区占 整个年轻代的1/6。 
-XX:MaxTenuringThreshold=7:表示一个对象如果在救助空间(Survivor区)移动7次还没有被回收就放入年老代。 
如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,对于年老代比较多的应用,这样做可以提高效率。 
如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代即被回收的概率。 
回收器选择
JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。
默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行智能判断。
串行收集器 
-XX:+UseSerialGC:设置串行收集器 
并行收集器(吞吐量优先) 
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。 
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。 
-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。 
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间(单位毫秒),如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。 
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等。 
此参数建议使用并行收集器时,一直打开。 
并发收集器(响应时间优先) 
-XX:+UseParNewGC:设置年轻代为并发收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。 
CMS, 全称Concurrent Low Pause Collector,是jdk1.4后期版本开始引入的新gc算法,在jdk5和jdk6中得到了进一步改进,它的主要适合场景是对响应时间的重要性需求 大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用。CMS是用于对tenured generation的回收,也就是年老代的回收,目标是尽量减少应用的暂停时间,减少FullGC发生的几率,利用和应用程序线程并发的垃圾回收线程来 标记清除年老代。 
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了。所以,此时年轻代大小最好用-Xmn设置。 
-XX:CMSFullGCsBeforeCompaction=:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此参数设置运行次FullGC以后对内存空间进行压缩、整理。 
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除内存碎片。 
-XX:+CMSIncrementalMode:设置为增量收集模式。一般适用于单CPU情况。 
-XX:CMSInitiatingOccupancyFraction=70:表示年老代空间到70%时就开始执行CMS,确保年老代有足够的空间接纳来自年轻代的对象。 
注:如果使用 throughput collector 和 concurrent low pause collector 这两种垃圾收集器,需要适当的挺高内存大小,为多线程做准备。
其它
-XX:+ScavengeBeforeFullGC:新生代GC优先于Full GC执行。 
-XX:-DisableExplicitGC:禁止调用System.gc(),但JVM的gc仍然有效。 
-XX:+MaxFDLimit:最大化文件描述符的数量限制。 
-XX:+UseThreadPriorities:启用本地线程优先级API,即使 java.lang.Thread.setPriority() 生效,反之无效。 
-XX:SoftRefLRUPolicyMSPerMB=0:“软引用”的对象在最后一次被访问后能存活0毫秒(默认为1秒)。 
-XX:TargetSurvivorRatio=90:允许90%的Survivor空间被占用(默认为50%)。提高对于Survivor的使用率——超过就会尝试垃圾回收。 
辅助信息
-XX:-CITime:打印消耗在JIT编译的时间 
-XX:ErrorFile=./hs_err_pid.log:保存错误日志或者数据到指定文件中 
-XX:-ExtendedDTraceProbes:开启solaris特有的dtrace探针 
-XX:HeapDumpPath=./java_pid.hprof:指定导出堆信息时的路径或文件名 
-XX:-HeapDumpOnOutOfMemoryError:当首次遭遇内存溢出时导出此时堆中相关信息 
-XX:OnError=";":出现致命ERROR之后运行自定义命令 
-XX:OnOutOfMemoryError=";":当首次遭遇内存溢出时执行自定义命令 
-XX:-PrintClassHistogram:遇到Ctrl-Break后打印类实例的柱状信息,与jmap -histo功能相同 
-XX:-PrintConcurrentLocks:遇到Ctrl-Break后打印并发锁的相关信息,与jstack -l功能相同 
-XX:-PrintCommandLineFlags:打印在命令行中出现过的标记 
-XX:-PrintCompilation:当一个方法被编译时打印相关信息 
-XX:-PrintGC:每次GC时打印相关信息 
-XX:-PrintGC Details:每次GC时打印详细信息 
-XX:-PrintGCTimeStamps:打印每次GC的时间戳 
-XX:-TraceClassLoading:跟踪类的加载信息 
-XX:-TraceClassLoadingPreorder:跟踪被引用到的所有类的加载信息 
-XX:-TraceClassResolution:跟踪常量池 
-XX:-TraceClassUnloading:跟踪类的卸载信息 
-XX:-TraceLoaderConstraints:跟踪类加载器约束的相关信息 
JVM服务调优实战 
服务器:8 cup, 8G mem
e.g. 
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
调优方案:
-Xmx5g:设置JVM最大可用内存为5G。 
-Xms5g:设置JVM初始内存为5G。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。 
-Xmn2g:设置年轻代大小为2G。整个堆内存大小 = 年轻代大小 + 年老代大小 + 持久代大小 。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。 
-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。 
-XX:ParallelGCThreads=8:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。 
-XX:SurvivorRatio=6:设置年轻代中Eden区与Survivor区的大小比值。根据经验设置为6,则两个Survivor区与一个Eden区的比值为2:6,一个Survivor区占整个年轻代的1/8。 
-XX:MaxTenuringThreshold=30: 设置垃圾最大年龄(次数)。如果设置为0的话,则年轻代对象不经过Survivor区直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值 设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。设置为30表示 一个对象如果在Survivor空间移动30次还没有被回收就放入年老代。 
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试配置这个参数以后,参数-XX:NewRatio=4就失效了,所以,此时年轻代大小最好用-Xmn设置,因此这个参数不建议使用。 
参考资料 - JVM堆内存的分代 
虚 拟机的堆内存共划分为三个代:年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集器要收集的Java对象关系不大。所以,年轻代和年老代的划分才是对垃圾 收集影响比较大的。
年轻代 
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个 Survivor区(一般而言)。
大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当一个Survivor区满 时,此区的存活对象将被复制到另外一个Survivor区,当另一个Survivor区也满了的时候,从前一个Survivor区复制过来的并且此时还存 活的对象,将被复制“年老区(Tenured)”。
需要注意,两个Survivor区是对称的,没先后关系,所以同一个Survivor区中可能同时存在从Eden区复制过来对象,和从另一个 Survivor区复制过来的对象;而复制到年老区的只有从前一个Survivor区(相对的)过来的对象。而且,Survivor区总有一个是空的。特 殊的情况下,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
年老代 
在年轻代中经历了N(可配置)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
持久代 
用于存放静态数据,如 Java Class, Method 等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些Class,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中动态增加的类型。持久代大小通过 -XX:MaxPermSize= 进行设置。

 原哥博客http://www.yuange.tech

CMSInitiatingOccupancyFraction值与Xmn的关系公式

上面介绍了promontion faild产生的原因是EDEN空间不足的情况下将EDEN与From survivor中的存活对象存入To survivor区时,To survivor区的空间不足,再次晋升到old gen区,而old gen区内存也不够的情况下产生了promontion faild从而导致full gc.那可以推断出:eden+from survivor < old gen区剩余内存时,不会出现promontion faild的情况,即:
(Xmx-Xmn)*(1-CMSInitiatingOccupancyFraction/100)>=(Xmn-Xmn/(SurvivorRatior+2))  进而推断出:

CMSInitiatingOccupancyFraction <=((Xmx-Xmn)-(Xmn-Xmn/(SurvivorRatior+2)))/(Xmx-Xmn)*100

例如:

当xmx=128 xmn=36 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((128.0-36)-(36-36/(1+2)))/(128-36)*100 =73.913

当xmx=128 xmn=24 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((128.0-24)-(24-24/(1+2)))/(128-24)*100=84.615…

当xmx=3000 xmn=600 SurvivorRatior=1时  CMSInitiatingOccupancyFraction<=((3000.0-600)-(600-600/(1+2)))/(3000-600)*100=83.33

CMSInitiatingOccupancyFraction低于70% 需要调整xmn或SurvivorRatior值。

 

 

一:理解GC日志格式,读GC日志的方法

1:开启日志

-verbose:gc 

-XX:+PrintGCDetails 

-XX:+PrintGCDateStamps

-Xloggc:/path/gc.log

-XX:+UseGCLogFileRotation  启用GC日志文件的自动转储 (Since Java)

-XX:NumberOfGClogFiles=1  GC日志文件的循环数目 (Since Java)

-XX:GCLogFileSize=1M  控制GC日志文件的大小 (Since Java)

-XX:+PrintGC包含-verbose:gc

-XX:+PrintGCDetails //包含-XX:+PrintGC

只要设置-XX:+PrintGCDetails 就会自动带上-verbose:gc和-XX:+PrintGC

-XX:+PrintGCDateStamps/-XX:+PrintGCTimeStamps 输出gc的触发时间

 


 2:新生代(Young GC)gc日志分析

Java代码  收藏代码
  1. 2014-02-28T11:59:00.638+0800: 766.537:[GC2014-02-28T11:59:00.638+0800: 766.537:  
  2. [ParNew: 1770882K->212916K(1835008K), 0.0834220 secs]  
  3. 5240418K->3814487K(24903680K), 0.0837310 secs] [Times: user=1.12 sys=0.02, real=0.08 secs]  

  2014-02-28T11:59:00 ...(时间戳):[GC(Young GC)(时间戳):[ParNew(使用ParNew作为年轻代的垃圾回收期):

1770882K(年轻代垃圾回收前的大小)->212916K(年轻代垃圾回收以后的大小)(1835008K)(年轻代的capacity), 0.0834220 secs(回收时间)]
5240418K(整个heap垃圾回收前的大小)->3814487K(整个heap垃圾回收后的大小)(24903680K)(heap的capacity), 0.0837310secs(回收时间)]
[Times: user=1.12(Young GC用户耗时) sys=0.02(Young GC系统耗时), real=0.08 secs(Young GC实际耗时)]

   其中 Young GC回收了1770882-212916=1557966K内存
Heap通过这次回收总共减少了 5240418-3814487=1425931 K的内存。1557966-1425931=132035K说明这次Young GC有约128M的内存被移动到了Old Gen,
 提示:进代量(Young->Old)需要重点观察,预防promotion failed.

 

 3:老年代(CMS old gc)分析

Java代码  收藏代码
  1. 2014-02-28T23:58:42.314+0800: 25789.661: [GC [1 CMS-initial-mark: 17303356K(23068672K)] 18642315K(24903680K), 1.0400410 secs] [Times: user=1.04 sys=0.00, real=1.04 secs]  
  2. 2014-02-28T23:58:43.354+0800: 25790.701: [CMS-concurrent-mark-start]  
  3. 2014-02-28T23:58:43.717+0800: 25791.064: [CMS-concurrent-mark: 0.315/0.363 secs] [Times: user=1.64 sys=0.02, real=0.37 secs]  
  4. 2014-02-28T23:58:43.717+0800: 25791.064: [CMS-concurrent-preclean-start]  
  5. 2014-02-28T23:58:43.907+0800: 25791.254: [CMS-concurrent-preclean: 0.181/0.190 secs] [Times: user=0.20 sys=0.01, real=0.19 secs]  
  6. 2014-02-28T23:58:43.907+0800: 25791.254: [CMS-concurrent-abortable-preclean-start]  
  7.  CMS: abort preclean due to time 2014-02-28T23:58:49.082+0800: 25796.429: [CMS-concurrent-abortable-preclean: 5.165/5.174 secs] [Times: user=5.40 sys=0.04, real=5.17 secs]  
  8. 2014-02-28T23:58:49.083+0800: 25796.430: [GC[YG occupancy: 1365142 K (1835008 K)]2014-02-28T23:58:49.083+0800: 25796.430: [Rescan (parallel) , 0.9690640 secs]2014-02-28T23:58:50.052+0800: 25797.399: [weak refs processing, 0.0006190 secs]2014-02-28T23:58:50.053+0800: 25797.400: [scrub string table, 0.0006290 secs] [1 CMS-remark: 17355150K(23068672K)] 18720292K(24903680K), 0.9706650 secs] [Times: user=16.49 sys=0.06, real=0.97 secs]  
  9. 2014-02-28T23:58:50.054+0800: 25797.401: [CMS-concurrent-sweep-start]  
  10. 2014-02-28T23:58:51.940+0800: 25799.287: [CMS-concurrent-sweep: 1.875/1.887 secs] [Times: user=2.03 sys=0.03, real=1.89 secs]  
  11. 2014-02-28T23:58:51.941+0800: 25799.288: [CMS-concurrent-reset-start]  
  12. 2014-02-28T23:58:52.067+0800: 25799.414: [CMS-concurrent-reset: 0.127/0.127 secs] [Times: user=0.13 sys=0.00, real=0.13 secs]  
  13. 2014-03-01T00:00:36.293+0800: 25903.640: [GC2014-03-01T00:00:36.293+0800: 25903.640: [ParNew: 1805234K->226801K(1835008K), 0.1020510 secs] 10902912K->9434796K(24903680K), 0.1023150 secs] [Times: user=1.35 sys=0.02, real=0.10 secs]  
  14. 2014-03-01T00:07:13.559+0800: 26300.906: [GC2014-03-01T00:07:13.559+0800: 26300.906: [ParNew: 1799665K->248991K(1835008K), 0.0876870 secs] 14086673K->12612462K(24903680K), 0.0879620 secs] [Times: user=1.24 sys=0.01, real=0.09 secs]  

 CMS的gc日志分为一下几个步骤,重点关注initial-mark和remark这两个阶段,因为这两个阶段会stop进程。

初始标记(init mark):收集根引用,这是一个stop-the-world阶段。

并发标记(concurrent mark):这个阶段可以和用户应用并发进行。遍历老年代的对象图,标记出活着的对象。

并发预清理(concurrent preclean):这同样是一个并发的阶段。主要的用途也是用来标记,用来标记那些在前面标记之后,发生变化的引用。主要是为了缩短remark阶段的stop-the-world的时间。

重新标记(remark):这是一个stop-the-world的操作。暂停各个应用,统计那些在发生变化的标记。

并发清理(concurrent sweep):并发扫描整个老年代,回收一些在对象图中不可达对象所占用的空间。

并发重置(concurrent reset):重置某些数据结果,以备下一个回收周期

提示:红色为全部暂停阶段重点关注.

 

Java代码  收藏代码
  1. [GC [1 CMS-initial-mark: 17303356K(23068672K)] 18642315K(24903680K), 1.0400410 secs] [Times: user=1.04 sys=0.00, real=1.04 secs]  

 其中数字依表示标记前后old区的所有对象占内存大小和old的capacity,整个JavaHeap(不包括perm)所有对象占内存总的大小和JavaHeap的capacity。

 

Java代码  收藏代码
  1. [GC[YG occupancy: 1365142 K (1835008 K)]2014-02-28T23:58:49.083+0800: 25796.430:   
  2. [Rescan (parallel) , 0.9690640 secs]2014-02-28T23:58:50.052+0800: 25797.399:   
  3. [weak refs processing, 0.0006190 secs]2014-02-28T23:58:50.053+0800: 25797.400: [scrub string table, 0.0006290 secs]   
  4. [1 CMS-remark: 17355150K(23068672K)] 18720292K(24903680K), 0.9706650 secs] [Times: user=16.49 sys=0.06, real=0.97 secs]  

 Rescan (parallel)表示的是多线程处理young区和多线程扫描old+perm的总时间, parallel 表示多GC线程并行。

weak refs processing 处理old区的弱引用的总时间,用于回收native memory.等等


参考资料:

 https://blogs.oracle.com/jonthecollector/entry/the_unspoken_cms_and_printgcdetails

 https://blogs.oracle.com/poonam/entry/understanding_cms_gc_logs

 原哥博客http://www.yuange.tech

4:老年代(CMS old GC ) concurrent mode failure日志

Java代码  收藏代码
  1. 2014-03-03T09:38:26.457+0800: 233373.804: [GC [1 CMS-initial-mark: 17319615K(23068672K)] 17351070K(24903680K), 0.0419440 secs]   
  2. [Times: user=0.04 sys=0.00, real=0.04 secs]  
  3. 2014-03-03T09:38:26.499+0800: 233373.846: [CMS-concurrent-mark-start]  
  4. 2014-03-03T09:38:28.175+0800: 233375.522: [GC2014-03-03T09:38:28.175+0800: 233375.522: [CMS2014-03-03T09:38:28.887+0800: 233376.234:   
  5. [CMS-concurrent-mark: 1.989/2.388 secs] [Times: user=14.37 sys=0.24, real=2.39 secs]  
  6.  (concurrent mode failure): 17473174K->8394653K(23068672K), 19.3309170 secs] 18319691K->8394653K(24903680K),   
  7.  [CMS Perm : 23157K->23154K(98304K)], 19.3311700 secs] [Times: user=22.18 sys=0.00, real=19.33 secs]  

 concurrent mode failure一般发生在CMS GC 运行过程中,老年代空间不足,引发MSC(Full GC)

上面的这条发日志说明CMS运行到CMS-concurrent-mark过程中就出现空间不足,产生并发失败(17319615K(23068672K)占77%),


解决思路:降低YGC频率,降低CMS GC触发时机,适当降低CMSInitiatingOccupancyFraction.

 

5:新生代(ParNew YGC)promotion failed日志 

Java代码  收藏代码
  1. 2014-02-27T21:19:42.460+0800: 210095.040: [GC 210095.040: [ParNew (promotion failed): 1887487K->1887488K(1887488K), 0.4818790 secs]210095.522: [CMS: 13706434K->7942818K(23068672K), 9.7152990 secs] 15358303K->7942818K(24956160K), [CMS Perm : 27424K->27373K(98304K)], 10.1974110 secs] [Times: user=12.06 sys=0.01, real=10.20 secs]  

 promotion failed一般发生在新生代晋升老年代时,老年代空间不够或连续空间不够却还没达到old区的触发值,引发Full Gc.

 

解决思路:由于heap碎片,YGC晋升对象过大,过长.(mid/long Time Object),调整-XX:PretenureSizeThreshold=65535,-XX:MaxTenuringThreshold=6

 

6:system.gc()产生的Full GC日志

Java代码  收藏代码
  1. <strong>2014-01-21T17:44:01.554+0800: 50212.568: [Full GC (System) 50212.568:   
  2. [CMS: 943772K220K(2596864K), 2.3424070 secs] 1477000K->220K(4061184K), [CMS Perm : 3361K->3361K(98304K)], 2.3425410 secs] [Times: user=2.33 sys=0.01, real=2.34 secs]</strong>  

 

Full GC (System)意味着这是个system.gc调用产生的MSC。
“943772K->220K(2596864K), 2.3424070 secs”表示:这次MSC前后old区内总对象大小,old的capacity及这次MSC耗时。
“1477000K->220K(4061184K)”表示:这次MSC前后JavaHeap内总对象大小,JavaHeap的capacity。
“3361K->3361K(98304K)], 2.3425410 secs”表示:这次MSC前后Perm区内总对象大小,Perm区的capacity。


解决:
使用-XX:+DisableExplicitGC参数,System.gc()会变成空调用.
如果应用有地方大量使用direct memory 或 rmi,那么使用-XX:+DisableExplicitGC要小心。
可以使用-XX:+ExplicitGCInvokesConcurrent替换把 System.gc()从Full GC换成CMS GC.

 

原因:
DirectByteBuffer没有finalizer,native memory的清理工作是通过sun.misc.Cleaner自动完成
sun.misc.Cleaner是基于PhantomReference的清理工具,Full GC/Old GC会对old gen做reference processing,同时触发Cleaner对已死的DirectByteBuffer对象做清理。
如果长时间没有GC或者只做了young GC的话,不会触发old区Cleaner的工作,容易产生DirectMemory OOM.
参考:https://gist.github.com/rednaxelafx/1614952

原哥博客http://www.yuange.tech

RMI会做的是分布式GC。Sun JDK的分布式GC是用纯Java实现的,为RMI服务。 
参考:http://docs.oracle.com/javase/6/docs/technotes/guides/rmi/sunrmiproperties.html

 

7:特殊的Full GC日志,根据动态计算直接进行MSC

Java代码  收藏代码
  1. 2014-02-13T13:48:06.349+0800: 7.092: [GC 7.092: [ParNew: 471872K->471872K(471872K), 0.0000420 secs]7.092: [CMS: 366666K->524287K(524288K), 27.0023450 secs] 838538K->829914K(996160K), [CMS Perm : 3196K->3195K(131072K)], 27.0025170 secs]  

 ParNew的时间特别短,jvm在minor gc前会首先确认old是不是足够大,如果不够大,这次young gc直接返回,进行MSC(Full GC)。


二:参数配置和理解

1:参数分类和说明

jvm参数分固定参数和非固定参数

1):固定参数

如:-Xmx,-Xms,-Xmn,-Xss.

2):非固定参数

如:

-XX:+<option> 启用选项

-XX:-<option> 不启用选项

-XX:<option>=<number> 给选项设置一个数字类型值,可跟单位,例如 128k, 2g

-XX:<option>=<string> 给选项设置一个字符串值,例如-XX:HeapDumpPath=./dump.log

  原哥博客http://www.yuange.tech

2:JVM可设置参数和默认值

1):-XX:+PrintCommandLineFlags

打印出JVM初始化完毕后所有跟最初的默认值不同的参数及它们的值,jdk1.5后支持.

线上建议打开,可以看到自己改了哪些值.

2):-XX:+PrintFlagsFinal

显示所有可设置的参数及"参数处理"后的默认值。参数本身只从JDK6 U21后支持

可是查看不同版本默认值,以及是否设置成功.输出的信息中"="表示使用的是初始默认值,而":="表示使用的不是初始默认值

如:jdk6/7 -XX:+MaxTenuringThreshold 默认值都是15,但是在使用CMS收集器后,jdk6默认4 , jdk7默认6.

 

Java代码  收藏代码
  1. [hbase96 logs]# java -version  
  2. java version "1.6.0_27-ea"  
  3. [hbase96 logs]# java -XX:+PrintFlagsInitial | grep MaxTenuringThreshold  
  4. intx MaxTenuringThreshold = 15 {product}  
  5. [hbase96 logs]# java -XX:+PrintFlagsFinal -XX:+UseConcMarkSweepGC | grep MaxTenuringThreshold   
  6. intx MaxTenuringThreshold := 4 {product}   
  7.   
  8. [zw-34-71 logs]# java -version  
  9. java version "1.7.0_45"  
  10. [zw-34-71 logs]# java -XX:+PrintFlagsInitial | grep MaxTenuringThreshold  
  11. intx MaxTenuringThreshold = 15 {product}  
  12. [zw-34-71 logs]# java -XX:+PrintFlagsFinal -XX:+UseConcMarkSweepGC | grep MaxTenuringThreshold  
  13. intx MaxTenuringThreshold := 6 {product}  

 

3):-XX:+PrintFlagsInitial

 在"参数处理"之前所有可设置的参数及它们的值,然后直接退出程序.

这里的"参数处理"指: 检查参数之间是否有冲突,通过ergonomics调整某些参数的值等.

 

Java代码  收藏代码
  1. [hbase96 logs]# java -version  
  2. java version "1.6.0_27-ea"  
  3. [hbase96 logs]# java -XX:+PrintFlagsInitial | grep UseCompressedOops  
  4. bool UseCompressedOops = false {lp64_product}   
  5. [hbase96 logs]# java -XX:+PrintFlagsFinal | grep UseCompressedOops  
  6. bool UseCompressedOops := true {lp64_product}  

 

4)CMSInitiatingOccupancyFraction 默认值是多少 

jdk6/7:

Java代码  收藏代码
  1. #java -server -XX:+UseConcMarkSweepGC -XX:+PrintFlagsFinal | grep -P "CMSInitiatingOccupancyFraction|CMSTriggerRatio|MinHeapFreeRatio"  
  2. intx CMSInitiatingOccupancyFraction            = -1              {product}             
  3. intx CMSTriggerRatio                           = 80              {product}             
  4. uintx MinHeapFreeRatio                          = 40              {product}  

 

计算公式:

CMSInitiatingOccupancyFraction = (100 - MinHeapFreeRatio) + (CMSTriggerRatio * MinHeapFreeRatio / 100)

最终结果: 在jdk6/7中 CMSInitiatingOccupancyFraction默认值是92% .不是网上传的68%;  都这么传,是因为 "深入理解Java虚拟机"一书中是68%,但它用的是jdk5 , jdk5的CMSTriggerRatio默认值是20,坑爹...

 

三:JVM内存区域理解和相关参数

一图胜千言,直接上图

1):物理分代图.

 

物理分代是除G1之外的JVM 内存分配方式,jvm 通过-Xmx,-Xmn/newRatio等参数将jvm heap划分成物理固定大小,对于不同场景比例应该设置成多少很考验经验.

一篇JVM CMS优化讲解的非常好的文章: how-tame-java-gc-pauses

 

2) 逻辑分代图(G1)

逻辑分代是以后的趋势(PS:jkd8连perm都不区分了。), 不需要使用者在纠结Xms/Xmn,SurvivorRatio等比例问题,采用动态算法调整分代大小。

posted on 2018-12-28 20:47  专注移动开发  阅读(548)  评论(0编辑  收藏  举报