在 上一篇 中,我们了解到参数调优,就是在成本、吞吐量、延迟之间找一个平衡点。那具体如何进行估算与调优呢?
估算
拿一个简单的 Feed 业务来说。查询用户在社交网站上发送的帖子,还需要查询第一页的留言(大概是 15 条),它们共同组成了每次查询后的实体。
class Feed{private User user;private List<Comment> commentList;private String content;}
这种类型的数据结构,一般返回体都比较大,大概会有几 KB 到几十 KB 不等。我们就可以对这些数据进行以大体估算。具体的数据来源可以看日志,也可以分析线上的请求。

这个接口每天有 10 亿次请求,假如每次请求的大小有 20KB(很容易达到),那么一天的流量就有 18TB 之巨!
假如高峰请求 6w/s,我们部署了 10 台机器,那么每个 JVM 的流量就可以达到 120MB/s,这个速度算是比较快的了。
如果你实在不知道怎么去算这个数字,那就按照峰值的 2 倍进行准备,一般都是 OK 的。
调优
我们的机器是 4C8GB 的,分配给了 JVM 1024*8GB/3*2= 5460MB 的空间。那么年轻代大小就有 5460MB/3=1820MB。
进而可以推断出,Eden 区的大小约 1456MB,那么大约只需要 12 秒,就会发生一次 Minor GC。不仅如此,每隔半个小时,会发生一次 Major GC。不管是年轻代还是老年代,这个 GC 频率都有点频繁了。
Survivor 区大小大约是 182MB 左右,如果稍微有点流量偏移,或者流量突增,再或者和其他接口共用了 JVM,那么这个 Survivor 区已经装不下 Minor GC 后的内容了。总有一部分超出的容量,需要老年代来补齐。这些垃圾信息就要保存更长时间,直到老年代空间不足。

用户请求完这些信息之后,很快它们就会变成垃圾。所以每次 MinorGC 之后,剩下的对象都很少。也就是说,我们的流量虽然很多,但大多数都在年轻代就销毁了。
如果我们加大年轻代的大小,由于 GC 的时间受到活跃对象数的影响,回收时间并不会增加太多。
如果我们把一半空间给年轻代。也就是下面的配置:
-XX:+UseConcMarkSweepGC -Xmx5460M -Xms5460M -Xmn2730M
重新估算一下,发现 Minor GC 的间隔,由 12 秒提高到了 18 秒。
线上观察:
[ParNew: 2292326K‐>243160K(2795520K), 0.1021743 secs]3264966K‐>10880154K(1215800K), 0.1021417 secs][Times: user=0.52 sys=0.02, real=0.2 secs]
Minor GC 有所改善,但是并没有显著的提升。相比较而言,Major GC 的间隔却增加到了 3 小时,是一个非常大的性能优化。这就是在容量限制下的初步调优方案。
此种场景,我们可以更加激进一些,调大年轻代(顺便调大了幸存区),让对象在年轻代停留的时间更长一些,有更多的 buffer 空间。这样 Minor GC 间隔又可以提高到 23 秒。参数配置:
-XX:+UseConcMarkSweepGC -Xmx5460M -Xms5460M -Xmn3460M
一切看起来很美好,但还是有一个瑕疵。
由于每秒的请求都非常大,如果应用重启或者更新,流量瞬间打过来,JVM 还没预热完毕,这时候就会有大量的用户请求超时、失败。
为了解决这种问题,通常会逐步把新发布的机器进行放量预热。比如第一秒 100 请求,第二秒 200 请求,第三秒 5000 请求。大型的应用都会有这个预热过程。

如图所示,负载均衡器负责服务的放量,server4 将在 6 秒之后流量正常流通。但是奇怪的是,每次重启大约 20 多秒以后,就会发生一次诡异的 Full GC。
注意是 Full GC,而不是老年代的 Major GC,也不是年轻代的 Minor GC。此时年轻代和老年代的空间还有很大一部分,那 Full GC 是怎么产生的呢?
不要忘了,我们还有一个区域叫作 Metaspace,它的容量是没有上限的,但是每当它扩容时,就会发生 Full GC。
使用下面的命令可以看到它的默认值:
java -XX:+PrintFlagsFinal 2>&1 | grep Meta
默认值如下:
size_t MetaspaceSize = 21807104 {pd product} {default}size_t MaxMetaspaceSize = 18446744073709547520 {product} {default}
可以看到 MetaspaceSize 的大小大约是 20MB,这个初始值太小了,可以试着调大这个数值。
按照经验,一般调整成 256MB 就足够了。同时,为了避免无限制使用造成操作系统内存溢出,我们同时设置它的上限。配置参数如下:
-XX:+UseConcMarkSweepGC -Xmx5460M -Xms5460M -Xmn3460M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
经观察,启动后停顿消失。
这种方式通常是行之有效的,也可以通过扩容机器内存或者扩容机器数量的办法,显著地降低 GC 频率。这些都是在估算容量后的优化手段。
我们把部分机器升级到 8C16GB 的机器,使用如下的参数:
-XX:+UseConcMarkSweepGC -Xmx10920M -Xms10920M -Xmn5460M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
相比较其他实例,系统运行的特别棒。系统平均 1 分钟左右发生一次 MinorGC,老年代观察了一天才发生 GC,响应水平明显提高。
这是一种非常简单粗暴的手段,但是有效。我们看到,对 JVM 的优化,不仅仅是优化参数本身。我们的目的是解决问题,寻求多种有用手段。
推荐阅读:
!






