JVM调优

1.调优原则

在想要JVM调优前一定要清楚地认识到一件事:JVM调优是Java性能优化的最后一步,是万不得已才会执行的步骤!
在执行JVM调优前请确保已经做了以下几件事:

  • 排查了数据库瓶颈问题,并已优化。包括但不限于索引优化、表结构优化、分库分表等。
  • 考虑了扩容的情况,横向和纵向都考虑了。
  • 排查代码,进行逻辑优化、架构优化等。
  • 拥有全面监控的手段以及收集了足够多的性能数据。

JVM调优的先制度是永远低于程序优化的

2.调优指标

JVM调优指标有以下三个:

  • 吞吐量:运行用户代码的时间占总运行时间的行例 (总运行时间=程序的运行时间+内存回收的时间)。
  • 停顿时间:执行垃圾收集(GC)时,程序的工作线程被暂停的时间。
  • 内存占用:系统运行时,JVM需要使用的内存。

该三者构成了不可能三角,我们调优时无法同时满足它们三个,所以只能取其中两个进行调优。
我们理应在业务的帮助下正确选取调优指标,并将其优化至调优目标。如:

  • Heap内存使用率 <= 70%。
  • Old Generation内存使用率<= 70%。
  • avg pause <= 1秒。
  • Full gc 次数0 或 avg pause 间隔 >= 24小时。

通常我们选择的调优目标为「吞吐量」和「停顿时间」,它们的目的最终是一致的,即尽量缩短STW(Stop-The-World)的时间

3.调优设置

3.1.堆设置

-Xms与-Xmx一般设置成相同的值,可以避免内存扩充的动作。

3.2.年轻代设置

参数-Xmn,1-1.5倍FullGC之后的老年代空间占用。
避免新生代设置过小,当新生代设置过小时,会带来两个问题:一是minor GC次数频繁,二是可能导致minor GC对象直接进老年代。当老年代内存不足时,会触发Full GC。 避免新生代设置过大,当新生代设置过大时,会带来两个问题:一是老年代变小,可能导致Full GC频繁执行;二是 minor GC 执行回收的时间大幅度增加。

3.3.老年代设置

老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。
如果堆设置偏小,可能会造成内存碎片、高回收频率以及应用暂停。
如果堆设置偏大,则需要较长的收集时间 吞吐量优先的应用。
一般吞吐量优先的应用都有一个较大的年轻代和一个较小的老年代。这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽可能存放长期存活对象

3.4.元空间设置

在jdk1.8及以后的版本中,元空间取代了永久代。相应的参数为-XX:MetaspaceSize和-XX:MaxMetaspaceSize; 通常设置为相同的值,避免运行时要不断扩展。

4.调优步骤

4.1.监控分析

主要分为实时监控和事后监控:

  • 实时监控:Linux命令(top,ps等)、JDK命令(jps,jstat等)、实时监控平台(JConsole等)。
  • 事后监控:GC日志分析、dump堆转储快照分析

4.2.监控参数设置

  1. JVM启动时增加参数:
  2. # 出现OOME时生成堆dump:
    -XX:+HeapDumpOnOutOfMemoryError
    # 生成堆文件地址:
    -XX:HeapDumpPath=/home/hadoop/dump/
  3. jmap生成,命令:
  4. jmap -dump:file=文件名.dump [pid]
    # 9257是指JVM的进程号
    jmap -dump:format=b,file=testmap.dump 9257

第一种为事后方式,只有在JVM出现OOM问题后才会生成堆栈信息;第二种是在执行时使用的,使用后JVM会暂停服务,会对服务的提供产生影响。推荐还是使用第一种方式。

4.3.调试相关命令

主要有以下几种:

  • jps:用于展示当前系统中该用户有权访问的java进程。
  • jstat:可以查看Java程序运行时相关信息,可以通过它查看堆信息的相关情况。
  • jinfo:可以用来查看正在运行的java程序的扩展参数,甚至支持运行时修改部分参数。
  • jmap:可以查看堆内存使用状况,一般结合jhat使用。
  • jhat:解析Java堆转储文件并启动一个 web server,然后用浏览器来查看、浏览 dump 出来的 heap。
  • jstack:用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
  • jconsole:可以从命令行或在 GUI shell 中运行。您可以轻松地使用 JConsole来监控 Java 应用程序性能和跟踪Java 中的代码。

4.4.JVM参数调优

在分析过报告后便需要对报告中有问题的部分进行调优,而除了程序调优外,影响最大的便是JVM的参数调优了,如设置堆大小等。
JVM参数主要分为:标准参数、非标准参数、不稳定参数。

  1. 标准参数,顾名思义,标准参数中包括功能以及输出的结果都是很稳定的,基本上不会随着JVM版本的变化而变化。它以-开头,如:java -jar、java -help、java -version等。
  2. 非标准参数以-X开头,是标准参数的扩展。对应前面讲的标准化参数,这是非标准化参数。表示在将来的JVM版本中可能会发生改变,但是这类以-X开始的参数变化的比较小。 我们可以通过 Java -X 命令来检索所有-X 参数。常见的非标准参数有:
  3. 不稳定参数相对来说不稳定,随着JVM版本的变化可能会发生变化,主要用于JVM调优和debug。
    不稳定参数以-XX开头,此类参数的设置很容易引起JVM性能上的差异,使JVM存在极大的不稳定性。如果此类参数设置合理将大大提高JVM的性能及稳定性。
    参数分析:
  4.               -Xms4g:初始化堆内存大小为4GB,ms是memory start的简称,等价于-XX:InitialHeapSize。
    -Xmx4g:堆内存最大值为4GB,mx是memory max的简称,等价于-XX:MaxHeapSize。
    -Xmn1200m:设置年轻代大小为1200MB。增大年轻代后,将会减小老年代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
    -Xss512k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1MB,以前每个线程堆栈大小为256K。
        应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。
        但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
    -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与老年代的比值(除去持久代)。
        设置为4,则年轻代与老年代所占比值为1:4,年轻代占整个堆栈的1/5
    -XX:SurvivorRatio=8:设置年轻代中Eden区与Survivor区的大小比值。
        设置为8,则两个Survivor区与个Eden区的比值为2:8,个Survivor区占整个年轻代的1/10
    -XX:PermSize=100m:初始化永久代大小为100MB。 -XX:MaxPermSize=256m:设置持久代大小为256MB。
    -XX:MaxTenuringThreshold=15:设置垃圾最大年龄。
        如果设置为0的话,则年轻代对象不经过Survivor区,直接进入老年代。对于老年代比较多的应用,可以提高效率。
        如果将此值设置为个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概率。
    -XX:MaxDirectMemorySize=1G:直接内存。报java.lang.OutOfMemoryError: Direct buffermemory异常可以上调这个值。
    -XX:+DisableExplicitGC:禁止运行期显式地调用System.gc()来触发fulll GC。 注意: Java
        RMI的定时GC触发机制可通过配置-Dsun.rmi.dgc.server.gcInterval=86400来控制触发的时间。
    -XX:CMSInitiatingOccupancyFraction=60:老年代内存回收阈值,默认值为68。
    -XX:ConcGCThreads=4:CMS垃圾回收器并行线程线,推荐值为CPU核心数。
    -XX:ParallelGCThreads=8:新生代并行收集器的线程数。
    -XX:CMSMaxAbortablePrecleanTime=500:当abortable-preclean预清理阶段执行达到这个时间时就会结束。
    -XX:+DoEscapeAnalysis:开启逃逸分析(1.7后默认开启)