java(10)-JVM性能监控和优化

2022-04-14 16:56:05 浏览数 (1)

常用命令: jstack pid 堆栈信息 jstat -gcutil pid 1000 间隔1000ms采样GC信息 jmap -heap pid打印jvm heap的情况 jmap -histo pid 打印jvm heap的直方图。其输出信息包括类名,对象数量,对象占用大小。 jmap -histo:live pid 同上,但是只打印存活对象的情况。

一、JVM监控

1、GC监控

垃圾回收收集监控指的是搞清楚JVM如何执行GC的过程,例如,我们可以查明:

  1. 何时一个新生代中的对象被移动到老年代时,所花费的时间。
  2. Stop-the-world 何时发生的,持续了多长时间。

GC监控是为了鉴别JVM是否在高效地执行GC,以及是否有必要进行额外的性能调优。基于以上信息,我们可以修改应用程序或者调整GC算法(GC优化)。

查看gc信息 -Dapollo.cluster=prod -Dlog4j.configuration=prod_log.properties -Xmx3g -Xms3g -Xmn1280m -Xss256K -XX:MetaspaceSize=256M -XX: PrintGCDetails -XX: PrintGCDateStamps -XX: PrintTenuringDistribution -XX: PrintGCApplicationStoppedTime -Xloggc:/mnt/logs/market-openapi-v2-controll/controlGc.log -XX: UseGCLogFileRotation -XX:NumberOfGCLogFiles=32 -XX:GCLogFileSize=64m

2、常用监控工具(Garbage Collect)

监控工具帮助我们在运行时或问题发生后分析现场,分析内存分布状态,哪里导致内存泄漏等(本该被释放的对象仍然被引用)。

1)、命令行工具:

JDK内置工具使用,在bin目录下:

一、javah命令(C Header and Stub File Generator) 二、jps命令(Java Virtual Machine Process Status Tool) 三、jstack命令(Java Stack Trace) 四、jstat命令(Java Virtual Machine Statistics Monitoring Tool) 五、jmap命令(Java Memory Map) 六、jinfo命令(Java Configuration Info) 七、jconsole命令(Java Monitoring and Management Console) 八、jvisualvm命令(Java Virtual Machine Monitoring, Troubleshooting, and Profiling Tool) 九、jhat命令(Java Heap Analyse Tool) 十、Jdb命令(The Java Debugger) 十一、Jstatd命令(Java Statistics Monitoring Daemon)

1、JPS 查看进程号

● jps:即java版的ps,可以查看当前用户启动了哪些java进程。jps -l 或者jps -lv

2、jinfo查看进程号

初次使用jinfo和jmap时出现以下错误:

引用

Attaching to process ID 3538, please wait... Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process

解决方法: 1. echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope 该方法在下次重启前有效。 2. 永久有效方法 sudo vi /etc/sysctl.d/10-ptrace.conf 编辑下面这行: kernel.yama.ptrace_scope = 1 修改为: kernel.yama.ptrace_scope = 0 重启系统,使修改生效。

该命令可以打印出Java进程的配置信息:包括jvm参数,系统属性等

代码语言:javascript复制
#指定进程号(pid)的进程
jinfo [ option ] pid
#指定核心文件
jinfo [ option ] <executable <core>
#指定远程调试服务器
jinfo [ option ] [server-id@]<remote-hostname-or-IP>
jinfo [ option ] pid
#指定核心文件
jinfo [ option ] <executable <core>
#指定远程调试服务器
jinfo [ option ] [server-id@]<remote-hostname-or-IP>

option

选项参数是互斥的(不可同时使用)。想要使用选项参数,直接跟在命令名称后即可。

pid

需要打印配置信息的进程ID。该进程必须是一个Java进程。想要获取运行的Java进程列表,你可以使用jps。

executable

产生核心dump的Java可执行文件。

core

需要打印配置信息的核心文件。

remote-hostname-or-IP

远程调试服务器的(请查看jsadebugd)主机名或IP地址。

server-id

可选的唯一id,如果相同的远程主机上运行了多台调试服务器,用此选项参数标识服务器。

描述

jinfo用于打印指定Java进程、核心文件或远程调试服务器的Java配置信息。配置信息包括Java系统属性、Java虚拟机命令行标识参数。如果给定的进程运行于64位的虚拟机上,你可能需要使用指定-J-d64选项,例如:

代码语言:javascript复制
jinfo -J-d64 -sysprops pid-J-d64 -sysprops pid

注意 - 此工具是不受支持的,不确定在未来版本的JDK中是否可用。在不存在dbgeng.dll的Windows系统中,需要安装'Windows调试工具',才能让这些工具工作。另外, PATH环境变量应该包含目标进程或Crash Dump文件产生目录使用的jvm.dll的路径。 例如, set PATH=<jdk>jrebinclient;%PATH%

选项

<no option>

打印命令行标识参数和系统属性键值对。

-flag name

打印指定的命令行标识参数的名称和值。

-flag [ |-]name

启用或禁用指定的boolean类型的命令行标识参数。

-flag name=value

为给定的命令行标识参数设置指定的值。

-flags

成对打印传递给JVM的命令行标识参数。

-sysprops

以键值对形式打印Java系统属性。虚拟机进程的System.getProperties()的内容打印出来:jinfo -sysprops pid

-h

打印帮助信息。

-help

打印帮助信息。

下面重点介绍gc,内存、堆栈相关工具

二、jstat 间隔采样监控信息:GC,类加载,jit信息


Jstat是JDK自带的一个轻量级小工具。全称“Java Virtual Machine statistics monitoring tool”,主要利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控。可见,Jstat是轻量级的、专门针对JVM的工具,非常适用。

jstat是一个多种用途的工具,更多需要man jstat或直接输入jstat查看提示。利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控。查看GC操作的信息,类装载操作的信息以及运行时编译器操作的信息

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]] (如 jstat -gcutil pid 100 10) pid指jps命令查看的java进程号。

假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是 :jstat-gc 2764 250 20 或者 $ jstat -gcutil 17551 100 10

Warning: Unresolved Symbol: sun.gc.metaspace.capacity substituted NaN Warning: Unresolved Symbol: sun.gc.metaspace.used substituted NaN Warning: Unresolved Symbol: sun.gc.metaspace.capacity substituted NaN Warning: Unresolved Symbol: sun.gc.compressedclassspace.capacity substituted NaN Warning: Unresolved Symbol: sun.gc.compressedclassspace.used substituted NaN Warning: Unresolved Symbol: sun.gc.compressedclassspace.capacity substituted NaN S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 90.63 0.00 85.26 61.38 - - 75134 394.921 280 45.762 440.683

jstat1.8 jstat1.7的输出不同,jstat 1.7 有 P这个项目表示Permanent space utilization as a percentage of the space‘s current capacity.

jstat1.8 没有P这个项目,而是有个M和CCS两个项目分别表示。

使用/usr/java/bin/jstat -gcutil 17551 100 10就可以。

可以列出当前JVM版本支持的选项,常见的有

  • l class (类加载器)
  • l compiler (JIT)
  • l gc (GC堆状态)
  • l gccapacity (各区大小)
  • l gccause (最近一次GC统计和原因)
  • l gcnew (新区统计)
  • l gcnewcapacity (新区大小)
  • l gcold (老区统计)
  • l gcoldcapacity (老区大小)
  • l gcpermcapacity (永久区大小)
  • l gcutil (GC统计汇总)
  • l printcompilation (HotSpot编译统计)

不同的jstat参数输出不同类型的列,如下表所示,根据你使用的”jstat option”会输出不同列的信息。

说明

Jstat参数

S0C

输出Survivor0空间的大小。单位KB。

-gc -gccapacity -gcnew -gcnewcapacity

S1C

输出Survivor1空间的大小。单位KB。

-gc -gccapacity -gcnew -gcnewcapacity

S0U

输出Survivor0已用空间的大小。单位KB。

-gc -gcnew

S1U

输出Survivor1已用空间的大小。单位KB。

-gc -gcnew

EC

输出Eden空间的大小。单位KB。

-gc -gccapacity -gcnew -gcnewcapacity

EU

输出Eden已用空间的大小。单位KB。

-gc -gcnew

OC

输出老年代空间的大小。单位KB。

-gc -gccapacity -gcold -gcoldcapacity

OU

输出老年代已用空间的大小。单位KB。

-gc -gcold

PC

输出持久代空间的大小。单位KB。

-gc -gccapacity -gcold -gcoldcapacity -gcpermcapacity

PU

输出持久代已用空间的大小。单位KB。

-gc -gcold

YGC

新生代空间GC时间发生的次数。

-gc -gccapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause

YGCT

新生代GC处理花费的时间。

-gc -gcnew -gcutil -gccause

FGC

full GC发生的次数。

-gc -gccapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause

FGCT

full GC操作花费的时间

-gc -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause

GCT

GC操作花费的总时间。

-gc -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause

NGCMN

新生代最小空间容量,单位KB。

-gccapacity -gcnewcapacity

NGCMX

新生代最大空间容量,单位KB。

-gccapacity -gcnewcapacity

NGC

新生代当前空间容量,单位KB。

-gccapacity -gcnewcapacity

OGCMN

老年代最小空间容量,单位KB。

-gccapacity -gcoldcapacity

OGCMX

老年代最大空间容量,单位KB。

-gccapacity -gcoldcapacity

OGC

老年代当前空间容量制,单位KB。

-gccapacity -gcoldcapacity

PGCMN

持久代最小空间容量,单位KB。

-gccapacity -gcpermcapacity

PGCMX

持久代最大空间容量,单位KB。

-gccapacity -gcpermcapacity

PGC

持久代当前空间容量,单位KB。

-gccapacity -gcpermcapacity

PC

持久代当前空间大小,单位KB

-gccapacity -gcpermcapacity

PU

持久代当前已用空间大小,单位KB

-gc -gcold

LGCC

最后一次GC发生的原因

-gccause

GCC

当前GC发生的原因

-gccause

TT

老年化阈值。被移动到老年代之前,在新生代空存活的次数。

-gcnew

MTT

最大老年化阈值。被移动到老年代之前,在新生代空存活的次数。

-gcnew

DSS

幸存者区所需空间大小,单位KB。

-gcnew

1、jstat –class<pid> : 显示加载class的数量,及所占空间等信息。

Loaded:装载的类的数量 Bytes:装载类所占用的字节数 Unloaded:卸载类的数量 Bytes:卸载类的字节数 Time:装载和卸载类所花费的时间

2、jstat -compiler <pid>显示VM实时编译的数量等信息。

Compiled:编译任务执行数量 Failed:编译任务执行失败数量 Invalid :编译任务执行失效数量 Time :编译任务消耗时间 FailedType:最后一个编译失败任务的类型 FailedMethod:最后一个编译失败任务所在的类及方法

三、jmap查看内存状况

Jmap是一个可以输出所有内存中对象的工具,甚至可以将VM 中的heap,以二进制输出成文本。打印出某个Java进程(使用pid)内存内的,所有‘对象’的情况(如:产生那些对象,及其数量)。

使用方法 jmap -histo pid。如果使用SHELL ,可采用jmap -histo pid>a.log日志将其保存到文件中,在一段时间后,使用文本对比工具,可以对比出GC回收了哪些对象。jmap -dump:format=b,file=outfile 3024可以将3024进程的内存heap输出出来到outfile文件里,再配合MAT(内存分析工具)。

64位机上使用需要使用如下方式:

jmap -J-d64 -heap pid

jmap常用参数:

-heap:打印jvm heap的情况   -histo:打印jvm heap的直方图。其输出信息包括类名,对象数量,对象占用大小。   -histo:live :同上,但是只打印存活对象的情况。   -permstat:打印permanent generation heap情况。

-heap 打印heap的概要信息,GC使用的算法,heap的配置及wise heap的使用情况.

1、查看java内存信息jmap –heap

$jmap –heap 3772

代码语言:javascript复制
sing parallel threads in the new generation.  ##新生代采用的是并行线程处理方式using thread-local object allocation.   
Concurrent Mark-Sweep GC   ##同步并行垃圾回收,也可能是这样:Parallel GC with 4 thread(s) 
Heap Configuration:  ##堆初始化配置情况
   MinHeapFreeRatio = 40 ##最小堆使用比例:对应jvm启动参数-XX:MinHeapFreeRatio设置JVM堆最小空闲比率(default 40)
   MaxHeapFreeRatio = 70 ##最大堆可用比例:对应jvm启动参数 -XX:MaxHeapFreeRatio设置JVM堆最大空闲比率(default 70)
   MaxHeapSize      = 2147483648 (2048.0MB) ##最大堆空间大小:对应jvm启动参数-Xmx(-XX:MaxHeapSize=)设置JVM堆的最大大小
   NewSize          = 268435456 (256.0MB) ##新生代分配大小:对应jvm启动参数-XX:NewSize=设置JVM堆的‘新生代’的默认大小
   MaxNewSize       = 268435456 (256.0MB) ##最大可新生代分配大小:对应jvm启动参数-XX:MaxNewSize=设置JVM堆的‘新生代’的最大大小
   OldSize          = 5439488 (5.1875MB) ##老生代大小:对应jvm启动参数-XX:OldSize=<value>:设置JVM堆的‘老生代’的大小
   NewRatio         = 2  ##新生代比例:对应jvm启动参数-XX:NewRatio=:‘新生代’和‘老生代’的大小比率
   SurvivorRatio    = 8 ##新生代与suvivor的比例:对应jvm启动参数-XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值 
   PermSize         = 134217728 (128.0MB) ##perm区大小:对应jvm启动参数-XX:PermSize=<value>:设置JVM堆的‘永生代’的初始大小
   MaxPermSize      = 134217728 (128.0MB) ##最大可分配perm区大小:对应jvm启动参数-XX:MaxPermSize=<value>:设置JVM堆的‘永生代’的最大大小

Heap Usage: ##堆使用情况:堆内存分步
   New Generation (Eden   1 Survivor Space):  ##新生代(伊甸区   survior空间)
   capacity = 241631232 (230.4375MB)  ##伊甸区容量
   used     = 77776272 (74.17323303222656MB) ##已经使用大小
   free     = 163854960 (156.26426696777344MB) ##剩余容量
   32.188004570534986% used ##使用比例

Eden Space:  ##Eden区内存分布
   capacity = 214827008 (204.875MB) ##Eden区总容量
   used     = 74442288 (70.99369812011719MB) ##Eden区已使用
   free     = 140384720 (133.8813018798828MB) ##Eden区剩余容量
   34.65220164496263% used ##Eden区使用比率

From Space: ##survior1区
   capacity = 26804224 (25.5625MB) ##survior1区容量
   used     = 3333984 (3.179534912109375MB) ##surviror1区已使用情况
   free     = 23470240 (22.382965087890625MB) ##surviror1区剩余容量
   12.43827838477995% used ##survior1区使用比例

To Space: ##survior2 区
   capacity = 26804224 (25.5625MB) ##survior2区容量
   used     = 0 (0.0MB) ##survior2区已使用情况
   free     = 26804224 (25.5625MB) ##survior2区剩余容量
   0.0% used ## survior2区使用比例
 PS Old Generation  //当前的Old区内存分布
       capacity = 366280704 (349.3125MB)
       used     = 322179848 (307.25464630126953MB)
       free     = 44100856 (42.05785369873047MB)
        87.95982001825573% used
 PS Perm Generation  //当前的 “永生代” 内存分布
       capacity = 32243712 (30.75MB)
       used     = 28918584 (27.57891082763672MB)
       free     = 3325128 (3.1710891723632812MB)
       89.68751488662348% used
concurrent mark-sweep generation: ##老生代使用情况
   capacity = 1879048192 (1792.0MB) ##老生代容量
   used     = 30847928 (29.41887664794922MB) ##老生代已使用容量
   free     = 1848200264 (1762.5811233520508MB) ##老生代剩余容量
   1.6416783843721663% used ##老生代使用比例

Perm Generation: ##perm区使用情况
   capacity = 134217728 (128.0MB) ##perm区容量
   used     = 47303016 (45.111671447753906MB) ##perm区已使用容量
   free     = 86914712 (82.8883285522461MB) ##perm区剩余容量
   35.24349331855774% used ##perm区使用比例
初始化配置情况
   MinHeapFreeRatio = 40 ##最小堆使用比例:对应jvm启动参数-XX:MinHeapFreeRatio设置JVM堆最小空闲比率(default 40)
   MaxHeapFreeRatio = 70 ##最大堆可用比例:对应jvm启动参数 -XX:MaxHeapFreeRatio设置JVM堆最大空闲比率(default 70)
   MaxHeapSize      = 2147483648 (2048.0MB) ##最大堆空间大小:对应jvm启动参数-XX:MaxHeapSize=设置JVM堆的最大大小
   NewSize          = 268435456 (256.0MB) ##新生代分配大小:对应jvm启动参数-XX:NewSize=设置JVM堆的‘新生代’的默认大小
   MaxNewSize       = 268435456 (256.0MB) ##最大可新生代分配大小:对应jvm启动参数-XX:MaxNewSize=设置JVM堆的‘新生代’的最大大小
   OldSize          = 5439488 (5.1875MB) ##老生代大小:对应jvm启动参数-XX:OldSize=<value>:设置JVM堆的‘老生代’的大小
   NewRatio         = 2  ##新生代比例:对应jvm启动参数-XX:NewRatio=:‘新生代’和‘老生代’的大小比率
   SurvivorRatio    = 8 ##新生代与suvivor的比例:对应jvm启动参数-XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值 
   PermSize         = 134217728 (128.0MB) ##perm区大小:对应jvm启动参数-XX:PermSize=<value>:设置JVM堆的‘永生代’的初始大小
   MaxPermSize      = 134217728 (128.0MB) ##最大可分配perm区大小:对应jvm启动参数-XX:MaxPermSize=<value>:设置JVM堆的‘永生代’的最大大小

Heap Usage: ##堆使用情况:堆内存分步
   New Generation (Eden   1 Survivor Space):  ##新生代(伊甸区   survior空间)
   capacity = 241631232 (230.4375MB)  ##伊甸区容量
   used     = 77776272 (74.17323303222656MB) ##已经使用大小
   free     = 163854960 (156.26426696777344MB) ##剩余容量
   32.188004570534986% used ##使用比例

Eden Space:  ##Eden区内存分布
   capacity = 214827008 (204.875MB) ##Eden区总容量
   used     = 74442288 (70.99369812011719MB) ##Eden区已使用
   free     = 140384720 (133.8813018798828MB) ##Eden区剩余容量
   34.65220164496263% used ##Eden区使用比率

From Space: ##survior1区
   capacity = 26804224 (25.5625MB) ##survior1区容量
   used     = 3333984 (3.179534912109375MB) ##surviror1区已使用情况
   free     = 23470240 (22.382965087890625MB) ##surviror1区剩余容量
   12.43827838477995% used ##survior1区使用比例

To Space: ##survior2 区
   capacity = 26804224 (25.5625MB) ##survior2区容量
   used     = 0 (0.0MB) ##survior2区已使用情况
   free     = 26804224 (25.5625MB) ##survior2区剩余容量
   0.0% used ## survior2区使用比例
 PS Old Generation  //当前的Old区内存分布
       capacity = 366280704 (349.3125MB)
       used     = 322179848 (307.25464630126953MB)
       free     = 44100856 (42.05785369873047MB)
        87.95982001825573% used
 PS Perm Generation  //当前的 “永生代” 内存分布
       capacity = 32243712 (30.75MB)
       used     = 28918584 (27.57891082763672MB)
       free     = 3325128 (3.1710891723632812MB)
       89.68751488662348% used
concurrent mark-sweep generation: ##老生代使用情况
   capacity = 1879048192 (1792.0MB) ##老生代容量
   used     = 30847928 (29.41887664794922MB) ##老生代已使用容量
   free     = 1848200264 (1762.5811233520508MB) ##老生代剩余容量
   1.6416783843721663% used ##老生代使用比例

Perm Generation: ##perm区使用情况
   capacity = 134217728 (128.0MB) ##perm区容量
   used     = 47303016 (45.111671447753906MB) ##perm区已使用容量
   free     = 86914712 (82.8883285522461MB) ##perm区剩余容量
   35.24349331855774% used ##perm区使用比例

-Xms:表示java虚拟机堆区内存初始内存分配的大小,通常为操作系统可用内存的1/64大小即可,但仍需按照实际情况进行分配。 -Xmx:表示java虚拟机堆区内存可被分配的最大上限,通常为操作系统可用内存的1/4大小。 通常会将-Xms 与-Xmx两个参数的配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。 -XX:newSize:表示新生代初始内存的大小,应该小于-Xms的值; -XX:MaxnewSize:表示新生代可被分配的内存的最大上限;当然这个值应该小于-Xmx的值; -Xmn:至于这个参数则是对 -XX:newSize、-XX:MaxnewSize两个参数的同时配置,也就是说如果通过-Xmn来配置新生代的内存大小,那么-XX:newSize = -XX:MaxnewSize = -Xmn。

2、jmap -histo pid 查看堆内存(histogram)中的对象数量和大小

jmap -histo pid 查看堆内存(histogram)中的对象数量和大小

代码语言:javascript复制
     num     #instances         #bytes  class name
                序号         实例个数            字节数       类名    
        ----------------------------------------------
         1:       3174877      107858256  [C
         2:       3171499       76115976  java.lang.String
         3:       1397884       38122240  [B
         4:        214690       37785440  com.tongli.book.form.Book
         5:        107345       18892720  com.tongli.book.form.Book
         6:         65645       13953440  [Ljava.lang.Object;
         7:         59627        7648416  <constMethodKlass>
         8:        291852        7004448  java.util.HashMap$Entry
         9:        107349        6871176  [[B

         ..........

        total       9150732      353969416

3、jmap - dump pid 导出内存

jmap - dump pid 将内存使用的详细情况输出到文件 jmap -dump:format=b,file=m.datpid 用jhat命令可以参看 jhat -port 5000 m.dat

在浏览器中访问:http://localhost:5000/ 查看详细信息

如果你的Heap Dump文件超过了几百MB,那就不要再寄希望于jhat了,因为jhat需要数倍于dump文件的内存。这个时候你可以用MAT(Memory Analyzer),用MAT你可以在有2GB可用内存的机器上分析大约1GB左右的Dump文件。

生成Heap Dump文件的方法:

方法一:让运行中的JVM生成Dump文件 jmap -F -dump:format=b,file=heap.bin PID

方法二:让JVM在遇到OOM(OutOfMemoryError)时生成Dump文件 -XX: HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/heap/dump

一)用jhat分析Dump文件 注:只有在Dump文件比较小的时候才适合用jhat分析Dump文件 jhat(Java Heap Analysis Tool) jhat -stack false -refs false -J-Xmx2g /path/to/heap/dump/heap.bin

2)用MAT分析Dump文件 MAT(Memory Analyzer)

http://www.eclipse.org/mat/

安装MAT插件

4、jmap总结

1.如果程序内存不足或者频繁GC,很有可能存在内存泄露情况,这时候就要借助Java堆Dump查看对象的情况。 2.要制作堆Dump可以直接使用jvm自带的jmap命令 3.可以先使用jmap -heap命令查看堆的使用情况,看一下各个堆空间的占用情况。 4.使用jmap -histo:[live]查看堆内存中的对象的情况。如果有大量对象在持续被引用,并没有被释放掉,那就产生了内存泄露,就要结合代码,把不用的对象释放掉。 5.也可以使用 jmap -dump:format=b,file=<fileName>命令将堆信息保存到一个文件中,再借助jhat命令查看详细内容 6.在内存出现泄露、溢出或者其它前提条件下,建议多dump几次内存,把内存文件进行编号归档,便于后续内存整理分析。

5、jmap -head pid 报错解决

Heap Usage: Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at sun.tools.jmap.JMap.runTool(JMap.java:201) at sun.tools.jmap.JMap.main(JMap.java:130) Caused by: java.lang.RuntimeException: unknown CollectedHeap type : class sun.jvm.hotspot.gc_interface.CollectedHeap at sun.jvm.hotspot.tools.HeapSummary.run(HeapSummary.java:157) at sun.jvm.hotspot.tools.Tool.startInternal(Tool.java:260) at sun.jvm.hotspot.tools.Tool.start(Tool.java:223) at sun.jvm.hotspot.tools.Tool.execute(Tool.java:118) at sun.jvm.hotspot.tools.HeapSummary.main(HeapSummary.java:50) 去网站http://debuginfo.centos.org/7/x86_64里面找匹配当前的openjdk的版本,小版本号也要匹配上(否则也不生效)。

下载:wget http://debuginfo.centos.org/7/x86_64/java-1.8.0-openjdk-debuginfo-1.8.0.45-30.b13.el7_1.x86_64.rpm 安装:rpm -ivh java-1.8.0-openjdk-debuginfo-1.8.0.45-30.b13.el7_1.x86_64.rpm 就可以解决。

四、jstack查看线程状况

1、线程状态

要看懂堆栈信息,需要先简单了解java thread的运行周期中的几种状态, 在 java.lang.Thread.State 中有详细定义和说明:

NEW :状态是指线程刚创建, 尚未启动(new 线程后,调用start()之前)。

RUNNABLE :状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等(start()之后)。

BLOCKED 这个状态下, 是在多个线程有同步操作的场景, 比如正在等待另一个线程的synchronized 块的执行释放, 或者可重入的 synchronized块里别人调用wait() 方法, 也就是这里是线程在等待进入临界区。

WAITING 这个状态下是指线程拥有了某个锁之后, 调用了他的wait(0)/join(0)方法(不限时的wait/join), 等待其他线程/锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束

TIMED_WAITING 这个状态就是有限的(时间限制)的WAITING, 一般出现在调用wait(long), join(long)等情况下, 另外一个线程sleep后, 也会进入TIMED_WAITING状态.

TERMINATED 这个状态下表示 该线程的run方法已经执行完毕了, 基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)

2、jstack pid使用

jstack用于打印出给定的Java进程ID或core file或远程调试服务的Java堆栈信息,如果是在64位机器上,需要指定选项"-J-d64",Windows的jstack使用方式只支持以下的这种方式:jstack [-l] pid

如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。

需要注意的问题: 1、不同的 JAVA虚机的线程 DUMP的创建方法和文件格式是不一样的,不同的 JVM版本, dump信息也有差别。 2、在实际运行中,往往一次 dump的信息,还不足以确认问题。建议产生三次 dump信息,如果每次 dump都指向同一个问题,我们才确定问题的典型性。

命令格式

$jstack [ option ] pid

$jstack [ option ] executable core

$jstack [ option ] [server-id@]remote-hostname-or-IP

参数说明:

pid: java应用程序的进程号,一般可以通过jps来获得;

executable:产生core dump的java可执行程序;

core:打印出的core文件;

remote-hostname-or-ip:远程debug服务器的名称或IP;

server-id: 唯一id,假如一台主机上多个远程debug服务;

示例:

$jstack –l 23561

3、线程分析:

一般情况下,通过jstack输出的线程信息主要包括:jvm自身线程、用户线程等。其中jvm线程会在jvm启动时就会存在。对于用户线程则是在用户访问时才会生成。

1)jvm线程:

在线程中,有一些 JVM内部的后台线程,来执行譬如垃圾回收,或者低内存的检测等等任务,这些线程往往在JVM初始化的时候就存在,如下所示:

Attach Listener" daemon prio=10 tid=0x0000000052fb8000 nid=0xb8f waiting on condition [0x0000000000000000]

java.lang.Thread.State: RUNNABLE

Locked ownable synchronizers:

- None

destroyJavaVM" prio=10 tid=0x00002aaac1225800 nid=0x7208 waiting on condition [0x0000000000000000]

java.lang.Thread.State: RUNNABLE

Locked ownable synchronizers:

- None

2、用户级别的线程

还有一类线程是用户级别的,它会根据用户请求的不同而发生变化。该类线程的运行情况往往是我们所关注的重点。而且这一部分也是最容易产生死锁的地方。

qtp496432309-42" prio=10 tid=0x00002aaaba2a1800 nid=0x7580 waiting on condition [0x00000000425e9000]

java.lang.Thread.State: TIMED_WAITING (parking)

at sun.misc.Unsafe.park(Native Method)

- parking to wait for <0x0000000788cfb020> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)

at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:198)

at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2025)

at org.eclipse.jetty.util.BlockingArrayQueue.poll(BlockingArrayQueue.java:320)

at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:479)

at java.lang.Thread.run(Thread.java:662)

Locked ownable synchronizers:

- None

从上述的代码示例中我们可以看到该用户线程的以下几类信息:

Ø 线程的状态:waiting on condition(等待条件发生)/Runnable

Ø 线程的调用情况;

Ø 线程对资源的锁定情况;

dump 文件里,值得关注的线程状态有: 死锁,Deadlock(重点关注) 执行中,Runnable 等待资源,Waiting on condition(重点关注) 等待获取监视器,Waiting on monitor entry(重点关注) 暂停,Suspended 对象等待中,Object.wait() 或 TIMED_WAITING 阻塞,Blocked(重点关注) 停止,Parked

4、线程的状态分析:

正如我们刚看到的那样,线程的状态是一个重要的指标,它会显示在线程每行结尾的地方。那么线程常见的有哪些状态呢?线程在什么样的情况下会进入这种状态呢?我们能从中发现什么线索?

Runnable

该状态表示线程具备所有运行条件,在运行队列中准备操作系统的调度,或者正在运行。

Waiton condition

该状态出现在线程等待某个条件的发生。具体是什么原因,可以结合stacktrace来分析。最常见的情况是线程在等待网络的读写,比如当网络数据没有准备好读时,线程处于这种等待状态,而一旦有数据准备好读之后,线程会重新激活,读取并处理数据。在 Java引入 NIO之前,对于每个网络连接,都有一个对应的线程来处理网络的读写操作,即使没有可读写的数据,线程仍然阻塞在读写操作上,这样有可能造成资源浪费,而且给操作系统的线程调度也带来压力。在 NIO里采用了新的机制,编写的服务器程序的性能和可扩展性都得到提高。

如果发现有大量的线程都在处在 Wait on condition,从线程 stack看, 正等待网络读写,这可能是一个网络瓶颈的征兆。因为网络阻塞导致线程无法执行:

一种情况是网络非常忙,几乎消耗了所有的带宽,仍然有大量数据等待网络读写;

另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达。

所以要结合系统的一些性能观察工具来综合分析,比如 netstat统计单位时间的发送包的数目,如果很明显超过了所在网络带宽的限制 ;观察 cpu的利用率,如果系统态的 CPU时间,相对于用户态的 CPU时间比例较高;如果程序运行在 Solaris 10平台上,可以用 dtrace工具看系统调用的情况,如果观察到 read/write的系统调用的次数或者运行时间遥遥领先;这些都指向由于网络带宽所限导致的网络瓶颈。

另外一种出现 Wait on condition的常见情况是该线程在 sleep,等待 sleep的时间到了时候,将被唤醒。

lWaitingfor monitor entry 和 in Object.wait()

在多线程的 JAVA程序中,实现线程之间的同步,就要说说Monitor。Monitor是Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者 Class的锁。每一个对象都有,也仅有一个 monitor。下面这个图,描述了线程和 Monitor之间关系,以及线程的状态转换图:

从图中可以看出,每个 Monitor在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitorentry”,而在 “Wait Set”中等待的线程状态是“in Object.wait()”。

先看 “Entry Set”里面的线程。我们称被 synchronized保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了 “Entry Set”队列。对应的 code就像:

synchronized(obj){

.........

}

这时有两种可能性:

该 monitor不被其它线程拥有,Entry Set里面也没有其它等待线程。本线程即成为相应类或者对象的 Monitor的 Owner,执行临界区的代码 。此时线程将处于Runnable状态;

该 monitor被其它线程拥有,本线程在 Entry Set队列中等待。此时dump的信息显示“waiting for monitor entry”。

"Thread-0" prio=10 tid=0x08222eb0 nid=0x9 waiting for monitor entry [0xf927b000..0xf927bdb8] at testthread.WaitThread.run(WaitThread.java:39) - waiting to lock <0xef63bf08> (a java.lang.Object) - locked <0xef63beb8> (a java.util.ArrayList) at java.lang.Thread.run(Thread.java:595)

临界区的设置,是为了保证其内部的代码执行的原子性和完整性。但是因为临界区在任何时间只允许线程串行通过,这和我们多线程的程序的初衷是相反的。如果在多线程的程序中,大量使用 synchronized,或者不适当的使用了它,会造成大量线程在临界区的入口等待,造成系统的性能大幅下降。如果在线程 DUMP中发现了这个情况,应该审查源码,改进程序。

现在我们再来看现在线程为什么会进入 “Wait Set”。当线程获得了 Monitor,进入了临界区之后,如果发现线程继续运行的条件没有满足,它则调用对象(一般就是被 synchronized 的对象)的 wait() 方法,放弃了 Monitor,进入 “Wait Set”队列。只有当别的线程在该对象上调用了 notify() 或者 notifyAll() , “ Wait Set”队列中线程才得到机会去竞争,但是只有一个线程获得对象的Monitor,恢复到运行态。在 “Wait Set”中的线程, DUMP中表现为: in Object.wait(),类似于:

"Thread-1" prio=10 tid=0x08223250 nid=0xa in Object.wait() [0xef47a000..0xef47aa38] at java.lang.Object.wait(Native Method) - waiting on <0xef63beb8> (a java.util.ArrayList) at java.lang.Object.wait(Object.java:474) at testthread.MyWaitThread.run(MyWaitThread.java:40) - locked <0xef63beb8> (a java.util.ArrayList) at java.lang.Thread.run(Thread.java:595)

仔细观察上面的 DUMP信息,你会发现它有以下两行:

² locked <0xef63beb8> (ajava.util.ArrayList)

² waiting on <0xef63beb8> (ajava.util.ArrayList)

这里需要解释一下,为什么先 lock了这个对象,然后又 waiting on同一个对象呢?让我们看看这个线程对应的代码:

synchronized(obj){

.........

obj.wait();

.........

}

线程的执行中,先用 synchronized 获得了这个对象的 Monitor(对应于 locked <0xef63beb8> )。当执行到 obj.wait(), 线程即放弃了 Monitor的所有权,进入 “wait set”队列(对应于 waiting on<0xef63beb8> )。

往在你的程序中,会出现多个类似的线程,他们都有相似的 dump也可能是正常的。比如,在程序中有多个服务线程,设计成从一个队列里面读取请求数据。这个队列就是 lock以及 waiting on的对象。当队列为空的时候,这些线程都会在这个队列上等待,直到队列有了数据,这些线程被notify,当然只有一个线程获得了 lock,继续执行,而其它线程继续等待。

0 人点赞