JVM参数类型大体分为三种:
常见的标准参数:
常见的X参数:
XX参数又分为两大类,一种是Boolean类型,如下:
格式 :-XX : [ + - ] < name > 表示启用或禁用name属性
比如:
-XX:+UseConcMarkSweepGC 表示启用UseConcMarkSweepGC
-XX:+UseG1GC 表示启用UseG1GC
另一种则是key/value类型的,如下:
格式:-XX : < name > = < value > 表示name属性的值是value
比如:
-XX:MaxGCPauseMillis=500 表示MaxGCPauseMillis属性的值是500
-XX:GCTimeRatio=19 表示GCTimeRatio属性的值是19
要说最常见的JVM参数应该是 -Xmx 与 -Xms 这两个参数,前者用于指定初始化堆的大小,而后者用于指定堆的最大值。然后就是-Xss参数,它用于指定线程的堆栈大小。可以看到这三个参数都是以-X开头的,它们是-X参数吗?实际上不是的,它们是XX参数,是属于一种缩写形式:
-Xms 等价于 -XX:InitialHeapSize
-Xmx 等价于 -XX:MaxHeapSize
-Xss 等价于 -XX:ThreadStackSize
查看JVM运行时的参数是很重要的,因为只有知道当前运行的参数值,才知道要如何去调优。我这里的服务器跑了一个Tomcat,我们就以这个Tomcat进程来作为一个例子,该进程的pid是1200,如下:
常用的查看JVM运行时参数:
我们来看看 -XX:+PrintFlagsInitial
参数的使用方式,如下:
[root@server ~]# java -XX:+PrintFlagsFinal -version bool UseCodeCacheFlushing = true {product} bool UseCompiler = true {product} bool UseCompilerSafepoints = true {product} bool UseCompressedClassPointers := true {lp64_product} bool UseCompressedOops := true {lp64_product}
加上-version是因为让它最后的时候输出版本信息,不然的话就会输出帮助信息了。以上这里只是截取了部分的内容,实际打印出来的内容是很多的,大约七百多行。可以看到截取的这部分的参数都是bool类型的(还有其他类型的),而且有 = 和 := 两种符号,= 表示JVM的默认值, := 表示被用户或JVM修改的值,也就是非默认值。
注:这种直接使用java命令 + 参数的方式,实际查看的是当前这条java命令的JVM运行时参数值。
我们来介绍一个命令:jps,这个命令与Linux的ps命令类似,也是查看进程的,但jps是专门查看Java进程的,使用也很简单:
以下简单演示一下jps命令的常见使用方式:
[root@server ~]# jps // 没添加option的时候,默认列出进程编号和简单的class或jar名称 1200 Bootstrap 2847 Jps [root@server ~]# jps -l // 输出应用程序主类完整package名称或jar完整名称. 2880 sun.tools.jps.Jps 1200 org.apache.catalina.startup.Bootstrap [root@server ~]# jps -q // 仅仅显示进程编号,不显示jar,class, main参数等信息. 1200 2901 [root@server ~]# jps -m // 输出主函数传入的参数.-m就是在执行程序时从命令行输入的参数 1200 Bootstrap start 2911 Jps -m [root@server ~]# jps -v // 列出jvm参数 1200 Bootstrap -Djava.util.logging.config.file=/home/tomcat/apache-tomcat-8.5.8/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dcatalina.base=/home/tomcat/apache-tomcat-8.5.8 -Dcatalina.home=/home/tomcat/apache-tomcat-8.5.8 -Djava.io.tmpdir=/home/tomcat/apache-tomcat-8.5.8/temp 2921 Jps -Dapplication.home=/usr/java/jdk1.8.0_111 -Xms8m [root@server ~]#
还需了解更多的话,可以查看官方的文档,jps命令的官方文档地址如下:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jps.html
如果我们需要查看一个运行时的Java进程的JVM参数,就可以使用jinfo命令。jinfo是jdk自带的命令,可以用来查看正在运行的Java应用程序的扩展参数,甚至支持在运行时,修改部分参数。以下简单演示一下jinfo命令的常见使用方式:
[root@server ~]# jinfo -flag MaxHeapSize 1200 // 查看该java进程的最大内存 -XX:MaxHeapSize=482344960 [root@server ~]# jinfo -flag UseConcMarkSweepGC 1200 // 查看是否使用了UseConcMarkSweepGC垃圾回收器 -XX:-UseConcMarkSweepGC [root@server ~]# jinfo -flag UseG1GC 1200 // 查看是否使用了UseG1GC垃圾回收器 -XX:-UseG1GC [root@server ~]# jinfo -flag UseParallelGC 1200 // 查看是否使用了UseParallelGC 1200垃圾回收器 -XX:-UseParallelGC [root@server ~]# jinfo -flag PrintGC 1200 // 查看是否使用了PrintGC -XX:-PrintGC [root@server ~]# jinfo -flag +PrintGC 1200 // 使用PrintGC,就只需要加上+号即可 [root@server ~]# jinfo -flag PrintGC 1200 -XX:+PrintGC [root@server ~]# jinfo -flag -PrintGC 1200 // 不使用PrintGC ,就只需要加上-号即可 [root@server ~]# jinfo -flags 1200 // 查看手动赋过值的JVM参数 Attaching to process ID 1200, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.111-b14 Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=31457280 -XX:MaxHeapSize=482344960 -XX:MaxNewSize=160759808 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=10485760 -XX:OldSize=20971520 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops Command line: -Djava.util.logging.config.file=/home/tomcat/apache-tomcat-8.5.8/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dcatalina.base=/home/tomcat/apache-tomcat-8.5.8 -Dcatalina.home=/home/tomcat/apache-tomcat-8.5.8 -Djava.io.tmpdir=/home/tomcat/apache-tomcat-8.5.8/temp [root@server ~]#
还需了解更多的话,可以查看官方的文档,jinfo命令的官方文档地址如下:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jinfo.html#BCGEBFDD
Jstat 用于监控基于HotSpot的JVM,对其堆的使用情况进行实时的命令行的统计,使用jstat我们可以对指定的JVM做如下监控:
官方文档地址如下:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html#BEHHGFAE
Option | Displays |
---|---|
-class | 类加载的行为统计 |
-class 类加载的行为统计,命令示例:
[root@server ~]# jstat -class 1200 1000 3 Loaded Bytes Unloaded Bytes Time 3249 6451.6 0 0.0 2.40 3249 6451.6 0 0.0 2.40 3249 6451.6 0 0.0 2.40 [root@server ~]#
命令说明:
打印的信息说明:
Option | Displays |
---|---|
-gc | 垃圾回收堆的行为统计 |
-gcutil | 垃圾回收统计概述(百分比) |
-gccause | 垃圾收集统计概述(同-gcutil) |
-gcnew | 新生代行为统计 |
-gcold | 老年代和Metaspace区行为统计 |
-gccapacity | 各个垃圾回收代容量(young,old,perm)和他们相应的空间统计 |
-gcnewcapacity | 新生代与其相应的内存空间的统计 |
-gcoldcapacity | 年老代行为统计 |
-gcmetacapacity | Metaspace区大小统计 |
-gc 垃圾回收堆的行为统计,命令示例:
[root@server ~]# jstat -gc 1200 S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 1024.0 1024.0 0.0 35.2 8192.0 4826.9 20480.0 17994.0 20864.0 20268.3 2432.0 2238.1 29 0.134 0 0.000 0.134 [root@server ~]#
打印的信息说明,C即 Capacity 总容量,U即 Used 已使用的容量:
注:我这里使用的是JDK1.8版本的,如果是其他版本的JDK在这一块打印的信息会有些不一样
JVM大致的内存结构图(JDK1.8版本):
-gccapacity 各个垃圾回收代容量(young,old,perm)和他们相应的空间统计。(同-gc,还会输出Java堆各区域使用到的最大、最小空间),命令示例:
[root@server ~]# jstat -gccapacity 1200 NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC MCMN MCMX MC CCSMN CCSMX CCSC YGC FGC 10240.0 156992.0 10240.0 1024.0 1024.0 8192.0 20480.0 314048.0 20480.0 20480.0 0.0 1067008.0 20864.0 0.0 1048576.0 2432.0 29 0 [root@server ~]#
打印的信息说明:
-gcutil 垃圾回收统计概述(同-gc,输出的是已使用空间占总空间的百分比)。命令示例:
[root@server ~]# jstat -gcutil 1200 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 3.44 90.53 87.86 97.14 92.03 29 0.134 0 0.000 0.134 [root@server ~]#
-gccause 垃圾收集统计概述(垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因)。命令示例:
[root@server ~]# jstat -gccause 1200 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT LGCC GCC 0.00 3.44 92.49 87.86 97.14 92.03 29 0.134 0 0.000 0.134 Allocation Failure No GC [root@server ~]#
打印的信息说明:
-gcnew(统计新生代行为)。命令示例:
[root@server ~]# jstat -gcnew 1200 S0C S1C S0U S1U TT MTT DSS EC EU YGC YGCT 1024.0 1024.0 0.0 35.2 15 15 512.0 8192.0 7738.1 29 0.134 [root@server ~]#
打印的信息说明:
-gcnewcapacity(新生代与其相应的内存空间的统计)。命令示例:
[root@server ~]# jstat -gcnewcapacity 1200 NGCMN NGCMX NGC S0CMX S0C S1CMX S1C ECMX EC YGC FGC 10240.0 156992.0 10240.0 15680.0 1024.0 15680.0 1024.0 125632.0 8192.0 29 0 [root@server ~]#
打印的信息说明:
-gcold(老年代和Metaspace区行为统计)。命令示例:
[root@server ~]# jstat -gcold 1200 MC MU CCSC CCSU OC OU YGC FGC FGCT GCT 20864.0 20268.3 2432.0 2238.1 20480.0 17994.0 29 0 0.000 0.134 [root@server ~]#
-gcoldcapacity(老年代与其相应的内存空间的统计)。命令示例:
[root@server ~]# jstat -gcoldcapacity 1200 OGCMN OGCMX OGC OC YGC FGC FGCT GCT 20480.0 314048.0 20480.0 20480.0 29 0 0.000 0.134 [root@server ~]#
-gcmetacapacity(Metaspace区与其相应内存空间的统计)。命令示例:
[root@server ~]# jstat -gcmetacapacity 1200 MCMN MCMX MC CCSMN CCSMX CCSC YGC FGC FGCT GCT 0.0 1067008.0 20864.0 0.0 1048576.0 2432.0 30 0 0.000 0.136 [root@server ~]#
Option | Displays |
---|---|
-compiler | HotSpt JIT编译器行为统计 |
-printcompilation | HotSpot编译方法统计 |
-compiler HotSpt JIT编译器行为统计,命令示例:
[root@server ~]# jstat -compiler 1200 Compiled Failed Invalid Time FailedType FailedMethod 2332 1 0 4.80 1 org/apache/tomcat/util/IntrospectionUtils setProperty [root@server ~]#
打印的信息说明:
-printcompilation HotSpot编译方法统计,命令示例:
[root@server ~]# jstat -printcompilation 1200 Compiled Size Type Method 2332 5 1 org/apache/tomcat/util/net/SocketWrapperBase getEndpoint [root@server ~]#
打印的信息说明:
我们都知道部署在线上的项目,是不能够直接修改其代码或随意关闭、重启服务的,所以当发生内存溢出错误时,我们需要通过监控工具去分析错误的原因。所以本小节简单演示一下JVM堆区和非堆区的内存溢出,然后我们再通过工具来分析内存溢出的原因。首先使用IDEA创建一个SpringBoot工程,工程的目录结构如下:
我这里只勾选了web和Lombok以及增加了asm依赖,因为在演示非堆区内存溢出时,我们需要通过asm来动态生成class文件。所以pom.xml文件里所配置的依赖如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.22</version> </dependency> <dependency> <groupId>asm</groupId> <artifactId>asm</artifactId> <version>3.3.1</version> </dependency> </dependencies>
先来演示堆区的内存溢出,为了能够让内存更快的溢出,所以我们需要设置JVM内存参数值。如下:
1、
2、
创建一个实体类,因为对象是存放在堆区的,所以我们需要有一个实体对象来制造内存的溢出。代码如下:
package org.zero01.monitor_tuning.vo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; private String name; }
然后创建一个controller类,方便我们通过postman等工具去进行测试。代码如下:
package org.zero01.monitor_tuning.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.zero01.monitor_tuning.vo.User; import java.util.ArrayList; import java.util.List; import java.util.UUID; /** * @program: monitor_tuning * @description: 演示内存溢出接口 * @author: 01 * @create: 2018-07-08 15:41 **/ @RestController public class MemoryController { // 对象的成员变量会随着对象本身而存储在堆上 private List<User> userList = new ArrayList<>(); /** * 演示堆区内存溢出接口 * 设定jvm参数:-Xmx32M -Xms32M * * @return */ @GetMapping("/heap") public String heap() { int i = 0; while (true) { // 所以不断的往成员变量里添加数据就会导致内存溢出 userList.add(new User(i++, UUID.randomUUID().toString())); } } }
启动SpringBoot,访问 localhost:8080/heap
后,控制台输出的错误日志如下:
演示完堆区内存溢出后,我们再来看看非堆区的内存溢出,从之前的JVM内存结构图可以看到,在JDK1.8中,非堆区就是Metaspace区。同样的为了能够让内存更快的溢出,所以我们需要设置JVM的Metaspace区参数值如下:
Metaspace区可以存储class,所以我们通过不断的存储class来制造Metaspace区的内存溢出。使用asm框架我们可以动态的创建class文件。新建一个 Metaspace 类,代码如下:
package org.zero01.monitor_tuning.loader; import java.util.ArrayList; import java.util.List; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; /** * @program: monitor_tuning * @description: 继承ClassLoader是为了方便调用defineClass方法,因为该方法的定义为protected * @author: 01 * @create: 2018-07-08 15:58 **/ public class Metaspace extends ClassLoader { /** * 动态创建class文件 * * @return */ public static List<Class<?>> createClasses() { // 类持有 List<Class<?>> classes = new ArrayList<Class<?>>(); // 循环1000w次生成1000w个不同的类。 for (int i = 0; i < 10000000; ++i) { ClassWriter cw = new ClassWriter(0); // 定义一个类名称为Class{i},它的访问域为public,父类为java.lang.Object,不实现任何接口 cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); // 定义构造函数<init>方法 MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null); // 第一个指令为加载this mw.visitVarInsn(Opcodes.ALOAD, 0); // 第二个指令为调用父类Object的构造函数 mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V"); // 第三条指令为return mw.visitInsn(Opcodes.RETURN); mw.visitMaxs(1, 1); mw.visitEnd(); Metaspace test = new Metaspace(); byte[] code = cw.toByteArray(); // 定义类 Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length); classes.add(exampleClass); } return classes; } }
在 MemoryController 类中增加一个成员变量和一个方法,用于制造非堆区的内存溢出。代码如下:
package org.zero01.monitor_tuning.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.zero01.monitor_tuning.loader.Metaspace; import org.zero01.monitor_tuning.vo.User; import java.util.ArrayList; import java.util.List; import java.util.UUID; /** * @program: monitor_tuning * @description: 演示内存溢出接口 * @author: 01 * @create: 2018-07-08 15:41 **/ @RestController public class MemoryController { private List<User> userList = new ArrayList<>(); // class会被放在Metaspace区 private List<Class<?>> classList = new ArrayList<>(); /** * 演示堆区内存溢出接口 * 设定jvm参数:-Xmx32M -Xms32M * * @return */ @GetMapping("/heap") public String heap() { int i = 0; while (true) { userList.add(new User(i++, UUID.randomUUID().toString())); } } /** * 演示非堆区内存溢出接口 * 设定jvm参数:-XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M * @return */ @GetMapping("/nonheap") public String nonHeap() { int i = 0; while (true) { // 不断的存储class文件,就会导致Metaspace区内存溢出 classList.addAll(Metaspace.createClasses()); } } }
启动SpringBoot,访问 localhost:8080/nonheap
后,控制台输出的错误日志如下:
上一小节中,我们演示了两种内存溢出,堆区内存溢出与非堆区内存溢出。如果我们线上的项目出现这种内存溢出的错误该如何解决?我们一般主要通过分析内存映像文件,来查看是哪些类一直占用着内存没有被释放。
导出内存映像文件的几种方式:
注:-XX:HeapDumpPath=./ 用于指定将内存映像文件导出到哪个路径
我们先演示第一种导出内存映像文件的方式,同样的,需要先设置一下JVM的参数,如下:
启动SpringBoot,访问 localhost:8080/heap
后,控制台输出的错误日志如下,可以看到内存映像文件被导出到当前工程的根目录了:
打开工程的根目录,就可以看到这个内存映像文件:
接着我们再来演示一下使用jmap命令来导出内存映像文件,命令如下:
C:/Users/admin/Desktop>jps // 查看进程的pid 10328 Jps 1100 Launcher 12124 1308 MonitorTuningApplication C:/Users/admin/Desktop>jmap -dump:format=b,file=heap.hprof 1308 // 导出内存映像文件 Dumping heap to C:/Users/admin/Desktop/heap.hprof ... Heap dump file created C:/Users/admin/Desktop>
命令选项说明:
因为当前的路径是在桌面,所以就导出到桌面上了:
如果需要了解更多关于jmap的用法,可以查阅官方文档,地址如下:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jmap.html#CEGCECJB
在上一小节中,我们已经演示了两种导出内存映像文件的方式。但是这些内存映像文件里都是些什么东西呢?我们要如何利用内存映像文件去分析问题所在呢?那这就需要用到另一个工具MAT了。
MAT是Eclipse的一个内存分析工具,全称Memory Analyzer Tools,官网地址如下:
http://www.eclipse.org/mat/
MAT的下载地址如下:
http://www.eclipse.org/mat/downloads.php
下载并解压之后,点击MemoryAnalyzer.exe即可打开该工具,并不需要打开Eclipse,虽然下载的压缩包里包含了Eclipse:
正常打开后界面如下:
然后我们打开之前演示的发生内存溢出时,JVM自动导出的内存映像文件:
内存映像文件打开后,MAT会自动分析出一个饼状图,把可能出现问题的三个地方列了出来,并通过饼状图分为了三块。Problem Suspect 1表示最有可能导致问题出现的原因所在,而且也可以看到,的确是指向了我们演示内存溢出的那个 MemoryController 类。上面也描述了,该类的一个实例所占用的内存达到了55.57%:
这样我们就很轻易的找到了问题的所在,当然线上环境肯定不会这么简单。毕竟这是我们故意去制造的内存溢出,如果是实际的生产环境会更复杂一些。
所以我们还会进行更多的分析,例如查看所有类的实例对象的数量:
或者查看指定类的实例对象数量,可以看到,User这个类的实例对象有十万多个,一个类的实例对象存在十万多个,肯定是有问题的:
右键点击这个有问题的对象,查看其强引用:
从下图中,可以看到首先是Tomcat的一个TaskThread引用了MemoryController,而MemoryController里包含了一个名为userList的集合类型成员变量,该集合中存放了十万多个User实例对象,这下基本上就可以确定是这个MemoryController里userList的问题了:
除此之外还可以查看对象所占的字节数,使用方式和查看对象数量是一样的:
MAT的常用功能就先介绍到这里,一般我们使用这些常用功能就已经能够定位问题的所在了,而且这种图形化的工具也比较好上手,这里就不过多赘述了。
jstack可以打印JVM内部所有的线程数据,是java虚拟机自带的一种线程堆栈跟踪工具。使用jstack打印线程堆栈信息时,可以将这些信息重定向到一个文件里,这样就相当于生成了JVM当前时刻的线程快照。线程快照是当前JVM内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。
jstack官方文档地址如下:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstack.html#BABGJDIF
使用jstack打印Java程序里所有线程堆栈信息示例:
[root@server ~]# jps 1200 Bootstrap 4890 Jps [root@server ~]# jstack 1200 // 直接加上pid即可打印该Java程序里的所有线程堆栈信息 2018-07-08 21:48:01 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.111-b14 mixed mode): "Attach Listener" #35 daemon prio=9 os_prio=0 tid=0x00007fd944006000 nid=0xb90 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE // 该线程的状态为RUNNABLE "http-nio-8080-exec-10" #34 daemon prio=5 os_prio=0 tid=0x00007fd96c31e800 nid=0x4d3 waiting on condition [0x00007fd9487ac000] java.lang.Thread.State: WAITING (parking) // 该线程的状态为WAITING at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000000edae5bb8> (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:2039) at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:745) ...
注:nid是线程的唯一标识符,是16进制的,通常用于定位某一个线程
这里只是截取了前面两条线程的信息,可以看到这些线程都有一个java.lang.Thread.State参数,该参数的值就是该线程的状态。
Java线程状态:
线程状态转换示意图:
本小节我们使用一个例子演示死循环与死锁,然后介绍如何利用jstack分析、定位问题的所在。
在controller包中,新建一个 CpuController 类,用于演示发生死循环与死锁时CPU占用率飙高的情况。这是一个解析json的代码,并不需要注意代码的细节,只需要知道访问这个接口会导致死循环即可。代码如下:
package org.zero01.monitor_tuning.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; /** * @program: monitor_tuning * @description: 演示死循环与死锁 * @author: 01 * @create: 2018-07-08 22:14 **/ @RestController public class CpuController { /** * 演示死循环 */ @RequestMapping("/loop") public List<Long> loop() { String data = "{/"data/":[{/"partnerid/":]"; return getPartneridsFromJson(data); } public static List<Long> getPartneridsFromJson(String data) { //{/"data/":[{/"partnerid/":982,/"count/":/"10000/",/"cityid/":/"11/"},{/"partnerid/":983,/"count/":/"10000/",/"cityid/":/"11/"},{/"partnerid/":984,/"count/":/"10000/",/"cityid/":/"11/"}]} //上面是正常的数据 List<Long> list = new ArrayList<Long>(2); if (data == null || data.length() <= 0) { return list; } int datapos = data.indexOf("data"); if (datapos < 0) { return list; } int leftBracket = data.indexOf("[", datapos); int rightBracket = data.indexOf("]", datapos); if (leftBracket < 0 || rightBracket < 0) { return list; } String partners = data.substring(leftBracket + 1, rightBracket); if (partners == null || partners.length() <= 0) { return list; } while (partners != null && partners.length() > 0) { int idpos = partners.indexOf("partnerid"); if (idpos < 0) { break; } int colonpos = partners.indexOf(":", idpos); int commapos = partners.indexOf(",", idpos); if (colonpos < 0 || commapos < 0) { //partners = partners.substring(idpos+"partnerid".length());//1 continue; } String pid = partners.substring(colonpos + 1, commapos); if (pid == null || pid.length() <= 0) { //partners = partners.substring(idpos+"partnerid".length());//2 continue; } try { list.add(Long.parseLong(pid)); } catch (Exception e) { throw new RuntimeException(e); } partners = partners.substring(commapos); } return list; } }
将工程使用maven进行打包,并上传到服务器中,打包命令如下:
mvn clean package -Dmaven.test.skip=true
将jar包上传到服务器中,然后使用如下命令进行启动:
[root@server ~]# nohup java -jar monitor_tuning-0.0.1-SNAPSHOT.jar &
接着使用浏览器开多几个标签页来访问该工程的接口,为了让CPU负载更快飚上去:
在Linux的命令行输入top命令来查看CPU负载情况,等那么一两分钟后,会发现CPU的负载就上去了,如下:
当我们服务器的CPU像这样负载很高的时候,就可以使用jstack命令去定位哪一个线程的CPU占用率最高。通过jstack命令打印线程的堆栈信息,并重定向到一个文件中:
[root@server ~]# jstack 4999 > loop.txt
接着使用top命令指定查看某个进程中的线程:
[root@server ~]# top -p 4999 -H
通过以上这个命令,可以看到该进程中占用率最高的那几个线程,我们把占用率第一的线程的pid给记录一下:
然后通过printf命令,将pid转换成16进制的nid,实际上这里的pid就是十进制的nid,如下:
[root@server ~]# printf "%x" 5016 1398 [root@server ~]#
得出nid后,使用vim命令打开loop.txt文件,通过nid来搜索该线程的数据:
如上,通过分析线程堆栈的信息,就能定位到是哪个类的哪个方法里的哪句代码出了问题,这就是如何利用jstack命令,定位问题代码。
以上演示完如何定位发生死循环的代码后,接下来就是演示一下如何使用jstack定位发生死锁的代码。首先,在CpuController类中,增加如下代码:
private Object lock1 = new Object(); private Object lock2 = new Object(); /** * 演示死锁 * */ @RequestMapping("/deadlock") public String deadlock(){ new Thread(()->{ synchronized(lock1) { try {Thread.sleep(1000);}catch(Exception e) {} synchronized(lock2) { System.out.println("Thread1 over"); } } }) .start(); new Thread(()->{ synchronized(lock2) { try {Thread.sleep(1000);}catch(Exception e) {} synchronized(lock1) { System.out.println("Thread2 over"); } } }) .start(); return "deadlock"; }
增加完以上代码后,重新使用maven命令进行打包。
回到服务器上,杀掉之前启动的服务,并把旧的jar包给删除掉:
[root@server ~]# jps 4999 jar 5103 Jps [root@server ~]# kill -9 4999 // 杀掉进程 [root@server ~]# rm -rf monitor_tuning-0.0.1-SNAPSHOT.jar // 删除之前的jar包
删除掉旧的jar包后,再重新上传新打包好的jar包,然后和之前一样使用如下命令运行该jar包:
[root@server ~]# nohup java -jar monitor_tuning-0.0.1-SNAPSHOT.jar &
成功运行后,同样的使用浏览器进行访问,可以看到是能够正常返回数据的,这是因为发生死锁的是子线程,并不会影响主线程:
那么我们要怎么定位死锁发生的代码呢?因为这种情况下的死锁和死循环不一样,并不会导致CPU负载率的飙高。所以我们无法使用之前那种方式去定位问题代码,但jstack比较好的一点就是,会自动帮我们找出死锁。和之前一样,使用如下命令生成一个线程快照文件:
[root@server ~]# jps 5128 jar 5177 Jps [root@server ~]# jstack 5128 > deadlock.txt [root@server ~]# vim deadlock.txt
使用vim打开该文件后,直接定位到文件的末尾,就可以看到死锁的信息,jstack会自动找出死锁,并把死锁信息放在末尾。我已经使用蓝色和红色框框标出了两个线程互相等待的锁: