Java 虚拟机知识记录

Posted by zengchengjie on Monday, May 9, 2022

概念

  • 程序计数器

  • 本地方法栈

  • java堆

    对象实例,数组

  • java栈

    局部变量,栈操作,栈帧数据

  • 方法区

    类信息,常量,静态变量,运行时常量池

常用命令

  • 查看jdk位置

    /usr/libexec/java_home -V
    

    找到jdk路径:

    /Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home
    

    进入到对应文件夹

    in /Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home [16:39:06]
    $ ls
    ASSEMBLY_EXCEPTION       THIRD_PARTY_README       include                  readme.txt
    CLASSPATH_EXCEPTION_NOTE Welcome.html             jre                      release
    DISCLAIMER               bin                      lib                      sample
    LICENSE                  demo                     man                      src.zip
    

    进入到bin查看相关工具

    /Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/bin [16:39:54]
    $ ls
    appletviewer javac        jdb          jmap         jstatd       rmic         tnameserv
    extcheck     javadoc      jdeps        jps          keytool      rmid         unpack200
    idlj         javah        jfr          jrunscript   native2ascii rmiregistry  wsgen
    jar          javap        jhat         jsadebugd    orbd         schemagen    wsimport
    jarsigner    jcmd         jinfo        jstack       pack200      serialver    xjc
    java         jconsole     jjs          jstat        policytool   servertool
    

一、jdk自带命令

工具 类型 作用
jps 命令行 JVM进程状态工具,列出系统上的JVM进程
jinfo 命令行 JVM信息查看工具,查看JVM的各种配置信息
jvisualvm 图形界面 综合的JVM监控工具,查看JVM基本情况、做栈和堆转储、做内存和CPUprofiling等
jconsole 图形界面 JMX兼容的图形工具,用于监控JVM基本情况,查看MBean
jstat 命令行 JVM统计监控工具,附加到一个JVM进程
jstack 命令行 JVM栈查看工具,可以打印JVM进程的线程栈和锁情况
jcmd 命令行 JVM命令行调试工具,用于向JVM进程发送调试命令
jmap 命令行 JVM堆内存分析工具,可以打印JVM进程对象直方图、类加载统计,以及做堆转储操作

注:不同的jdk可能命令用法有些细微的不一致,建议使用前看看命令的说明

Jps

jps(Java Virtual Machine Process Status Tool)是JDK 1.5提供的一个显示当前所有java进程pid的命令,简单实用,非常适合在linux/unix平台上简单察看当前java进程的一些简单情况

  • jps -l

    查看Java进程的pid

    $ jps -l
    83683
    10819 /Applications/HBuilderX.app/Contents/HBuilderX/plugins/ls/ls.jar
    83799 sun.tools.jps.Jps
    83785 org.jetbrains.jps.cmdline.Launcher
    83723 org.jetbrains.idea.maven.server.RemoteMavenServer36
    83786 com.example.springbootdemo.SpringbootDemoApplication
    

    执行命令:jps -l,列出了所有Java进程,包括idea、HBuilderX等工具产生的Java进程,其中,83638这个空白的进程,其实也是属于idea的,我执行kill -9后idea会退出。而实际我启动的应用是83786 com.example.springbootdemo.SpringbootDemoApplication这个进程。

  • Jps -vl

    输出传递给JVM的参数

    执行命令,并过滤PID:

    $ jps -vl |grep 83786
    83786 com.example.springbootdemo.SpringbootDemoApplication -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:50666,suspend=y,server=n -XX:TieredStopAtLevel=1 -Xverify:none -Dspring.output.ansi.enabled=always -javaagent:/Users/zengchengjie/Library/Caches/JetBrains/IntelliJIdea2021.1/captureAgent/debugger-agent.jar -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dfile.encoding=UTF-8
    
    

jstat

使用 jstat 工具可以监测 Java 应用程序的实时运行情况,可以看到VM内的Eden、Survivor、老年代的内存使用情况,还有 YoungGC 和 FullGC 的执行次数以及耗时。通过这些指标,我们可以轻松的分析出当前系统的运行情况,判断当前系统的内存使用压力以及GC压力,还有内存分配是否合理。

用法:

$ jstat
invalid argument count
Usage: jstat -help|-options
       jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

Definitions:
  <option>      An option reported by the -options option
  <vmid>        Virtual Machine Identifier. A vmid takes the following form:
                     <lvmid>[@<hostname>[:<port>]]
                Where <lvmid> is the local vm identifier for the target
                Java virtual machine, typically a process id; <hostname> is
                the name of the host running the target Java virtual machine;
                and <port> is the port number for the rmiregistry on the
                target host. See the jvmstat documentation for a more complete
                description of the Virtual Machine Identifier.
  <lines>       Number of samples between header lines.
  <interval>    Sampling interval. The following forms are allowed:
                    <n>["ms"|"s"]
                Where <n> is an integer and the suffix specifies the units as
                milliseconds("ms") or seconds("s"). The default units are "ms".
  <count>       Number of samples to take before terminating.
  -J<flag>      Pass <flag> directly to the runtime system.

jstat的操作:

  • jstat -options

    $ jstat -options
    -class
    -compiler
    -gc
    -gccapacity
    -gccause
    -gcmetacapacity
    -gcnew
    -gcnewcapacity
    -gcold
    -gcoldcapacity
    -gcutil
    -printcompilation
    
    • -class:显示 ClassLoad 的相关信息;
    • -compiler:显示 JIT 编译的相关信息;
    • -gc:显示和 gc 相关的堆信息;
    • -gccapacity:显示各个代的容量以及使用情况;
    • -gcmetacapacity:显示 Metaspace 的大小;
    • -gcnew:显示新生代信息;
    • -gcnewcapacity:显示新生代大小和使用情况;
    • -gcold:显示老年代和永久代的信息;
    • -gcoldcapacity :显示老年代的大小;
    • -gcutil:显示垃圾收集信息;
    • -gccause:显示垃圾回收的相关信息(同 -gcutil),同时显示最后一次或当前正在发生的垃圾回收的诱因;
    • -printcompilation:输出 JIT 编译的方法信息

    其中 jstat -gc 是最完整、最常用、最实用的命令,基本足够分析jvm的运行情况了。

    那我们执行刚刚启动的进程83786看看

    $ jstat -gc 83786
     S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
    8192.0 10752.0 8007.7  0.0   116736.0 42372.5   83968.0     5493.6   27904.0 25870.5 3840.0 3415.2      4    0.016   1      0.016    0.032
    
  • 查看classload的相关信息

    命令:

    jstat -class <pid>
    

    用法

    $ jstat -class 83786
    Loaded  Bytes  Unloaded  Bytes     Time
      5769 10479.5        0     0.0       0.73
    
  • 查看内存使用和GC情况

    命令:

    jstat -class <pid> [<interval>[<count>]]
    

    用法:

    $ jstat -gc 83786 1000 10
     S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
    8192.0 10752.0 8007.7  0.0   116736.0 42372.5   83968.0     5493.6   27904.0 25870.5 3840.0 3415.2      4    0.016   1      0.016    0.032
    8192.0 10752.0 8007.7  0.0   116736.0 42372.5   83968.0     5493.6   27904.0 25870.5 3840.0 3415.2      4    0.016   1      0.016    0.032
    8192.0 10752.0 8007.7  0.0   116736.0 42372.5   83968.0     5493.6   27904.0 25870.5 3840.0 3415.2      4    0.016   1      0.016    0.032
    8192.0 10752.0 8007.7  0.0   116736.0 42372.5   83968.0     5493.6   27904.0 25870.5 3840.0 3415.2      4    0.016   1      0.016    0.032
    8192.0 10752.0 8007.7  0.0   116736.0 42372.5   83968.0     5493.6   27904.0 25870.5 3840.0 3415.2      4    0.016   1      0.016    0.032
    8192.0 10752.0 8007.7  0.0   116736.0 42372.5   83968.0     5493.6   27904.0 25870.5 3840.0 3415.2      4    0.016   1      0.016    0.032
    8192.0 10752.0 8007.7  0.0   116736.0 42372.5   83968.0     5493.6   27904.0 25870.5 3840.0 3415.2      4    0.016   1      0.016    0.032
    8192.0 10752.0 8007.7  0.0   116736.0 42372.5   83968.0     5493.6   27904.0 25870.5 3840.0 3415.2      4    0.016   1      0.016    0.032
    8192.0 10752.0 8007.7  0.0   116736.0 42372.5   83968.0     5493.6   27904.0 25870.5 3840.0 3415.2      4    0.016   1      0.016    0.032
    8192.0 10752.0 8007.7  0.0   116736.0 42372.5   83968.0     5493.6   27904.0 25870.5 3840.0 3415.2      4    0.016   1      0.016    0.032
    

    1000ms一次,打印10条信息

    • S0C:年轻代中 To Survivor 的容量(单位 KB);
    • S1C:年轻代中 From Survivor 的容量(单位 KB);
    • S0U:年轻代中 To Survivor 目前已使用空间(单位 KB);
    • S1U:年轻代中 From Survivor 目前已使用空间(单位 KB);
    • EC:年轻代中 Eden 的容量(单位 KB);
    • EU:年轻代中 Eden 目前已使用空间(单位 KB);
    • OC:老年代的容量(单位 KB);
    • OU:老年代目前已使用空间(单位 KB);
    • MC:Metaspace 的容量(单位 KB);
    • MU:Metaspace 目前已使用空间(单位 KB);
    • CCSC:压缩类空间大小
    • CCSU:压缩类空间使用大小
    • YGC:从应用程序启动到采样时年轻代中 gc 次数;
    • YGCT:从应用程序启动到采样时年轻代中 gc 所用时间 (s);
    • FGC:从应用程序启动到采样时 old 代(全 gc)gc 次数;
    • FGCT:从应用程序启动到采样时 old 代(全 gc)gc 所用时间 (s);
    • GCT:从应用程序启动到采样时 gc 用的总时间 (s)

jmap查看对象分布情况

使用 jmap 可查看堆内存初始化配置信息以及堆内存的使用情况,输出堆内存中的对象信息,包括产生了哪些对象,对象数量多少等。

  • jmap -heap 查看堆内存情况

    这个命令会打印出堆内存相关的一些参数设置以及各个区域的情况,要查看这些信息一般使用 jstat 命令就足够了。

    不过我的jdk版本并没有-heap选项,可能是版本的差异,因此堆内存这块用jstat命令代替

  • map -histo[:live] 查看系统运行时对象分布

    带上 live 则只统计活对象

    jmap -histo:live 83786

    截取部分结果:

    num     #instances         #bytes  class name
    ----------------------------------------------
       1:         27063        2478472  [C
       2:          6194         685192  java.lang.Class
       3:         27029         648696  java.lang.String
       4:         12950         414400  java.util.concurrent.ConcurrentHashMap$Node
       5:          5981         304008  [Ljava.lang.Object;
       ...
       2476:             1             16  sun.util.locale.provider.SPILocaleProviderAdapter
       2477:             1             16  sun.util.locale.provider.TimeZoneNameUtility$TimeZoneNameGetter
       2478:             1             16  sun.util.resources.LocaleData
       2479:             1             16  sun.util.resources.LocaleData$LocaleDataResourceBundleControl
      Total        161335        7706160
    

    不带live参数:

    jmap -histo 83786

    部分结果:

     num     #instances         #bytes  class name
    ----------------------------------------------
       1:         40619       17788456  [B
       2:          9052       15091912  [I
       3:         85965       10604536  [C
       4:         56158        1347792  java.lang.String
       5:         40443        1294176  java.util.concurrent.locks.AbstractQueuedSynchronizer$Node
       6:         11222         987536  java.lang.reflect.Method
       7:         13388         749728  java.util.concurrent.ConcurrentHashMap$KeyIterator
       ...
       3064:             1             16  	 sun.util.locale.provider.TimeZoneNameUtility$TimeZoneNameGetter
       3065:             1             16  sun.util.resources.LocaleData
       3066:             1             16  sun.util.resources.LocaleData$LocaleDataResourceBundleControl
    Total        633245       61000088
    

    jmap -histo 这个命令会按照各种对象占用内存空间的大小降序排列,把占用内存最多的对象放在最上面。通过这个命令可以简单的了解下当前jvm中的对象对内存占用的情况以及当前内存里到底是哪个对象占用了大量的内存空间。

  • 生成堆内存转储快照

    jmap -dump:format=b,file=<path> <pid>
    # 或者
    jmap -dump:live,format=b,file=<path> <pid>
    

    jmap -dump 是输出堆中所有对象;jmap -dump:live 是输出堆中所有活着的对象,而且 jmap -dump:live 会触发 FullGC,线上使用要注意。

    format=b 是以二进制格式输出;file 是文件路径,格式为 hrpof 后缀。

    这个命令会在当前目录下生成一个 dump.hrpof 文件,这是个二进制的格式,无法直接打开,可以使用MAT等工具来分析。这个命令把这一时刻VM堆内存里所有对象的快照放到文件里去了,供你后续去分析。

    $ jmap -dump:format=b,file=/Users/zengchengjie/Documents/heap.hprof 83786
    Dumping heap to /Users/zengchengjie/Documents/heap.hprof ...
    Heap dump file created
    

    该二进制文件直接打开乱码:

jstack 分析线程栈

查看jstack命令用法:

$ jstack
Usage:
    jstack [-l] <pid>
        (to connect to running process)

Options:
    -l  long listing. Prints additional information about locks
    -h or -help to print this help message

打印结果(部分):

$ jstack -l 83786
2022-05-27 11:35:12
Full thread dump OpenJDK 64-Bit Server VM (25.312-b07 mixed mode):

"RMI Scheduler(0)" #36 daemon prio=5 os_prio=31 tid=0x000000012e81d000 nid=0x9103 waiting on condition [0x00000001745ce000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x00000006c03fb3f8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2044)
	at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1081)
	at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)
	at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
	- None

"Attach Listener" #34 daemon prio=9 os_prio=31 tid=0x000000012c20c000 nid=0x9203 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
	- None
...
  • pool-11-thread-6:线程名称
  • #1920:线程编号
  • prio=5:线程的优先级别
  • os_prio=0:系统级别的线程优先级
  • tid=0x00007f87e028c000:线程ID
  • nid=0x6724:native线程的id,通过 printf “%x\n” 命令转换线程ID
  • waiting on condition [0x00007f87b97d2000]:线程当前的状态

Jstack实际案例

#查看自己的Java进程
jps -l
#查看进程中占用CPU高的线程,并记录
top -Hp <pid>
#打印线程栈信息
jstack -l <pid> >jstack.log
#将找到的线程ID转换成16进制,比如20转成16进制是14
printf "%x\n" 20
#去jstack.log中找到nid=0x14的线程信息,开始做下一步分析

二、linux命令行工具

主要使用top、vmstat、pidstat这几个命令,他们都是常用的命令,就不多赘述,摘抄一些网上的概念以作备忘:

top命令

top 命令是我们在 Linux 下最常用的命令之一,它可以实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载等信息。其中上半部分显示的是系统的统计信息,下半部分显示的是进程的使用率统计信息。

img

看第一行:主要展示了CPU的负载情况

  • 23:22:23:指的是当前时间
  • up 12 days, 12::18:指的是机器已经运行了多长时间
  • 1 user:当前机器有一个用户在使用
  • load average: 0.19, 0.27, 0.30:指 CPU 在1分钟、5分钟、15分钟内的负载情况。

最重要的就是看 load average,比如机器是4核CPU,那么 0.19、0.27、0.30,说明4核中连一个核都没用满,4核CPU基本很空闲。如果CPU负载是1,说明有1个核被使用的比较繁忙了。如果负载是4,说明4核CPU都跑满了;如果超过4,说明4核CPU被繁忙的使用还不够处理当前的任务,很多进程可能一直在等待CPU去执行自己的任务。

vmstat命令

vmstat 是 Virtual Meomory Statistics(虚拟内存统计)的缩写,可对操作系统的虚拟内存、进程、CPU活动进行监控。

命令格式:【vmstat [ 选项 ] [ <时间间隔> ] [ <次数> ]】

img

字段说明:

  • Procs(进程):
    • r:等待运行的进程数
    • b:处于非中断睡眠状态的进程数
  • Memory(内存,单位Kb):
    • swpd:虚拟内存使用情况
    • free:空闲的内存
    • buff:用来作为缓冲的内存数
    • cache:用作缓存的内存大小
  • Swap(交换区):
    • si:从磁盘交换到内存的交换页数量
    • so:从内存交换到磁盘的交换页数量
  • IO:(现在的Linux版本块的大小为1024bytes)
    • bi:发送到块设备的块数
    • bo:从块设备接收到的块数
  • System(系统):
    • in:每秒中断数,包括时钟中断。【interrupt】
    • cs:每秒上下文切换数。【count/second】
  • CPU(以百分比表示):
    • us:用户 CPU 使用时间(user time)
    • sy:内核 CPU 系统使用时间 (system time)
    • id:空闲时间(包括IO等待时间),中央处理器的空闲时间 。以百分比表示。
    • wa:等待IO时间

判断指标:

  • 如果 r 经常大于4,id 经常少于40,表示cpu的负荷很重。
  • 如果 bi,bo 长期不等于0,表示内存不足。
  • 如果 disk 经常不等于0,且在 b 中的队列大于3,表示io性能不好。
  • 通过 cs 观察 Java 程序运行过程中系统的上下文切换频率。过高说明程序创建了过多的线程导致频繁的上下文切换。

pidstat命令

如果是监视某个应用的上下文切换,可以使用 pidstat 命令监控指定进程的上下文切换。

pidstat 是 Sysstat 中的一个组件,也是一款功能强大的性能监测工具,我们可以通过命令:yum install sysstat 安装该监控组件。top 和 vmstat 两个命令都是监测进程的内存、CPU 以及 I/O 使用情况,而 pidstat 命令则是深入到线程级别。

命令格式

pidstat [ 选项 ] [ <时间间隔> ] [ <次数>

1)常用的选项:

  • -u:默认的参数,显示各个进程的 cpu 使用情况
  • -r:显示各个进程的内存使用情况
  • -d:显示各个进程的 I/O 使用情况
  • -p:指定进程号
  • -w:显示每个进程的上下文切换情况
  • -t:显示进程中线程的统计信息
  • -T { TASK | CHILD | ALL }
    • TASK表示报告独立的task,CHILD关键字表示报告进程下所有线程统计信息。ALL表示报告独立的task和task下面的所有线程。
    • 注意:task和子线程的全局的统计信息和pidstat选项无关。这些统计信息不会对应到当前的统计间隔,这些统计信息只有在子线程kill或者完成的时候才会被收集。
  • -V:版本号
  • -h:在一行上显示了所有活动,这样其他程序可以容易解析。
  • -I:在SMP环境,表示任务的CPU使用率/内核数量
  • -l:显示命令名和所有参数

三、可视化工具

jvisualVM工具

我的jdk里没有这个工具,参考这篇文章后发现需去jvisualvm官网下载一个jvisualvm工具

于是点击下载-安装-打开,点击你的项目,如图所示

在左边选择需要监控的程序,右边就可以可查看CPU、堆、线程等波动情况,也可以直接在这里进行手动 GC 和堆 Dump 操作。

线程面板:

可看到所有的线程,以及线程的运行状态。点击面板的线程 Dump 按钮,可以查看线程瞬时的线程栈。(通过 jstack 也可以抓取线程栈)

在插件列表里可以安装对应的插件

GC面板:

可以很方便的看到GC趋势图。(也可使用 jstat 工具监控)

分析堆存储快照

点击file-load加载刚刚使用jmap -dump命令生成的heap.hprof文件,加载堆转储快照

其他更多插件用法可以自行搜索并试用

GCViewver-离线分析GC日志

GCViewer 可以离线查看GC日志,下载地址为 https://github.com/chewiebug/GCViewer。下载下来之后执行 [mvn clean install -Dmaven.test.skip=true] 命令打包编译,编译完成后在target目录下会看到jar包,然后在命令行运行这个jar包就可以启动 GCViewer。

然后通过 File 打开GC日志文件,就可以看到GC统计图和GC情况。通过工具,我们可以看到吞吐量、停顿时间以及 GC 的频率等信息,从而可以非常直观地了解到 GC 的性能情况。

GCeasy-在线分析GC日志

GCeasy 是一款在线版的非常直观的 GC 日志分析工具,我们可以将日志文件压缩之后,上传到 GCeasy 官网即可看到非常清楚的 GC 日志分析结果。

FastThread-分析线程栈

线程栈使用 jstack 命令 dump 下来后,可以使用 FastThread 在线工具分析线程栈信息,可以直观的看到有多少线程、线程池、线程的状态、是否有死锁等信息。

MAT-分析堆转储文件

我们使用 jmap 命令 dump 下来的堆转储文件可以使用 MAT 来分析,可以分析创建了哪些对象,然后分析对象的引用链,找出问题所在。MAT 下载地址:http://www.eclipse.org/mat/downloads.php

MAT 主要功能:

  • 找出内存泄漏的原因
  • 找出重复引用的类和jar
  • 分析集合的使用
  • 分析类加载器

其他在线工具地址

笨马网络(perfma)的在线工具:

线程 Dump 分析(PerfMa):https://thread.console.perfma.com/

内存 Dump 分析(PerfMa):https://memory.console.perfma.com/

JVM 参数分析(PerfMa):https://opts.console.perfma.com/

阿里的Arthas(阿尔萨斯):

Java 诊断(Arthas):https://alibaba.github.io/arthas/

介绍:

Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。

当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

  1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  5. 是否有一个全局视角来查看系统的运行状况?
  6. 有什么办法可以监控到JVM的实时运行状态?
  7. 怎么快速定位应用的热点,生成火焰图?
  8. 怎样直接从JVM内查找某个类的实例?

Arthas支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。

调优工具选择

调优的工具很多,一般对于线上系统,使用 jstat 工具足以分析出JVM的运行情况,如果有GC日志,也可以使用GCeasy快速分析JVM的运行情况。

遇到CPU负载过高,可以使用 top + pidstat 找出负载高的线程,或者直接使用 jstat 观察是不是在频繁FullGC。

遇到死锁、OOM等问题,可以用 jstack 把线程栈dump下来分析,还可以结合 FastThread 在线工具,分析线程栈、哪些线程阻塞等待等。

遇到OOM问题,使用 jmap 把堆转储快照dump下来,用MAT来分析创建了哪些大量的对象。

JVM监控平台

系统上线后,如果不部署可视化的监控平台,我们一般就通过上面的这些工具来分析JVM的内存运转模型、GC情况等,可以在机器上运行jstat,让其把监控信息写入一个文件,每天定时检查—下。

监控平台则可以通过可视化的界面看到JVM各个区域的内存变化,GC次数和GC耗时等信息,以及出现性能问题时能及时报警。

一般可以部署 Zabbix、Ganglia、Open-Falcon、Prometheus 之类的可视化监控平台,把线上系统接入到这些平台,就可以直接图形化看到JVM的表现。

实践jvm参数调优

新建示例代码,这段代码模拟每秒钟在新生代创建20M对象,1秒之后就变为垃圾对象了:

public class GCMain {
    static final int _1M = 1024 * 1024;

    public static void main(String[] args) {
        sleep(20);

        for (int i = 0; i < 100; i++) {
            loadData(i);
        }
    }

    // loadData 每次请求产生20M对象,每次请求耗时1秒
    public static void loadData(int index) {
        System.out.println("load data: " + index);
        byte[] data1 = new byte[_1M * 10];
        byte[] data2 = new byte[_1M * 10];

        sleep(1);
    }

    public static void sleep(long seconds) {
        try {
            Thread.sleep(seconds * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

配置jvm参数

idea中可以在help菜单设置全局jvm参数,或者run的菜单里配置某个项目的jvm参数,这里我为这个项目单独配置jvm参数:

我们先拿自带的默认jvm参数看看,点击help-edit custom vmoptions:

具体的参数示意参考阿里云产品文档

-Xms128m
-Xmx2048m
-XX:ReservedCodeCacheSize=512m
-XX:+UseG1GC
-XX:SoftRefLRUPolicyMSPerMB=50
-XX:CICompilerCount=2
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow
-ea
-Dsun.io.useCanonCaches=false
-Djdk.http.auth.tunneling.disabledSchemes=""
-Djdk.attach.allowAttachSelf=true
-Djdk.module.illegalAccess.silent=true
-Dkotlinx.coroutines.debug=off

-XX:ErrorFile=$USER_HOME/java_error_in_idea_%p.log
-XX:HeapDumpPath=$USER_HOME/java_error_in_idea.hprof

JVM参数:新生代、老年代各100M,Eden区80M,Survivor区10M,大对象阀值20M。

# 设置最大堆大小。
# -Xmx3550m,设置JVM最大可用内存为3550 MB。
-Xms200M
# 设置JVM初始内存。	
# -Xms3550m,设置JVM初始内存为3550 MB。此值建议与-Xmx相同,避免每次垃圾回收完成后JVM重新分配内存
-Xmx200M
# 设置年轻代大小。	
# -Xmn2g,设置年轻代大小为2 GB。整个JVM内存大小=年轻代大小+年老代大小+持久代大小。持久代一般固定大小为64 MB,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xmn100M
# 设置年轻代中Eden区与Survivor区的大小比值。如果设置为4,那么两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6。
-XX:SurvivorRatio=8
# 设置垃圾最大年龄。
# 如果设置为0,那么年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,提高了效率。
# 如果将此值设置为较大值,那么年轻代对象会在Survivor区进行多次复制,增加了对象在年轻代的存活时间,增加在年轻代即被回收的概率。
-XX:MaxTenuringThreshold=5
# 
-XX:PretenureSizeThreshold=20M
# 设置年轻代为并行收集。可与CMS收集同时使用。JDK 5.0以上版本,JVM根据系统配置自行设置,无需再设置此值
-XX:+UseParNewGC
# 设置年老代为并发收集。说明 配置了-XX:+UseConcMarkSweepGC,建议年轻代大小使用-Xmn设置。
-XX:+UseConcMarkSweepGC
# 
-XX:CMSInitiatingOccupancyFraction=92
# 
-XX:+UseCMSInitiatingOccupancyOnly
# 打开对年老代的压缩。该值可能会影响性能,但是可以消除碎片。
-XX:+UseCMSCompactAtFullCollection
# 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:CMSFullGCsBeforeCompaction=0
# 用于输出GC日志
-XX:+PrintGC
# 用于输出GC日志
-XX:+PrintGCDetails
# 用于输出GC时间戳(日期形式)
-XX:+PrintGCDateStamps
# 日志文件的输出路径
-Xloggc:./gc.log