一、jvm优化的必要性
在本地开发环境中我们很少会遇到需要对jvm进行优化的需求,但是到了生产环境,我们可能将会碰到下面的问题
1、应用夯住,日志不输出,程序没有反应
原因有很多,内存溢出,tomcat假死都有可能出现这种情况。
详细描述见视频
2、服务器的CPU负载突然升高
你的线程量不断爆发,就像bio模型,一请求,一线程的这种模式,线程不断创建,回收不及时,那cpu肯定会居高不下,这种问题你在开发环境根本不会出现,因为就你一个人在跑,没有高并发,也就不会出现线程暴增的情况。
详细描述见视频
3、在多线程应用下,如何分配线程的数量?
上线之后才会出现多线程的情况,在本地开发环境是不会有这个需求的,因为就你一个人在开发这个功能,就一个线程在跑,所以上线后会有这个需求,线程分配的数量会根据系统软硬件环境,业务并发量做综合考量。
详细描述见视频
4、系统频繁FULL GC,最后内存泄漏
这种问题通常在上线之后的一段时间内会出现,原因也有很多,比如说代码原因,造成对象回收不及时,最后存活对象在老年代越来越多,最终没有内存可分配,导致内存泄漏。
在本次课程中,我们将对jvm更深入的学习,我们不仅要让程序能跑起来,而且是可以跑的更快!可以分析解决在生产环境中所遇到的各种“棘手”的问题 。
二、jvm的运行参数
在jvm中有很多的参数可以设置,这样可以让jvm在各种环境中都能够高效的运行。 绝大部分的参数保持默认即可,这一节介绍比较常见的以及比较经常设置的一些参数。
1、标准参数
1)、-help,-version,-D参数
jvm的标准参数,一般都是很稳定的,在未来的JVM版本中不会改变,可以使用java -help 检索出所有的标准参数。 通过以下命令查看:
java -help
可以看到我们经常会用到的 -sever,-version等参数。
实战1:查看JVM版本
java -version
jvm版本是1.8.0_202,而且是64位,server,混合模式。
实战2:通过-D设置系统属性参数
先写一段代码:
public class TestVM {
public static void main(String[] args) {
String name = System.getProperty("name");
if(name!=null){
System.out.println(name);
}
else{
System.out.println("ling");
}
}
}
运行上面这段代码,通过-D带入一个参数name,根据name的值进行判断如果name不为null,则打印,如果为null则打印 ling。
# 在root下创建一个文件夹下创建这个文件
vi TestJVM.java
#把上面的java代码复制进去
#执行代码
[root@localhost test]# javac TestJVM.java
[root@localhost test]# java TestJVM
ling
#设置参数进行,设置系统属性:‐D<名称>=<值>
[root@localhost test]# java -Dname=smart哥
smart哥
2)、-server,-client参数
可以通过-server或-client设置jvm的运行参数。 它们的区别是:
-
Server VM的初始堆空间会大一些,默认使用的是并行垃圾回收器,启动慢运行快。
-
Client VM相对来讲会保守一些,初始堆空间会小一些,使用串行的垃圾回收器,它 的目标是为了让JVM的启动速度更快,但运行速度会比Serverm模式慢些。
-
JVM在启动的时候会根据硬件和操作系统自动选择使用Server还是Client类型的 JVM。
-
32位操作系统:
- 如果是Windows系统,不论硬件配置如何,都默认使用Client类型的JVM。
- 如果是其他操作系统上,机器配置有2GB以上的内存同时有2个以上CPU的话默 认使用server模式,否则使用client模式。
-
64位操作系统
- 只有server类型,不支持client类型。
测试如下:
[root@myshop02 ~]# java -client -showversion TestVM
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
ling
可以看出因为服务器是64位的,所以都是server启动!
2、-X参数(非标准参数)
jvm的-X参数是非标准参数,在不同版本的jvm中,参数可能会有所不同,可以通过java -X查看非标准参数。
[root@myshop02 ~]#java -X
‐Xmixed 混合模式执行 (默认)
‐Xint 仅解释模式执行
‐Xbootclasspath:<用 : 分隔的目录和 zip/jar 文件>
设置搜索路径以引导类和资源
‐Xbootclasspath/a:<用 : 分隔的目录和 zip/jar 文件>
附加在引导类路径末尾
‐Xbootclasspath/p:<用 : 分隔的目录和 zip/jar 文件>
置于引导类路径之前
‐Xdiag 显示附加诊断消息
‐Xnoclassgc 禁用类垃圾收集
‐Xincgc 启用增量垃圾收集
‐Xloggc:<file> 将 GC 状态记录在文件中 (带时间戳)
‐Xbatch 禁用后台编译
‐Xms<size> 设置初始 Java 堆大小
‐Xmx<size> 设置最大 Java 堆大小
‐Xss<size> 设置 Java 线程堆栈大小
‐Xprof 输出 cpu 配置文件数据
‐Xfuture 启用最严格的检查, 预期将来的默认值
‐Xrs 减少 Java/VM 对操作系统信号的使用 (请参阅文档)
‐Xcheck:jni 对 JNI 函数执行其他检查
‐Xshare:off 不尝试使用共享类数据
‐Xshare:auto 在可能的情况下使用共享类数据 (默认)
‐Xshare:on 要求使用共享类数据, 否则将失败。
‐XshowSettings 显示所有设置并继续
‐XshowSettings:all
显示所有设置并继续
‐XshowSettings:vm 显示所有与 vm 相关的设置并继续
‐XshowSettings:properties
显示所有属性设置并继续
‐XshowSettings:locale
显示所有与区域设置相关的设置并继续
‐X 选项是非标准选项, 如有更改,恕不另行通知。
1)、-Xint、-Xcomp、-Xmixed
- 在解释模式(interpreted mode)下,-Xint标记会强制JVM执行所有的字节码,当然这 会降低运行速度,通常低10倍或更多。
- -Xcomp参数与它(-Xint)正好相反,JVM在第一次使用时会把所有的字节码编译成 本地代码,从而带来最大程度的优化。 然而,很多应用在使用-Xcomp也会有一些性能损失,当然这比使用-Xint损失的 少,原因是-xcomp没有让JVM启用JIT编译器的全部功能。JIT编译器可以对是否 需要编译做判断,如果所有代码都进行编译的话,对于一些只执行一次的代码就 没有意义了。
- -Xmixed是混合模式,将解释模式与编译模式进行混合使用,由jvm自己决定,这是 jvm默认的模式,也是推荐使用的模式。
测试:
#强制设置为解释模式
[root@myshop02 ~]# java -showversion -Xint TestVM
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, interpreted mode)
ling
#强制设置为编译模式
#注意:编译模式下,第一次执行会比解释模式下执行慢一些,注意观察。
[root@myshop02 ~]# java -showversion -Xcomp TestVM
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, compiled mode)
ling
#默认的混合模式
[root@myshop02 ~]# java -showversion TestVM
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
ling
3、-XX参数(使用率较高)
-XX参数也是非标准参数,主要用于jvm的调优和debug操作。
-XX参数的使用有2种方式,一种是boolean类型,一种是非boolean类型:
-
boolean类型
- 格式:-XX:[±]
- 如:-XX:+DisableExplicitGC 表示禁用手动调用gc操作,也就是说调用 System.gc()无效
-
非boolean类型
- 格式:-XX:
- 如:-XX:NewRatio=1 表示新生代和老年代的比值
用法:
[root@myshop02 ~]# java -showversion -XX:+DisableExplicitGC TestVM
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
ling
4、-Xms与-Xmx参数
-Xms与-Xmx分别是设置jvm的堆内存的初始大小和最大大小。
-Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。
-Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。 适当的调整jvm的内存大小,可以充分利用服务器资源,让程序跑的更快。
示例:
[root@myshop02 ~]# java -Xms512m -Xmx2048m TestVM
ling
5、查看jvm的运行参数
有些时候我们需要查看jvm的运行参数,这个需求可能会存在2种情况:
第一,运行java命令时打印出运行参数;
运行java命令时打印参数,需要添加-XX:+PrintFlagsFinal参数即可。
如:
[root@myshop02 ~]#java -XX:+PrintFlagsFinal -version
[Global flags]
intx ActiveProcessorCount = -1 {product}
uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
uintx AdaptiveSizePausePolicy = 0 {product}
uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product}
由上述的信息可以看出,参数有boolean类型和数字类型,值的操作符是=或:=,分别代 表默认值和被修改的值。
[root@myshop02 ~]# java -XX:+PrintFlagsFinal -XX:+VerifySharedSpaces -version
bool C1ProfileVirtualCalls = true {C1 product}
bool C1UpdateMethodData = true {C1 product}
intx CICompilerCount := 2 {product}
bool CICompilerCountPerCPU = true {product}
bool CITime = false {product}
#可以看到CICompilerCount这个参数已经被修改了。
第二,查看正在运行的java进程的参数;
如果想要查看正在运行的jvm就需要借助于jinfo命令查看。
首先,启动一个tomcat用于测试,来观察下运行的jvm参数。 我们就用之前部署的tomcat9
##启动tomcat9
[root@myshop02 ~]# cd /usr/local/tomcat9/bin
[root@myshop02 bin]# ./startup.sh
##启动后查看是否启动成功,jps查看进程,pid 3694,已经启动了
[root@myshop02 bin]# jps -l
4972 sun.tools.jps.Jps
3694 org.apache.catalina.startup.Bootstrap
##执行jinfo -flags 3694查看信息
[root@myshop02 bin]# jinfo -flags 3694
Attaching to process ID 3694, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.202-b08
Non-default VM flags: -XX:CICompilerCount=2 -XX:ConcGCThreads=1 -XX:G1HeapRegionSize=1048576 -XX:InitialHeapSize=134217728 -XX:+ManagementServer -XX:MarkStackSize=4194304 -XX:MaxHeapSize=1073741824 -XX:MaxNewSize=643825664 -XX:MinHeapDeltaBytes=1048576 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC
Command line: -Djava.util.logging.config.file=/usr/local/tomcat9/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -XX:+UseG1GC -Xms128m -Xmx1024m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=192.168.0.108 -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.rmi.port=9999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dignore.endorsed.dirs= -Dcatalina.base=/usr/local/tomcat9 -Dcatalina.home=/usr/local/tomcat9 -Djava.io.tmpdir=/usr/local/tomcat9/temp
##这是所有的信息,还可以查看单个信息,查看某一参数的值,用法:jinfo ‐flag <参数名> <进程id>
[root@myshop02 bin]# jinfo -flag MaxHeapSize 3694
-XX:MaxHeapSize=1073741824
三、jvm内存模型
jvm的内存模型在1.7和1.8有较大的区别,虽然本套课程是以1.8为例进行讲解,但是我们也是需要对1.7的内存模型有所了解,所以接下里,我们将先学习1.7再学习1.8的内存模型。
1、jdk1.7的堆内存模型
-
Young 年轻区(代):
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中, Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制 对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间 中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。
-
Tenured 年老区:
Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young 复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了 application级别的缓存,缓存中的对象往往会被转移到这一区间。
-
Perm 永久区:
Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性 加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到 java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因 就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造 成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以 解决问题。
-
Virtual区:
最大内存和初始内存的差值,就是Virtual区。
2、jdk1.8的堆内存模型
由上图可以看出,jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。 年轻代:Eden + 2*Survivor 年老代:OldGen 在jdk1.8中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。 需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存 空间中,这也是与1.7的永久代最大的区别所在 。
3、为什么要废弃1.7中的永久区?
官网给出了解释:http://openjdk.java.net/jeps/122
This is part of the JRockit and Hotspot convergence effort. JRockit
customers do not need to configure the permanent generation (since JRockit
does not have a permanent generation) and are accustomed to not
configuring the permanent generation.
移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,
不需要配置永久代。
现实使用中,由于永久代内存经常不够用或发生内存泄露,报出异常 java.lang.OutOfMemoryError: PermGen。 基于此,将永久区废弃,而改用元空间,改为了使用本地内存空间。
4、通过jstat命令查看堆内存使用情况
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。
命令的格式如下:
jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]
- 查看class加载数统计
[root@myshop02 bin]# jps
5030 Jps
3694 Bootstrap
[root@myshop02 bin]# jstat -class 3694
Loaded Bytes Unloaded Bytes Time
3367 6665.3 0 0.0 6.06
说明:
Loaded:加载class的数量
Bytes:所占用空间大小
Unloaded:未加载数量
Bytes:未加载占用空间
Time:时间
- 查看编译统计
[root@myshop02 bin]# jstat -compiler 3694
Compiled Failed Invalid Time FailedType FailedMethod
2485 0 0 16.95 0
说明: Compiled:编译数量。 Failed:失败数量 Invalid:不可用数量 Time:时间 FailedType:失败类型 FailedMethod:失败的方法
- 垃圾回收统计
[root@myshop02 bin]# jstat -gc 3694
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
0.0 4096.0 0.0 4096.0 77824.0 26624.0 49152.0 14074.5 21296.0 20696.2 2432.0 2274.5 11 0.430 0 0.000 0.430
说明:
S0C:第一个Survivor区的大小(KB)
S1C:第二个Survivor区的大小(KB)
S0U:第一个Survivor区的使用大小(KB)
S1U:第二个Survivor区的使用大小(KB)
EC:Eden区的大小(KB)
EU:Eden区的使用大小(KB)
OC:Old区大小(KB)
OU:Old使用大小(KB)
MC:方法区大小(KB)
MU:方法区使用大小(KB)
CCSC:压缩类空间大小(KB)
CCSU:压缩类空间使用大小(KB)
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
四、jmap的使用以及内存溢出分析
前面通过jstat可以对jvm堆的内存进行统计分析,而jmap可以获取到更加详细的内容。如:内存使用情况的汇总、对内存溢出的定位与分析。
1、查看内存使用情况
[root@myshop02 bin]# jmap -heap 3694
Attaching to process ID 3694, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.202-b08
##我这边用的是G1垃圾收集器
using thread-local object allocation.
Garbage-First (G1) GC with 1 thread(s)
Heap Configuration:#堆内存配置信息
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 1073741824 (1024.0MB)
NewSize = 1363144 (1.2999954223632812MB)
MaxNewSize = 643825664 (614.0MB)
OldSize = 5452592 (5.1999969482421875MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 1048576 (1.0MB)
Heap Usage:# 堆内存的使用情况
G1 Heap:#G1堆情况
regions = 1024
capacity = 1073741824 (1024.0MB)
used = 45869552 (43.74461364746094MB)
free = 1027872272 (980.2553863525391MB)
4.271934926509857% used
G1 Young Generation:#年轻代
Eden Space:
regions = 26
capacity = 79691776 (76.0MB)
used = 27262976 (26.0MB)
free = 52428800 (50.0MB)
34.21052631578947% used
Survivor Space:
regions = 4
capacity = 4194304 (4.0MB)
used = 4194304 (4.0MB)
free = 0 (0.0MB)
100.0% used
G1 Old Generation:##G1老年代
regions = 14
capacity = 50331648 (48.0MB)
used = 14412272 (13.744613647460938MB)
free = 35919376 (34.25538635253906MB)
28.63461176554362% used
16879 interned Strings occupying 1583944 bytes.
2、查看内存中对象数量及大小
#查看所有对象,包括活跃以及非活跃的
jmap ‐histo <pid> | more
#查看活跃对象
jmap ‐histo:live <pid> | more
[root@myshop02 bin]# jmap -histo:live 3694 | more
num #instances #bytes class name
----------------------------------------------
1: 31130 2898400 [C
2: 1402 802944 [B
3: 30895 741480 java.lang.String
4: 16607 531424 java.util.HashMap$Node
5: 3728 422344 java.lang.Class
6: 4621 406648 java.lang.reflect.Method
7: 7931 253792 java.util.concurrent.ConcurrentHashMap$Node
8: 4409 240128 [Ljava.lang.Object;
9: 1138 197296 [Ljava.util.HashMap$Node;
10: 1979 129448 [I
11: 112 107808 [Ljava.util.concurrent.ConcurrentHashMap$Node;
12: 1800 86400 java.util.HashMap
13: 5325 85200 java.lang.Object
14: 2608 55976 [Ljava.lang.Class;
15: 1292 51680 java.util.LinkedHashMap$Entry
16: 100 50304 [Ljava.util.WeakHashMap$Entry;
17: 1544 49408 java.util.Hashtable$Entry
#对象说明 B byte C char D double F float I int J long Z boolean [ 数组,如[I表示int[] [L+类名 其他对象
3、将内存使用情况dump到文件中
有些时候我们需要将jvm当前内存中的情况dump到文件中,然后对它进行分析,jmap也是支持dump到文件中的。
#用法:
jmap ‐dump:format=b,file=dumpFileName <pid>
#示例
jmap ‐dump:format=b,file=/root/dump.dat 3694
已经在/root下生成了dump.dat文件
4、jhat对dump文件进行分析
在上一小节中,我们将jvm的内存dump到文件中,这个文件是一个二进制的文件,不方便查看,这时我们可以借助于jhat工具进行查看。
#用法:
jhat ‐port <port> <file>
#示例:
[root@myshop02 ~]# jhat -port 9998 /root/dump.dat
Reading from /root/dump.dat...
Dump file created Mon Apr 06 15:10:56 CST 2020
Snapshot read, resolving...
Resolving 269315 objects...
Chasing references, expect 53 dots.....................................................
Eliminating duplicate references.....................................................
Snapshot resolved.
Started HTTP server on port 9998
Server is ready.
打开浏览器进行访问:http://192.168.0.108:9998
如果访问不了,需要防火墙开放9998端口。
[root@myshop02 ~]# firewall-cmd --zone=public --add-port=9998/tcp --permanent;
success
[root@myshop02 ~]# systemctl restart firewalld.service;
在最后面有OQL查询功能 。
点开之后可以执行OQL查询语句,如下,左侧是查询结果,字符串长度>=100的都查询出来了
5、MAT工具对dump文件进行分析
1)、MAT工具介绍
MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。
2)、下载安装
官网下载地址:https://www.eclipse.org/mat/downloads.php
下载解压后,双击打开
3)、使用
打开一个dump文件
选择自动检测
可疑对象查找
点击Histogram
点击dominator_tree查看依赖对象
查看可能存在内存泄露的分析
五、实战:内存溢出的定位与分析
内存溢出在实际的生产环境中经常会遇到,比如,不断的将数据写入到一个集合中,出 现了死循环,读取超大的文件等等,都可能会造成内存溢出。
如果出现了内存溢出,首先我们需要定位到发生内存溢出的环节,并且进行分析,是正 常还是非正常情况,如果是正常的需求,就应该考虑加大内存的设置,如果是非正常需 求,那么就要对代码进行修改,修复这个bug,我们需要借助 于jmap与MAT工具进行定位分析。
接下来,我们模拟内存溢出的场景。
1、模拟内存溢出
编写代码,向List集合中添加100万个字符串,每个字符串由1000个UUID组成。如果程序能够正常执行,最后打印ok(idea编辑器中需要设置内存溢出相关参数)
#参数如下:
‐Xms8m ‐Xmx8m ‐XX:+HeapDumpOnOutOfMemoryError
package com.zte.oom;
import java.util.ArrayList;
import java.util.UUID;
/**************************************************
*
* @title qq184480602
* @desc ling
* @author smart哥
*
**************************************************/
public class TestOOM {
public static void main(String[] args) {
ArrayList<String> stringArrayList = new ArrayList<>();
for (int i = 0; i <1000000 ; i++) {
String str="";
for (int j = 0; j <1000 ; j++) {
str=str+UUID.randomUUID().toString();
}
stringArrayList.add(str);
}
System.out.println("it is over!!");
}
}
2、运行测试
运行一段时间后会抛出java.lang.OutOfMemoryError: Java heap space 异常。
可以看到,发生了内存溢出,此时会dump文件到java_pid13320.hprof文件中。
3、导入到MAT工具中进行分析
查看Leak Suspects视图
可以看到,有89.07%的内存由Object[]数组占有,所以比较可疑。 分析:这个可疑是正确的,因为已经有超过89%的内存都被它占有,这是非常有可能出现内存溢出的。 查看Object Details视图详情如下
查看Object Details视图
可以看到集合中存储了大量的uuid字符串。
继续查看dominator视图进一步验证。
查看dominator视图
具体分析过程请观看视频
六、jstack的使用
有些时候我们需要查看下jvm中的线程执行情况,比如,发现服务器的CPU的负载突然增 高了、出现了死锁、死循环等,我们该如何分析呢?
由于程序是正常运行的,没有任何的输出,从日志方面也看不出什么问题,所以就需要 看下jvm的内部线程的执行情况,然后再进行分析查找出原因。
这个时候,就需要借助于jstack命令了,jstack的作用是将正在运行的jvm的线程情况进 行快照,并且打印出来 :
#用法:jstack <pid>
[root@myshop02 ~]# jstack 3694
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.202-b08 mixed mode):
"Attach Listener" #32 daemon prio=9 os_prio=0 tid=0x00007fed14008800 nid=0x138c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Catalina-utility-2" #31 prio=1 os_prio=0 tid=0x00007fed2005c000 nid=0xe91 waiting on condition [0x00007fed0cfc9000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000c030dbc8> (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.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1088)
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 org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
......
1、线程的状态
由上图可知,在Java中线程的状态一共被分成6种:
1)、初始态(NEW)
创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。
2)、运行态(RUNNABLE)
在Java中,运行态包括 就绪态和运行态。
就绪态:
该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运 行。
所有就绪态的线程存放在就绪队列中。
运行态:
获得CPU执行权,正在执行的线程。
由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条 运行态的线程。
3)、阻塞态(BLOCKED)
当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。
而在Java中,阻塞态专指请求锁失败时进入的状态。
由一个阻塞队列存放所有阻塞态的线程。
处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。
4)、等待态(WAITING)
当前线程中调用wait、join、park函数时,当前线程就会进入等待态。
也有一个等待队列存放所有等待态的线程。
线程处于等待态表示它需要等待其他线程的指示才能继续运行。
进入等待态的线程会释放CPU执行权,并释放资源(如:锁)
5)、超时等待态(TIMED_WAITING)
当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就 会进入该状态;
它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后需要其 他线程唤醒;
进入该状态后释放CPU执行权 和 占有的资源。
与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁。
6)、终止态(TERMINATED)
线程执行结束后的状态。
2、实战:死锁问题
如果在生产环境发生了死锁,我们将看到的是部署的程序没有任何反应了,这个时候我们可以借助jstack进行分析,下面我们实战下查找死锁的原因。
-
构造死锁
思路:两个线程thread1,thread2,thread1拥有obj1锁,thread2拥有obj2锁。此时thread1想要获取obj2锁,thread2也想要获取obj1锁,于是出现死锁。
代码如下:
public class TestDeadLock {
private static Object obj1 = new Object();
private static Object obj2 = new Object();
public static void main(String[] args) {
new Thread(new Thread1()).start();
new Thread(new Thread2()).start();
}
private static class Thread1 implements Runnable {
@Override
public void run() {
synchronized (obj1) {
try {
System.out.println(">>>>>>>>>>>" + Thread.currentThread().getName() + "获取了obj1锁");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
System.out.println(">>>>>>>>>>>" + Thread.currentThread().getName() + "获取了obj2锁");
}
}
}
}
private static class Thread2 implements Runnable {
@Override
public void run() {
synchronized (obj2) {
try {
System.out.println(">>>>>>>>>>>" + Thread.currentThread().getName() + "获取了obj2锁");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1) {
System.out.println(">>>>>>>>>>>" + Thread.currentThread().getName() + "获取了obj1锁");
}
}
}
}
}
- 编码测试
E:\dev_env\jdk8\bin\java "-javaagent:E:\smart_an1\devtool\JetBrains\IntelliJ IDEA 2018.1\lib\idea_rt.jar=20149:E:\smart_an1\devtool\JetBrains\IntelliJ IDEA 2018.1\bin" -Dfile.encoding=UTF-8 -classpath E:\dev_env\jdk8\jre\lib\charsets.jar;E:\dev_env\jdk8\jre\lib\deploy.jar;E:\dev_env\jdk8\jre\lib\ext\access-bridge-64.jar;E:\dev_env\jdk8\jre\lib\ext\cldrdata.jar;E:\dev_env\jdk8\jre\lib\ext\dnsns.jar;E:\dev_env\jdk8\jre\lib\ext\jaccess.jar;E:\dev_env\jdk8\jre\lib\ext\jfxrt.jar;E:\dev_env\jdk8\jre\lib\ext\localedata.jar;E:\dev_env\jdk8\jre\lib\ext\nashorn.jar;E:\dev_env\jdk8\jre\lib\ext\sunec.jar;E:\dev_env\jdk8\jre\lib\ext\sunjce_provider.jar;E:\dev_env\jdk8\jre\lib\ext\sunmscapi.jar;E:\dev_env\jdk8\jre\lib\ext\sunpkcs11.jar;E:\dev_env\jdk8\jre\lib\ext\zipfs.jar;E:\dev_env\jdk8\jre\lib\javaws.jar;E:\dev_env\jdk8\jre\lib\jce.jar;E:\dev_env\jdk8\jre\lib\jfr.jar;E:\dev_env\jdk8\jre\lib\jfxswt.jar;E:\dev_env\jdk8\jre\lib\jsse.jar;E:\dev_env\jdk8\jre\lib\management-agent.jar;E:\dev_env\jdk8\jre\lib\plugin.jar;E:\dev_env\jdk8\jre\lib\resources.jar;E:\dev_env\jdk8\jre\lib\rt.jar;E:\dev_env\IdeaProjects\performance-tuning\out\production\oom com.zte.deadlock.TestDeadLock
>>>>>>>>>>>Thread-0获取了obj1锁
>>>>>>>>>>>Thread-1获取了obj2锁
//程序卡死在这里,互相等待锁
-
使用jstack进行分析
使用jstack命令查看死锁结果并分析如下:
在输出的信息中,已经看到,发现了1个死锁,关键看这个
七、jvisualvm的使用
使用jvisualvm能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。
jvisualvm使用简单,几乎0配置,功能还是比较丰富的,几乎囊括了其它JDK自带命令的所有功能。
- 内存信息
- 线程信息
- Dump堆(本地进程)
- Dump线程(本地进程)
- 打开堆Dump。堆Dump可以用jmap来生成。
- 打开线程Dump
- 生成应用快照(包含内存信息、线程信息等等)
- 性能分析。CPU分析(各个方法调用时间,检查哪些方法耗时多),内存分析(各类 对象占用的内存,检查哪些类占用内存多)
- ……
1、启动
在jdk的安装目录的bin目录下,找到jvisualvm.exe,双击打开即可
2、查看本地进程
在本地进程页(idea)可以看到jvm基本信息及参数信息
3、查看CPU、内存、类、线程运行信息
4、查看线程详情
点击右上角Dump线程,将线程的信息导出,其实就是执行的jstack命令
5、抽样器
抽样器可以对CPU、内存在一段时间内进行抽样,以供分析
6、监控远程的jvm
jvisualvm不仅是可以监控本地jvm进程,还可以监控远程的jvm进程,需要借助于JMX技术实现。
-
什么是JMX?
JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系 统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络 传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。
-
监控远程的tomcat
想要监控远程的tomcat,就需要在远程的tomcat进行对JMX配置,配置完毕重启tomcat
#在tomcat的bin目录下,修改catalina.sh,添加如下的参数
CATALINA_OPTS="$CATALINA_OPTS
-Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=192.168.0.108
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.rmi.port=9999
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false"
#这几个参数的意思是:
#‐Dcom.sun.management.jmxremote :允许使用JMX远程管理
#‐Dcom.sun.management.jmxremote.port=9999 :JMX远程连接端口
#‐Dcom.sun.management.jmxremote.authenticate=false :不进行身份认证,任何用户都可以连接
#‐Dcom.sun.management.jmxremote.ssl=false :不使用ssl
-
使用jvisualvm连接远程tomcat
添加远程主机(右击远程,添加主机)
在一个主机下可能会有很多的jvm需要监控,所以在该主机上添加需要监控的jvm
添加成功,如下图所示