zoukankan      html  css  js  c++  java
  • JVM系列:JVM性能调优,看这一篇就够了!

    原文链接:https://mp.weixin.qq.com/s?__biz=Mzg5NDA0ODQ2OQ==&mid=2247483915&idx=1&sn=c83bd02c79e357c3b56387e2c72abeb3&chksm=c024c3b1f7534aa72fa8959fbb599add8bf04d99ea2a7d89a811128625605dc62fdd6ff7c962&token=349832407&lang=zh_CN#rd

    参考:https://juejin.im/post/5b6b986c6fb9a04fd1603f4a#heading-30

    1、JVM 调优目标:使用较小的内存占用来获得较高的吞吐量或者较低的延迟

     程序在上线前的测试或运行中有时会出现一些大大小小的 JVM 问题,比如 cpu load 过高、请求延迟、tps 降低等,甚至出现内存泄漏(每次垃圾收集使用的时间越来越长,垃圾收集频率越来越高,每次垃圾收集清理掉的垃圾数据越来越少)、内存溢出导致系统崩溃,因此需要对 JVM 进行调优,使得程序在正常运行的前提下,获得更高的用户体验和运行效率。

    这里有几个比较重要的指标:

    • 内存占用:程序正常运行需要的内存大小。

    • 延迟:由于垃圾收集而引起的程序停顿时间。

    • 吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值。

    当然,和 CAP 原则一样,同时满足一个程序内存占用小、延迟低、高吞吐量是不可能的,

    程序的目标不同,调优时所考虑的方向也不同,

    在调优之前,必须要结合实际场景,有明确的的优化目标,找到性能瓶颈,对瓶颈有针对性的优化,最后进行测试,通过各种监控工具确认调优后的结果是否符合目标。

    2、JVM 调优工具

    (1)调优可以依赖、参考的数据有系统运行日志、堆栈错误信息、gc 日志、线程快照、堆转储快照等。

            ① 系统运行日志:系统运行日志就是在程序代码中打印出的日志,描述了代码级别的系统运行轨迹(执行的方法、入参、返回值等),一般系统出现问题,系统运行日志是首先要查看的日志。

            ② 堆栈错误信息:当系统出现异常后,可以根据堆栈信息初步定位问题所在,

                   比如根据 java.lang.OutOfMemoryError: Java heap space 可以判断是堆内存溢出;

                  根据 java.lang.StackOverflowError 可以判断是栈溢出;

                  根据 java.lang.OutOfMemoryError: PermGen space 可以判断是方法区溢出等。

         ③ GC 日志:

                 程序启动时用 -XX:+PrintGCDetails 和 -Xloggc:/data/jvm/gc.log 可以在程序运行时把 gc 的详细过程记录下来,或者直接配置 -verbose:gc 参数把 gc 日志打印到控制台,通过记录的 gc 日志可以分析每块内存区域 gc 的频率、时间等,从而发现问题,进行有针对性的优化。

                  gc 日志配置可参考 https://www.cnblogs.com/shay/p/13162677.html

                  比如如下一段GC日志:

    2018-08-02T14:39:11.560-0800: 10.171: [GC [PSYoungGen: 30128K->4091K(30208K)] 51092K->50790K(98816K), 0.0140970 secs] [Times: user=0.02 sys=0.03, real=0.01 secs]
    2018-08-02T14:39:11.574-0800: 10.185: [Full GC [PSYoungGen: 4091K->0K(30208K)] [ParOldGen: 46698K->50669K(68608K)] 50790K->50669K(98816K) [PSPermGen: 2635K->2634K(21504K)], 0.0160030 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
    2018-08-02T14:39:14.045-0800: 12.656: [GC [PSYoungGen: 14097K->4064K(30208K)] 64766K->64536K(98816K), 0.0117690 secs] [Times: user=0.02 sys=0.01, real=0.01 secs]
    2018-08-02T14:39:14.057-0800: 12.668: [Full GC [PSYoungGen: 4064K->0K(30208K)] [ParOldGen: 60471K->401K(68608K)] 64536K->401K(98816K) [PSPermGen: 2634K->2634K(21504K)], 0.0102020 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
    

    上面一共是 4 条 GC 日志,

    来看第一行日志解析:

    2018-08-02T14:39:11.560-0800 是精确到了毫秒级别的 UTC 通用标准时间格式,配置了 -XX:+PrintGCDateStamps 这个参数可以跟随gc日志打印出这种时间戳,

    10.171是从 JVM 启动到发生 gc 经过的秒数。

    第一行日志正文开头的 [GC 说明这次 GC 没有发生 Stop-The-World(用户线程停顿),

    第二行日志正文开头的 [Full GC 说明这次 GC 发生了 Stop-The-World,所以说,[GC 和 [Full GC 跟新生代和老年代没关系,和垃圾收集器的类型有关系,如果直接调用 System.gc(),将显示 [Full GC(System)。

    接下来的 [PSYoungGen 、 [ParOldGen 表示 GC 发生的区域,具体显示什么名字也跟垃圾收集器有关,比如这里的 [PSYoungGen 表示 Parallel Scavenge 收集器,[ParOldGen 表示 Serial Old 收集器,此外,Serial 收集器显示 [DefNew,ParNew 收集器显示 [ParNew 等。

    再往后的 30128K->4091K(30208K) 表示进行了这次 gc 后,该区域的内存使用空间由 30128K 减小到 4091K,总内存大小为 30208K。

    每个区域gc 描述后面的 51092K->50790K(98816K), 0.0140970 secs 表示进行了这次垃圾收集后,整个堆内存的内存使用空间由 51092K 减小到 50790K,整个堆内存总空间为 98816K,gc 耗时 0.0140970秒。

      ④ 线程快照:thread dump

           顾名思义,根据线程快照可以看到线程在某一时刻的状态,当系统中可能存在请求超时、死循环、死锁等情况是,可以根据线程快照来进一步确定问题。

           通过执行虚拟机自带的“jstack pid”命令,可以dump出当前进程中线程的快照信息,更详细的使用和分析网上有很多例,

          获取线程快照的命令:dump 文件:进入soa logs 目录,然后jstack应用的线程号,然后输出到文件

                                         命令:jstack pid >xxx.log

                                        示例: jstack 1 >dump.log

          这篇文章写到这里已经很长了就不过多叙述了,贴一篇博客供参考: http://www.cnblogs.com/kongzhongqijing/articles/3630264.html

      ⑤ 堆转储快照:(heap dump)

             程序启动时可以使用 -XX:+HeapDumpOnOutOfMemory 和 -XX:HeapDumpPath=/data/jvm/dumpfile.hprof

             当程序发生内存溢出时,把当时的内存快照以文件形式进行转储

           (也可以直接用 jmap 命令转储程序运行时任意时刻的内存快照),事后对当时的内存使用情况进行分析。       

               使用jmap命令获取heap dump 文件:jmap -dump:format=b,file=xxx.hprof pid,就生成了 xxx.hprof dump文件( 其中pid是JVM进程的id,xxx.bin是生成的heap dump文件,在执行命令的目录下面,如果要使用Eclipse Memory Analyzer来分析则文件的后缀要为hprof

               jmap -dump:format=b,file=xxx.bin 1

              jmap -dump:format=b,file=heap.hprof 1

              jmap -dump:live,format=b,file=heap.bin <pid> 将当前的存活对象dump到文件,此时会触发FullGC

    (2)JVM 调优工具

       ① 用 jps(JVM process Status)

        可以查看虚拟机启动的所有进程、执行主类的全名、JVM启动参数,比如当执行了 JPSTest 类中的 main 方法后(main 方法持续执行),执行 jps -l可看到下面的JPSTest类的 pid 为 31354,加上 -v 参数还可以看到JVM启动参数。

    3265 
    32914 sun.tools.jps.Jps
    31353 org.jetbrains.jps.cmdline.Launcher
    31354 com.danny.test.code.jvm.JPSTest
    380 

    示例:

         jps -l

         jps -v

      ② 用 jstat(JVM Statistics Monitoring Tool)监视虚拟机信息 

         jstat -gc pid 500 10:每 500 毫秒打印一次 Java 堆状况(各个区的容量、使用容量、gc 时间等信息),打印 10 次。

                   示例:

                

               jstat 还可以以其他角度监视各区内存大小、监视类装载信息等,具体可以 google jstat 的详细用法。

      

       

    ③ 用 jmap(Memory Map for Java)查看堆内存信息 执行 jmap -histo pid 可以打印出当前堆中所有每个类的实例数量和内存占用,如下,class name 是每个类的类名([B 是 byte 类型,[C是 char 类型,[I 是 int 类型),bytes 是这个类的所有示例占用内存大小,instances 是这个类的实例数量:

         

        

    执行 jmap -dump 可以转储堆内存快照到指定文件,比如执行:

    jmap -dump:format=b,file=/data/jvm/dumpfile_jmap.hprof 3361

    可以把当前堆内存的快照转储到 dumpfile_jmap.hprof 文件中,然后可以对内存快照进行分析。

    ④ 利用 jconsole、jvisualvm 分析内存信息(各个区如 Eden、Survivor、Old 等内存变化情况)

    jconsole 启动:/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/bin » ./jconsole      

    jvisualvm启动:/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/bin » ./jvisualvm 

    如果查看的是远程服务器的 JVM,Java程序启动需要加上如下参数:

    jmx 端口是多少,
    是否开启认证,
    是否开启ssl,是否开启远程jxm
    "-Dcom.sun.management.jmxremote=true"
    "-Djava.rmi.server.hostname=12.34.56.78"
    "-Dcom.sun.management.jmxremote.port=18181"
    "-Dcom.sun.management.jmxremote.authenticate=false"
    "-Dcom.sun.management.jmxremote.ssl=false"

    示例,启动脚本init.script 里面:

    下图是 jconsole 界面

      

    概览选项可以观测堆内存使用量、线程数、类加载数和 CPU 占用率;

    内存选项可以查看堆中各个区域的内存使用量和左下角的详细描述(内存大小、GC 情况等);

    线程选项可以查看当前 JVM 加载的线程,查看每个线程的堆栈信息,还可以检测死锁;

    VM 概要描述了虚拟机的各种详细参数。

      

    下图是jvisualvm  界面

    功能比 jconsole 略丰富一些,不过大部分功能都需要安装插件

    概述跟 jconsole 的 VM 概要差不多,描述的是 jvm 的详细参数和程序启动参数;

    监视展示的和 jconsole 的概览界面差不多(CPU、堆/方法区、类加载、线程);

    线程和 jconsole 的线程界面差不多;

    抽样器可以展示当前占用内存的类的排行榜及其实例的个数;

    Visual GC 可以更丰富地展示当前各个区域的内存占用大小及历史信息(下图)。

      

    ⑤ 分析堆转储快照

    前面说到配置了 -XX:+HeapDumpOnOutOfMemory 参数可以在程序发生内存溢出时 dump 出当前的内存快照,

    也可以用 jmap 命令随时 dump 出当时内存状态的快照信息,

    dump 的内存快照一般是以 .hprof 为后缀的二进制格式文件。 

    jvisualvm 也可以分析内存快照,在 jvisualvm 菜单的 “ 文件 ” - “ 装入 ”,选择堆内存快照,快照中的信息就以图形界面展示出来了,如下,主要可以查看每个类占用的空间、实例的数量和实例的详情等:

     

    还有很多分析内存快照的第三方工具,比如 eclipse mat,它比 jvisualvm 功能更专业,出了查看每个类及对应实例占用的空间、数量,还可以查询对象之间的调用链,可以查看某个实例到 GC Root 之间的链,等等。

     内存快照分析工具:MAT:可以在 eclipse 中安装 mat 插件,也可以下载独立的版本(http://www.eclipse.org/mat/downloads.php ),

    MAT功能演示):

    (3)JVM 调优经验

     JVM 配置方面,一般情况可以先用默认配置(基本的一些初始参数可以保证一般的应用跑的比较稳定了),在测试中根据系统运行状况(会话并发情况、会话时间等),结合 gc 日志、内存监控、使用的垃圾收集器等进行合理的调整,当老年代内存过小时可能引起频繁 Full GC,当内存过大时 Full GC 时间会特别长。

    那么 JVM 的配置比如新生代、老年代应该配置多大最合适呢?答案是不一定,调优就是找答案的过程,物理内存一定的情况下,新生代设置越大,老年代就越小,Full GC 频率就越高,但 Full GC 时间越短;相反新生代设置越小,老年代就越大,Full GC 频率就越低,但每次 Full GC 消耗的时间越大。

     建议如下:

    -Xms 和 -Xmx 的值设置成相等,堆大小默认为 -Xms 指定的大小,默认空闲堆内存小于 40% 时,JVM 会扩大堆到 -Xmx 指定的大小;空闲堆内存大于 70% 时,JVM 会减小堆到 -Xms 指定的大小。如果在 Full GC 后满足不了内存需求会动态调整,这个阶段比较耗费资源。

    • 新生代尽量设置大一些,让对象在新生代多存活一段时间,每次 Minor GC 都要尽可能多的收集垃圾对象,防止或延迟对象进入老年代的机会,以减少应用程序发生 Full GC 的频率。

    • 老年代如果使用 CMS 收集器,新生代可以不用太大,因为 CMS 的并行收集速度也很快,收集过程比较耗时的并发标记和并发清除阶段都可以与用户线程并发执行。

    • 方法区大小的设置,1.6 之前的需要考虑系统运行时动态增加的常量、静态变量等,1.7 只要差不多能装下启动时和后期动态加载的类信息就行。

    代码实现方面,性能出现问题比如程序等待、内存泄漏除了 JVM 配置可能存在问题,代码实现上也有很大关系:

    • 避免创建过大的对象及数组:过大的对象或数组在新生代没有足够空间容纳时会直接进入老年代,如果是短命的大对象,会提前出发 Full GC。

    • 避免同时加载大量数据,如一次从数据库中取出大量数据,或者一次从 Excel 中读取大量记录,可以分批读取,用完尽快清空引用。

    • 当集合中有对象的引用,这些对象使用完之后要尽快把集合中的引用清空,这些无用对象尽快回收避免进入老年代。

    • 可以在合适的场景(如实现缓存)采用软引用、弱引用,比如用软引用来为 ObjectA 分配实例:SoftReference<ObjectA> objectA=new SoftReference<ObjectA>(); 在发生内存溢出前,会将 objectA 列入回收范围进行二次回收,如果这次回收还没有足够内存,才会抛出内存溢出的异常。

    避免产生死循环,产生死循环后,循环体内可能重复产生大量实例,导致内存空间被迅速占满。

    • 尽量避免长时间等待外部资源(数据库、网络、设备资源等)的情况,缩小对象的生命周期,避免进入老年代,如果不能及时返回结果可以适当采用异步处理的方式等。

    (5)常用 JVM 参数参考:

    参数说明实例
    -Xms 初始堆大小,默认物理内存的1/64 -Xms512M
    -Xmx 最大堆大小,默认物理内存的1/4 -Xms2G
    -Xmn 新生代内存大小,官方推荐为整个堆的3/8 -Xmn512M
    -Xss 线程堆栈大小,jdk1.5及之后默认1M,之前默认256k -Xss512k
    -XX:NewRatio=n 设置新生代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 -XX:NewRatio=3
    -XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:8,表示Eden:Survivor=8:1:1,一个Survivor区占整个年轻代的1/8 -XX:SurvivorRatio=8
    -XX:PermSize=n 永久代初始值,默认为物理内存的1/64 -XX:PermSize=128M
    -XX:MaxPermSize=n 永久代最大值,默认为物理内存的1/4 -XX:MaxPermSize=256M
    -verbose:class 在控制台打印类加载信息  
    -verbose:gc 在控制台打印垃圾回收日志  
    -XX:+PrintGC 打印GC日志,内容简单  
    -XX:+PrintGCDetails 打印GC日志,内容详细  
    -XX:+PrintGCDateStamps 在GC日志中添加时间戳  
    -Xloggc:filename 指定gc日志路径 -Xloggc:/data/jvm/gc.log
    -XX:+UseSerialGC 年轻代设置串行收集器Serial  
    -XX:+UseParallelGC 年轻代设置并行收集器Parallel Scavenge  
    -XX:ParallelGCThreads=n 设置Parallel Scavenge收集时使用的CPU数。并行收集线程数。 -XX:ParallelGCThreads=4
    -XX:MaxGCPauseMillis=n 设置Parallel Scavenge回收的最大时间(毫秒) -XX:MaxGCPauseMillis=100
    -XX:GCTimeRatio=n 设置Parallel Scavenge垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) -XX:GCTimeRatio=19
    -XX:+UseParallelOldGC 设置老年代为并行收集器ParallelOld收集器  
    -XX:+UseConcMarkSweepGC 设置老年代并发收集器CMS  
    -XX:+CMSIncrementalMode 设置CMS收集器为增量模式,适用于单CPU情况。  

    好了,今天就介绍到这里,谢谢!!!

  • 相关阅读:
    整数反转
    两数之和
    设计模式-备忘录模式
    设计模式-迭代器模式
    设计模式-中介者模式
    设计模式-观察者模式
    C# OpenFileDialog和SaveFileDialog的常见用法
    SQL数据库表结构的修改(sql2005)
    C# 时间格式处理
    C# 集合类(四)
  • 原文地址:https://www.cnblogs.com/shay/p/13215745.html
Copyright © 2011-2022 走看看