JVM解读-性能调优实例

作者: 高广超 | 来源:发表于2017-04-27 22:03 被阅读505次

    JVM性能调优

    1 堆设置调优

    年轻代大小选择

    • 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
    • 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
      通过-XX:NewRadio设置新生代与老年代的大小比例,通过-Xmn来设置新生代的大小。

    年老代大小选择

    • 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

      • 并发垃圾收集信息
      • 持久代并发收集次数
      • 传统GC信息
      • 花在年轻代和年老代回收上的时间比例
    • 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

    • 较小堆引起的碎片问题
      因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

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

    2 GC策略调优

    1. 能够忍受full gc的停顿?

      是:选择throughput

      否:如果堆较小,使用CMS或者G1;如果堆较大,选择G1

    2. 使用默认配置能达到期望目标吗?

      首先尽量使用默认配置,因为垃圾收集技术在不断发展成熟,自动优化大多数的效果是最好的。如果默认配置没有达到期望,请确认垃圾收集是否是性能瓶颈。如负荷较高的应用,如果垃圾收集上的时间不超过3%,即使进行垃圾回收调优效果也不大。

    3. 应用的停顿时间和预期的目标接近吗?

      是:调整最大停顿时间设定可能是需要做的

      否:需要进行其他调整

      如果停顿时间太长,但是吞吐量正常,可以尝试减少新生代大小(如果是full gc,则减少老年代大小),这样停顿时间变短,但是单次时间变长

    4. GC停顿很短了,但是吞吐量上不去?

      增大堆的大小,但是单次停顿时间会加长

    5. 使用并发收集器,发生了由并发模式失败引发的full gc?

      如果CPU资源充足,可以增加并发GC的线程数数

    6. 使用并发收集器,发生由晋升失败引起的full gc?

      如果是CMS,意味着发生了碎片化,这种情况下:使用跟大的堆;尽早启动后台回收
      如果堆空间较大,可以选择使用G1

    3 JIT调优

    1. 一般只需要选择是使用客户端版或者服务器版的JIT编译器即可。
    2. 客户端版的JIT编译器使用:-client指定,服务器版的使用:-server。
    3. 选择哪种类型一般和硬件的配置相关,当然随着硬件的发展,也没有一个确定的标准哪种硬件适合哪种配置。
    4. 两种JIT编译器的区别:
      • Client版对于代码的编译早于Server版,也意味着代码的执行速度在程序执行早期Client版更快。
      • Server版对代码的编译会稍晚一些,这是为了获取到程序本身的更多信息,以便编译得到优化程度更高的代码。因为运行在Server上的程序通常都会持续很久。
    5. Tiered编译的原理:
      • JVM启动之初使用Client版JIT编译器
      • 当HotSpot形成之后使用Server版JIT编译器再次编译
    6. 在Java 8中,默认使用Tiered编译方式。

    不过在Java7版本之后,一旦开发人员在程序中显式指定命令“-server”时,缺省将会开启分层编译(Tiered Compilation)策略,由client编译器和server编译器相互协作共同来执行编译任务。不过在早期版本中,开发人员则只能够通过命令“-XX:+TieredCompilation”手动开启分层编译策略。

    • -Xint:完全采用解释器模式执行程序;
    • -Xcomp:完全采用即时编译器模式执行程序;
    • -Xmixed:采用解释器+即时编译器的混合模式共同执行程序。

    启动优化

    Application -client -server -XX:+TieredCompilation 类数量
    HelloWorld 0.08s 0.08s 0.08s Few
    NetBeans 2.83s 3.92s 3.07s ~10000
    HelloWorld 51.5s 54.0s 52.0s ~20000

    总结

    1. 当程序的启动速度越快越好时,使用Client版的JIT编译器更好。
    2. 就启动速度而言,Tiered编译方式的性能和只使用Client的方式十分接近,因为Tiered编译本质上也会在启动是使用Client JIT编译器。

    批处理优化

    对于批处理任务,任务量的大小是决定运行时间和使用哪种编译策略的最重要因素:

    Number of Tasks -client -server -XX:+TieredCompilation
    1 0.142s 0.176s 0.165s
    10 0.211s 0.348s 0.226s
    100 0.454s 0.674s 0.472s
    1000 2.556s 2.158s 1.910s
    10000 23.78s 14.03s 13.56s

    可以发现几个结论:

    1. 当任务数量小的时候,使用Client或者Tiered方式的性能类似,而当任务数量大的时候,使用Tiered会获得最好的性能,因为它综合使用了Client和Server两种编译器,在程序运行之初,使用Client JIT编译器得到一部分编译过的代码,在程序“热点”逐渐形成之后,使用Server JIT编译器得到高度优化的编译后代码。
    2. Tiered编译方式的性能总是好于单独使用Server JIT编译器。
    3. Tiered编译方式在任务量不大的时候,和单独使用Client JIT编译器的性能相当。

    总结

    1. 当一段批处理程序需要被执行时,使用不同的策略进行测试,使用速度最快的那一种。
    2. 对于批处理程序,考虑使用Tiered编译方式作为默认选项。

    长时间运行应用的优化

    对于长时间运行的应用,比如Servlet程序等,一般会使用吞吐量来测试它们的性能。 以下的一组数据表示了一个典型的数据获取程序在使用不同“热身时间”以及不同编译策略时,对吞吐量(OPS)的影响(执行时间为60s):

    Warm-upPeriod -client -server -XX:+TieredCompilation
    0s 15.87 23.72 24.23
    60s 16.00 23.73 24.26
    300s 16.85 24.42 24.43

    即使当“热身时间”为0秒,因为执行时间为60秒,所以编译器也有机会在次期间做出优化。

    从上面的数据可以发现的几个结论:

    1. 对于典型的数据获取程序,编译器对代码编译和优化发生的十分迅速,当“热身时间”显著增加时,如从60秒增加到300秒,最后得到的OPS差异并不明显。
    2. -server JIT编译器和Tiered编译的性能显著优于-client JIT编译器。

    总结

    1. 对于长时间运行的应用,总是使用-server JIT编译器或者Tiered编译策略。

    代码缓存调优(Tuning the Code Cache)

    当JVM对代码进行编译后,被编译的代码以汇编指令的形式存在于代码缓存中(Code Cache),显然这个缓存区域也是有大小限制的,当此区域被填满了之后,编译器就不能够再编译其他Java字节码了。

    Code Cache的最大空间可以通过:-XX:ReservedCodeCacheSize=N来进行设置。

    4 JVM线程调优

    调节线程栈大小

    通过设置-Xss参数,在内存比较稀缺的机器上,可以减少线程栈的大小,在32位的JVM上,可以减少线程栈大小,可以稍稍增加堆的可用内存。每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。

    偏向锁

    使用-XX:UseBiasedLocking选项来禁用偏向锁,偏向锁默认开启。偏向锁可以提高缓存命中率,但是因为偏向锁也需要一些簿记信息,有时候性能会更糟,比如使用了某些线程池,同步资源或代码一直都是多线程访问的,那么消除偏向锁这一步骤对你来说就是多余的。

    自旋锁

    使用-XX:UseSpinning参数可以设置自旋锁是否开启,但是Java7以后自旋锁无法禁用。

    线程优先级

    每个线程都可以由开发人员指定优先级,不过真正执行时的优先级还取决于操作系统为每个线程计算的当前优先级。开发人员不能依赖线程优先级来影响其性能,如果要提高某些任务的优先级,就必须使用应用层逻辑来划分优先级,可以通过将任务指派给不同线程池并修改哪些池子大小来实现。

    总结

    理解线程如何运作,可以获得很大的性能优势,不过就线程的性能而言,没有太多可以调优的:可以修改的JVM标识相当少,而且效果不明显。

    5 典型案例

    $JAVA_ARGS
    .=
    "
    -Dresin.home=$SERVER_ROOT
    -server
    -Xmx3000M
    -Xms3000M
    -Xmn600M
    -XX:PermSize=500M
    -XX:MaxPermSize=500M
    -Xss256K
    -XX:+DisableExplicitGC
    -XX:SurvivorRatio=1
    -XX:+UseConcMarkSweepGC
    -XX:+UseParNewGC
    -XX:+CMSParallelRemarkEnabled
    -XX:+UseCMSCompactAtFullCollection
    -XX:CMSFullGCsBeforeCompaction=0
    -XX:+CMSClassUnloadingEnabled
    -XX:LargePageSizeInBytes=128M
    -XX:+UseFastAccessorMethods
    -XX:+UseCMSInitiatingOccupancyOnly
    -XX:CMSInitiatingOccupancyFraction=70
    -XX:SoftRefLRUPolicyMSPerMB=0
    -XX:+PrintClassHistogram
    -XX:+PrintGCDetails
    -XX:+PrintGCTimeStamps
    -XX:+PrintHeapAtGC
    -Xloggc:log/gc.log
    ";
    

    说明:

    64位jdk参考设置,年老代涨得很慢,CMS执行频率变小,CMS没有停滞,也不会有promotion failed问题,内存回收得很干净


    欢迎关注 高广超的简书博客 与 收藏文章 !
    欢迎关注 头条号:互联网技术栈

    个人介绍:

    高广超 :多年一线互联网研发与架构设计经验,擅长设计与落地高可用、高性能互联网架构。

    本文首发在 高广超的简书博客 转载请注明!

    image.png

    相关文章

      网友评论

        本文标题:JVM解读-性能调优实例

        本文链接:https://www.haomeiwen.com/subject/bswnzttx.html