目录
一、我们为什么要对jvm做优化?
在本地开发环境中我们很少会遇到需要对jvm进行优化的需求,但是到了生产环境,我们可能将有下面 的需求:
- 运行的应用“卡住了”,日志不输出,程序没有反应
- 服务器的CPU负载突然升高
- 在多线程应用下,如何分配线程的数量?
- ……
在本次课程中,我们将对jvm有更深入的学习,我们不仅要让程序能跑起来,而且是可以跑的更快!
可以分析解决在生产环境中所遇到的各种“棘手”的问题。
说明:本套课程使用的jdk版本为1.8。
二、jvm的运行参数
在jvm中有很多的参数可以进行设置,这样可以让jvm在各种环境中都能够高效的运行。 绝大部分的参数保持默认即可。
1. 三种参数类型
jvm的参数类型分为三类,分别是:
- 标准参数
-
- -help
- -version
- -X参数 (非标准参数)
-
- -Xint
- -Xcomp
- -XX参数(使用率较高)
-
- -XX:newSize
- -XX:+UseSerialGC
1.1. 标准
1> 参数介绍
jvm的标准参数,一般都是很稳定的,在未来的JVM版本中不会改变,可以使用java -help 检索出所 有的标准参数。
[root@node01 ~]# java -help
用法: java [-options] class [args...] (执行类) 或 java [-options] -jar jarfile [args...]
(执行 jar 文件)
其中选项包括:
-d32 使用 32 位数据模型 (如果可用)
-d64 使用 64 位数据模型 (如果可用)
-server 选择 "server" VM 默认 VM 是 server, 因为您是在服务器类计算机上运行。
-cp <目录和 zip/jar 文件的类搜索路径>
-classpath <目录和 zip/jar 文件的类搜索路径> 用 : 分隔的目录, JAR 档案 和 ZIP
档案列表, 用于搜索类文件。
-D<名称>=<值> 设置系统属性
-verbose:[class|gc|jni] 启用详细输出
-version 输出产品版本并退出
-version:<值> 警告: 此功能已过时, 将在 未来发行版中删除。 需要指定的版本才能运行
-showversion 输出产品版本并继续
-jre-restrict-search | -no-jre-restrict-search 警告: 此功能已过时, 将在 未来
发行版中删除。 在版本搜索中包括/排除用户专用 JRE
-? -help 输出此帮助消息
-X 输出非标准选项的帮助
-ea[:<packagename>...|:<classname>]
-enableassertions[:<packagename>...|:<classname>] 按指定的粒度启用断言
-da[:<packagename>...|:<classname>]
-disableassertions[:<packagename>...|:<classname>] 禁用具有指定粒度的断言
-esa | -enablesystemassertions 启用系统断言
-dsa | -disablesystemassertions 禁用系统断言
-agentlib:<libname>[=<选项>] 加载本机代理库 <libname>, 例如 -agentlib:hprof
另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<选项>] 按完整路径名加载本机代理库
-javaagent:<jarpath>[=<选项>] 加载 Java 编程语言代理, 请参阅
java.lang.instrument
-splash:<imagepath> 使用指定的图像显示启动屏幕
2> 实战
实战1:查看jvm版本
[root@node01 ~]# java -version
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)
# -showversion参数是表示,先打印版本信息,再执行后面的命令,在调试时非常有用, 后面会使用到
实战2:通过-D设置系统属性参数
public class TestJVM {
public static void main(String[] args) {
String str = System.getProperty("str");
if (str == null) {
System.out.println("kkb");
} else {
System.out.println(str);
}
}
}
进行编译、测试:
#编译
[root@node01 test]# javac TestJVM.java
#测试
[root@node01 test]# java TestJVM
kkb
[root@node01 test]# java -Dstr=123 TestJVM
123
3> -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@node01 test]# java -client -showversion TestJVM
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)
kkb
[root@node01 test]# java -server -showversion TestJVM
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)
kkb
#由于机器是64位系统,所以不支持client模式
1.2. -X参数
1> 参数介绍
jvm的-X参数是非标准参数,在不同版本的jvm中,参数可能会有所不同,可以通过java - X查看非标准参数。
[root@node01 test]# 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 选项是非标准选项, 如有更改, 恕不另行通知。
2> -Xint、-Xcomp、-Xmixed
- 在解释模式(interpreted mode)下,-Xint标记会强制JVM执行所有的字节码,当然这会降低运 行速度,通常低10倍或更多。
- -Xcomp参数与它(-Xint)正好相反,JVM在第一次使用时会把所有的字节码编译成本地代码,从 而带来最大程度的优化。 然而,很多应用在使用-Xcomp也会有一些性能损失,当然这比使用-Xint损失的少,原因是xcomp没有让JVM启用JIT编译器的全部功能。JIT编译器可以对是否需要编译做判断,如果所有代 码都进行编译的话,对于一些只执行一次的代码就没有意义了。
- -Xmixed是混合模式,将解释模式与编译模式进行混合使用,由jvm自己决定,这是jvm默认的模 式,也是推荐使用的模式。
示例:强制设置运行模式
#强制设置为解释模式
[root@node01 test]# java -showversion -Xint TestJVM
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, interpreted mode)
kkb
#强制设置为编译模式
[root@node01 test]# java -showversion -Xcomp TestJVM
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, compiled mode)
kkb
#注意:编译模式下,第一次执行会比解释模式下执行慢一些,注意观察。
#默认的混合模式
[root@node01 test]# java -showversion TestJVM
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)
kkb
1.3. -XX参数
-XX参数也是非标准参数,主要用于jvm的调优和debug操作。
-XX参数的使用有2种方式,一种是boolean类型,一种是非boolean类型:
- boolean类型格式:-XX:[+-] 如:-XX:+DisableExplicitGC 表示禁用手动调用gc操作,也就是说调用System.gc()无效
- 非boolean类型格式:-XX: 如:-XX:NewRatio=1 表示新生代和老年代的比值
用法:
[root@node01 test]# java -showversion -XX:+DisableExplicitGC TestJVM
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)
kkb
-Xms与-Xmx参数
-Xms与-Xmx分别是设置jvm的堆内存的初始大小和最大大小。
-Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。
-Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。
适当的调整jvm的内存大小,可以充分利用服务器资源,让程序跑的更快。
示例:
[root@node01 test]# java -Xms512m -Xmx2048m TestJVM
kkb
2. 查看jvm的运行参数
有些时候我们需要查看 jvm 的运行参数,这个需求可能会存在 2 种情况:
第一,运行 java 命令时打印出运行参数;
第二,查看正在运行的 java 进程的参数;
2.1. 运行 java 命令时打印参数
运行 java 命令时打印参数,需要添加 -XX:+PrintFlagsFinal 参数即可。
[root@node01 test]# java -XX:+PrintFlagsFinal -version
[Global flags]
uintx AdaptiveSizeDecrementScaleFactor = 4
{product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10
{product}
uintx AdaptiveSizePausePolicy = 0
{product}
uintx AdaptiveSizePolicyCollectionCostMargin = 50
{product}
uintx AdaptiveSizePolicyInitializingSteps = 20
{product}
uintx AdaptiveSizePolicyOutputInterval = 0
{product}
uintx AdaptiveSizePolicyWeight = 10
{product}
uintx AdaptiveSizeThroughPutPolicy = 0
{product}
uintx AdaptiveTimeWeight = 25
{product}
bool AdjustConcurrency = false
{product}
bool AggressiveOpts = false
{product}
intx AliasLevel = 3
{C2 product}
bool AlignVector = true
{C2 product}
intx AllocateInstancePrefetchLines = 1
{product}
intx AllocatePrefetchDistance = 256
{product}
intx AllocatePrefetchInstr = 0
{product}
…………………………略…………………………………………
bool UseXmmI2D = false
{ARCH product}
bool UseXmmI2F = false
{ARCH product}
bool UseXmmLoadAndClearUpper = true
{ARCH product}
bool UseXmmRegToRegMoveAll = true
{ARCH product}
bool VMThreadHintNoPreempt = false
{product}
intx VMThreadPriority = -1
{product}
intx VMThreadStackSize = 1024
{pd product}
intx ValueMapInitialSize = 11
{C1 product}
intx ValueMapMaxLoopSize = 8
{C1 product}
intx ValueSearchLimit = 1000
{C2 product}
bool VerifyMergedCPBytecodes = true
{product}
bool VerifySharedSpaces = false
{product}
intx WorkAroundNPTLTimedWaitHang = 1
{product}
uintx YoungGenerationSizeIncrement = 20
{product}
uintx YoungGenerationSizeSupplement = 80
{product}
uintx YoungGenerationSizeSupplementDecay = 8
{product}
uintx YoungPLABSize = 4096
{product}
bool ZeroTLAB = false
{product}
intx hashCode = 5
{product}
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)
由上述的信息可以看出,参数有boolean类型和数字类型,值的操作符是=或:=,分别代 表默认值和被 修改的值。
示例:
java -XX:+PrintFlagsFinal -XX:+VerifySharedSpaces -version
intx ValueMapInitialSize = 11 {C1 product}
intx ValueMapMaxLoopSize = 8 {C1 product}
intx ValueSearchLimit = 1000 {C2 product}
bool VerifyMergedCPBytecodes = true {product}
bool VerifySharedSpaces := true {product}
intx WorkAroundNPTLTimedWaitHang = 1 {product}
uintx YoungGenerationSizeIncrement = 20 {product}
uintx YoungGenerationSizeSupplement = 80 {product}
uintx YoungGenerationSizeSupplementDecay = 8 {product}
uintx YoungPLABSize = 4096 {product}
bool ZeroTLAB = false {product}
intx hashCode = 5 {product}
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)
#可以看到VerifySharedSpaces这个参数已经被修改了。
2.2. 查看正在运行的 jvm 参数
如果想要查看正在运行的jvm就需要借助于jinfo命令查看。
首先,启动一个tomcat用于测试,来观察下运行的jvm参数。
cd /tmp/
rz 上传
tar -xvf apache-tomcat-7.0.57.tar.gz
cd apache-tomcat-7.0.57
cd bin/
./startup.sh
#http://192.168.40.133:8080/ 进行访问
访问成功:
#查看所有的参数,用法:jinfo -flags <进程id>
#通过jps 或者 jps -l 查看java进程
[root@node01 bin]# jps
6346 Jps
6219 Bootstrap
[root@node01 bin]# jps -l
6358 sun.tools.jps.Jps
6219 org.apache.catalina.startup.Bootstrap
[root@node01 bin]#
[root@node01 bin]# jinfo -flags 6219
Attaching to process ID 6219, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.141-b15
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=31457280
-XX:MaxHeapSize=488636416 -XX:MaxNewSize=162529280 -
XX:MinHeapDeltaBytes=524288 -XX:NewSize=10485760 -XX:OldSize=20971520 -
XX:+UseCompressedClassPointers -XX:+UseCompressedOops -
XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC Command line:
-Djava.util.logging.config.file=/tmp/apache-tomcat
7.0.57/conf/logging.properties
-
Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -
Djava.endorsed.dirs=/tmp/apache-tomcat-7.0.57/endorsed -
Dcatalina.base=/tmp/apache-tomcat-7.0.57 -Dcatalina.home=/tmp/apache
tomcat-7.0.57
-Djava.io.tmpdir=/tmp/apache-tomcat-7.0.57/temp
#查看某一参数的值,用法:jinfo -flag <参数名> <进程id>
[root@node01 bin]# jinfo -flag MaxHeapSize 6219
-XX:MaxHeapSize=488636416
三、jvm的内存模型
jvm的内存模型在1.7和1.8有较大的区别,虽然本套课程是以1.8为例进行讲解,但是我们也是需要对 1.7的内存模型有所了解,
所以接下里,我们将先学习1.7再学习1.8的内存模型。
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对象 保存在了permq中,这种情况下,一般重新启动应用服务器可以解决问题。
- Virtual区最大内存和初始内存的差值,就是Virtual区。
jdk1.8的堆内存模型
由上图可以看出,jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。
年轻代:Eden + 2*Survivor
年老代:OldGen
在jdk1.8中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。
需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是 与1.7的永久代最大的区别所在。
为什么要废弃1.7中的永久区?
官网给出了解释:JEP 122: Remove the Permanent Generation
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。
基于此,将永久区废弃,而改用元空间,改为了使用本地内存空间。
四、通过jstat命令进行查看堆内存使用情况
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:
jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]
查看class加载统计
[root@node01 ~]# jps
7080 Jps
6219 Bootstrap
[root@node01 ~]# jstat -class 6219
Loaded Bytes Unloaded Bytes Time
3273 7122.3 0 0.0 3.98
说明:
- Loaded:加载class的数量
- Bytes:所占用空间大小
- Unloaded:未加载数量
- Bytes:未加载占用空间
- Time:时间
查看编译统计
[root@node01 ~]# jstat -compiler 6219
Compiled Failed Invalid Time FailedType FailedMethod
2376 1 0 8.04 1 org/apache/tomcat/util/IntrospectionUtils
setProperty
说明:
- Compiled:编译数量。
- Failed:失败数量
- Invalid:不可用数量
- Time:时间
- FailedType:失败类型
- FailedMethod:失败的方法
垃圾回收统计
[root@node01 ~]# jstat -gc 6219
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
9216.0 8704.0 0.0 6127.3 62976.0 3560.4 33792.0 20434.9 23808.0 23196.1
2560.0 2361.6 7 1.078 1 0.244 1.323
#也可以指定打印的间隔和次数,每1秒中打印一次,共打印5次
[root@node01 ~]# jstat -gc 6219 1000 5
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
9216.0 8704.0 0.0 6127.3 62976.0 3917.3 33792.0 20434.9 23808.0 23196.1
2560.0 2361.6 7 1.078 1 0.244 1.323
9216.0 8704.0 0.0 6127.3 62976.0 3917.3 33792.0 20434.9 23808.0 23196.1
2560.0 2361.6 7 1.078 1 0.244 1.323
9216.0 8704.0 0.0 6127.3 62976.0 3917.3 33792.0 20434.9 23808.0 23196.1
2560.0 2361.6 7 1.078 1 0.244 1.323
9216.0 8704.0 0.0 6127.3 62976.0 3917.3 33792.0 20434.9 23808.0 23196.1
2560.0 2361.6 7 1.078 1 0.244 1.323
9216.0 8704.0 0.0 6127.3 62976.0 3917.3 33792.0 20434.9 23808.0 23196.1
2560.0 2361.6 7 1.078 1 0.244 1.323
说明:
- 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:年轻代垃圾回收消耗时间
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
五、jmap的使用以及内存溢出分析
前面通过jstat可以对jvm堆的内存进行统计分析,而jmap可以获取到更加详细的内容,
如:内存使用 情况的汇总、对内存溢出的定位与分析。
查看内存使用情况
[root@node01 ~]# jmap -heap 6219
Attaching to process ID 6219, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.141-b15
using thread-local object allocation.
Parallel GC with 2 thread(s)
Heap Configuration: #堆内存配置信息
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 488636416 (466.0MB)
NewSize = 10485760 (10.0MB)
MaxNewSize = 162529280 (155.0MB)
OldSize = 20971520 (20.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage: # 堆内存的使用情况
PS Young Generation #年轻代
Eden Space:
capacity = 123731968 (118.0MB)
used = 1384736 (1.320587158203125MB)
free = 122347232 (116.67941284179688MB)
1.1191416594941737% used
From Space:
capacity = 9437184 (9.0MB)
used = 0 (0.0MB)
free = 9437184 (9.0MB)
0.0% used
To Space:
capacity = 9437184 (9.0MB)
used = 0 (0.0MB)
free = 9437184 (9.0MB)
0.0% used
PS Old Generation #年老代
capacity = 28311552 (27.0MB)
used = 13698672 (13.064071655273438MB)
free = 14612880 (13.935928344726562MB)
48.38545057508681% used
13648 interned Strings occupying 1866368 bytes.
查看内存中对象数量及大小
#查看所有对象,包括活跃以及非活跃的 jmap -histo <pid> | more #查看活跃对象 jmap
-histo:live <pid> | more [root@node01 ~]# jmap -histo:live 6219 | more num
#instances #bytes class name ---------------------------------------------- 1:
37437 7914608 [C 2: 34916 837984 java.lang.String 3: 884 654848 [B 4: 17188
550016 java.util.HashMap$Node 5: 3674 424968 java.lang.Class 6: 6322 395512
[Ljava.lang.Object; 7: 3738 328944 java.lang.reflect.Method 8: 1028 208048
[Ljava.util.HashMap$Node; 9: 2247 144264 [I 10: 4305 137760
java.util.concurrent.ConcurrentHashMap$Node 11: 1270 109080
[Ljava.lang.String; 12: 64 84128
[Ljava.util.concurrent.ConcurrentHashMap$Node; 13: 1714 82272
java.util.HashMap 14: 3285 70072 [Ljava.lang.Class; 15: 2888 69312
java.util.ArrayList 16: 3983 63728 java.lang.Object 17: 1271 61008
org.apache.tomcat.util.digester.CallMethodRule 18: 1518 60720
java.util.LinkedHashMap$Entry 19: 1671 53472
com.sun.org.apache.xerces.internal.xni.QName 20: 88 50880
[Ljava.util.WeakHashMap$Entry; 21: 618 49440 java.lang.reflect.Constructor
22: 1545 49440 java.util.Hashtable$Entry 23: 1027 41080
java.util.TreeMap$Entry 24: 846 40608
org.apache.tomcat.util.modeler.AttributeInfo 25: 142 38032 [S
26: 946 37840 java.lang.ref.SoftReference 27: 226 36816 [[C。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。 #对象说明 B byte C char D
double F float I int J long Z boolean [ 数组,如[I表示int[] [L+类名 其他对象
将内存使用情况dump到文件中
有些时候我们需要将jvm当前内存中的情况dump到文件中,然后对它进行分析,jmap也是支持dump到文 件中的。
#用法:
jmap -dump:format=b,file=dumpFileName <pid>
#示例
jmap -dump:format=b,file=/tmp/dump.dat 6219
可以看到已经在/tmp下生成了dump.dat的文件。
六、通过jhat对dump文件进行分析
在上一小节中,我们将jvm的内存dump到文件中,这个文件是一个二进制的文件,不方便查看,
这时我 们可以借助于jhat工具进行查看。
用法:
jhat -port <port> <file>
示例:
[root@node01 tmp]# jhat -port 9999 /tmp/dump.dat
Reading from /tmp/dump.dat...
Dump file created Mon Sep 10 01:04:21 CST 2018
Snapshot read, resolving...
Resolving 204094 objects...
Chasing references, expect 40
dots........................................
Eliminating duplicate references........................................
Snapshot resolved.
Started HTTP server on port 9999
Server is ready.
打开浏览器进行访问:http://192.168.40.133:9999/
在最后面有OQL查询功能。
七、通过MAT工具对dump文件进行分析
1. MAT工具介绍
MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找
内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行 分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾
收集器的回收工作,并可以通过报 表直观的查看到可能造成这种结果的对象。
官网地址:Memory Analyzer (MAT) | The Eclipse Foundation
2. 下载安装
下载地址:Not Found
将下载得到的MemoryAnalyzer-1.8.0.20180604-win32.win32.x86_64.zip进行解压:
3. 使用
查看对象以及它的依赖:
查看可能存在内存泄露的分析:
4. 内存溢出的定位与分析(实战)
内存溢出在实际的生产环境中经常会遇到,比如,不断的将数据写入到一个集合中,出现了死循环,读 取超大的文件等等,都可能会造
成内存溢出。
如果出现了内存溢出,首先我们需要定位到发生内存溢出的环节,并且进行分析,是正 常还是非正常 情况,如果是正常的需求,就应该
考虑加大内存的设置,如果是非正常需求,那么就要对代码进行修 改,修复这个bug。
首先,我们得先学会如何定位问题,然后再进行分析。如何定位问题呢,我们需要借助于jmap与MAT工 具进行定位分析。
接下来,我们模拟内存溢出的场景。
4.1. 模拟内存溢出
编写代码,向List集合中添加100万个字符串,每个字符串由1000个UUID组成。如果程序能够正常执 行,最后打印ok。
package com.wclass.jvm;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class TestJvmOutOfMemory {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
for (int i = 0; i < 10000000; i++) {
String str = "";
for (int j = 0; j < 1000; j++) {
str += UUID.randomUUID().toString();
}
list.add(str);
}
System.out.println("ok");
}
}
为了演示效果,我们将设置执行的参数,这里使用的是Idea编辑器。
#参数如下:
-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
4.2. 运行测试
测试结果如下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid5348.hprof ...
Heap dump file created [8137186 bytes in 0.032 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at
java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilde
r.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.wclass.jvm.TestJvmOutOfMemory.main(TestJvmOutOfMemory.java:14)
Process finished with exit code 1
可以看到,当发生内存溢出时,会dump文件到java_pid5348.hprof。
4.3. 导入到MAT工具中进行分析
可以看到,有91.03%的内存由Object[]数组占有,所以比较可疑。
分析:这个可疑是正确的,因为已经有超过90%的内存都被它占有,这是非常有可能出现内存溢出的。
查看详情:
可以看到集合中存储了大量的uuid字符串。
八、jstack的使用
有些时候我们需要查看下jvm中的线程执行情况,
比如,发现服务器的CPU的负载突然增高了、出现了死 锁、死循环等,我们该如何分析呢?
由于程序是正常运行的,没有任何的输出,从日志方面也看不出什么问题,
所以就需要看下jvm的内部 线程的执行情况,然后再进行分析查找出原因。
这个时候,就需要借助于jstack命令了,jstack的作用是将正在运行的jvm的线程情况进行快照,并且 打印出来:
用法:jstack
[root@node01 bin]# jstack 2203
1. 线程的状态
在Java中线程的状态一共被分成6种:
- 初始态(NEW) 创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。
- 运行态(RUNNABLE),在Java中,运行态包括 就绪态 和 运行态。
-
- 就绪态该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。 所有就绪态的线程存放在就绪队列中。
- 运行态获得CPU执行权,正在执行的线程。 由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程。
- 阻塞态(BLOCKED) 当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。 而在Java中,阻塞态专指请求锁失败时进入的状态。 由一个阻塞队列存放所有阻塞态的线程。 处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。
- 等待态(WAITING) 当前线程中调用wait、join、park函数时,当前线程就会进入等待态。 也有一个等待队列存放所有等待态的线程。 线程处于等待态表示它需要等待其他线程的指示才能继续运行。 进入等待态的线程会释放CPU执行权,并释放资源(如:锁)
- 超时等待态(TIMED_WAITING) 当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状 态; 它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后需要其他线程唤醒; 进入该状态后释放CPU执行权 和 占有的资源。 与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁。
- 终止态(TERMINATED) 线程执行结束后的状态。
2. 死锁问题(实战)
如果在生产环境发生了死锁,我们将看到的是部署的程序没有任何反应了,这个时候我们可以借助 jstack进行分析,
下面我们实战下查找死锁的原因。
2.1. 构造死锁
编写代码,启动2个线程,Thread1拿到了obj1锁,准备去拿obj2锁时,obj2已经被Thread2锁定,所 以发送了死锁。
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){
System.out.println("Thread1 拿到了 obj1 的锁!");
try {
// 停顿2秒的意义在于,让Thread2线程拿到obj2的锁
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2){
System.out.println("Thread1 拿到了 obj2 的锁!");
}
}
}
}
private static class Thread2 implements Runnable{
@Override
public void run() {
synchronized (obj2){
System.out.println("Thread2 拿到了 obj2 的锁!");
try {
// 停顿2秒的意义在于,让Thread1线程拿到obj1的锁
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1){
System.out.println("Thread2 拿到了 obj1 的锁!");
}
}
}
}
}
2.2. 在linux上运行
[root@node01 test]# javac TestDeadLock.java
[root@node01 test]# ll
总用量 28
-rw-r--r--. 1 root root 184 9月 11 10:39 TestDeadLock$1.class
-rw-r--r--. 1 root root 843 9月 11 10:39 TestDeadLock.class
-rw-r--r--. 1 root root 1567 9月 11 10:39 TestDeadLock.java
-rw-r--r--. 1 root root 1078 9月 11 10:39 TestDeadLock$Thread1.class
-rw-r--r--. 1 root root 1078 9月 11 10:39 TestDeadLock$Thread2.class
-rw-r--r--. 1 root root 573 9月 9 10:21 TestJVM.class
-rw-r--r--. 1 root root 261 9月 9 10:21 TestJVM.java
[root@node01 test]# java TestDeadLock
Thread1 拿到了 obj1 的锁!
Thread2 拿到了 obj2 的锁!
#这里发生了死锁,程序一直将等待下去
2.3. 使用jstack进行分析
[root@node01 ~]# jstack 3256
可以清晰的看到:
Thread2获取了 <0x00000000f655dc50> 的锁,等待获取 <0x00000000f655dc40>这个锁
Thread1获取了 <0x00000000f655dc40> 的锁,等待获取 <0x00000000f655dc50>这个锁 由此可见,发生了死锁。
九、VisualVM工具的使用
VisualVM,能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,
反向查 看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。
VisualVM使用简单,几乎0配置,功能还是比较丰富的,几乎囊括了其它JDK自带命令的所有功能。
- 内存信息
- 线程信息
- Dump堆(本地进程)
- Dump线程(本地进程)
- 打开堆Dump。堆Dump可以用jmap来生成。
- 打开线程Dump
- 生成应用快照(包含内存信息、线程信息等等)
- 性能分析。CPU分析(各个方法调用时间,检查哪些方法耗时多),内存分析(各类对象占用的内 存,检查哪些类占用内存多)
- ……
1. 启动
在jdk的安装目录的bin目录下,找到jvisualvm.exe,双击打开即可。
2. 查看本地进程
3. 查看CPU、内存、类、线程运行信息
4. 查看线程详情
也可以点击右上角Dump按钮,将线程的信息导出,其实就是执行的jstack命令。
发现,显示的内容是一样的。
5. 抽样器
抽样器可以对CPU、内存在一段时间内进行抽样,以供分析。
6. 监控远程的jvm
VisualJVM不仅是可以监控本地jvm进程,还可以监控远程的jvm进程,需要借助于JMX技术
6.1. 什么是JMX?
JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理 功能的框架。
JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝 集成的系统、网络和服务管理应用。
6.2. 监控远程的tomcat
想要监控远程的tomcat,就需要在远程的tomcat进行对JMX配置,方法如下:
#在tomcat的bin目录下,修改catalina.sh,添加如下的参数
JAVA_OPTS="-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=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
保存退出,重启tomcat。
6.3. 使用VisualJVM连接远程tomcat
添加远程主机:
在一个主机下可能会有很多的jvm需要监控,所以接下来要在该主机上添加需要监控的jvm:
连接成功。使用方法和前面就一样了,就可以和监控本地jvm进程一样,监控远程的tomcat进程。