JVM-young gc调优学习参考记录

背景

先说一下基本情况,本次是对线上商品服务的JVM优化。商品服务的访问量非常高,单机QPS在3000左右,线上总共部署了15个商品服务节点。JVM堆内存大小是8G,其中给新生代分配了2G,老年代垃圾回收器采用CMS,新生代垃圾回收器是ParNew。

优化前的情况

首先我们使用 jstat 查看了 GC 的情况。又通过查看GC log,分析了GC 的详细状况。
使用 jstat -gcutil ${pid} 1000 每隔一秒打印一次 GC 统计信息。


image.png

可以看到,单次 Young GC 平均耗时是 60ms 左右,还是不错的,但是Young GC(YGC )非常频繁,基本上每秒一次,有时还会一秒两次,在一秒两次的时候,Young GC对系统响应的压力就会比较明显。
jstat相关指标说明:

YGCT:Young GC 总时间,单位为秒
YGC:Young GC 次数
FGCT:Full GC 总时间,单位为秒
FGC:Full GC 次数
GCT:GC 总时间,是 YGCT 和 FGCT 之和

接着查看 GC log,打印 GC log 需要在 JVM 启动参数里添加如下参数:

-XX:+PrintGCDateStamps:打印 GC 发生的时间戳。
-XX:+PrintTenuringDistribution:打印 GC 发生时的代龄信息。
-XX:+PrintGCApplicationStoppedTime:打印 GC 停顿时长
-XX:+PrintGCApplicationConcurrentTime:打印 GC 间隔的服务运行时长
-XX:+PrintGCDetails:打印 GC 详情,包括 GC 前/内存等。
-Xloggc:…/gclogs/gc.log.date:指定 GC log 的路径

GC log如下:


image.png

从Log中,我们可以看到 gc 前有很多次 18ms 左右的停顿。

进一步分析和优化

直接查看 GC log 不太直观,可以借助一些可视化JVM分析工具来帮助我们分析,推荐一款不错的在线分析工具GCeasy,我们把 GC log 上传到https://gceasy.io 后, GCeasy 会根据GC log生成各个维度的图表,让我们更直观的分析JVM问题。

通过查看 GCeasy 生成的图表,我们可以发现JVM的吞吐量是 93%,即 JVM 运行业务代码的时长占 JVM 总运行时长的93%,这个吞吐量确实比较低,运行 100 分钟就有 7 分钟在执行 GC 操作。幸好这些 GC 中绝大多数都是 Young GC,单次GC时长较短时间可控并且频率均匀,所以商品服务还能正常运行。

解决这个问题,可以从三方面入手:减少对象的创建,增大新生代以及调整幸存区。

减少对象创建,本质上不算是JVM调优,而是代码优化,而且需要花大量的时间去撸代码,再逐步优化代码,周期会相当长。所以就暂时作罢了!

调整新生代比例

增大新生代比例。只需要修改JVM参数即可,说起来简单,但需要多次调整并压测,最终找到一个平衡点,在保证FullGC的频次和耗时都在合理的范围之内,把Young GC的频次降到最低。
有人可能会问:增大新生代比例,会不会导致Young GC的耗时明显增大?虽然降低了GC频次,但是单次GC的耗时却明显增加了,岂不是得不偿失?
首先,我们需要先明确,目前主流的新生代收集器大多采用标记-复制算法,ParNew也一样。研究表明,绝大多数应用场景,新生代中98%的对象生命周期很短,在毫秒级别,基本上被使用一次后就会变成垃圾对象,会在下一次GC时被清理掉。在很多JVM中将堆内存分为一块较大的Eden空间和两块较小的Survivor空间(下图的S0和S1),新生对象存放在Eden区。当发生Young GC时,将Eden和当前Survivor中存活的对象一次性复制到另外一块Survivor中,最后整体清理Eden和当前的Survivor空间。每次Young GC时两块Survivor区互相更换。HotSpot虚拟机默认Eden和两块Survivor的大小比例是8:1:1,也就是说每次新生代中可用内存为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。


image.png

现在我们清楚了ParNew回收器采用了标记-复制算法。现在来分析一下ParNew回收器GC耗时和新生代大小的关系。我们知道标记-复制算法分为两个阶段,标记阶段和复制阶段。为了简化问题我们暂且认为标记阶段只扫描新生代的存活对象,其实该阶段还需要扫描部分老年代对象。假设我们要把新生代扩容1.5倍。

  • 扩容前:新生代容量为2G,假设某对象A的存活时间为600ms,Young GC间隔500ms,那么本次GC时间 = 扫描新生代时间 + 复制对象时间(Eden和当前Survivor复制到另一个Survivor)。

  • 扩容后:新生代容量为3G ,对象A的生命周期为600ms,但是由于新生代扩容了1.5倍,所以Young GC间隔理论上增加到了750ms。此时发生Young GC,对象A已经用完了生命周期,成为了垃圾对象,就不需要把对象A复制到另一个Survivor区了。那么本次GC时间 = 1.5 × 扫描新生代时间,没有增加复制时间。

所以,当扩大新生代容量时,实际上每次GC需要复制的存活对象并不会按照扩容比例递增。容量扩大到1.5倍,增加的存活对象会远小于1.5倍。虽然标记阶段消耗的时间提高到了1.5倍,但是复制阶段耗时并没有明显提高。更重要的是,对于虚拟机来说,复制对象的成本要远高于扫描标记的成本,所以,单次Young GC时间更多取决于存活对象的数量,而非Eden区的大小。如果堆内存中存在大量短生命周期的对象(大部分场景是这样的),那么扩容新生代后,Young GC时间不会显著增加。

分代调整

此外,观察了各代龄的对象数量情况后,对代龄设置也做了调整。

前文提到,当发生Young GC时,会将Eden和当前的Survivor中存活的对象一次性复制到另外一块Survivor中,最后整体清理Eden和当前的Survivor空间。每次Young GC时两块Survivor区会互相更换。存活对象在两块Survivor区之间每交换一次,对象年龄就会增长一岁。直到达到MaxTenuringThreshold设置的年龄(默认是15岁)时,相应的对象就会被转移到老年代。所以为了减少复制成本,MaxTenuringThreshold要尽量合理,不能设置太大,否则有些长寿对象在每次GC时都会在两个Survivor区之间来回复制,无疑是增加了复制阶段的耗时。

image.png

看上图,在15个分代中,7岁以上的对象80%都会被转移到老年代(769.02除以980.48 ≈ 80% )。于是我们把 MaxTenuringThreshold 的值调整为 7,将年龄超过7岁的对象直接转移到老年代。这样就减少了长寿对象在两个 survivor 区之间来回复制带来的性能开销。

偏向锁停顿

我们看到GC log里有很多18ms左右的停顿,虽然每次停顿时间不算长,但频繁的停顿对性能消耗还是比较明显。
这个问题曾经遇到过几次,基本都是偏向锁导致。JDK1.8 之后 JVM 对锁进行了优化,增加了偏向锁。所谓的偏向,就是偏心,偏向锁会偏向于当前已经占有锁的线程 。适合锁竞争不激烈的场景(某个同步块并发不高,很少会出现多线程同时竞争锁的场景)。大概过程如下,获得锁的线程再次获得锁时,会判断偏向锁是否指向自己,如果指向自己,该线程将不用再次获得锁,就可以直接进入同步块,以此来优化性能。当其他线程请求相同的锁时,偏向模式结束。偏向锁的实现就是将对象头的标记设置为偏向,并将线程ID写入对象头。

在竞争激烈的场景,偏向锁会增加系统负担, 因为每次都要加一次是否偏向的判断。关键是遇到锁竞争时,取消锁的过程需要等待全局安全点(safe point),会导致所有线程暂停,即会发生Stop-The-World。所以在锁竞争激烈的场景下,最好提前关闭掉偏向锁。
在JVM中默认会开启偏向锁,所以我们只需要关闭偏向锁即可:

-XX:-UseBiasedLocking

最后

经过一轮调整和压测,最终新生代调整到了2.9G,整个堆内存保持8G不变,MaxTenuringThreshold调成了7。新生代增大了将近1.5倍,Young GC 的频率减少了大概1/3。GC 的吞量提高了3.8%,达到了96.8%,。Young GC 平均耗时稍有上升,从60ms上升到了71ms,基本符合预期。另外Full GC 的频率和耗时也在可接受的范围。

调优是个复杂、细致的活儿,要因地制宜。不同的机器、不同的应用、不同的业务场景和不同的访问量级,调优的方式都不同,没有一个固定的模式。做JVM调优之前,建议先了解JVM运行原理,内存模型,GC过程,相关GC回收器回收机制,回收算法。先把基础知识打扎实,再加上耐心和决心才能够真正做好JVM优化,成为JVM高手。

参考

https://club.perfma.com/article/1846118

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354