JVM调优

JVM调优

理论知识见《深入了解JVM》

参数设置

(1)堆大小设置

最大堆大小有3方面的限制:

  1. OS是32位还是64位(32位一般限制在1.5~2G,64位的堆大小理论上可以无限大,但是在没有G1ZGC的情况下,堆越大,GC遍历的耗时就越长);
  2. 系统虚拟内存限制;
  3. 系统物理内存限制

比如:

  1. java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:PermSize=64m -XX:MaxTenuringThreshold=0

    -Xmx = -Xms,可以防止自动扩容,也可以防止每次GC后JVM重新分配内存

堆 = 新生代 + 老年代 + 永久代;永久代-XX:PermSize一般固定64m,所以-Xmn增加,老年代就会相应地减少。官方推荐新生代大小位整个堆的3/8

-Xss在JDK5之前默认256k,JDK5之后默认1M,一般来说-Xss越小能创建的线程就越多,但是OS对一个进程内的线程数有规定,一般来说控制到3000~5000就OK。

-XX:NewRatio=4,新生代:老年代=4:1

-XX:SurvivorRatio=4,Eden:From Survivor:To Survivor = 4:1:1

-XX:MaxTenuringThreshold=0年轻代垃圾最大年龄,默认15,如果设置为0,则GC存活对象不经过Survivor,直接进入老年代,增加此值可以增加对象存在于新生代的改率,减少full gc

(2)回收器选择(并行/并发)

串行不多讨论,一般用得多的是并行和并发收集。

<1>吞吐量优先 —— 并行

  1. java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads -XX:+UseParallelOldGC

    -XX:+UseParallelGC只适用于新生代,-XX:+UseParallelOldGC适用于老年代

  2. java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

    除了G1ZGC,一般都是堆越大,GC时间就越长-XX:MaxGCPauseMillis设置young gc的最长时间,如果无法满足此值,JVM会自动调整年轻代大小以满足此值

-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

<2>响应时间优先 —— 并发

减少GC时STW的时间,适用于应用服务器、电信领域……,JDK6下的并发GC收集器只有CMS

并发可以减少停顿时间,但是会消耗很多CPU资源,降低吞吐率

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

-XX:+UseConcMarkSweepGC:设置年老代为并发收集

-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

-XX:CMSFullGCsBeforeCompaction:由于CMS会产生内存碎片,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。

-XX:+UseCMSCompactAtFullCollection:打开对老年代的压缩。可能会影响性能,但是可以消除碎片

(3)辅助信息

  1. -XX:+PrintGC

    [GC 118250K->113543K(130112K), 0.0094143 secs]
    [Full GC 121376K->10414K(130112K), 0.0650971 secs]

  2. -XX:+PrintGCDetails

    [GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
    [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

  3. -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。

    Total time for which application threads were stopped: 0.0468229 seconds

  4. -XX:+PrintGCApplicationConcurrentTime:打印GC前程序未中断运行的时间。

    Application time: 0.5291524 seconds

  5. -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息

  6. -Xloggc:filename:将日志信息记录到文件以便分析


常见异常 & 原因 & 解决

java.lang.OutOfMemoryError: Java heap space

原因:堆空间被无法回收的对象填满,无法再为新对象分配空间
解决:根据对象引用情况,找到泄漏点,回收对象

java.lang.OutOfMemoryError: PermGen space

原因:永久代被填满,无法加载新的类,可能是由于反射大量调用造成的
解决:扩张永久代无法,或是采用MetaSpace取代PermGen

java.lang.StackOverflowError

原因:递归过深,栈空间不足

Fatal: Stack size too small

原因:线程空间不足
解决:提高-Xss无法根除此问题,还是应该检查代码中是否存在递归过深的情况

java.lang.OutOfMemoryError: unable to create new native thread

原因:系统总资源有限,JVM占用了太多,剩余的资源就有限,可以创建的本地线程就有限
解决:缩小-Xss,减少每个线程分配的内存,以创建更多线程;同样缩小JVM堆区大小也可以起到一定作用


建议

  1. full gc频率尽可能低、每次时间尽可能短
  2. 使用-Xmx = -Xms防止自动扩容,自动扩容需要系统开销
  3. -Xmx不要超过物理内存,防止类加载时没有空间而报错
  4. -Xmx不宜过大,因为整个OS的资源有限,分配给JVM的越多,OS持有的越少,能够允许产生的线程数量就越少,通过减小-Xss也可以增加能够产生的线程数

调优策略

(1)吞吐量优先应用:新生代尽可能大,并行收集,老年代尽可能小

  1. 吞吐量优先的应用新生代尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
  2. 吞吐量优先的应用一般采用一个很大的年轻代和一个较小的年老代*。这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代存放长期存活对象。

(2)响应时间优先应用:新生代尽可能大,并发收集,增加年轻代年龄

  1. 响应时间优先的应用新生代应该尽可能大,使用并发收集如G1/ZGC。这种情况下young gc频率很小,同时应该减少到达老年代的对象(比如调大-XX:MaxTenuringThreshold减少对象从新生代拷贝到老年代的停顿,防止full gc。
  2. 响应时间优先的应用老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间
  3. 应该对堆中主要对象进行分析,尽可能减少堆中对象数,从而减少每次GC的耗时

    比如相比于将所有对象从直接内存拷贝到堆内存进行操作,直接使用NIO,在堆内存中创建对象操作直接内存可以显著减少堆中对象数量。

但是要保证直接内存大小,防止溢出

  1. 如果其他线程过多消耗CPU,那么GC线程就可能分配不到时间片,造成停顿,所以使用线程池替代单个线程,减少系统中线程的总数

(3)内存碎片问题

CMS采用标记-清楚,存在内存碎片,需要对老年代进行压缩处理,防止full gc提前出现:

  1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
  2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

(4)关注堆外内存

如果使用了NIO,那么就需要关注堆外内存,堆外内存只有在full gc时才会顺带回收,所以如果堆外内存溢出时会不断抛出OOM,但是GC日志却查不到信息

解决方法:

  1. -Xmx = -Xms防止自动扩容
  2. -Xmx不要超过物理内存大小,也不要太大,不然会挤压到堆外内存

调优工具

  1. JConsole:JDK自带,功能简单,可以在系统有一定负载的情况下使用,对GC算法有详细的跟踪
  2. VisualVM:JDK自带,功能强大
  3. JProfiler:功能强大的JVM调优工具,可以分析动态内存使用情况、内存分配情况、堆上存活对象、GC对象、方法调用、线程、锁……

    参考《在IDEA中使用JProfiler插件》

命令行工具

  • -XX:+PrintGCDetails:获取多个代GC前后存活对象大小、每个代总共可用空间、GC耗时

  • -XX:+PrintGCTimeStamps:打印GC时间戳

  • java -XX:+PrintFlagInitial:JVM运行时初始值

  • java -XX:+PrintFlagFinal:JVM运行时最终值

  • java -XX:+PrintCommandLineFlags:打印命令行参数

  • ./jinfo -flags 进程id查看该进程运行时参数

  • jstat -class 进程id 每隔多少毫秒 一共输出多少次进程类装载信息

  • jstat -gc 进程id 每隔多少毫秒 一共输出多少次进程GC信息

  • jmap(Memory Map for Java):查看堆内存使用情况,生成虚拟机的内存转储快照(heapdump文件)

    语法格式:
    jmap -heap pid查看进程堆内存使用情况,包括使用的GC算法、堆配置参数和各代中堆内存使用情况

    jmap -histo[:live] pid查看堆内存中的对象数目、大小,如果带上:live则只统计存活对象
    jmap -dump:format=b,file=dumpFileName pid将进程内存使用情况dump到文件中,后续可以通过jhat进行分析

  • jhat:JVM Heap Dump Browser,用于分析heapdump文件,会创建一个HTTP/HTML服务器,让用户可以在浏览器查看分析结果

  • jstack查看Java进程内的线程堆栈信息

    语法格式:

1.jstat [option] pid
2.jstat [option] executable core
3.jstat [option] [server-id@]remote-hostname-or-ip
命令行参数:
-l:l for lock,打印锁信息,死锁时可以通过jstat -l pid查看锁持有信息;
-m:m for mixed,不仅仅打印Java堆栈信息,还会输出C/C++堆栈信息(比如Native方法)

  • jps(JVM Process Status Tool):输出JVM中运行的进程状态信息

    语法格式:jps [options] [hostid]
    jps -m -l
    如果不指定hostid就默认为当前主机;
    参数:
    -q:不输出类名、JAR名和传入main方法的参数;
    -m输出传入main方法的参数
    -l输出main类或JAR类的全限定名
    -v:输出传入JVM的参数

-------------本文结束感谢您的阅读-------------

本文标题:JVM调优

文章作者:DragonBaby308

发布时间:2019年08月05日 - 22:21

最后更新:2020年01月25日 - 13:07

原始链接:http://www.dragonbaby308.com/JVM-better/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

急事可以使用右下角的DaoVoice,我绑定了微信会立即回复,否则还是推荐Valine留言喔( ఠൠఠ )ノ
0%