jvm调优

1、我们为什么要对jvm做优化?

在本地开发环境中我们很少会遇到需要对jvm进行优化的需求,但是到了生产环境,我们可能将有下面的需求:

  • 运行的应用“卡住了”,日志不输出,程序没有反应
  • 服务器的CPU负载突然升高
  • 在多线程应用下,如何分配线程的数量?
  • ……

在本次课程中,我们将对jvm有更深入的学习,我们不仅要让程序能跑起来,而且是可以跑的更快!可以分析解决
在生产环境中所遇到的各种“棘手”的问题。

说明:本套课程使用的jdk版本为1.8。

2、jvm的运行参数

在jvm中有很多的参数可以进行设置,这样可以让jvm在各种环境中都能够高效的运行。绝大部分的参数保持默认即可。

2.1、三种参数类型

jvm的参数类型分为三类,分别是:

  • 标准参数
-help
-version
  • -X参数 (非标准参数)
-Xint
-Xcomp
  • -XX参数(使用率较高)
-XX:newSize
-XX:+UseSerialGC

2.2、标准参数

jvm的标准参数,一般都是很稳定的,在未来的JVM版本中不会改变,可以使用java -help检索出所有的标准参数。

[root@VM-12-5-centos test]# java -help
用法:java [options] <主类> [args...]
           (执行类)
   或  java [options] -jar <jar 文件> [args...]
           (执行 jar 文件)
   或  java [options] -m <模块>[/<主类>] [args...]
       java [options] --module <模块>[/<主类>] [args...]
           (执行模块中的主类)
   或  java [options] <源文件> [args]
           (执行单个源文件程序)

 将主类、源文件、-jar <jar 文件>、-m 或
 --module <模块>/<主类> 后的参数作为参数
 传递到主类。

 其中,选项包括:

    -cp <目录和 zip/jar 文件的类搜索路径>
    -classpath <目录和 zip/jar 文件的类搜索路径>
    --class-path <目录和 zip/jar 文件的类搜索路径>
                  使用 ; 分隔的, 用于搜索类文件的目录, JAR 档案
                  和 ZIP 档案列表。
    -p <模块路径>
    --module-path <模块路径>...
                  用 ; 分隔的目录列表, 每个目录
                  都是一个包含模块的目录。
    --upgrade-module-path <模块路径>...
                  用 ; 分隔的目录列表, 每个目录
                  都是一个包含模块的目录, 这些模块
                  用于替换运行时映像中的可升级模块
    --add-modules <模块名称>[,<模块名称>...]
                  除了初始模块之外要解析的根模块。
                  <模块名称> 还可以为 ALL-DEFAULT, ALL-SYSTEM,
                  ALL-MODULE-PATH.
    --list-modules
                  列出可观察模块并退出
    -d <module name>
    --describe-module <模块名称>
                  描述模块并退出
    --dry-run     创建 VM 并加载主类, 但不执行 main 方法。
                  此 --dry-run 选项对于验证诸如
                  模块系统配置这样的命令行选项可能非常有用。
    --validate-modules
                  验证所有模块并退出
                  --validate-modules 选项对于查找
                  模块路径中模块的冲突及其他错误可能非常有用。
    -D<名称>=<>
                  设置系统属性
    -verbose:[class|module|gc|jni]
                  为给定子系统启用详细输出
    -version      将产品版本输出到错误流并退出
    --version     将产品版本输出到输出流并退出
    -showversion  将产品版本输出到错误流并继续
    --show-version
                  将产品版本输出到输出流并继续
    --show-module-resolution
                  在启动过程中显示模块解析输出
    -? -h -help
                  将此帮助消息输出到错误流
    --help        将此帮助消息输出到输出流
    -X            将额外选项的帮助输出到错误流
    --help-extra  将额外选项的帮助输出到输出流
    -ea[:<程序包名称>...|:<类名>]
    -enableassertions[:<程序包名称>...|:<类名>]
                  按指定的粒度启用断言
    -da[:<程序包名称>...|:<类名>]
    -disableassertions[:<程序包名称>...|:<类名>]
                  按指定的粒度禁用断言
    -esa | -enablesystemassertions
                  启用系统断言
    -dsa | -disablesystemassertions
                  禁用系统断言
    -agentlib:<库名>[=<选项>]
                  加载本机代理库 <库名>, 例如 -agentlib:jdwp
                  另请参阅 -agentlib:jdwp=help
    -agentpath:<路径名>[=<选项>]
                  按完整路径名加载本机代理库
    -javaagent:<jar 路径>[=<选项>]
                  加载 Java 编程语言代理, 请参阅 java.lang.instrument
    -splash:<图像路径>
                  使用指定的图像显示启动屏幕
                  自动支持和使用 HiDPI 缩放图像
                  (如果可用)。应始终将未缩放的图像文件名 (例如, image.ext)
                  作为参数传递给 -splash 选项。
                  将自动选取提供的最合适的缩放
                  图像。
                  有关详细信息, 请参阅 SplashScreen API 文档
    @argument 文件
                  一个或多个包含选项的参数文件
    -disable-@files
                  阻止进一步扩展参数文件
    --enable-preview
                  允许类依赖于此发行版的预览功能
要为长选项指定参数, 可以使用 --<名称>=<> 或
--<名称> <>

2.2.1、实战

实战1:查看jvm版本

java -version

在这里插入图片描述
-showversion参数是表示,先打印版本信息,再执行后面的命令,在调试时非常有用,后面会使用到。

实战2:通过-D设置系统属性参数

public class TestJVM {
    public static void main(String[] args) {
        String str = System.getProperty("str");
        if (str == null) {
            System.out.println("hello JVM");
        } else {
            System.out.println(str);
        }
    }
}

进行编译、测试:

#编译
[root@VM-12-5-centos test]# javac TestJVM.java
#测试
[root@VM-12-5-centos test]# java TestJVM
hello JVM
[root@VM-12-5-centos test]# java -Dstr=jvmjvm TestJVM
jvmjvm

2.2.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类型。

2.3、-X参数

jvm的-X参数是非标准参数,在不同版本的jvm中,参数可能会有所不同,可以通过java -X查看非标准参数。

 [root@VM-12-5-centos test]# java -X
 	-Xbatch           禁用后台编译
    -Xbootclasspath/a:<; 分隔的目录和 zip/jar 文件>
                      附加在引导类路径末尾
    -Xcheck:jni       对 JNI 函数执行其他检查
    -Xcomp            强制在首次调用时编译方法
    -Xdebug           不执行任何操作。为实现向后兼容而提供。
    -Xdiag            显示附加诊断消息
    -Xfuture          启用最严格的检查,预期将来的默认值。
                      此选项已过时,可能会在
                      未来发行版中删除。
    -Xint             仅解释模式执行
    -Xinternalversion
                      显示比 -version 选项更详细的
                      JVM 版本信息
    -Xlog:<opts>      配置或启用采用 Java 虚拟
                      机 (Java Virtual Machine, JVM) 统一记录框架进行事件记录。使用 -Xlog:help
                      可了解详细信息。
    -Xloggc:<file>    将 GC 状态记录在文件中(带时间戳)。
                      此选项已过时,可能会在
                      将来的发行版中删除。它将替换为 -Xlog:gc:<file>。
    -Xmixed           混合模式执行(默认值)
    -Xmn<size>        为年轻代(新生代)设置初始和最大堆大小
                      (以字节为单位)
    -Xms<size>        设置初始 Java 堆大小
    -Xmx<size>        设置最大 Java 堆大小
    -Xnoclassgc       禁用类垃圾收集
    -Xrs              减少 Java/VM 对操作系统信号的使用(请参见文档)
    -Xshare:auto      在可能的情况下使用共享类数据(默认值)
    -Xshare:off       不尝试使用共享类数据
    -Xshare:on        要求使用共享类数据,否则将失败。
                      这是一个测试选项,可能导致间歇性
                      故障。不应在生产环境中使用它。
    -XshowSettings    显示所有设置并继续
    -XshowSettings:all
                      显示所有设置并继续
    -XshowSettings:locale
                      显示所有与区域设置相关的设置并继续
    -XshowSettings:properties
                      显示所有属性设置并继续
    -XshowSettings:vm
                      显示所有与 vm 相关的设置并继续
    -XshowSettings:system
                      (仅 Linux)显示主机系统或容器
                      配置并继续
    -Xss<size>        设置 Java 线程堆栈大小
    -Xverify          设置字节码验证器的模式
                      请注意,选项 -Xverify:none 已过时,
                      可能会在未来发行版中删除。
    --add-reads <module>=<target-module>(,<target-module>)*
                      更新 <module> 以读取 <target-module>,而无论
                      模块如何声明。
                      <target-module> 可以是 ALL-UNNAMED,将读取所有未命名
                      模块。
    --add-exports <module>/<package>=<target-module>(,<target-module>)*
                      更新 <module> 以将 <package> 导出到 <target-module>,
                      而无论模块如何声明。
                      <target-module> 可以是 ALL-UNNAMED,将导出到所有
                      未命名模块。
    --add-opens <module>/<package>=<target-module>(,<target-module>)*
                      更新 <module> 以在 <target-module> 中打开
                      <package>,而无论模块如何声明。
    --illegal-access=<value>
                      允许或拒绝通过未命名模块中的代码对命名模块中的
                      类型成员进行访问。
                      <value>"deny""permit""warn""debug" 之一
                      此选项将在未来发行版中删除。
    --limit-modules <module name>[,<module name>...]
                      限制可观察模块的领域
    --patch-module <module>=<file>(;<file>)*
                      使用 JAR 文件或目录中的类和资源
                      覆盖或增强模块。
    --source <version>
                      设置源文件模式中源的版本。

2.3.1、-Xint、-Xcomp、-Xmixed

  • 在解释模式(interpreted mode)下,-Xint标记会强制JVM执行所有的字节码,当然这会降低运行速度,通常低10倍或更多。
  • -Xcomp参数与它(-Xint)正好相反,JVM在第一次使用时会把所有的字节码编译成本地代码,从而带来最大程度的优化。

然而,很多应用在使用-Xcomp也会有一些性能损失,当然这比使用-Xint损失的少,原因是-xcomp没有让JVM启用JIT编译器的全部功能。JIT编译器可以对是否需要编译做判断,如果所有代码都进行编译的话,对于一些只执行一次的代码就没有意义了。

  • -Xmixed是混合模式,将解释模式与编译模式进行混合使用,由jvm自己决定,这是jvm默认的模式,也是推荐使用的模式。

示例:强制设置运行模式

[root@VM-12-5-centos test]# java -showversion -Xint TestJVM
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) Client VM (build 25.162-b12, interpreted mode)

hello word

:强制设置为编译模式

[root@VM-12-5-centos test]# java -showversion -Xcomp TestJVM
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) Client VM (build 25.162-b12, compiled mode)

hello word
#注意:编译模式下,第一次执行会比解释模式下执行慢一些,注意观察。

#默认的混合模式

[root@VM-12-5-centos test]# java -showversion TestJVM
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) Client VM (build 25.162-b12, mixed mode)

hello word

2.4、-XX参数

-XX参数也是非标准参数,主要用于jvm的调优和debug操作。
-XX参数的使用有2种方式,一种是boolean类型,一种是非boolean类型:

  • boolean类型
    • 格式:-XX:[±] 表示启用或禁用属性
    • 如:-XX:+DisableExplicitGC 表示禁用手动调用gc操作,也就是说调用System.gc()无效
  • 非boolean类型
    • 格式:-XX:= 表示属性的值为
    • 如:-XX:NewRatio=1 表示新生代和老年代的比值

用法:

[root@VM-12-5-centos test]# java -showversion -XX:+DisableExplicitGC TestJVM
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) Client VM (build 25.162-b12, mixed mode)

hello word

2.5、-Xms与-Xmx参数

-Xms与-Xmx分别是设置jvm的堆内存的初始大小和最大大小。
-Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。
-Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。
适当的调整jvm的内存大小,可以充分利用服务器资源,让程序跑的更快。
示例:

[root@VM-12-5-centos test]# java -Xms512m -Xmx2048m TestJVM
hello word

2.6、查看jvm的运行参数

有些时候我们需要查看jvm的运行参数,这个需求可能会存在2种情况:
第一,运行java命令时打印出运行参数;
第二,查看正在运行的java进程的参数;

2.6.1、运行java命令时打印参数

运行java命令时打印参数,需要添加-XX:+PrintFlagsFinal参数即可。
在这里插入图片描述参数有boolean类型和数字类型,值的操作符是=或:=,分别代表默认值和被修改的值。

2.6.2、查看正在运行的jvm参数

如果想要查看正在运行的jvm就需要借助于jinfo命令查看。
首先,启动一个tomcat用于测试,来观察下运行的jvm参数。(记得配置防火墙)

在这里插入图片描述

#通过jps 或者 jps -l 查看java进程
[root@VM-12-5-centos test]# jps
31739 Bootstrap
1215 Jps

[root@VM-12-5-centos test]# jps -l
31739 org.apache.catalina.startup.Bootstrap
1229 sun.tools.jps.Jps

[root@VM-12-5-centos test]# jinfo -flags 31739
Attaching to process ID 31739, please wait...
Debugger attached successfully.
Client compiler detected.
JVM version is 25.162-b12
Non-default VM flags: -XX:InitialHeapSize=16777216 -XX:+ManagementServer -XX:MaxHeapSize=268435456 -XX:MaxNewSize=89456640 -XX:MinHeapDeltaBytes=131072 -XX:NewSize=5570560 -XX:OldSize=11206656 
Command line:  -Djava.util.logging.config.file=/usr/local/tomcat/apache-tomcat-7.0.57/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.endorsed.dirs=/usr/local/tomcat/apache-tomcat-7.0.57/endorsed -Dcatalina.base=/usr/local/tomcat/apache-tomcat-7.0.57 -Dcatalina.home=/usr/local/tomcat/apache-tomcat-7.0.57 -Djava.io.tmpdir=/usr/local/tomcat/apache-tomcat-7.0.57/temp

#查看某一参数的值,用法:jinfo -flag <参数名> <进程id>
[root@VM-12-5-centos test]# jinfo -flag MaxHeapSize 31739
-XX:MaxHeapSize=268435456
[root@VM-12-5-centos test]# 

3、jvm的内存模型

jvm的内存模型在1.7和1.8有较大的区别,虽然本套课程是以1.8为例进行讲解,但是我们也是需要对1.7的内存模
型有所了解,所以接下里,我们将先学习1.7再学习1.8的内存模型。

3.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区。

3.2、jdk1.8的堆内存模型

在这里插入图片描述

由上图可以看出,jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。
年轻代:Eden + 2*Survivor
年老代:OldGen
在jdk1.8中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。
需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与1.7的永久代最大的区别所在。

在这里插入图片描述

3.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。
基于此,将永久区废弃,而改用元空间,改为了使用本地内存空间。

3.4、通过jstat命令进行查看堆内存使用情况

jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:
jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]

3.4.1、查看class加载统计

[root@VM-12-5-centos test]# jps
2614 Jps
31739 Bootstrap
[root@VM-12-5-centos test]# jstat -class 31739
Loaded  Bytes  Unloaded  Bytes     Time   
  3643  4083.1        9     5.6       1.26

说明:

  • Loaded:加载class的数量
  • Bytes:所占用空间大小
  • Unloaded:未加载数量
  • Bytes:未加载占用空间
  • Time:时间

3.4.2、查看编译统计

[root@VM-12-5-centos test]# jstat -compiler 31739
Compiled Failed Invalid   Time   FailedType FailedMethod
    1162      0       0     0.48          0             

说明:

  • Compiled:编译数量。
  • Failed:失败数量
  • Invalid:不可用数量
  • Time:时间
  • FailedType:失败类型
  • FailedMethod:失败的方法

3.4.3、垃圾回收统计

[root@VM-12-5-centos test]# jstat -gc 31739
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
1088.0 1088.0  0.0    98.4   9216.0   6498.1   22724.0    17765.5   15640.0 15309.7  0.0    0.0       49    0.116   2      0.043    0.159

#也可以指定打印的间隔和次数,每1秒中打印一次,共打印5次
[root@VM-12-5-centos test]# jstat -gc 31739 1000 5
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
1088.0 1088.0  0.0    98.4   9216.0   6674.2   22724.0    17765.5   15640.0 15309.7  0.0    0.0       49    0.116   2      0.043    0.159
1088.0 1088.0  0.0    98.4   9216.0   6674.2   22724.0    17765.5   15640.0 15309.7  0.0    0.0       49    0.116   2      0.043    0.159
1088.0 1088.0  0.0    98.4   9216.0   6674.2   22724.0    17765.5   15640.0 15309.7  0.0    0.0       49    0.116   2      0.043    0.159
1088.0 1088.0  0.0    98.4   9216.0   6674.2   22724.0    17765.5   15640.0 15309.7  0.0    0.0       49    0.116   2      0.043    0.159
1088.0 1088.0  0.0    98.4   9216.0   6674.2   22724.0    17765.5   15640.0 15309.7  0.0    0.0       49    0.116   2      0.043    0.159

说明:

  • 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:垃圾回收消耗总时间

4、jmap的使用以及内存溢出分析

前面通过jstat可以对jvm堆的内存进行统计分析,而jmap可以获取到更加详细的内容,如:内存使用情况的汇总、对内存溢出的定位与分析。

4.1、查看内存使用情况

[root@VM-12-5-centos test]# jmap -heap 31739
Attaching to process ID 31739, please wait...
Debugger attached successfully.
Client compiler detected.
JVM version is 25.162-b12

using thread-local object allocation.
Mark Sweep Compact GC

#堆内存配置信息
Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 268435456 (256.0MB)
   NewSize                  = 5570560 (5.3125MB)
   MaxNewSize               = 89456640 (85.3125MB)
   OldSize                  = 11206656 (10.6875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 12582912 (12.0MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 4294963200 (4095.99609375MB)
   G1HeapRegionSize         = 0 (0.0MB)

# 堆内存的使用情况
Heap Usage:
#年轻代
New Generation (Eden + 1 Survivor Space):
   capacity = 10551296 (10.0625MB)
   used     = 7484088 (7.137382507324219MB)
   free     = 3067208 (2.9251174926757812MB)
   70.93050938955746% used
Eden Space:
   capacity = 9437184 (9.0MB)
   used     = 7383336 (7.041297912597656MB)
   free     = 2053848 (1.9587020874023438MB)
   78.2366434733073% used
From Space:
   capacity = 1114112 (1.0625MB)
   used     = 100752 (0.0960845947265625MB)
   free     = 1013360 (0.9664154052734375MB)
   9.043255974264707% used
To Space:
   capacity = 1114112 (1.0625MB)
   used     = 0 (0.0MB)
   free     = 1114112 (1.0625MB)
   0.0% used
#年老代
tenured generation:
   capacity = 23269376 (22.19140625MB)
   used     = 18191888 (17.349136352539062MB)
   free     = 5077488 (4.8422698974609375MB)
   78.17952660183066% used

13136 interned Strings occupying 1681336 bytes.

4.2、查看内存中对象数量及大小

[root@VM-12-5-centos test]# jmap -histo:live 31739 | more

 num     #instances         #bytes  class name
----------------------------------------------
   1:         37082        7936896  [C
   2:          4319        1294824  [I
   3:          1004         825920  [B
   4:         34715         555440  java.lang.String
   5:         17662         423888  java.util.HashMap$Node
   6:          4062         405016  java.lang.Class
   7:          6168         370216  [Ljava.lang.Object;
   8:          3828         336864  java.lang.reflect.Method
   9:          1124         216216  [Ljava.util.HashMap$Node;
  10:          1343         108728  [Ljava.lang.String;
  11:          4275         102600  java.util.concurrent.ConcurrentHashMap$Node
  12:            72          84576  [Ljava.util.concurrent.ConcurrentHashMap$Node;
  13:          1839          73560  java.util.HashMap
  14:          2785          66840  java.util.ArrayList
  15:          3497          61056  [Ljava.lang.Class;
  16:          1271          61008  org.apache.tomcat.util.digester.CallMethodRule
  17:            97          50032  [Ljava.util.WeakHashMap$Entry;
  18:          1515          48480  java.util.LinkedHashMap$Entry
  19:           624          44928  java.lang.reflect.Constructor
  20:          1671          40104  com.sun.org.apache.xerces.internal.xni.QName
  21:          1634          39216  java.util.Hashtable$Entry
  22:           145          38200  [S
  23:          1072          34304  java.lang.ref.SoftReference
  24:           846          33840  org.apache.tomcat.util.modeler.AttributeInfo
  25:           123          33832  [[C
  26:          1392          33408  java.util.LinkedList$Node
  27:          1027          32864  java.util.TreeMap$Entry
  28:          4001          32008  java.lang.Object
  29:           953          22872  java.lang.ref.WeakReference
  30:           340          21760  java.util.logging.Logger
  31:           173          21432  [Ljava.util.Hashtable$Entry;
  32:           615          19680  javax.management.MBeanAttributeInfo
  33:           452          18080  org.apache.tomcat.util.buf.ByteChunk
  34:           356          17088  org.apache.tomcat.util.buf.MessageBytes
  35:           346          16608  java.util.logging.LogManager$LoggerWeakRef
  36:           391          15640  org.apache.tomcat.util.buf.CharChunk
  37:           360          14400  org.apache.tomcat.util.digester.CallParamRule
  38:           573          13752  java.util.concurrent.locks.ReentrantLock$NonfairSync
  39:           415          13280  java.math.BigInteger
  40:           518          12432  javax.management.ObjectName$Property
--More--

#对象说明

  • B byte
  • C char
  • D double
  • F float
  • I int
  • J long
  • Z boolean
  • [ 数组,如[I表示int[]
  • [L+类名 其他对象

4.3、将内存使用情况dump到文件中

有些时候我们需要将jvm当前内存中的情况dump到文件中,然后对它进行分析,jmap也是支持dump到文件中的。

#用法:
jmap -dump:format=b,file=dumpFileName <pid>
#示例
jmap -dump:format=b,file=/tmp/dump.dat 31739 
[root@VM-12-5-centos test]# ll
total 38712
-rw------- 1 root root 19453443 Apr 26 15:40 dump.dat

可以看到已经在/tmp下生成了dump.dat的文件。

4.4、通过jhat对dump文件进行分析

在上一小节中,我们将jvm的内存dump到文件中,这个文件是一个二进制的文件,不方便查看,这时我们可以借助于jhat工具进行查看。

#用法:
jhat -port <port> <file>
#示例:
[root@VM-12-5-centos test]# jhat -port 8999 dump.dat 
Reading from dump.dat...
Dump file created Tue Apr 26 15:40:57 CST 2022
Snapshot read, resolving...
Resolving 175302 objects...
Chasing references, expect 35 dots...................................
Eliminating duplicate references...................................
Snapshot resolved.
Started HTTP server on port 8999
Server is ready.

在这里插入图片描述注意防火墙

在最后面有OQL查询功能。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.5、通过MAT工具对dump文件进行分析

4.5.1、MAT工具介绍

MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。
官网地址:https://www.eclipse.org/mat/
在这里插入图片描述

4.5.2、下载安装

下载地址:https://www.eclipse.org/mat/previousReleases.php
在这里插入图片描述
将下载得到的MemoryAnalyzer-1.8.0.20180604-win32.win32.x86_64.zip进行解压:

在这里插入图片描述

4.5.3、使用

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
查看对象以及它的依赖:

在这里插入图片描述查看可能存在内存泄露的分析:

在这里插入图片描述

5、实战:内存溢出的定位与分析

内存溢出在实际的生产环境中经常会遇到,比如,不断的将数据写入到一个集合中,出现了死循环,读取超大的文件等等,都可能会造成内存溢出。

如果出现了内存溢出,首先我们需要定位到发生内存溢出的环节,并且进行分析,是正常还是非正常情况,如果是正常的需求,就应该考虑加大内存的设置,如果是非正常需求,那么就要对代码进行修改,修复这个bug。

首先,我们得先学会如何定位问题,然后再进行分析。如何定位问题呢,我们需要借助于jmap与MAT工具进行定位分析。

接下来,我们模拟内存溢出的场景。

5.1、模拟内存溢出

编写代码,向List集合中添加100万个字符串,每个字符串由1000个UUID组成。如果程序能够正常执行,最后打印ok。

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @author 1060785272@qq.com
 * @version 1.0
 * @description: TODO
 * @date 2022-4-27 下午 3:27
 */
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

5.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(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at cn.itcast.jvm.TestJvmOutOfMemory.main(TestJvmOutOfMemory.java:14)
Process finished with exit code 1

可以看到,当发生内存溢出时,会dump文件到java_pid5348.hprof。
在这里插入图片描述
5.3、导入到MAT工具中进行分析

在这里插入图片描述可以看到,有91.03%的内存由Object[]数组占有,所以比较可疑。
分析:这个可疑是正确的,因为已经有超过90%的内存都被它占有,这是非常有可能出现内存溢出的。
查看详情:

在这里插入图片描述可以看到集合中存储了大量的uuid字符串。

6、jstack的使用

有些时候我们需要查看下jvm中的线程执行情况,比如,发现服务器的CPU的负载突然增高了、出现了死锁、死循环等,我们该如何分析呢?

由于程序是正常运行的,没有任何的输出,从日志方面也看不出什么问题,所以就需要看下jvm的内部线程的执行情况,然后再进行分析查找出原因。

这个时候,就需要借助于jstack命令了,jstack的作用是将正在运行的jvm的线程情况进行快照,并且打印出来:

#用法:jstack <pid>
[root@VM-12-5-centos test]# jstack 2203
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.141-b15 mixed mode):
"Attach Listener" #24 daemon prio=9 os_prio=0 tid=0x00007fabb4001000 nid=0x906 waiting on
condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"http-bio-8080-exec-5" #23 daemon prio=5 os_prio=0 tid=0x00007fabb057c000 nid=0x8e1
waiting on condition [0x00007fabd05b8000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
more

6.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)
      • 线程执行结束后的状态。

6.2、实战:死锁问题

如果在生产环境发生了死锁,我们将看到的是部署的程序没有任何反应了,这个时候我们可以借助jstack进行分析,下面我们实战下查找死锁的原因。

6.2.1、构造死锁

编写代码,启动2个线程,Thread1拿到了obj1锁,准备去拿obj2锁时,obj2已经被Thread2锁定,所以发送了死锁。

/**
 * @author 1060785272@qq.com
 * @version 1.0
 * @description: TODO
 * @date 2022-4-27 下午 4:25
 */
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 {
                    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 {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj1){
                    System.out.println("Thread2 拿到了 obj1 的锁!");
                }
            }

        }
    }
}
[root@VM-12-5-centos test]# javac TestDeadLock.java
[root@VM-12-5-centos test]# ll
总用量 28
-rw-r--r--. 1 root root 184 911 10:39 TestDeadLock$1.class
-rw-r--r--. 1 root root 843 911 10:39 TestDeadLock.class
-rw-r--r--. 1 root root 1567 911 10:39 TestDeadLock.java
-rw-r--r--. 1 root root 1078 911 10:39 TestDeadLock$Thread1.class
-rw-r--r--. 1 root root 1078 911 10:39 TestDeadLock$Thread2.class
-rw-r--r--. 1 root root 573 99 10:21 TestJVM.class
-rw-r--r--. 1 root root 261 99 10:21 TestJVM.java
[root@VM-12-5-centos test]# java TestDeadLock
Thread1 拿到了 obj1 的锁!
Thread2 拿到了 obj2 的锁!
#这里发生了死锁,程序一直将等待下去

6.2.3、使用jstack进行分析

^C[root@VM-12-5-centos test]# java TestDeadLock
Thread1 拿到了 obj1 的锁!
Thread2 拿到了 obj2 的锁!

这里发生死锁,再打开一个窗口

[root@VM-12-5-centos ~]# jps
1556 TestDeadLock
1768 Jps
31739 Bootstrap
[root@VM-12-5-centos ~]# jstack 1556
2022-04-28 15:43:36
Full thread dump Java HotSpot(TM) Client VM (25.162-b12 mixed mode):

"Attach Listener" #10 daemon prio=9 os_prio=0 tid=0xf696c400 nid=0x742 waiting on condition [0x00000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" #9 prio=5 os_prio=0 tid=0xf6907400 nid=0x615 waiting on condition [0x00000000]
   java.lang.Thread.State: RUNNABLE

"Thread-1" #8 prio=5 os_prio=0 tid=0xf69b8800 nid=0x61e waiting for monitor entry [0xe39d1000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at TestDeadLock$Thread2.run(TestDeadLock.java:40)
	- waiting to lock <0xe4867040> (a java.lang.Object)
	- locked <0xe4867048> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)

"Thread-0" #7 prio=5 os_prio=0 tid=0xf69b7000 nid=0x61d waiting for monitor entry [0xe3a22000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at TestDeadLock$Thread1.run(TestDeadLock.java:22)
	- waiting to lock <0xe4867048> (a java.lang.Object)
	- locked <0xe4867040> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)

"Service Thread" #6 daemon prio=9 os_prio=0 tid=0xf6984000 nid=0x61b runnable [0x00000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0xf6980c00 nid=0x61a waiting on condition [0x00000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0xf697f400 nid=0x619 runnable [0x00000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=0 tid=0xf6965000 nid=0x618 in Object.wait() [0xe3ead000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0xe4807ec8> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
	- locked <0xe4807ec8> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:212)

"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0xf6962000 nid=0x617 in Object.wait() [0xe3efe000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0xe4805f60> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:502)
	at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
	- locked <0xe4805f60> (a java.lang.ref.Reference$Lock)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=0 tid=0xf695d800 nid=0x616 runnable 

"VM Periodic Task Thread" os_prio=0 tid=0xf6987000 nid=0x61c waiting on condition 

JNI global references: 5


Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x09c41c58 (object 0xe4867040, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x09c40fb0 (object 0xe4867048, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
	at TestDeadLock$Thread2.run(TestDeadLock.java:40)
	- waiting to lock <0xe4867040> (a java.lang.Object)
	- locked <0xe4867048> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)
"Thread-0":
	at TestDeadLock$Thread1.run(TestDeadLock.java:22)
	- waiting to lock <0xe4867048> (a java.lang.Object)
	- locked <0xe4867040> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

在输出的信息中,已经看到,发现了1个死锁,关键看这个:


"Thread-1" #8 prio=5 os_prio=0 tid=0xf69b8800 nid=0x61e waiting for monitor entry [0xe39d1000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at TestDeadLock$Thread2.run(TestDeadLock.java:40)
	- waiting to lock <0xe4867040> (a java.lang.Object)
	- locked <0xe4867048> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)

"Thread-0" #7 prio=5 os_prio=0 tid=0xf69b7000 nid=0x61d waiting for monitor entry [0xe3a22000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at TestDeadLock$Thread1.run(TestDeadLock.java:22)
	- waiting to lock <0xe4867048> (a java.lang.Object)
	- locked <0xe4867040> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)

可以清晰的看到:

  • Thread2获取了 <0x00000000f655dc50> 的锁,等待获取 <0x00000000f655dc40> 这个锁
  • Thread1获取了 <0x00000000f655dc40> 的锁,等待获取 <0x00000000f655dc50> 这个锁
  • 由此可见,发生了死锁。

7、VisualVM工具的使用

VisualVM,能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆
栈(如100个String对象分别由哪几个对象分配出来的)。
VisualVM使用简单,几乎0配置,功能还是比较丰富的,几乎囊括了其它JDK自带命令的所有功能。

  • 内存信息
  • 线程信息
  • Dump堆(本地进程)
  • Dump线程(本地进程)
  • 打开堆Dump。堆Dump可以用jmap来生成。
  • 打开线程Dump
  • 生成应用快照(包含内存信息、线程信息等等)
  • 性能分析。CPU分析(各个方法调用时间,检查哪些方法耗时多),内存分析(各类对象占用的内存,检查哪些类占用内存多)
  • ……

7.1、启动

在jdk的安装目录的bin目录下,找到jvisualvm.exe,双击打开即可。
在这里插入图片描述
在这里插入图片描述

7.2、查看本地进程

在这里插入图片描述

7.3、查看CPU、内存、类、线程运行信息

在这里插入图片描述

7.4、查看线程详情

在这里插入图片描述
也可以点击右上角Dump按钮,将线程的信息导出,其实就是执行的jstack命令。

在这里插入图片描述发现,显示的内容是一样的。

7.5、抽样器

抽样器可以对CPU、内存在一段时间内进行抽样,以供分析。
在这里插入图片描述

7.6、监控远程的jvm

VisualJVM不仅是可以监控本地jvm进程,还可以监控远程的jvm进程,需要借助于JMX技术实现。

7.6.1、什么是JMX?

JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。
JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。

7.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。

7.6.3、使用VisualJVM连接远程tomcat

添加远程主机:

在这里插入图片描述
在一个主机下可能会有很多的jvm需要监控,所以接下来要在该主机上添加需要监控的jvm:

在这里插入图片描述

在这里插入图片描述
连接成功。使用方法和前面就一样了,就可以和监控本地jvm进程一样,监控远程的tomcat进程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值