只是不愿随波逐流 ...|

lidongdongdong~

园龄:2年7个月粉丝:14关注:8

63、JVM 性能优化

内容来自王争 Java 编程之美

大部分工程师对 JVM 性能优化都不熟悉,因为大部分系统的性能压力都很小
内存充足,GC 频率很低,默认的 JVM 设置就已经够用,也不会出现 OOM、频繁 GC、GC 时间过长等问题,因此也就没有 JVM 性能优化的必要
不过对于一个性能压力比较大的项目,JVM 性能必然会是我们关注的重点之一,因此 JVM 性能优化也是 Java 技术面试常考的题目
所以本节我们就来讲一讲如何进行 JVM 性能优化

1、JVM 性能指标

既然要对 JVM 进行性能优化,那么我们首先要知道,有哪些评估 JVM 性能的指标
对于垃圾回收,应用程序直接关注的性能指标主要就两个:GC 频率和 GC 时间,也就是多久一次 GC 和 GC 一次多久

当然除了这两个应用程序直接关注的性能指标之外,JVM 内部还有很多更加细致的性能指标,如下所示,这些内部性能指标综合起来决定了 GC 频率和 GC 时间

  • 年轻代中对象的增长速率
  • 每次 YoungGC 之后存活对象大小
  • 每次 YoungGC 之后进入老年代的对象大小
  • 老年代对象的增长速率

2、JVM 参数设置

以上罗列的性能指标受 JVM 参数的影响,合理的 JVM 参数设置,可以将 GC 的效率发挥到极致,尽可能地减少 GC 对应用程序的影响
相反,不合理的 JVM 参数设置,就有可能引起 JVM 性能问题,比如频繁的 FullGC、GC 时间过长等问题

JVM 参数一般有 3 种类型

  • 标准参数(以 - 开头,比如 -version)
  • X 参数(以 -X 开头,比如 -Xint、-Xms2048m)
  • XX 参数(以 -XX 开头,比如 -XX:+PrintGCDetails、-XX:PermSize=512m)

这三种类型的参数的稳定性依次下降,也就是说,在 Java 版本更新的过程中,标准参数很少改动,X 参数有可能会改动,XX 参数改动的可能性比较大

JVM 参数非常多,有上百个,但是对绝大部分参数来说,默认的设置便是最普适、最合理的设置,除非真的有必要,否则我们不应该主动去设置这些参数
常用的 JVM GC 参数只有几个,主要集中在内存分配和垃圾回收器这两个方面,具体如下所示(实际上这些参数在之前的章节中都有讲到)

2.1、设置堆的大小

-Xms:Java 堆内存的初始大小
-Xmx:Java 堆内存的最大大小
一般情况下,我们会把 -Xms 和 -Xmx 设置为相同的值,以避免堆大小的调整而引起的性能损耗

2.2、设置年轻代和老年代的大小

-Xmn:年轻代大小
-XX:NewSize:年轻代的初始大小
-XX:MaxNewSize:年轻代的最大大小
-XX:NewRatio:年轻代与老年代的大小比值,比如 -XX:NewRatio=4 表示年轻代与老年代的大小比值是 1:4,年轻代占整个堆大小的 1 / 5
设置年轻代大小的方式有很多,-Xmn、-XX:NewSize 和 -XX:MaxNewSize、-XX:NewRatio 三者可以互相替换
对于老年代的大小,我们只需要通过堆大小减去年轻代大小即可得到

2.3、设置永久代或元空间的大小

-XX:PermSize:永久代的初始大小
-XX:MaxPermSize:永久代的最大大小,这两个参数仅在 Java 1.7 及其以前版本中有效
-XX:MetaspaceSize:元空间的初始大小
-XX:MaxMetaspaceSize:元空间的最大大小,这两个参数仅在 Java 1.8 及其以后版本中有效

2.4、设置 Eden 区和 Survivor 区的大小

-XX:SurvivorRatio:一个 Survivor 区跟 Eden 区的大小比例
比如:-XX:SurvivorRatio=8 表示一个 Survivor 区跟 Eden 区的大小比例是 1:8,也就是说,Eden 区占年轻代大小的 8 / 10,两个 Survivor 区分别占年轻代大小的 1 / 10

2.5、设置线程栈的大小

-Xss:每个线程的栈大小,HotSpot JVM 不区分虚拟机栈和本地方法栈,使用一个栈来同时存储 Java 方法和本地方法的栈帧,因此这里只有一个栈大小的设置参数
线程栈大小默认为 512 KB 或 1 MB,除非系统在运行的过程中,出现非代码因素导致的 StackOverflow,我们才需要调整线程栈的大小,否则一般使用默认的线程栈大小设置即可

2.6、设置垃圾回收器

-XX:+UseSerialGC:使用 Serial 垃圾回收器
-XX:+UseParallelGC:使用 Parallel 垃圾回收器
-XX:+UseConcMarkSweepGC:使用 CMS 垃圾回收器
-XX:+UseG1GC:使用 G1 垃圾回收器

3、JVM 性能预估

刚刚我们介绍了常用的 JVM 参数,那么如何设置合理的 JVM 参数呢?
大部分情况下,我们只需要对常用的 JVM 参数,预设一些经验值,然后根据线上或者压测的情况,再做调整优化即可
我们也可以事先预估一下系统对内存的使用情况,比如:每秒钟产生多少对象、对象的生存周期等等,然后依据期望的 GC 频率和 GC 时间,有针对性地设置 JVM 参数

假设我们在维护一个系统,通过分析,我们找出了系统中调用频率最高的几个接口
我们对每个接口进行分析,罗列出执行该接口请求所需要创建的所有对象
然后按照前面章节中讲到的方法,计算每个对象所占用的内存大小,最后累加起来便是执行这个接口请求所创建对象的大小
对这几个调用频率最高的接口所创建对象的大小求平均值,便粗略得到了执行一个接口请求平均创建对象的大小,假设这个值为 2 KB
我们预估系统的 QPS 是 500,那么我们就可以得知,系统每秒钟产生的对象大小大约 1 MB(500 * 2 KB)
如果年轻代中 Eden 区和 From Survivor 区大小为 1 GB,那么填满它们大约需要 1000 秒(即约 16 分钟),也就是说,每隔 16 分钟就会进行一次 YoungGC

我们假设执行 YoungGC 会回收 90% 的年轻代空间,剩余 100 MB(1 GB * 10%)存活对象会被复制到 To Survivor 区
前面讲到,根据动态年龄判定机制,To Survivior 区中的对象所占空间超过 50%,就会导致部分对象进入老年代
为了尽量避免对象进入老年代而引发 FullGC,我们需要保证 To Survivor 区大小至少为 200 MB,因此年轻代大小为 1.2 GB

我们再假设这 100 MB 存活对象中,有 10% 的对象的生命周期特别长,会经过多次 YoungGC 之后进入老年代,也就是说,每次 YoungGC 会有大约 10 MB 的对象进入老年代
我们假设老年代的大小约为 1.8 GB,每次垃圾回收只能回收老年代 50% 的空间,那么每经过大约 90 次(1.8 GB * 50% / 10 MB)YoungGC 就会执行一次 FullGC
由此推出 FullGC 的时间间隔大约为 24 小时(90 * 16 分钟 / 60)

根据以上假设和分析,我们设置 JVM 参数如下所示,这里假设系统名称为 WebServer
因为系统对接口的响应时间比较敏感,因此我们使用 CMS 垃圾回收器,年轻代的大小为 1.2 GB
为了保证 To Survivor 区大小至少为 200 MB,我们将 -XX:SurvivorRatio 设置为 4,除此之外,存放类等元信息的元空间大小设置为 512 MB,线程栈大小使用默认值

java -Xms3g -Xmx3g -Xmn1.2g -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -XX:SurvivorRatio=4 -XX:+UseConcMarkSweepGC -jar WebServer.jar

4、JVM 性能调优

不过上述分析过程包含大量的假设,这就导致最终得到的 JVM 参数值可能并不准确,甚至完全没有意义,反倒不如经验值合理
实际上,初始 JVM 参数如何设置关系并不大
毕竟在项目上线或者压测的一段时间内,我们还需要密切关注 JVM 的表现,并且基于 jstat 等工具得到的 JVM 性能统计数据,进一步做性能调优
调优之后的结果才是最重要的,那么具体如何进行 JVM 性能调优呢?

一般来讲 JVM 性能调优所努力的方向是减少 GC 频率和 GC 时间,特别是 FullGC 频率和 FullGC 时间
YoungGC 时间往往都比较短,正常情况下,一般都在几毫秒或几十毫秒,对应用程序的影响很小,因此 YoungGC 稍微频繁一点问题也不大
相比而言,FullGC 要慢很多,正常情况下,一般都在几十毫秒或几百毫秒,对应用程序的影响较大
我们需要通过调整 JVM 参数,比如:增大年轻代大小、增大 Survivor 区大小,让对象尽量在年轻代就被回收掉,减少老年代中对象的增长速率,从而降低 FullGC 频率
除此之外,增大老年代大小,也可以降低 FullGC 频率,但是又会增大 FullGC 时间,同理,如果我们希望减少 FullGC 时间,那么就可以适当减小老年代大小

一般来讲,如果堆不是很大,没有长期存活的大对象和内存泄漏
那么应用 CMS 垃圾回收器并调节年轻代、老年代、Survivor 区等内存分配,完全可以将 FullGC 时间优化到合适的范围
如果实在不行的话,我们可以选择 GC 时间可控的 G1 垃圾回收器

实际上多久执行一次 GC(YoungGC 或 FullGC)、一次 GC(YoungGC 或 FullGC)多久才算法合理,并没有唯一的标准答案,具体值还是要看系统需求来定
如果系统并不在意 STW 停顿时间,那么 FullGC 频繁一点、时间久一点,也问题不大
如果系统是响应时间比较敏感的系统,比如接口服务,那么 FullGC 时间超过 1 秒就是不可接受的

实际大部分情况下,经验值设置就已经满足绝大部分系统的需求,我们并不需要刻意的对 JVM 参数进行调优
只有当通过监控发现,GC 的频率过大或 GC 时间过长,严重影响系统性能时,我们才有必要对 JVM 参数进行调优
具体的 JVM 参数调优方法以及辅助工具的使用,我们留在下一节讲解

5、课后思考题

在工作中,你是否遇到过频繁 GC、GC 时间过长、OOM 等问题,具体又是如何解决的呢?

posted @   lidongdongdong~  阅读(67)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开