HotSpot的Young区诊断和优化 jvmjavagchotspot
在双11之前,做了一些UMP GC优化的事情,和大家分享下问题查找和优化的思路。
一. 一些GC基础知识
1. 大部分jvm都有分代的概念,堆被分成2个部分,一个Young区,一个old区
2. -Xmx设置堆的最大值,-Xmn设置young区的大小,减一下就是old区的大小
3. Young区又分为Eden,survivor(s0,s1,大小通过SuvivorRatio指定)区,新生对象一般都在eden区分配,Hotspot为了优化对象创建的效率,给每个线程默认分配了部分内存TLAB,线程先在自己的TLAB空间创建对象,如果不够则去eden区分配。Survivor区是为了让生命周期短的对象尽量在young区就被回收掉,也就是每次young区执行gc之后,eden区和s0/s1中的一个都会被清空,只剩下s0/s1的一个存放对象
4. Hotspot的Young区gc有3种算法,Serial,ParNew,PS
5. 一般Young区触发gc的条件是eden区满
6. ParNew算法是stop-the-world的,也是说执行的时候所有用户线程会暂停
7. 更多gc知识可以毕玄大师的分享,http://www.docin.com/p-417999249.html
二. ParNewGC运行时间分析
1. Gc root
Young gc运行时,gc root一般是从线程的栈帧出发查找活跃对象,还有一个不可忽视的root是old区的对象,比如在运行期某个线程修改了某个old区对象的某个引用,则这个对象也需要作为gc root进行扫描以决定哪些young区的对象还活着,为了避免young gc时扫描整个old区,hotspot提供了dirty card机制来优化old区的扫描。
2. Dirty card
Hotspot将old区按512字节的page进行划分,存放到内部的dirty card列表中,当线程修改某一个old区对象的某个引用时,就会更新这个对象对应的dirtycard为’脏数据’。这样young gc的时候就需要扫描这些dirty page中的对象来决定young区哪些对象还活着。
3. 所以Young gc的时间,就可以这样表述: Tyoung =Tstack_scan + Tcard_scan +Told_scan+ Tcopy 。 Tstack_scan 是扫描线程栈找出活跃对象的时间,一般比较快。Tcard_scan 扫描dirtycard表的时间,取决于old区的大小。Told_scan 如果某一页page有脏数据,则把它作为gc root扫描,取决于old区的大小和具体业务是否有产生’脏数据’。Tcopy 是复制存活对象到s0/s1,更新引用地址的时间,取决于存活对象数量,大小。其中主要影响的时间是Told_scan+ Tcopy 。
三. UMP的jvm参数和gc方式
为了了解UMP应用的gc情况,我们先看下线上ump应用的启动参数: ps –ef|grep java
1. ump的堆设为4G,young区为1600m,suvivorRatio为10,则eden区大小为1600* (1-2/12)=1333.33M,s0=s1=133.33M
2. –XX:+UseConcMarkSweepGC,ump的old回收使用了cms算法,这样默认的young区的gc算法就是ParNew
3. –verbose:gc打印gc信息到gc.log。观察gc.log:
a. Young使用了ParNew算法,从图中可知,一次young gc之后,当时young区的gc从1400541K减少到了40011K(只在一个survivor区),ParNew耗时0.0267970s,可见每次young gc效率是很高的。
b. 1501888K是eden+s0或s1的大小
c. 当时jvm堆总大小从2254921K降到了895376k,总耗时0.0271140s
d. Gc时间在用户代码执行是0.11s,系统调用时间是0.00,真实时间是0.03s,可以看出这里ParNew是起了多线程来执行的
e. 可以通过jstack的线程信息看出ParNew的gc线程数量:
可以看出默认情况下jvm是起了和cpu数量一样的ParNew线程,ump是5个。
四. Young区诊断
一般情况下,young gc的频率是比较高的,每次new都会在eden区创建新的对象。当然young gc的频率太高,会对应用有影响,毕竟是stop-the-world,应用会停顿。当我们发现young gc频率太高了,我们就会想知道到底是哪些对象导致,会不会有大对象?会不会有多余的对象?我们需要找出这些对象,可以借助于一些工具。
1. Mat - http://www.eclipse.org/mat/
Eclipse的mat插件是分析java堆的神器。我们使用jmap dump内存,注意jmap会stop-the-world的:
sudo -u admin /opt/taobao/java/bin/jmap -histo 22314>>jmap
将这个dump文件拉下来,启动mat分析。但是不幸的是,默认的mat只会分析存活的对象,但是young区中其实有很多对象是dead的。这个时候我们就需要mat的一个扩展选项:
保留unreachable的对象,这样我们就可以看到有哪些对象是new了之后立马就变成unreachable了,这些对象是优化的重点。这个需要mat比较新的版本才有~~
a. 打开dominator_tree栏,可以看到当前堆里对象的分布,’unreachable’的就是那些临时对象,在mat里可以看到这些对象的属性和引用它的对象,比较容易发现问题J
b. Shallow heap指对象本生的大小,retained heap指该对象被回收之后将一块回收的那些对象的总大小(比如内部属性)
2. TBJmap -https://github.com/jlusdy/TBJMap
是叔同大师写的一个jmap扩展,基于Serviceability Agent实现,会阻塞应用,建议使用前先把应用offline。使用比较简单:
java -classpath/home/admin/tbjmap.jar:/java/lib/sa-jdi.jar sun.jvm.hotspot.tools.TBJMap -histoPID > jmap_output.log
可以看到哪些大对象占据了eden区
五. Young区优化
优化的主要目标是减少应用因为younggc带来的停顿时间,可以取一段时间的总停顿时间做为benchmark。
1. jvm系统层面
a. Eden调大,比如调大Xmn,或者调大SuvivorRatio以增大eden区容量,这样就可以存放更多的对象,减少young gc频率。但是代价是单次young gc的时间要变长,所以总停顿时间未必就会降低,需要反复实践。
b. 增加young gc线程,通过XX:ParallelGCThreads设置,默认是逻辑cpu个数,应该说这个值已经是合理的了,因为gc都是cpu-bound的任务,适当增加线程数未必会带来好的效果,不过可以增加cpu以提高并发度。
c. 调小TenuringThreshold,减少对象在young区的停留时间,将压力转移给old区J
2. 应用层面
a. 减少每次请求创建的对象数量,这里有很多代码技巧,最常见的:
1. 使用object cache
2. 熟悉各种Collection的原理和使用场景,如果可以预见容量,则设置初始容量,比如用hashmap,默认的数组大小是16,如果只有2个元素,则可以new HashMap(2)这样用。
3. 减少复杂对象包装,在jvm中一个对象的大小可以简单表述: header8+klassoop4(开启压缩指针,不开的话是8)+ sizeof(属性)+pading(可选),也就是说每个对象都有12个字节固定消耗,如果对象嵌套复杂的话,带来的消耗也就多了。
b. 降低tps,比如增加机器,流控等
六. 小结
本文简单介绍了,hotspotjvm下的young区性能诊断和优化。Young区的优化比较难做,更多的需要从业务上进行优化,我们平时在写业务代码的时候也要注意下对gc的影响~~
七. 资料
毕玄大神的gc分享,必看 http://www.docin.com/p-417999249.html
计算一个对象的大小 http://sizeof.sourceforge.net/
Shallow和retained大小解释 http://kenwublog.com/understand-shallow-and-retained-size-in-hprofling
Cms gc日志解读 https://blogs.oracle.com/poonam/entry/understanding_cms_gc_logs
Mat手册 http://wiki.eclipse.org/index.php/MemoryAnalyzer