jvm基础3之性能监控与调优篇

概述篇

大厂面试题

支付宝:

支付宝三面:JVM 性能调优都做了什么?

小米:

有做过 JVM 内存优化吗?

从 SQL、JVM、架构、数据库四个方面讲讲优化思路

蚂蚁金服:

JVM 的编译优化

jvm 性能调优都做了什么

JVM 诊断调优工具用过哪些?

二面:jvm 怎样调优,堆内存、栈空间设置多少合适

三面:JVM 相关的分析工具使用过的有哪些?具体的性能调优步骤如何

阿里:

如何进行 JVM 调优?有哪些方法?

如何理解内存泄漏问题?有哪些情况会导致内存泄漏?如何解决?

字节跳动:

三面:JVM 如何调优、参数怎么调?

拼多多:

从 SQL、JVM、架构、数据库四个方面讲讲优化思路

京东:

JVM 诊断调优工具用过哪些?

每秒几十万并发的秒杀系统为什么会频繁发生 GC?

日均百万级交易系统如何优化 JVM?

线上生产系统 OOM 如何监控及定位与解决?

高并发系统如何基于 G1 垃圾回收器优化性能?

背景说明

生产环境中的问题

  • 生产环境发生了内存溢出该如何处理?
  • 生产环境应该给服务器分配多少内存合适?
  • 如何对垃圾回收器的性能进行调优?
  • 生产环境 CPU 负载飙高该如何处理?
  • 生产环境应该给应用分配多少线程合适?
  • 不加 log,如何确定请求是否执行了某一行代码?
  • 不加 log,如何实时查看某个方法的入参与返回值?

为什么要调优

  • 防止出现 OOM
  • 解决 OOM
  • 减少 Full GC 出现的频率

不同阶段的考虑

  • 上线前
  • 项目运行阶段
  • 线上出现 OOM

调优概述

监控的依据

  • 运行日志
  • 异常堆栈
  • GC 日志
  • 线程快照
  • 堆转储快照

调优的大方向

  • 合理地编写代码
  • 充分并合理的使用硬件资源
  • 合理地进行 JVM 调优

性能优化的步骤

第 1 步:性能监控

一种以非强行或者入侵方式收集或查看应用运营性能数据的活动。

监控通常是指一种在生产、质量评估或者开发环境下实施的带有预防或主动性的活动。

当应用相关干系人提出性能问题却没有提供足够多的线索时,首先我们需要进行性能监控,随后是性能分析。

  • GC 频繁
  • cpu load 过高
  • OOM
  • 内存泄露
  • 死锁
  • 程序响应时间较长

第 2 步:性能分析

一种以侵入方式收集运行性能数据的活动,它会影响应用的吞吐量或响应性。

性能分析是针对性能问题的答复结果,关注的范围通常比性能监控更加集中。

性能分析很少在生产环境下进行,通常是在质量评估、系统测试或者开发环境下进行,是性能监控之后的步骤。

  • 打印 GC 日志,通过 GCviewer 或者 http://gceasy.io 来分析异常信息
  • 灵活运用命令行工具、jstack、jmap、jinfo 等
  • dump 出堆文件,使用内存分析工具分析文件
  • 使用阿里 Arthas、jconsole、JVisualVM 来实时查看 JVM 状态
  • jstack 查看堆栈信息

第 3 步:性能调优

一种为改善应用响应性或香吐量而更改参数、源代码、属性配置的活动,性能调优是在性能监控、性能分析之后的活动。

  • 适当增加内存,根据业务背景选择垃圾回收器
  • 优化代码,控制内存使用
  • 增加机器,分散节点压力
  • 合理设置线程池线程数量
  • 使用中间件提高程序效率,比如缓存、消息队列等
  • 其他……

性能评价/测试指标

停顿时间(或响应时间)

提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间。常用操作的响应时间列表:

操作 响应时间
打开一个站点 几秒
数据库查询一条记录(有索引) 十几毫秒
机械磁盘一次寻址定位 4 毫秒
从机械磁盘顺序读取 1M 数据 2 毫秒
从 SSD 磁盘顺序读取 1M 数据 0.3 毫秒
从远程分布式换成 Redis 读取一个数据 0.5 毫秒
从内存读取 1M 数据 十几微妙
Java 程序本地方法调用 几微妙
网络传输 2Kb 数据 1 微妙

在垃圾回收环节中:

  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
    • STW
  • -XX:MaxGCPauseMillis

吞吐量

  • 对单位时间内完成的工作量(请求)的量度
  • 在 GC 中:运行用户代码的事件占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间)
  • 吞吐量为 1-1/(1+n),其中-XX::GCTimeRatio=n

并发数

  • 同一时刻,对服务器有实际交互的请求数
    • 网站在线用户,估计并发数在5%-15%之间
    • 例如:1000人同时在线,并发数在50-150之间

内存占用

  • Java 堆区所占的内存大小

相互间的关系

以高速公路通行状况为例

  • 吞吐量:每天通过高速公路收费站的车辆的数据
  • 并发数:高速公路上正在行驶的车辆的数目
  • 响应时间:车速

JVM 监控及诊断工具-命令行篇

概述

性能诊断是软件工程师在日常工作中需要经常面对和解决的问题,在用户体验至上的今天,解决好应用的性能问题能带来非常大的收益。

Java 作为最流行的编程语言之一,其应用性能诊断一直受到业界广泛关注。可能造成 Java 应用出现性能问题的因素非常多,例如线程控制、磁盘读写、数据库访问、网络 I/O、垃圾收集等。想要定位这些问题,一款优秀的性能诊断工具必不可少。

体会 1:==使用数据说明问题,使用知识分析问题,使用工具处理问题==。

体会 2:==无监控、不调优!==

简单命令行工具

在我们刚接触 java 学习的时候,大家肯定最先了解的两个命令就是 javac,java,那么除此之外,还有没有其他的命令可以供我们使用呢?

我们进入到安装 jdk 的 bin 目录,发现还有一系列辅助工具。这些辅助工具用来获取目标 JVM 不同方面、不同层次的信息,帮助开发人员很好地解决 Java 应用程序的一些疑难杂症。

mac系统下:

image-20210504195803526

Windows系统下:

image-20210927161412430 image-20210927161650207

官方源码地址:http://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/jdk.jcmd/share/classes/sun/tools

jps:查看正在运行的 Java 进程

概念

jps(Java Process Status):

显示指定系统内所有的 HotSpot 虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。

说明:对于本地虚拟机进程来说,进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的,是唯一的。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author lvxiaoyi
* @date 2021/9/27 16:37
*/
public class ScannerTest {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String info = scanner.next();

}
}

1
2
3
4
5
6
7
8
9
10
11
C:\Users\Think>jps
# java.exe,我的idea的插件
23336 RemoteMavenServer
# idea的运行程序
21428
# jps程序
12544 Jps
# 我们的运行程序
6604 ScannerTest
# 虚拟机的类加载器,所有类加载器的父类
13880 Launcher

基本使用语法为:jps [options] [hostid]

我们还可以通过追加参数,来打印额外的信息。

1
2
3
4
5
6
C:\Users\Think>jps -help
usage: jps [-help]
jps [-q] [-mlvV] [<hostid>]

Definitions:
<hostid>: <hostname>[:<port>]

options 参数

  • -q:仅仅显示 LVMID(local virtual machine id),即本地虚拟机唯一 id。不显示主类的名称等

    1
    2
    3
    4
    5
    6
    C:\Users\Think>jps -q
    21428
    13880
    23336
    7128
    6604
  • -l:输出应用程序主类的全类名 或 如果进程执行的是 jar 包,则输出 jar 完整路径

    1
    2
    3
    4
    5
    6
    C:\Users\Think>jps -l
    14804 sun.tools.jps.Jps
    21428
    13880 org.jetbrains.jps.cmdline.Launcher
    23336 org.jetbrains.idea.maven.server.RemoteMavenServer
    6604 top.lvxiaoyi.three.chapter02.code02.ScannerTest
  • -m:输出虚拟机进程启动时传递给主类 main()的参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    C:\Users\Think>jps -m
    21428

    13880 Launcher
    D:/ideaJetBrains/IntelliJIDEA2019.3/lib/trove4j.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/plugins/java/lib/maven-builder-support3.3.9.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/lib/util.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/lib/jna-platform.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/plugins/java/lib/maven-aether-provider-3.3.9.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/lib/netty-codec-4.1.41.Final.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/lib/netty-buffer-4.1.41.Final.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/lib/protobuf-java-3.5.1.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/lib/annotations.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/lib/httpcore-4.4.12.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/plugins/java/lib/javac2.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/plugins/java/lib/maven-repository-metadata-3.3.9.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/plugins/java/lib/maven-artifact-3.3.9.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/plugins/java/lib/aether-spi-1.1.0.jar;
    D:/ideaJetBrains/IntelliJIDEA2019.3/p

    23336 RemoteMavenServer

    23932 Jps -m

    6604 ScannerTest
  • -v:列出虚拟机进程启动时的 JVM 参数。比如:-Xms20m -Xmx50m 是启动程序指定的 jvm 参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    C:\Users\Think>jps -v
    21428 exit -Xms128m -Xmx2035m -XX:ReservedCodeCacheSize=240m -XX:+UseConcMarkSweepGC -XX:SoftRefLRUPolicyMSPerMB=50 -ea -XX:CICompilerCount=2 -Dsun.io.useCanonPrefixCache=false -Djava.net.preferIPv4Stack=true -Djdk.http.auth.tunneling.disabledSchemes="" -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -Djdk.attach.allowAttachSelf=true -Dkotlinx.coroutines.debug=off -Djdk.module.illegalAccess.silent=true -javaagent:D:\ideaJetBrains\IntelliJIDEA2019.3\bin\jetbrains-agent.jar -Djb.vmOptionsFile=C:\Users\Think\.IntelliJIdea2019.3\config\idea64.exe.vmoptions -Djava.library.path=D:\ideaJetBrains\IntelliJIDEA2019.3\jbr\\bin;D:\ideaJetBrains\IntelliJIDEA2019.3\jbr\\bin\server -Didea.jre.check=true -Dide.native.launcher=true -Didea.paths.selector=IntelliJIdea2019.3 -XX:ErrorFile=C:\Users\Think\java_error_in_idea_%p.log -XX:HeapDumpPath=C:\Users\Think\java_error_in_idea.hprof

    13880 Launcher -Xmx700m -Djava.awt.headless=true -Djava.endorsed.dirs="" -Djdt.compiler.useSingleThread=true -Dpreload.project.path=D:/interview/jvm -Dpreload.config.path=C:/Users/Think/.IntelliJIdea2019.3/config/options -Dcompile.parallel=false -Drebuild.on.dependency.change=true -Djava.net.preferIPv4Stack=true -Dio.netty.initialSeedUniquifier=-7881113560423034109 -Dfile.encoding=GBK -Duser.language=zh -Duser.country=CN -Didea.paths.selector=IntelliJIdea2019.3 -Didea.home.path=D:\ideaJetBrains\IntelliJIDEA2019.3 -Didea.config.path=C:\Users\Think/.IntelliJIdea2019.3/config -Didea.plugins.path=C:\Users\Think/.IntelliJIdea2019.3/config/plugins -Djps.log.dir=C:/Users/Think/.IntelliJIdea2019.3/system/log/build-log -Djps.fallback.jdk.home=D:/ideaJetBrains/IntelliJIDEA2019.3/jbr -Djps.fallback.jdk.version=11.0.4 -Dio.netty.noUnsafe=true -Djava.io.tmpdir=C:/Users/Think/.IntelliJIdea2019.3/system/compile-server/jvm_9ac3b829/_temp_ -Djps.backward.ref.index.builder=true -Dkotlin.incremental.compilation=true -Dkotlin.incremental.compi

    23336 RemoteMavenServer -Djava.awt.headless=true -Dmaven.defaultProjectBuilder.disableGlobalModelCache=true -Xmx768m -Didea.maven.embedder.version=3.5.2 -Dmaven.ext.class.path=D:\ideaJetBrains\IntelliJIDEA2019.3\plugins\maven\lib\maven-event-listener.jar -Dfile.encoding=GBK

    18156 Jps -Dapplication.home=C:\Program Files\Java\jdk1.8.0_101 -Xms8m

    6604 ScannerTest -javaagent:D:\ideaJetBrains\IntelliJIDEA2019.3\lib\idea_rt.jar=59998:D:\ideaJetBrains\IntelliJIDEA2019.3\bin -Dfile.encoding=UTF-8

说明:以上参数可以综合使用。

1
C:\Users\Think>jps -lmv

==jps命令中的q命令与lmv命令是独立的,优先展示q==

补充:如果某 Java 进程关闭了默认开启的 UsePerfData 参数(即使用参数-XX:-UsePerfData),那么 jps 命令(以及下面介绍的 jstat)将无法探知该 Java 进程。

1
-XX:-UsePerfData
1
2
3
4
5
C:\Users\Think>jps
21428
23336 RemoteMavenServer
15612 Jps
20412 Launcher

hostid 参数

RMI 注册表中注册的主机名。如果想要远程监控主机上的 java 程序,需要安装 jstatd。

对于具有更严格的安全实践的网络场所而言,可能使用一个自定义的策略文件来显示对特定的可信主机或网络的访问,尽管这种技术容易受到 IP 地址欺诈攻击。

如果安全问题无法使用一个定制的策略文件来处理,那么最安全的操作是不运行 jstatd 服务器,而是在本地使用 jstat 和 jps 工具。

jstat:查看 JVM 统计信息

概念

jstat(JVM Statistics Monitoring Tool):用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据。

在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。

官方文档:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html

基本使用语法为:

1
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

查看命令相关参数:jstat-h 或 jstat-help

其中 vmid 是进程 id 号,也就是 jps 之后看到的前面的号码,如下:

image-20210504201703222

option 参数

选项 option 可以由以下值构成。

类装载相关的:

  • -class:显示 ClassLoader 的相关信息:类的装载、卸载数量、总空间、类装载所消耗的时间等

垃圾回收相关的:

  • -gc:显示与 GC 相关的堆信息。包括 Eden 区、两个 Survivor 区、老年代、永久代等的容量、已用空间、GC 时间合计等信息。

  • -gccapacity:显示内容与-gc 基本相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间。

  • -gcutil:显示内容与-gc 基本相同,但输出主要关注已使用空间占总空间的百分比。

  • -gccause:与-gcutil 功能一样,但是会额外输出导致最后一次或当前正在发生的 GC 产生的原因。

  • -gcnew:显示新生代 GC 状况

  • -gcnewcapacity:显示内容与-gcnew 基本相同,输出主要关注使用到的最大、最小空间

  • -geold:显示老年代 GC 状况

  • -gcoldcapacity:显示内容与-gcold 基本相同,输出主要关注使用到的最大、最小空间

  • -gcpermcapacity(**-gcmetacapacity**):显示永久代使用到的最大、最小空间。

    • ==注意-gcpermcapacity是jdk1.7前的指令,在jdk1.8后,永久代改为了元空间,所以如果你用永久代的参数在jdk1.8中会提示没有该条命令,但是显示的参数都是一样的==

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      C:\Users\Think>jstat -options -help
      -class
      -compiler
      -gc
      -gccapacity
      -gccause
      -gcmetacapacity
      -gcnew
      -gcnewcapacity
      -gcold
      -gcoldcapacity
      -gcutil
      -printcompilation

这些参数的意义参考官网文档:https://docs.oracle.com/en/java/javase/14/docs/specs/man/jstat.html

https://www.cnblogs.com/756623607-zhang/p/8980774.html#4951951由这边文章的博主提供,我开始也没有找到

JIT 相关的:

  • -compiler:显示 JIT 编译器编译过的方法、耗时等信息
  • -printcompilation:输出已经被 JIT 编译的方法

类装载相关的

jstat -class
1
2
3
4
5
6
C:\Users\Think>jps
3376 Jps
11940 Launcher
21428
12312 ScannerTest
23336 RemoteMavenServer
1
2
3
C:\Users\Think>jstat -class 12312
Loaded Bytes Unloaded Bytes Time
699 1398.0 0 0.0 0.35
参数 loaded Bytes Unloaded Bytes Time
说明 加载类的个数 加载类的字节数 卸载类的个数 卸载来的字节数 花费的总体时间

JIT 相关的

jstat -compiler
1
2
3
C:\Users\Think>jstat -compiler 12312
Compiled Failed Invalid Time FailedType FailedMethod
94 0 0 0.10 0
参数 Compiled Failed Invalid Time FailedTypeTime FailedMethod
说明 编译的数量 失败的数量 不可用数量 编译时间 失败编译的时间 失败的方法
jstat -printcompilation
1
2
3
C:\Users\Think>jstat -printcompilation 12312
Compiled Size Type Method
94 138 1 java/lang/StringBuffer append
参数 Compiled Size Type Method
说明 最近编译方法的数量 最近编译方法的字节码数量 最近编译方法的编译类型 方法名标识

垃圾回收相关的

jstat -gc

显示与 GC 相关的堆信息。包括 Eden 区、两个 Survivor 区、老年代、永久代等的容量、已用空间、GC 时间合计等信息。

例一:
1
2
3
C:\Users\Think>jstat -gc 12312
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
10752.0 10752.0 0.0 0.0 65536.0 6559.6 175104.0 0.0 4480.0 769.9 384.0 75.8 0 0.000 0 0.000 0.000

image-20210927180425090

参数 解释
S0C 幸存者 0 区的大小
S1C 幸存者 1 区的大小
S0U 幸存者 0 区已使用的大小
S1U 幸存者 1 区已使用的大小
EC Eden 区的大小
EU Eden 区已使用的大小
OC 老年代的大小
OU 老年代已使用的大小
MC 元空间(方法区)的大小
MU 元空间(方法区)已使用的大小
CCSC 压缩类空间的大小
CCSU 压缩类空间已使用的大小
YGC 从应用程序启动到采样时 young gc 的次数
YGCT 从应用程序启动到采样时 young gc 消耗时间(秒)
FGC 从应用程序启动到采样时 full gc 的次数
FGCT 从应用程序启动到采样时的 full gc 的消耗时间(秒)
GCT 从应用程序启动到采样时 gc 的总时间
例二:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author lvxiaoyi
* -Xms60m -Xmx60m -XX:SurvivorRatio=8
* @date 2021/9/27 18:12
*/
public class GCTest {
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();

for (int i = 0; i < 1000; i++) {
byte[] arr = new byte[1024 * 100];
list.add(arr);
try {
Thread.sleep(120);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
C:\Users\Think>jps
14372 GCTest
14804 Jps
21428
17160 Launcher
23336 RemoteMavenServer
12364 KotlinCompileDaemon

C:\Users\Think>jstat -gc 14372 1000 10
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
2048.0 2048.0 0.0 2016.2 16384.0 1807.1 40960.0 12353.8 4864.0 3782.2 512.0 419.4 1 0.006 0 0.000 0.006
2048.0 2048.0 0.0 2016.2 16384.0 2607.2 40960.0 12353.8 4864.0 3782.2 512.0 419.4 1 0.006 0 0.000 0.006
2048.0 2048.0 0.0 2016.2 16384.0 3407.3 40960.0 12353.8 4864.0 3782.2 512.0 419.4 1 0.006 0 0.000 0.006
2048.0 2048.0 0.0 2016.2 16384.0 4314.3 40960.0 12353.8 4864.0 3782.2 512.0 419.4 1 0.006 0 0.000 0.006
2048.0 2048.0 0.0 2016.2 16384.0 5014.4 40960.0 12353.8 4864.0 3782.2 512.0 419.4 1 0.006 0 0.000 0.006
2048.0 2048.0 0.0 2016.2 16384.0 5914.5 40960.0 12353.8 4864.0 3782.2 512.0 419.4 1 0.006 0 0.000 0.006
2048.0 2048.0 0.0 2016.2 16384.0 6714.7 40960.0 12353.8 4864.0 3782.2 512.0 419.4 1 0.006 0 0.000 0.006
2048.0 2048.0 0.0 2016.2 16384.0 7514.8 40960.0 12353.8 4864.0 3782.2 512.0 419.4 1 0.006 0 0.000 0.006
2048.0 2048.0 0.0 2016.2 16384.0 8314.9 40960.0 12353.8 4864.0 3782.2 512.0 419.4 1 0.006 0 0.000 0.006
2048.0 2048.0 0.0 2016.2 16384.0 9115.0 40960.0 12353.8 4864.0 3782.2 512.0 419.4 1 0.006 0 0.000 0.006

image-20210927204921489

1
2
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at top.lvxiaoyi.three.chapter02.code02.GCTest.main(GCTest.java:16)
1
2
-Xms60m -Xmx60m -XX:SurvivorRatio=8
# 设置堆初始大小为60m,设置堆最大为60m,设置幸存区和伊甸园区比例为1:1:8(幸存1区和二区的大小要一致)

默认新生代与老年代比例为1:2,所以新生代为为20M,老年代为40M

幸存区和伊甸园区比例为1:1:8,所以S0C为2M,S1C为2M,EC为16M,OC为40M,发生了YGC1次,FGC没有出现,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
S0C:第一个幸存区的大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
jstat -gccapacity

-gccapacity:显示内容与-gc 基本相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间。

image-20210927221432416

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NGCMN:新生代最小容量
NGCMX:新生代最大容量
NGC:当前新生代容量
S0C:第一个幸存区大小
S1C:第二个幸存区的大小
EC:伊甸园区的大小
OGCMN:老年代最小容量
OGCMX:老年代最大容量
OGC:当前老年代大小
OC:当前老年代大小
MCMN:最小元数据容量
MCMX:最大元数据容量
MC:当前元数据空间大小
CCSMN:最小压缩类空间大小
CCSMX:最大压缩类空间大小
CCSC:当前压缩类空间大小
YGC:年轻代gc次数
FGC:老年代GC次数
jstat -gcutil

显示内容与-gc 基本相同,但输出主要关注已使用空间占总空间的百分比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
C:\Users\Think>jps
21428
22484 GCTest
23336 RemoteMavenServer
3384 Launcher
12364 KotlinCompileDaemon
12780 Jps

C:\Users\Think>jstat -gcutil 22484 500 30
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 98.65 87.46 29.92 77.76 81.91 1 0.004 0 0.000 0.004
0.00 98.65 89.90 29.92 77.76 81.91 1 0.004 0 0.000 0.004
0.00 98.65 92.95 29.92 77.76 81.91 1 0.004 0 0.000 0.004
0.00 98.65 94.79 29.92 77.76 81.91 1 0.004 0 0.000 0.004
0.00 98.65 97.84 29.92 77.76 81.91 1 0.004 0 0.000 0.004
0.00 0.00 0.61 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 3.14 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 5.58 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 8.02 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 10.46 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 12.90 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 15.35 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 17.79 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 20.84 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 23.28 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 25.72 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 28.17 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 30.61 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 33.05 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 35.49 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 37.93 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 40.37 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 42.82 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 45.26 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 47.70 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 50.14 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 52.58 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 55.64 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 58.08 74.42 77.76 81.91 2 0.011 1 0.016 0.026
0.00 0.00 60.52 74.42 77.76 81.91 2 0.011 1 0.016 0.026
1
2
3
4
5
6
7
8
9
10
S0:幸存1区当前使用比例
S1:幸存2区当前使用比例
E:伊甸园区使用比例
O:老年代使用比例
M:元数据区使用比例
CCS:压缩使用比例
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:所有GC总耗时
jstat -gccause

与-gcutil 功能一样,但是会额外输出导致最后一次或当前正在发生的 GC 产生的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
C:\Users\Think>jps
19600 Jps
21428
23892 Launcher
23336 RemoteMavenServer
12364 KotlinCompileDaemon
22604 GCTest

C:\Users\Think>jstat -gccause 22604 1000 30
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT LGCC GCC
0.00 98.65 3.09 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 6.15 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 11.03 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 16.52 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 21.41 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 26.33 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 31.22 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 36.10 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 40.98 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 45.87 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 50.75 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 56.24 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 61.13 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 66.01 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 70.94 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 75.82 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 81.97 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 85.63 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 90.51 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 98.65 95.40 30.05 77.76 81.91 1 0.005 0 0.000 0.005 Allocation Failure No GC
0.00 0.00 3.14 74.42 77.76 81.91 2 0.010 1 0.012 0.022 Allocation Failure No GC
0.00 0.00 6.19 74.42 77.76 81.91 2 0.010 1 0.012 0.022 Allocation Failure No GC
0.00 0.00 11.07 74.42 77.76 81.91 2 0.010 1 0.012 0.022 Allocation Failure No GC
0.00 0.00 15.96 74.42 77.76 81.91 2 0.010 1 0.012 0.022 Allocation Failure No GC
0.00 0.00 20.84 74.42 77.76 81.91 2 0.010 1 0.012 0.022 Allocation Failure No GC
0.00 0.00 25.72 74.42 77.76 81.91 2 0.010 1 0.012 0.022 Allocation Failure No GC
0.00 0.00 30.61 74.42 77.76 81.91 2 0.010 1 0.012 0.022 Allocation Failure No GC
0.00 0.00 35.49 74.42 77.76 81.91 2 0.010 1 0.012 0.022 Allocation Failure No GC
0.00 0.00 40.37 74.42 77.76 81.91 2 0.010 1 0.012 0.022 Allocation Failure No GC
0.00 0.00 45.26 74.42 77.76 81.91 2 0.010 1 0.012 0.022 Allocation Failure No GC
1
2
3
4
5
6
7
8
9
10
11
12
S0:幸存1区当前使用比例
S1:幸存2区当前使用比例
E:伊甸园区使用比例
O:老年代使用比例
M:元数据区使用比例
CCS:压缩使用比例
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:所有GC总耗时
LGCC:上次GC的原因
GCC:当前GC的原因
jstat -gcnew

显示新生代 GC 状况

1
2
3
4
5
6
7
8
9
10
11
12
C:\Users\Think>jstat -gcnew 24584 1000 10
S0C S1C S0U S1U TT MTT DSS EC EU YGC YGCT
2048.0 2048.0 0.0 2032.3 7 15 2048.0 16384.0 15129.6 1 0.004
2048.0 2048.0 0.0 2032.3 7 15 2048.0 16384.0 16029.8 1 0.004
2048.0 2048.0 0.0 0.0 7 15 2048.0 16384.0 841.7 2 0.011
2048.0 2048.0 0.0 0.0 7 15 2048.0 16384.0 1641.8 2 0.011
2048.0 2048.0 0.0 0.0 7 15 2048.0 16384.0 2442.0 2 0.011
2048.0 2048.0 0.0 0.0 7 15 2048.0 16384.0 3242.1 2 0.011
2048.0 2048.0 0.0 0.0 7 15 2048.0 16384.0 4042.2 2 0.011
2048.0 2048.0 0.0 0.0 7 15 2048.0 16384.0 4842.3 2 0.011
2048.0 2048.0 0.0 0.0 7 15 2048.0 16384.0 5642.5 2 0.011
2048.0 2048.0 0.0 0.0 7 15 2048.0 16384.0 6442.6 2 0.011
1
2
3
4
5
6
7
8
9
10
11
S0C:第一个幸存区大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
TT:对象在新生代存活的次数
MTT:对象在新生代存活的最大次数
DSS:期望的幸存区大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
jstat -gcnewcapacity

显示内容与-gcnew 基本相同,输出主要关注使用到的最大、最小空间

1
2
3
4
5
6
7
8
9
10
11
12
C:\Users\Think>jstat -gcnewcapacity 10620 1000 10
NGCMN NGCMX NGC S0CMX S0C S1CMX S1C ECMX EC YGC FGC
20480.0 20480.0 20480.0 2048.0 2048.0 2048.0 2048.0 19456.0 16384.0 1 0
20480.0 20480.0 20480.0 2048.0 2048.0 2048.0 2048.0 19456.0 16384.0 1 0
20480.0 20480.0 20480.0 2048.0 2048.0 2048.0 2048.0 19456.0 16384.0 1 0
20480.0 20480.0 20480.0 2048.0 2048.0 2048.0 2048.0 19456.0 16384.0 1 0
20480.0 20480.0 20480.0 2048.0 2048.0 2048.0 2048.0 19456.0 16384.0 1 0
20480.0 20480.0 20480.0 2048.0 2048.0 2048.0 2048.0 19456.0 16384.0 1 0
20480.0 20480.0 20480.0 2048.0 2048.0 2048.0 2048.0 19456.0 16384.0 1 0
20480.0 20480.0 20480.0 2048.0 2048.0 2048.0 2048.0 19456.0 16384.0 1 0
20480.0 20480.0 20480.0 2048.0 2048.0 2048.0 2048.0 19456.0 16384.0 2 1
20480.0 20480.0 20480.0 2048.0 2048.0 2048.0 2048.0 19456.0 16384.0 2 1
1
2
3
4
5
6
7
8
9
10
11
NGCMN:新生代最小容量
NGCMX:新生代最大容量
NGC:当前新生代容量
S0CMX:最大幸存1区大小
S0C:当前幸存1区大小
S1CMX:最大幸存2区大小
S1C:当前幸存2区大小
ECMX:最大伊甸园区大小
EC:当前伊甸园区大小
YGC:年轻代垃圾回收次数
FGC:老年代回收次数
jstat -gcold

显示老年代 GC 状况

1
2
3
4
5
6
7
8
9
10
11
12
C:\Users\Think>jstat -gcold 21912 1000 10
MC MU CCSC CCSU OC OU YGC FGC FGCT GCT
4480.0 769.9 384.0 75.8 40960.0 0.0 0 0 0.000 0.000
4864.0 3781.9 512.0 419.4 40960.0 12265.8 1 0 0.000 0.005
4864.0 3781.9 512.0 419.4 40960.0 12265.8 1 0 0.000 0.005
4864.0 3781.9 512.0 419.4 40960.0 12265.8 1 0 0.000 0.005
4864.0 3781.9 512.0 419.4 40960.0 12265.8 1 0 0.000 0.005
4864.0 3781.9 512.0 419.4 40960.0 12265.8 1 0 0.000 0.005
4864.0 3781.9 512.0 419.4 40960.0 12265.8 1 0 0.000 0.005
4864.0 3781.9 512.0 419.4 40960.0 12265.8 1 0 0.000 0.005
4864.0 3781.9 512.0 419.4 40960.0 12265.8 1 0 0.000 0.005
4864.0 3781.9 512.0 419.4 40960.0 12265.8 1 0 0.000 0.005
1
2
3
4
5
6
7
8
9
10
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
OC:老年代大小
OU:老年代使用大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
jstat -gcoldcapacity

显示内容与-gcold 基本相同,输出主要关注使用到的最大、最小空间

1
2
3
4
5
6
7
8
9
10
11
12
C:\Users\Think>jstat -gcoldcapacity 24112 1000 10
OGCMN OGCMX OGC OC YGC FGC FGCT GCT
40960.0 40960.0 40960.0 40960.0 1 0 0.000 0.004
40960.0 40960.0 40960.0 40960.0 1 0 0.000 0.004
40960.0 40960.0 40960.0 40960.0 1 0 0.000 0.004
40960.0 40960.0 40960.0 40960.0 1 0 0.000 0.004
40960.0 40960.0 40960.0 40960.0 1 0 0.000 0.004
40960.0 40960.0 40960.0 40960.0 1 0 0.000 0.004
40960.0 40960.0 40960.0 40960.0 1 0 0.000 0.004
40960.0 40960.0 40960.0 40960.0 1 0 0.000 0.004
40960.0 40960.0 40960.0 40960.0 1 0 0.000 0.004
40960.0 40960.0 40960.0 40960.0 1 0 0.000 0.004
1
2
3
4
5
6
7
8
OGCMN:老年代最小容量
OGCMX:老年代最大容量
OGC:当前老年代大小
OC:老年代大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
jstat -gcmetacapacity
1
2
3
4
5
6
7
8
9
10
11
12
C:\Users\Think>jstat -gcmetacapacity 15232 1000 10
MCMN MCMX MC CCSMN CCSMX CCSC YGC FGC FGCT GCT
0.0 1056768.0 4480.0 0.0 1048576.0 384.0 0 0 0.000 0.000
0.0 1056768.0 4480.0 0.0 1048576.0 384.0 0 0 0.000 0.000
0.0 1056768.0 4480.0 0.0 1048576.0 384.0 0 0 0.000 0.000
0.0 1056768.0 4480.0 0.0 1048576.0 384.0 0 0 0.000 0.000
0.0 1056768.0 4864.0 0.0 1048576.0 512.0 1 0 0.000 0.004
0.0 1056768.0 4864.0 0.0 1048576.0 512.0 1 0 0.000 0.004
0.0 1056768.0 4864.0 0.0 1048576.0 512.0 1 0 0.000 0.004
0.0 1056768.0 4864.0 0.0 1048576.0 512.0 1 0 0.000 0.004
0.0 1056768.0 4864.0 0.0 1048576.0 512.0 1 0 0.000 0.004
0.0 1056768.0 4864.0 0.0 1048576.0 512.0 1 0 0.000 0.004
1
2
3
4
5
6
7
8
9
10
MCMN: 最小元数据容量
MCMX:最大元数据容量
MC:当前元数据空间大小
CCSMN:最小压缩类空间大小
CCSMX:最大压缩类空间大小
CCSC:当前压缩类空间大小
YGC:年轻代垃圾回收次数
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间

interval 参数:

用于指定输出统计数据的周期,单位为毫秒。即:查询间隔

1
2
3
4
5
6
7
8
C:\Users\Think>jstat -class 12312 1000
Loaded Bytes Unloaded Bytes Time
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35

count 参数:

用于指定查询的总次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
C:\Users\Think>jstat -class 12312 1000 10
Loaded Bytes Unloaded Bytes Time
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35
699 1398.0 0 0.0 0.35

C:\Users\Think>

-t 参数:

可以在输出信息前加上一个 Timestamp 列,显示程序的运行时间(从项目执行到现在运行的总时间)。单位:秒

1
2
3
C:\Users\Think>jstat -class -t 12312
Timestamp Loaded Bytes Unloaded Bytes Time
464.9 699 1398.0 0 0.0 0.35

我们可以比较Java进程的启动时间以及总GC时间(GCT列),或者两次测量的间隔时间以及总GC时间的增量,来得出GC时间占运行时间的比例

如果该比例超过20%,则说明目前堆的压力较大;如果该比例超过90%,则说明堆里几乎没有可用空间,随时都可能抛出OOM异常。

-h 参数:

可以在周期性数据输出时,输出多少行数据后输出一个表头信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C:\Users\Think>jstat -class -t -h3 12312 1000 10
Timestamp Loaded Bytes Unloaded Bytes Time
647.3 699 1398.0 0 0.0 0.35
648.4 699 1398.0 0 0.0 0.35
649.4 699 1398.0 0 0.0 0.35
Timestamp Loaded Bytes Unloaded Bytes Time
650.4 699 1398.0 0 0.0 0.35
651.4 699 1398.0 0 0.0 0.35
652.4 699 1398.0 0 0.0 0.35
Timestamp Loaded Bytes Unloaded Bytes Time
653.4 699 1398.0 0 0.0 0.35
654.5 699 1398.0 0 0.0 0.35
655.5 699 1398.0 0 0.0 0.35
Timestamp Loaded Bytes Unloaded Bytes Time
656.5 699 1398.0 0 0.0 0.35

补充:

jstat 还可以用来判断是否出现内存泄漏。

第 1 步:在长时间运行的 Java 程序中,我们可以运行 jstat 命令连续获取多行性能数据,并取这几行数据中 OU 列(即已占用的老年代内存)的最小值。

第 2 步:然后,我们每隔一段较长的时间重复一次上述操作,来获得多组 OU 最小值。如果这些值呈上涨趋势,则说明该 Java 程序的老年代内存已使用量在不断上涨,这意味着无法回收的对象在不断增加,因此很有可能存在内存泄漏。

jinfo:实时查看和修改 JVM 配置参数

概述

jinfo(Configuration Info for Java):查看虚拟机配置参数信息,也可用于调整虚拟机的配置参数。

在很多情况下,Java 应用程序不会指定所有的 Java 虚拟机参数。而此时,开发人员可能不知道某一个具体的 Java 虚拟机参数的默认值。在这种情况下,可能需要通过查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了 jinfo 工具,开发人员可以很方便地找到 Java 虚拟机参数的当前值。

官方地址:https://docs.oracle.com/en/java/javase/11/tools/jinfo.html#GUID-69246B58-28C4-477D-B375-278F5F9830A5

基本使用语法为:jinfo [options] pid

说明:java 进程 ID 必须要加上

选项 选项说明
no option 输出全部的参数和系统属性
-flag name 输出对应名称的参数
-flag [+-]name 开启或者关闭对应名称的参数 只有被标记为 manageable 的参数才可以被动态修改
-flag name=value 设定对应名称的参数
-flags 输出全部的参数
-sysprops 输出系统属性
1
2
3
4
5
6
C:\Users\Think>jps
21428
24660 KotlinCompileDaemon
21288 ScannerTest
23896 Jps
15948 Launcher

查看

jinfo -sysprops PID

输出系统属性,可以查看由System.getProperties()取得参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
C:\Users\Think>jinfo -sysprops 21288
Attaching to process ID 21288, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.101-b13
java.runtime.name = Java(TM) SE Runtime Environment
java.vm.version = 25.101-b13
sun.boot.library.path = C:\Program Files\Java\jdk1.8.0_101\jre\bin
java.vendor.url = http://java.oracle.com/
java.vm.vendor = Oracle Corporation
path.separator = ;
file.encoding.pkg = sun.io
java.vm.name = Java HotSpot(TM) 64-Bit Server VM
sun.os.patch.level =
sun.java.launcher = SUN_STANDARD
user.script =
user.country = CN
user.dir = D:\interview\jvm
java.vm.specification.name = Java Virtual Machine Specification
java.runtime.version = 1.8.0_101-b13
java.awt.graphicsenv = sun.awt.Win32GraphicsEnvironment
os.arch = amd64
java.endorsed.dirs = C:\Program Files\Java\jdk1.8.0_101\jre\lib\endorsed
line.separator =

java.io.tmpdir = C:\Users\Think\AppData\Local\Temp\
java.vm.specification.vendor = Oracle Corporation
user.variant =
os.name = Windows 10
sun.jnu.encoding = GBK
java.library.path = C:\Program Files\Java\jdk1.8.0_101\bin;C:\WINDOWS\Sun\Java\bin;C:\WINDOWS\system32;C:\WINDOWS;D:\Program Files (x86)\MEGA5;%\jre\bin\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\myJava\apache-maven-3.5.2\bin;C:\Program Files (x86)\Intel\iCLS Client\;C:\Program Files\Intel\iCLS Client\;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\IPT;C:\Program Files\Intel\Intel(R) Management Engine Components\IPT;C:\Program Files\Java\jdk1.8.0_101\bin;C:\Program Files\Java\jdk1.8.0_101\bin\jre\bin;c:\Program Files\JAVA\jdk1.8.0_101\\bin;C:\WINDOWS\System32\OpenSSH\;C:\Program Files\MySQL\MySQL Server 8.0\bin;C:\Program Files\OpenVPN\bin;D:\Dwimperl\c\bin;D:\Dwimperl\perl\bin;D:\Dwimperl\perl\site\bin;D:\Program Files (x86)\gradle-6.1.1/bin;D:\Program Files\Git\cmd;C:\Program Files\Calibre2\;D:\Program Files (x86)\Graphviz\bin;D:\Users\Think\AppData\Roaming\nvm;D:\Program Files\nodejs;D:\Program Files\MEGA-X;C:\Program Files\mingw-w64\x86_64-8.1.0-win32-seh-rt_v6-rev0\mingw64\bin;C:\Program Files\MySQL\mysql-5.7.34-winx64\bin;D:\Program Files (x86)\Lua\5.1;D:\Program Files (x86)\Lua\5.1\clibs;C:\Users\Think\AppData\Local\Microsoft\WindowsApps;C:\Program Files\Bandizip\;D:\Program Files (x86)\Microsoft VS Code\bin;D:\Users\Think\AppData\Roaming\nvm\v12.18.3\node_modules\npm;D:\idea\JetBrains\IntelliJIDEA2019.3\bin;;D:\Users\Think\AppData\Roaming\nvm;D:\Program Files\nodejs;";D:\Program Files\curl-7.78.0-win64-mingw\I386;";;.
java.specification.name = Java Platform API Specification
java.class.version = 52.0
sun.management.compiler = HotSpot 64-Bit Tiered Compilers
os.version = 10.0
user.home = C:\Users\Think
user.timezone = Asia/Shanghai
java.awt.printerjob = sun.awt.windows.WPrinterJob
file.encoding = UTF-8
java.specification.version = 1.8
user.name = Think
java.class.path = C:\Program Files\Java\jdk1.8.0_101\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\rt.jar;D:\interview\jvm\target\classes;D:\ideaJetBrains\IntelliJIDEA2019.3\lib\idea_rt.jar
java.vm.specification.version = 1.8
sun.arch.data.model = 64
sun.java.command = top.lvxiaoyi.three.chapter02.code02.ScannerTest
java.home = C:\Program Files\Java\jdk1.8.0_101\jre
user.language = zh
java.specification.vendor = Oracle Corporation
awt.toolkit = sun.awt.windows.WToolkit
java.vm.info = mixed mode
java.version = 1.8.0_101
java.ext.dirs = C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
sun.boot.class.path = C:\Program Files\Java\jdk1.8.0_101\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\rt.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\sunrsasign.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_101\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_101\jre\classes
java.vendor = Oracle Corporation
file.separator = \
java.vendor.url.bug = http://bugreport.sun.com/bugreport/
sun.io.unicode.encoding = UnicodeLittle
sun.cpu.endian = little
sun.desktop = windows
sun.cpu.isalist = amd64

jinfo -flags PID

输出全部的参数

1
2
3
4
5
6
7
C:\Users\Think>jinfo -flags 21288
Attaching to process ID 21288, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.101-b13
Non-default VM flags: -XX:CICompilerCount=4 -XX:InitialHeapSize=62914560 -XX:MaxHeapSize=62914560 -XX:MaxNewSize=20971520 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=20971520 -XX:OldSize=41943040 -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
Command line: -Xms60m -Xmx60m -XX:SurvivorRatio=8 -javaagent:D:\ideaJetBrains\IntelliJIDEA2019.3\lib\idea_rt.jar=52057:D:\ideaJetBrains\IntelliJIDEA2019.3\bin -Dfile.encoding=UTF-8

jinfo -flag name PID

输出对应名称的参数

1
2
3
4
5
C:\Users\Think>jinfo -flag UseParallelGC 21288
-XX:+UseParallelGC

C:\Users\Think>jinfo -flag UseSerialGC 21288
-XX:-UseSerialGC

修改

jinfo不仅可以查看运行时某一个Java虚拟机参数的实际取值,甚至可以在运行时修改部分参数,并使之立即生效。

但是,并非所有参数都支持动态修改。参数只有被标记为manageable的flag可以被实时修改。其实,这个修改能力是极其有限的

  • 可以查看被标记为manageable的参数
  • 下面为linux的指令,|就是管道符(如果不知道看操作系统,就是前面的的命令结果作为后面的标准输入),grep就是正则匹配筛选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@iZ2ze0xc1pcwgfqh1z6tfgZ ~]# java -XX:+PrintFlagsFinal -version | grep manageable
intx CMSAbortablePrecleanWaitMillis = 100 {manageable}
intx CMSTriggerInterval = -1 {manageable}
intx CMSWaitDuration = 2000 {manageable}
bool HeapDumpAfterFullGC = false {manageable}
bool HeapDumpBeforeFullGC = false {manageable}
bool HeapDumpOnOutOfMemoryError = false {manageable}
ccstr HeapDumpPath = {manageable}
uintx MaxHeapFreeRatio = 70 {manageable}
uintx MinHeapFreeRatio = 40 {manageable}
bool PrintClassHistogram = false {manageable}
bool PrintClassHistogramAfterFullGC = false {manageable}
bool PrintClassHistogramBeforeFullGC = false {manageable}
bool PrintConcurrentLocks = false {manageable}
bool PrintGC = false {manageable}
bool PrintGCDateStamps = false {manageable}
bool PrintGCDetails = false {manageable}
bool PrintGCID = false {manageable}
bool PrintGCTimeStamps = false {manageable}
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

jinfo -flag [+-]name PID

开启或者关闭对应名称的参数 只有被标记为 manageable 的参数才可以被动态修改

1
2
3
4
5
6
7
C:\Users\Think>jinfo -flag PrintGCDetails 21288
-XX:-PrintGCDetails

C:\Users\Think>jinfo -flag +PrintGCDetails 21288

C:\Users\Think>jinfo -flag PrintGCDetails 21288
-XX:+PrintGCDetails

jinfo -flag name=value PID

设定对应名称的参数

1
2
3
4
5
6
7
C:\Users\Think>jinfo -flag MaxHeapFreeRatio 21288
-XX:MaxHeapFreeRatio=100

C:\Users\Think>jinfo -flag MaxHeapFreeRatio=90 21288

C:\Users\Think>jinfo -flag MaxHeapFreeRatio 21288
-XX:MaxHeapFreeRatio=90

拓展:

  • java -XX:+PrintFlagsInitial 查看所有 JVM 参数启动的初始值

    1
    2
    3
    4
    5
    6
    7
    8
    C:\Users\Think>java -XX:+PrintFlagsInitial
    [Global flags]
    uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
    uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
    uintx AdaptiveSizePausePolicy = 0 {product}
    bool UseLargePagesIndividualAllocation := false {pd product}
    bool UseLockedTracing = false {product}
    ...
  • java -XX:+PrintFlagsFinal 查看所有 JVM 参数的最终值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    C:\Users\Think>java -XX:+PrintFlagsFinal
    [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}
    uintx InitialHeapSize := 268435456 {product}
    ...
    bool UseCompressedClassPointers := true {lp64_product}
    bool UseCompressedOops := true {lp64_product}
    ...
  • java -XX:+PrintCommandLineFlags 查看哪些已经被用户或者 JVM 设置过的详细的 XX 参数的名称和值

    1
    2
    3
    4
    5
    6
    7
    C:\Users\Think>java -XX:PrintCommandLineFlags
    Missing +/- setting for VM option 'PrintCommandLineFlags'
    Error: Could not create the Java Virtual Machine.
    Error: A fatal exception has occurred. Program will exit.

    C:\Users\Think>java -XX:+PrintCommandLineFlags
    -XX:InitialHeapSize=266742976 -XX:MaxHeapSize=4267887616 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC

jmap:导出内存映像文件&内存使用情况

基本情况

jmap(JVM Memory Map):作用一方面是获取 dump 文件(堆转储快照文件,二进制文件),它还可以获取目标 Java 进程的内存相关信息,包括 Java 堆各区域的使用情况、堆中对象的统计信息、类加载信息等。开发人员可以在控制台中输入命令“jmap -help”查阅 jmap 工具的具体使用方式和一些标准选项配置。

官方帮助文档:https://docs.oracle.com/en/java/javase/11/tools/jmap.html

基本语法

基本使用语法为:

  • jmap [option] <pid>
  • jmap [option] <executable <core>
  • jmap [option] [server_id@] <remote server IP or hostname>
选项 作用
-dump 生成 dump 文件(Java 堆转储快照),-dump:live 只保存堆中的存活对象
-heap 输出整个堆空间的详细信息,包括 GC 的使用、堆配置信息,以及内存的使用信息等
-histo 输出堆空间中对象的统计信息,包括类、实例数量和合计容量,-histo:live 只统计堆中的存活对象
-J <flag> 传递参数给 jmap 启动的 jvm
-finalizerinfo 显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象,仅 linux/solaris 平台有效
-permstat 以 ClassLoader 为统计口径输出永久代的内存状态信息,仅 linux/solaris 平台有效
-F 当虚拟机进程对-dump 选项没有任何响应时,强制执行生成 dump 文件,仅 linux/solaris 平台有效

说明:这些参数和 linux 下输入显示的命令多少会有不同,包括也受 jdk 版本的影响。

导出内存映像文件

一般来说,使用jmap指令生成dump文件的操作算得上是最常用的jmap命令之一,将堆中所有存活对象导出至一个文件之中。

Heap Dump又叫做堆存储文件,指一个Java进程在某个时间点的内存快照。Heap Dump在触发内存快照的时候会保存此刻的信息如下:

  • All 0bjects

    Class、fields 、 primitive values and references

  • All classes

    classLoader、name 、 super class、static fields

  • Garbage Collection Roots

    0bjects defined to be reachable by the JVM

  • Thread Stacks and Local Variables

The call-stacks of threads at the moment of the snapshot, and per-frameinformation about local objects

说明:

  1. 通常在写Heap Dump文件前会触发一次Full GC,所以heap dump文件里保存的都是Full GC后留下的对象信息。

  2. 由于生成dump文件比较耗时,需要耐心等待,尤其是大内存镜像生成dump文件则需要耗费更长的时间来完成。

手动

jmap -dump

jmap -dump:format=b,file=<filename.hprof> <pid>:导出所有的dump文件,b代表着格式对齐

jmap -dump:live,format=b,file=<filename.hprof> <pid>:只导出存活的dump文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
C:\Users\Think>jps
2576 Jps
19044 Launcher
21428
25096 GCTest

C:\Users\Think>jmap -dump:format=b,file=D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\1.hprof 25096
Dumping heap to D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\1.hprof ...
Heap dump file created

C:\Users\Think>jmap -dump:format=b,file=D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\2.hprof 25096
Dumping heap to D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\2.hprof ...
Heap dump file created

C:\Users\Think>jmap -dump:format=b,file=D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\3.hprof 25096
Dumping heap to D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\3.hprof ...
Heap dump file created

C:\Users\Think>jmap -dump:live,format=b,file=D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\4.hprof 25096
Dumping heap to D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\4.hprof ...
Heap dump file created

更多的OOM是因为存活的对象所造成的,所有用live一个是文件更小,速度更快,一个也是因为更多的问题在存活的对象上

自动

当程序发生oOM退出系统时,一些瞬时信息都随着程序的终止而消失,而重现OOM问题往往比较困难或者耗时。此时若能在0OM时,自动导出dump文件就显得非常迫切。

这里介绍一种比较常用的取得堆快照文件的方法,即使用:

-XX:+HeapDumpOnOutOfMemoryError:在程序发生O0M时,导出应用程序的当前堆快照。

-XX:HeapDumpPath:可以指定堆快照的保存位置。
比如:

1
2
3
4
5
-Xms60m
-Xmx60m
-XX:SurvivorRatio=8
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\m.hprof

image-20210928145002893

对m.hprof分析

image-20210928145640507

image-20210928145823203

从上面分析我们可以看出来是arrylist导致的OOM

显示堆内存相关信息

jmap -heap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
C:\Users\Think>jmap -heap 25652
Attaching to process ID 25652, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.101-b13

using thread-local object allocation.
Parallel GC with 8 thread(s)
# heap配置信息
Heap Configuration:
# 空闲堆空间的最小百分比,HeapFreeRatio =(CurrentFreeHeapSize/CurrentTotalHeapSize) * 100,值的区间为0到100,默认值为 40。如果HeapFreeRatio < MinHeapFreeRatio,则需要进行堆扩容,扩容的时机应该在每次垃圾回收之后
MinHeapFreeRatio = 0
# 空闲堆空间的最大百分比,计算公式为:HeapFreeRatio =(CurrentFreeHeapSize/CurrentTotalHeapSize) * 100,值的区间为0到100,默认值为 70。如果HeapFreeRatio > MaxHeapFreeRatio,则需要进行堆缩容,缩容的时机应该在每次垃圾回收之后
MaxHeapFreeRatio = 100
# JVM 堆空间允许的最大值
MaxHeapSize = 62914560 (60.0MB)
# JVM 新生代堆空间的默认值
NewSize = 20971520 (20.0MB)
# JVM 新生代堆空间允许的最大值
MaxNewSize = 20971520 (20.0MB)
# JVM 老年代堆空间的默认值。
OldSize = 41943040 (40.0MB)
# 新生代(2个Survivor区和Eden区 )与老年代(不包括永久区)的堆空间比值,表示新生代:老年代=1:2。
NewRatio = 2
# 两个Survivor区和Eden区的堆空间比值为 8,表示 S0 : S1 :Eden = 1:1:8。
SurvivorRatio = 8
# JVM 元空间的默认值。
MetaspaceSize = 21807104 (20.796875MB)
# 压缩后类空间大小
CompressedClassSpaceSize = 1073741824 (1024.0MB)
# JVM 元空间允许的最大值。
MaxMetaspaceSize = 17592186044415 MB
# 在使用 G1 垃圾回收算法时,JVM 会将 Heap 空间分隔为若干个 Region,该参数用来指定每个 Region 空间的大小。
G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
PS Young Generation
# 伊甸园区(新生区分为Eden和survivor)
Eden Space:
# 容量
capacity = 16777216 (16.0MB)
# 已使用
used = 15072304 (14.374069213867188MB)
# 空闲
free = 1704912 (1.6259307861328125MB)
# 使用率
89.83793258666992% used
# 幸存0区(也叫form区,新生区的sruvivor)
From Space:
capacity = 2097152 (2.0MB)
used = 0 (0.0MB)
free = 2097152 (2.0MB)
0.0% used
# 幸存1区(也叫to区,新生区的sruvivor)
To Space:
capacity = 2097152 (2.0MB)
used = 0 (0.0MB)
free = 2097152 (2.0MB)
0.0% used
# 老年代
PS Old Generation
capacity = 41943040 (40.0MB)
used = 0 (0.0MB)
free = 41943040 (40.0MB)
0.0% used

3151 interned Strings occupying 258456 bytes.

小总结:jmap只能是这一个时间点的,而jstat虽然也是时间点,但是能多个时间点。但是,还是图像化界面用着爽!

jmap -histo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
C:\Users\Think>jmap -histo 25652

num #instances #bytes class name
----------------------------------------------
# 都在jvm基础2的Class文件结构章节中解释过
1: 624 21741720 [B #byte数组
2: 4649 446144 [C #char数组
3: 224 173696 [I #int数组
4: 4488 107712 java.lang.String
5: 704 80360 java.lang.Class
6: 633 42808 [Ljava.lang.Object;
7: 847 33880 java.util.TreeMap$Entry
8: 628 25120 java.util.LinkedHashMap$Entry
9: 426 19056 [Ljava.lang.String;
10: 421 13472 java.util.HashMap$Node
...
270: 1 16 sun.util.locale.provider.AuxLocaleProviderAdapter$NullProvider
271: 1 16 sun.util.locale.provider.SPILocaleProviderAdapter
272: 1 16 sun.util.locale.provider.TimeZoneNameUtility$TimeZoneNameGetter
273: 1 16 sun.util.resources.LocaleData
274: 1 16 sun.util.resources.LocaleData$LocaleDataResourceBundleControl
Total 15712 22778864

其他

jmap -clstats pid

查看系统的ClassLoader信息

也就是视频上所说的jmap -permstat pid,在jdk8(无论是在windows还是linux中)中使用clstats,而不是permstat

windows(Jdk1.8.0_101):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
where <option> is one of:
<none> to print same info as Solaris pmap
-heap to print java heap summary
-histo[:live] to print histogram of java object heap; if the "live"
suboption is specified, only count live objects
# jdk1.8中没有-permstat ,改为了-clstats
-clstats to print class loader statistics
-finalizerinfo to print information on objects awaiting finalization
-dump:<dump-options> to dump java heap in hprof binary format
dump-options:
live dump only live objects; if not specified,
all objects in the heap are dumped.
format=b binary format
file=<file> dump heap to <file>
Example: jmap -dump:live,format=b,file=heap.bin <pid>
-F force. Use with -dump:<dump-options> <pid> or -histo
to force a heap dump or histogram when <pid> does not
respond. The "live" suboption is not supported
in this mode.
-h | -help to print this help message
-J<flag> to pass <flag> directly to the runtime system

linux(jdk1.8.0_144):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
where <option> is one of:
<none> to print same info as Solaris pmap
-heap to print java heap summary
-histo[:live] to print histogram of java object heap; if the "live"
suboption is specified, only count live objects
-clstats to print class loader statistics
-finalizerinfo to print information on objects awaiting finalization
-dump:<dump-options> to dump java heap in hprof binary format
dump-options:
live dump only live objects; if not specified,
all objects in the heap are dumped.
format=b binary format
file=<file> dump heap to <file>
Example: jmap -dump:live,format=b,file=heap.bin <pid>
-F force. Use with -dump:<dump-options> <pid> or -histo
to force a heap dump or histogram when <pid> does not
respond. The "live" suboption is not supported
in this mode.
-h | -help to print this help message
-J<flag> to pass <flag> directly to the runtime system

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
C:\Users\Think>jmap -clstats 9080
Attaching to process ID 9080, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.101-b13
finding class loader instances ..done.
computing per loader stat ..done.
please wait.. computing liveness..................................................................................................done.
class_loader classes bytes parent_loader alive? type

<bootstrap> 618 1153211 null live <internal>
0x00000000ffc28010 0 0 null live sun/misc/Launcher$ExtClassLoader@0x000000010000fa30
0x00000000fcced778 0 0 0x00000000ffc202d8 live java/util/ResourceBundle$RBClassLoader@0x0000000100066f40
0x00000000ffc202d8 25 72291 0x00000000ffc28010 live sun/misc/Launcher$AppClassLoader@0x000000010000f688

total = 4 643 1225502 N/A alive=4, dead=0 N/A

jmap -finalizerinfo

查看堆积在finalizer队列中的对象

1
2
3
4
5
6
C:\Users\Think>jmap -finalizerinfo 20504
Attaching to process ID 20504, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.101-b13
Number of objects pending for finalization: 0

小结

由于 jmap 将访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap 需要借助安全点机制,让所有线程停留在不改变堆中数据的状态。也就是说,由 jmap 导出的堆快照必定是安全点位置的。这可能导致基于该堆快照的分析结果存在偏差。

举个例子,假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么:live 选项将无法探知到这些对象。

另外,如果某个线程长时间无法跑到安全点,jmap 将一直等下去。

与前面讲的 jstat 则不同,垃圾回收器会主动将 jstat 所需要的摘要数据保存至固定位置之中,而 jstat 只需直接读取即可。

jhat:JDK 自带堆分析工具

概述

jhat(JVM Heap Analysis Tool):

Sun JDK 提供的 jhat 命令与 jmap 命令搭配使用,用于分析 jmap 生成的 heap dump 文件(堆转储快照)。jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 文件的分析结果后,用户可以在浏览器中查看分析结果(分析虚拟机转储快照信息)。

使用了 jhat 命令,就启动了一个 http 服务,端口是 7000,即 http://localhost:7000/,就可以在浏览器里分析。

说明:jhat 命令在 JDK9、JDK10 中已经被删除,官方建议用 VisualVM 代替。

语法

基本适用语法:jhat <option> <dumpfile>

1
jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>
option 参数 作用
-stack false | true 关闭|打开对象分配调用栈跟踪
-refs false | true 关闭|打开对象引用跟踪
-port port-number 设置 jhat HTTP Server 的端口号,默认 7000
-exclude exclude-file 执行对象查询时需要排除的数据成员
-baseline exclude-file 指定一个基准堆转储
-debug int 设置 debug 级别
-version 启动后显示版本信息就退出
-J <flag> 传入启动参数,比如-J-Xmx512m

jhat

1
2
3
4
5
6
7
8
9
10
C:\Users\Think>jhat D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\2.hprof
Reading from D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\2.hprof...
Dump file created Tue Sep 28 14:32:46 CST 2021
Snapshot read, resolving...
Resolving 15757 objects...
Chasing references, expect 3 dots...
Eliminating duplicate references...
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

访问:http://localhost:7000/

image-20210928164147327

访问:http://localhost:7000/oql/

image-20210928164055033

jstack:打印 JVM 中线程快照

概述

jstack(JVM Stack Trace):

用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合。

生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用 jstack 显示各个线程调用的堆栈情况。

官方帮助文档:https://docs.oracle.com/en/java/javase/11/tools/jstack.html

在 thread dump 中,要留意下面几种状态

  • 死锁,Deadlock(重点关注)
  • 等待资源,Waiting on condition(重点关注)
  • 等待获取监视器,Waiting on monitor entry(重点关注)
  • 阻塞,Blocked(重点关注)
  • 执行中,Runnable
  • 暂停,Suspended
  • 对象等待中,Object.wait() 或 TIMED_WAITING
  • 停止,Parked

语法

option 参数 作用
-F 当正常输出的请求不被响应时,强制输出线程堆栈
-l 除堆栈外,显示关于锁的附加信息
-m 如果调用本地方法的话,可以显示 C/C++的堆栈

例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package top.lvxiaoyi.three.chapter02.code02;

/**
* @author lvxiaoyi
* @date 2021/9/28 16:50
*/
public class ThreadDeadLock {
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();

new Thread(() -> {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (s2) {
s1.append("b");
s2.append("2");

System.out.println(s1);
System.out.println(s2);
}
}
}).start();


new Thread(() -> {
synchronized (s2) {
s1.append("C");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (s1) {
s1.append("d");
s2.append("4");

System.out.println(s1);
System.out.println(s2);
}
}
}).start();
}
}

发生死锁的四个条件:互斥、请求与保持、不可剥夺、循环等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
C:\Users\Think>jstack 15488
2021-09-28 17:06:13
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.101-b13 mixed mode):

"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x00000000031e2800 nid=0x6b58 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Thread-1" #13 prio=5 os_prio=0 tid=0x000000001fa15000 nid=0x70fc waiting for monitor entry [0x00000000203ee000]
java.lang.Thread.State: BLOCKED (on object monitor)
at top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock.lambda$main$1(ThreadDeadLock.java:44)
- waiting to lock <0x000000076b5b2ee8> (a java.lang.StringBuilder)
- locked <0x000000076b5b2f30> (a java.lang.StringBuilder)
at top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock$$Lambda$2/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)

"Thread-0" #12 prio=5 os_prio=0 tid=0x000000001fa12000 nid=0x790 waiting for monitor entry [0x00000000202ef000]
java.lang.Thread.State: BLOCKED (on object monitor)
at top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock.lambda$main$0(ThreadDeadLock.java:23)
- waiting to lock <0x000000076b5b2f30> (a java.lang.StringBuilder)
- locked <0x000000076b5b2ee8> (a java.lang.StringBuilder)
at top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock$$Lambda$1/2003749087.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)

"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x000000001ed68000 nid=0x5888 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x000000001ecdc800 nid=0x52f0 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001eccf800 nid=0x700 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000000001ecca000 nid=0x6908 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000000001ecc4800 nid=0x5ab8 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001ecbe800 nid=0xb78 runnable [0x000000001f3ee000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:170)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x000000076b487b20> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x000000076b487b20> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001ec7b800 nid=0x1480 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001ec2c000 nid=0x6fdc runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

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

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

"VM Thread" os_prio=2 tid=0x000000001cd19800 nid=0x3e80 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00000000031f8000 nid=0x358c runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00000000031fa000 nid=0x6024 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000031fb800 nid=0x4704 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000031fe000 nid=0x5a1c runnable

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x0000000003200000 nid=0x4e78 runnable

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000000003201800 nid=0x6d58 runnable

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000000003204800 nid=0x6c70 runnable

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000000003205800 nid=0x72f8 runnable

"VM Periodic Task Thread" os_prio=2 tid=0x000000001ed9e000 nid=0x31b0 waiting on condition

JNI global references: 335


Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x000000001cd23708 (object 0x000000076b5b2ee8, a java.lang.StringBuilder),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000000001cd22268 (object 0x000000076b5b2f30, a java.lang.StringBuilder),
which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
at top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock.lambda$main$1(ThreadDeadLock.java:44)
- waiting to lock <0x000000076b5b2ee8> (a java.lang.StringBuilder)
- locked <0x000000076b5b2f30> (a java.lang.StringBuilder)
at top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock$$Lambda$2/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Thread-0":
at top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock.lambda$main$0(ThreadDeadLock.java:23)
- waiting to lock <0x000000076b5b2f30> (a java.lang.StringBuilder)
- locked <0x000000076b5b2ee8> (a java.lang.StringBuilder)
at top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock$$Lambda$1/2003749087.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)

Found 1 deadlock.

例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author lvxiaoyi
* @date 2021/9/28 17:12
*/
public class ThreadSleepTest {
public static void main(String[] args) {
System.out.println("hello lvxiaoyi");
try {
Thread.sleep(1000 * 60 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello lvxiaoyi , ");
}
}

例三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package top.lvxiaoyi.three.chapter02.code02;

/**
* @author lvxiaoyi
* @date 2021/9/28 17:16
*/
public class ThreadSyncTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);

t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}

class Number implements Runnable {
private int number = 1;

@Override
public void run() {
while (true) {
synchronized (this) {
if (number <= 100) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
} else {
break;
}
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
C:\Users\Think>jps
15488 ThreadDeadLock
21428
19240 Jps
25512 Program
28808 ThreadSyncTest
28968 Launcher
29208 KotlinCompileDaemon

C:\Users\Think>jstack 28808
2021-09-28 17:27:13
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.101-b13 mixed mode):

"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x0000000002782800 nid=0x72e4 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"线程2" #13 prio=5 os_prio=0 tid=0x000000001e41a800 nid=0x304c waiting for monitor entry [0x000000001f25e000]
java.lang.Thread.State: BLOCKED (on object monitor)
at top.lvxiaoyi.three.chapter02.code02.Number.run(ThreadSyncTest.java:27)
- waiting to lock <0x000000076b5b4df0> (a top.lvxiaoyi.three.chapter02.code02.Number)
at java.lang.Thread.run(Thread.java:745)

"线程1" #12 prio=5 os_prio=0 tid=0x000000001e41a000 nid=0x3f2c waiting on condition [0x000000001f15f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at top.lvxiaoyi.three.chapter02.code02.Number.run(ThreadSyncTest.java:29)
- locked <0x000000076b5b4df0> (a top.lvxiaoyi.three.chapter02.code02.Number)
at java.lang.Thread.run(Thread.java:745)

"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x000000001e36f000 nid=0x670c runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x000000001e2e3000 nid=0x4730 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001e2dc000 nid=0x432c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000000001e2d8000 nid=0x3e88 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000000001e2d5000 nid=0x51cc waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001e2cf000 nid=0x62e0 runnable [0x000000001ea5e000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:170)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x000000076b487b20> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x000000076b487b20> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001e28c000 nid=0x45ac waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001e23c000 nid=0x55a4 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

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

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

"VM Thread" os_prio=2 tid=0x000000001c309800 nid=0x5a60 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000002798000 nid=0x335c runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x000000000279a000 nid=0x5f04 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000000000279b800 nid=0x4a2c runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x000000000279e000 nid=0x32cc runnable

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x00000000027a0000 nid=0x4980 runnable

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x00000000027a1800 nid=0x523c runnable

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x00000000027a4800 nid=0x448 runnable

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x00000000027a5800 nid=0x2748 runnable

"VM Periodic Task Thread" os_prio=2 tid=0x000000001e394800 nid=0x7288 waiting on condition

JNI global references: 33

jstack -l

image-20210928174640000

例四

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package top.lvxiaoyi.three.chapter02.code02;

import java.util.Map;
import java.util.Set;

/**
* @author lvxiaoyi
* @date 2021/9/28 18:02
*/
public class AllStackTrace {
public static void main(String[] args) {
Map<Thread, StackTraceElement[]> all = Thread.getAllStackTraces();
Set<Map.Entry<Thread, StackTraceElement[]>> entries =all.entrySet();
for (Map.Entry<Thread, StackTraceElement[]> en : entries) {
Thread t = en.getKey();
StackTraceElement[] v = en.getValue();
System.out.println("Thread name is :" + t.getName());
for (StackTraceElement s : v) {
System.out.println("\t" + s.toString());
}
}
}
}

例五

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package top.lvxiaoyi.three.chapter02.code02;

import java.util.Map;
import java.util.Set;

/**
* @author lvxiaoyi
* @date 2021/9/28 16:50
*/
public class ThreadDeadLock2 {
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();

new Thread(() -> {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (s2) {
s1.append("b"); // 我是26行
s2.append("2");

System.out.println(s1);
System.out.println(s2);
}
}
}).start();


new Thread(() -> {
synchronized (s2) {
s1.append("C");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (s1) {
s1.append("d"); // 我是47行
s2.append("4");

System.out.println(s1);
System.out.println(s2);
}
}
}).start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(new Runnable() {
@Override
public void run() {
// 追踪线程中的所有线程
Map<Thread, StackTraceElement[]> all = Thread.getAllStackTraces(); // 我是65行
Set<Map.Entry<Thread, StackTraceElement[]>> entries =all.entrySet();
for (Map.Entry<Thread, StackTraceElement[]> en : entries) {
Thread t = en.getKey();
StackTraceElement[] v = en.getValue();
System.out.println("Thread name is :" + t.getName());
for (StackTraceElement s : v) {
System.out.println("\t" + s.toString());
}
}
}
}).start();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Thread name is :Thread-0
// 访问线程1的资源的时候停止了
top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock2.lambda$main$0(ThreadDeadLock2.java:26)
top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock2$$Lambda$1/2003749087.run(Unknown Source)
java.lang.Thread.run(Thread.java:745)
Thread name is :Attach Listener
Thread name is :Finalizer
java.lang.Object.wait(Native Method)
java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
Thread name is :Thread-2
java.lang.Thread.dumpThreads(Native Method)
java.lang.Thread.getAllStackTraces(Thread.java:1603)
top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock2$1.run(ThreadDeadLock2.java:65)
java.lang.Thread.run(Thread.java:745)
Thread name is :DestroyJavaVM
Thread name is :Signal Dispatcher
Thread name is :Monitor Ctrl-Break
java.net.SocketInputStream.socketRead0(Native Method)
java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
java.net.SocketInputStream.read(SocketInputStream.java:170)
java.net.SocketInputStream.read(SocketInputStream.java:141)
sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
java.io.InputStreamReader.read(InputStreamReader.java:184)
java.io.BufferedReader.fill(BufferedReader.java:161)
java.io.BufferedReader.readLine(BufferedReader.java:324)
java.io.BufferedReader.readLine(BufferedReader.java:389)
com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
Thread name is :Reference Handler
java.lang.Object.wait(Native Method)
java.lang.Object.wait(Object.java:502)
java.lang.ref.Reference.tryHandlePending(Reference.java:191)
java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
Thread name is :Thread-1
// 访问线程1的资源的时候停止了
top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock2.lambda$main$1(ThreadDeadLock2.java:47)
top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock2$$Lambda$2/990368553.run(Unknown Source)
java.lang.Thread.run(Thread.java:745)

jcmd:多功能命令行

概述

在 JDK 1.7 以后,新增了一个命令行工具 jcmd。它是一个多功能的工具,可以用来实现前面除了 jstat 之外所有命令的功能。比如:用它来导出堆、内存使用、查看 Java 进程、导出线程信息、执行 GC、JVM 运行时间等。

官方帮助文档:https://docs.oracle.com/en/java/javase/11/tools/jcmd.html

jcmd 拥有 jmap 的大部分功能,并且在 Oracle 的官方网站上也推荐使用 jcmd 命令代 jmap 命令

基本语法

1
jcmd <pid | main class> <command ...|PerfCounter.print|-f file>

jcmd -l

列出所有的 JVM 进程,等于jps -m

1
2
3
4
5
6
C:\Users\Think>jcmd -l
22560
4720 org.jetbrains.jps.cmdline.Launcher D:/ideaJetBrains/IntelliJIDEA2019.3/lib/trove4j.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/plugins/java/lib/maven-builder-support-3.3.9.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/lib/util.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/lib/jna-platform.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/pluginsjava/lib/maven-aether-provider-3.3.9.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/lib/netty-codec-4.1.41.Final.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/lib/netty-buffer-4.1.41.Final.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/lib/protobuf-java-3.5.1.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/lib/annotations.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/lib/httpcore-4.4.12.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/plugins/java/lib/javac2.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/plugins/java/lib/maven-repository-metadata-3.3.9.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/plugins/java/lib/maven-artifact-3.3.9.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/plugins/java/lib/aether-spi-1.1.0.jar;D:/ideaJetBrains/IntelliJIDEA2019.3/p
24948 sun.tools.jcmd.JCmd -l
16520 top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock2
29532 org.jetbrains.kotlin.daemon.KotlinCompileDaemon --daemon-runFilesPath C:\Users\Think\AppData\Local\kotlin\daemon --daemon-autoshutdownIdleSeconds=7200 --daemon-compilerClasspath D:\ideaJetBrains\IntelliJIDEA2019.3\plugins\Kotlin\kotlinc\lib\kotlin-compiler.jar;C:\Program Files\Java\jdk1.8.0_101\lib\tools.jar;D:\ideaJetBrains\IntelliJIDEA2019.3\plugins\Kotlin\kotlinc\lib\kotlin-daemon.jar

jcmd 进程号 help

针对指定的进程,列出支持的所有具体命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
C:\Users\Think>jcmd 16520 help
16520:
The following commands are available:
JFR.stop
JFR.start
JFR.dump
JFR.check
VM.native_memory
VM.check_commercial_features
VM.unlock_commercial_features
ManagementAgent.stop
ManagementAgent.start_local
ManagementAgent.start
GC.rotate_log
Thread.print
GC.class_stats
GC.class_histogram
GC.heap_dump
GC.run_finalization
GC.run
VM.uptime
VM.flags
VM.system_properties
VM.command_line
VM.version
help
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C:\Users\Think>jcmd 16520 Thread.print
16520:
2021-09-29 09:02:13
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.101-b13 mixed mode):

"DestroyJavaVM" #15 prio=5 os_prio=0 tid=0x0000000003092800 nid=0x745c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Thread-1" #13 prio=5 os_prio=0 tid=0x000000001f8c7000 nid=0x57d4 waiting for monitor entry [0x00000000202af000]
java.lang.Thread.State: BLOCKED (on object monitor)
at top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock2.lambda$main$1(ThreadDeadLock2.java:47)
- waiting to lock <0x000000076b5b3078> (a java.lang.StringBuilder)
- locked <0x000000076b5b30c0> (a java.lang.StringBuilder)
at top.lvxiaoyi.three.chapter02.code02.ThreadDeadLock2$$Lambda$2/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
...
...

jcmd 进程号 具体命令:显示指定进程的指令命令的数据

  • Thread.print 可以替换 jstack 指令
  • GC.class_histogram 可以替换 jmap 中的-histo 操作
  • GC.heap_dump 可以替换 jmap 中的-dump 操作
  • GC.run 可以查看 GC 的执行情况
  • VM.uptime 可以查看程序的总执行时间,可以替换 jstat 指令中的-t 操作
  • VM.system_properties 可以替换 jinfo -sysprops 进程 id
  • VM.flags 可以获取 JVM 的配置参数信息

jstatd:远程主机信息收集

之前的指令只涉及到监控本机的 Java 应用程序,而在这些工具中,一些监控工具也支持对远程计算机的监控(如 jps、jstat)。为了启用远程监控,则需要配合使用 jstatd 工具。

命令 jstatd 是一个 RMI 服务端程序,它的作用相当于代理服务器,建立本地计算机与远程监控工具的通信。jstatd 服务器将本机的 Java 应用程序信息传递到远程计算机。

image-20210504213301077

JVM 监控及诊断工具-GUI 篇

工具概述

使用上一章命令行工具或组合能帮您获取目标 Java 应用性能相关的基础信息,但它们存在下列局限:

  • 无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)。
  • 要求用户登录到目标 Java 应用所在的宿主机上,使用起来不是很方便。
  • 分析数据通过终端输出,结果展示不够直观。

为此,JDK 提供了一些内存泄漏的分析工具,如 jconsole,jvisualvm 等,用于辅助开发人员定位问题,但是这些工具很多时候并不足以满足快速定位的需求。所以这里我们介绍的工具相对多一些、丰富一些。

JDK 自带的工具

  • jconsole:JDK 自带的可视化监控工具。查看 Java 应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等

    • 位置:jdk\bin\jconcole.exe
  • Visual VM:Visual VM 是一个工具,它提供了一个可视界面,用于查看 Java 虚拟机上运行的基于 Java 技术的应用程序的详细信息。

    • 位置:jdk\bin\jvisualvm.exe
  • JMC:Java Mission Control,内置 Java Flight Recorder。能够以极低的性能开销收集 Java 虚拟机的性能数据。

第三方工具

  • MAT:MAT(Memory Analyzer Tool)是基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
  • JProfiler:商业软件,需要付费。功能强大。
    • 与VisualVM类似
  • Arthas:Alibaba开源的Java诊断工具。深受开发者喜爱。
  • Btrace:Java运行时追踪工具。可以在不停机的情况下,跟踪指定的方法调用、构造函数调用和系统内存等信息。

JConsole

概述

jconsole:从 Java5 开始,在 JDK 中自带的 java 监控和管理控制台。用于对 JVM 中内存、线程和类等的监控,是一个基于 JMX(java management extensions)的 GUI 性能监控工具。

官方地址:https://docs.oracle.com/javase/7/docs/technotes/guides/management/jconsole.html

启动

在jdk安装目录中找到jconsole.exe,双击该可执行文件就可以或者打开DOS窗口,直接输入jconsole就可以了

连接方式

local

使用JConsole连接一个正在本地系统运行的JVM,并且执行程序的和运行JConsole的需要是同一个用户。

JConsole使用文件系统的授权通过RMI连接起链接到平台的MBean的服务器上。这种从本地连接的监控能力只有Sun的JDK具有。

Remote

使用下面的URL通过RMI连接器连接到一个JMX代理,service:jmx:rmi:///jndi/rmi://hostName:portNum/jmxrmi。JConsole为建立连接,需要在环境变量中设置mx.remote.credentials来指定用户名和密码,从而进行授权。

Advanced

使用一个特殊的URL连接JMX代理。一般情况使用自己定制的连接器而不是RMI提供的连接器来连接JMX代理,或者是一个使用JDK1.4的实现了JMX和JMX Rmote的应用

作用

概览

image-20210929101103771

内存

image-20210929101117155

根据线程检测死锁

image-20210929101524427

线程

image-20210929101131921

VM概要

image-20210929101209659

Visual VM

概述

Visual VM 是一个功能强大的多合一故障诊断和性能监控的可视化工具。

它集成了多个 JDK 命令行工具,使用 Visual VM 可用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的 CPU、GC、堆、方法区及线程的信息(jstat、jstack)等,甚至代替 JConsole。

在 JDK 6 Update 7 以后,Visual VM 便作为 JDK 的一部分发布(VisualVM 在 JDK/bin 目录下)即:它完全免费。

此外,Visual VM也可以作为独立的软件安装。

官方地址:https://visualvm.github.io/index.html

使用:

  • 在jdk安装目录中找到jvisualvm.exe,然后双击执行即可
  • 打开DOS窗口,输入jvisualvm就可以打开该软件

插件的安装

软件安装

在jdk高版本中没有带jvisualvm,需要到官网安装

https://visualvm.github.io/download.html

插件安装

Visual VM的一大特点是支持插件扩展,并且插件安装非常方便。我们既可以通过离线下载插件文件*.nbm,然后在Plugin对话框的已下载页面下,添加已下载的插件。也可以在可用插件页面下,在线安装插件。**(这里建议安装上: VisualGC**) ·
插件地址:https://visualvm.github.io/pluginscenters.html

idea插件安装

  1. 安装插件
image-20210929111150710
  1. 重启配置

    image-20210929111307637

image-20210929111431926

连接方式

本地连接

监控本地Java进程的CPU、类、线程等

远程连接

1-确定远程服务器的ip地址

2-添加JMX(通过JMX技术具体监控远程服务器哪个Java进程)

3-修改bin/catalina.sh文件,连接远程的tomcat

4-在…/conf中添加jmxremote.access和jmxremote.password文件

5-将服务器地址改成公网ip地址

6-设置阿里云安全策略和防火墙策略

7-启动tomcat,查看tomcat启动日志和端口监听

8-JMX中输入端口号、用户名、密码登录

主要功能:

生成/读取堆内存/线程快照

image-20210929140550825 image-20210929140956618

image-20210929142933165

image-20210929143255518

查看 JVM 参数和系统属性

查看运行中的虚拟机进程

生成/读取线程快照

image-20210929143859912

image-20210929144107979

程序资源的实时监控

JMX 代理连接、远程环境监控、CPU 分析和内存分析

image-20210929144449125

其他功能

JMX代理连接
远程环境监控
CPU分析和内存分析

Eclipse MAT

基本概述

MAT(Memory Analyzer Tool)工具是一款功能强大的 Java 堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。

MAT 是基于 Eclipse 开发的,不仅可以单独使用,还可以作为插件的形式嵌入在 Eclipse 中使用。是一款免费的性能分析工具,使用起来非常方便。

ecplise最新版下载地址:https://ftp.jaist.ac.jp/pub/eclipse/technology/epp/downloads/release/2021-09/R/eclipse-java-2021-09-R-win32-x86_64.zip

==我之前的2018款的ecplice在插件商场中找不到这个插件,下载最新版后可以找到==

插件安装:

点击help->ecplise marketpace ,搜索mat

image-20210929162728837

重启后打开window - > open perspective,看到Memory Analysis证明安装成功。

image-20210929170610664

然后双击Memory Analysis,就跳转到mat分析界面

获取堆dump文件

dump文件内容

MAT 可以分析 heap dump 文件。在进行内存分析时,只要获得了反映当前设备内存映像的 hprof 文件,通过 MAT 打开就可以直观地看到当前的内存信息。一般说来,这些内存信息包含:

  • 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
  • 所有的类信息,包括 classloader、类名称、父类、静态变量等
  • GCRoot 到所有的这些对象的引用路径
  • 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)

说明

说明1:

MAT 不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如 Sun,HP,SAP 所采用的 HPROF 二进制堆存储文件,以及 IBM 的 PHD 堆存储文件等都能被很好的解析。

说明2:

最吸引人的还是能够快速为开发人员生成内存泄漏报表,方便定位问题和分析问题。虽然 MAT 有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从 MAT 展现给我们的信息当中通过经验和直觉来判断才能发现。

官方地址: https://www.eclipse.org/mat/downloads.php

获取dump文件

方法一:通过前一章介绍的 jmap工具生成,可以生成任意一个java进程的dump文件;

方法二:通过配置VM参数生成。

  • 选项”-XX:+HeapDumpOnOutOfMemoryError”或”-XX:+HeapDumpBeforeFullGc”
  • 选项”-XX:HeapDumpPath”所代表的含义就是当程序出现OutofMemory时,将会在相应的目录下生成一份dump文件。如果不指定选项“-XX:HeapDumpPath”则在当前目录下生成dump文件。

对比:考虑到生产环境中几乎不可能在线对其进行分析,大都是采用离线分析,因此使用jmap+MAT工具是最常见的组合。

方法三:使用VisualVM可以导出堆dump文件

方法四:

使用MAT既可以打开一个已有的堆快照,也可以通过MAT直接从活动Java程序中导出堆快照。该功能将借助jps列出当前正在运行的Java进程,以供选择并获取快照。

分析堆dump文件

size为堆空间的大小

image-20210929165743693

1
2
3
4
5
6
7
8
9
10
11
12
13
C:\Users\Think>jps
21744 Jps
22096
25952 OOMTest
33616 Eclipse
15540 Main
35572 Launcher
28668 RemoteMavenServer
7292 KotlinCompileDaemon

C:\Users\Think>jmap -dump:format=b,file=D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\myhporf-analyse\mat_log\a.hprof 25952
Dumping heap to D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter02\code02\myhporf-analyse\mat_log\a.hprof ...
Heap dump file created

image-20210929165432561

image-20210929165605249

histogram(直方图)

展示了各个类的实例数目以及这些实例的Shallow heap或者Retained heap的总和

image-20210929170852247

image-20210929171051121

thread overview

查看系统中的java线程

image-20210929171335637

查看局部变量表的信息

获得对象相互引用的关系

image-20210929172058916

with outgoing references

当前对象对象引用谁了

with incoming references

当前对象被谁引用了

浅堆和深堆

shallow heap(浅堆)

浅堆(Shallow Heap)是指一个对象所消耗的内存。在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节(32位JVM)。根据堆快照格式不同,对象的大小可能会向8字节进行对齐。

以String为例:2个int值共占8字节,对象引用占用4字节,对象头8字节,合计20字节,向8字节对齐,故占24字节。(jdk7中)

int hash32 0
int hash 0
ref value

这24字节为String对象的浅堆大小。它与String的value实际取值无关,无论字符串长度如何,浅堆大小始终是24字节。

对象头代表根据类创建的对象的对象头,还有对象的大小不是可能向8字节对齐,而是就向8字节对齐

retained heap(深堆)
保留集(Retained Set):

对象A的保留集指当对象A被垃圾回收后,可以被释放的所有的对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接或间接访问到的所有对象的集合。通俗地说,就是指仅被对象A所持有的对象的集合

也就是一个对象被回收后,能够释放的集合

深堆(Retained Heap):

深堆是指对象的保留集中所有的对象的浅堆大小之和。

注意:浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。

补充:对象的实际大小

另外一个常用的概念是对象的实际大小。这里,对象的实际大小定义为一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小。与深堆相比,似乎这个在日常开发中更为直观和被人接受。但实际上,这个概念和垃圾回收无关

下图显示了一个简单的对象引用关系图,对象A引用了C和D,对象B引用了C和E。那么对象A的浅堆大小只是A本身,不含C和D,而A的实际大小为A、C、D三者之和。而A的深堆大小为A与D之和,由于对象C还可以通过对象B访问到,因此不在对象A的深堆范围内。

image-20210929173833165
练习

GC Roots直接引用了A和B两个对象

image-20210929174243951

A的浅堆:A

A的深堆:A

B的浅堆:B

B的深堆:B+C

如果GC Roots不引用了A和B两个对象,则B的深堆:B+C+D

案例分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package top.lvxiaoyi.three.chapter03;


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

/**
* @author lvxiaoyi
* @date 2021/9/29 17:49
* -XX:+HeapDumpBeforeFullGC
* -XX:HeapDumpPath=D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter03\student-dump\student.hprof
*/
public class StudentTrance {
static List<WebPage> webPages = new ArrayList<>();

public static void createWebPages() {
for (int i = 0; i < 100; i++) {
WebPage wp = new WebPage();
wp.setUrl("http://www." + Integer.toString(i) + ".com");
wp.setContent(Integer.toString(i));
webPages.add(wp);
}
}

public static void main(String[] args) {
// 创建100个网页
createWebPages();
// 创建三个学生对象
Student st3 = new Student(3, "Xiaoyi lv");
Student st5 = new Student(5, "Taylor swift");
Student st7 = new Student(7, "Ariana Grande");

for (int i = 0; i < webPages.size(); i++) {
if (i % st3.getId() == 0) {
st3.visit(webPages.get(i));
}
if (i % st5.getId() == 0) {
st5.visit(webPages.get(i));
}
if (i % st7.getId() == 0) {
st7.visit(webPages.get(i));
}
}
webPages.clear();
System.gc();
}
}

class Student {
private int id;
private String name;
private List<WebPage> history = new ArrayList<>();

public Student(int id, String name) {
super();
this.id = id;
this.name = name;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<WebPage> getHistory() {
return history;
}

public void setHistory(List<WebPage> history) {
this.history = history;
}

public void visit(WebPage wp) {
if (wp != null) {
history.add(wp);
}
}
}


class WebPage {
private String url;
private String content;

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}
}

对象大小分析:

我发现这个地方存在疑问,搜索了两篇博客,感觉可信度较高:

https://cloud.tencent.com/developer/article/1658707

https://www.zhihu.com/question/63340239/answer/207968569

小结论:基本数据类型的大小基本没有疑问,主要是对象头的大小。

对象头的大小

jdk8版本是默认开启指针压缩的

1
2
3
4
5
6
7
8
9
10
11
12
13
# 还是原来的程序,只是在最后添加了一个休眠,避免程序停止
C:\Users\Think>jps
22096
33616 Eclipse
37744 KotlinCompileDaemon
35604 Launcher
35800 Jps
37192 StudentTrance
28668 RemoteMavenServer

C:\Users\Think>jinfo -flag UseCompressedOops 37192
-XX:+UseCompressedOops
# 所以确实jdk8,64位的版本压缩指针是默认开启的

开启压缩指针的情况下:对象头占12个字节,引用类型占据4个字节,而int也是占据四个字节,所以一共是4*3+12=24个字节,不需要对齐。(对齐的意思是,大小必须是8的倍数)

验证:只有int的对象大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package top.lvxiaoyi.three.chapter03;


/**
* @author lvxiaoyi
* @date 2021/9/29 21:01
*/
public class ClassHeaderTest {
public static void main(String[] args) {
studenHeader studenHeader = new studenHeader();

try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

class studenHeader {
private int id;
// private String name;
// private List<WebPage> history = new ArrayList<>();
}
1
2
3
4
5
6
7
8
9
10
11
12
C:\Users\Think>jps
17664 ClassHeaderTest
22096
33616 Eclipse
37744 KotlinCompileDaemon
33992 Launcher
28668 RemoteMavenServer
36700 Jps

C:\Users\Think>jmap -dump:format=b,file=D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter03\student-dump\studentHeader.hprof 17664
Dumping heap to D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter03\student-dump\studentHeader.hprof ...
Heap dump file created

image-20210929211528345

验证:空的对象大小

1
2
class studenHeader {
}

生成dump文件

1
2
3
4
5
6
7
8
9
10
11
12
C:\Users\Think>jps
22096
33616 Eclipse
37744 KotlinCompileDaemon
36484 Launcher
34536 ClassHeaderTest
28668 RemoteMavenServer
36796 Jps

C:\Users\Think>jmap -dump:format=b,file=D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter03\student-dump\studentHeader2.hprof 34536
Dumping heap to D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter03\student-dump\studentHeader2.hprof ...
Heap dump file created

image-20210929211818659

在只有int的对象中,对象的大小为4(int大小)+12(对象头大小)=16,刚好是8的倍数,不用对齐

在空对象中,对象的大小为12(对象头大小)+4(对齐的大小)=16

小结论:通过经验和博客,==在jdk8,64位,默认开启指针压缩的情况下,对象头的大小为12==

深堆分析

15个webpage

每个对应152个字节 15*152-16=2264字节

这个144我感觉确实应该加上去,这个是因为不同的url长度不一致导致的,有的是一位数,有的是两位数:

【普通对象头(12) + 数组长度(4)】 + 16个字符(32) = 48字节,符合8字节对齐

【普通对象头(12) + 数组长度(4)】 + 17个字符(34) = 50字节,不符合8字节对齐,对齐为56

image-20210930085251803

7个数能被 7整除,且能被3整除,以及能被7整除,且能被5整除的值数值有 :0,21,42,63,84,35,70

这个可以通过incomeing references查看,当前对象被几个对象引用

6*152+144=1056

2264-1056=1208

80个字节是什么呢(==下面为个人猜测,不一定正确==):

每个arrylist中都是用的elementData存储,占据4个字节

15个elementData的元素 * 4 = 60字节

60 + 12对象头的字节数 + 4字节(数组) + 4(补齐) = 80字节。

==验证:==

下面验证80个字节到底对不对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// vm参数
// -XX:+HeapDumpBeforeFullGC
// -XX:HeapDumpPath=D:\interview\jvm\src\main\java\top\lvxiaoyi\three\chapter03\student-dump\student6.hprof
public static void createWebPages() {
// 创建15个webpage对象,这个15个像的url地址不一样,且都是二位数
for (int i = 10; i < 25; i++) {
WebPage wp = new WebPage();
wp.setUrl("http://www." + i + ".com");
wp.setContent(Integer.toString(i));
webPages.add(wp);
}
}

public static void main(String[] args) {
// 创建100个网页
createWebPages();
// 创建三个学生对象
Student st3 = new Student(3, "Xiaoyi lv");
for (int i = 0; i < webPages.size(); i++) {
// 15个对象全部由一个人占用
st3.visit(webPages.get(i));
}
webPages.clear();
System.gc();
}
image-20210930085719134

深堆分析

15个webpage

每个对应152个字节 15*152=2280字节

2280+80=2360字节

80个字节是什么呢(==这个验证了,确实应该为80字节==)

每个arrylist中都是用的elementData存储,占据4个字节

1
2
// arrylist的jdk1.8源码,transient关键字是为了禁止这个字段序列化
transient Object[] elementData;

15个elementData的元素 * 4 = 60字节

60 + 12对象头的字节数 + 4字节(数组) + 4(补齐) = 80字节。

支配树(Dominator Tree)

支配树的概念源自图论。

MAT提供了一个称为支配树(Dominator Tree)的对象图。支配树体现了对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。支配树是基于对象间的引用图所建立的,它有以下基本性质:

  • 对象A的子树(所有被对象A支配的对象集合)表示对象A的保留集(retained set),即深堆。

  • 如果对象A支配对象B,那么对象A的直接支配者也支配对象B。

  • 支配树的边与对象引用图的边不直接对应。

如下图所示:左图表示对象引用图,右图表示左图所对应的支配树。对象A和B由根对象直接支配,由于在到对象C的路径中,可以经过A,也可以经过B,因此对象C的直接支配者也是根对象。对象F与对象D相互引用,因为到对象F的所有路径必然经过对象D,因此,对象D是对象F的直接支配者。而到对象D的所有路径中,必然经过对象c,即使是从对象F到对象D的引用,从根节点出发,也是经过对象c的,所以,对象D的直接支配者为对象C。

image-20210930093637404

同理,对象E支配对象G。到达对象H的可以通过对象D,也可以通过对象E,因此对象D和E都不能支配对象H,而经过对象C既可以到达D也可以到达E,因此对象C为对象H的直接支配者。

在MAT中,单击工具栏上的对象支配树按钮,可以打开对象支配树视图。

image-20210930095214205

案例:Tomcat堆溢出分析

说明

Tomcat是最常用的Java Servlet容器之一,同时也可以当做单独的web服务器使用。Tomcat本身使用Java实现,并运行于Java虚拟机之上。在大规模请求时,Tomcat有可能会因为无法承受压力而发生内存溢出错误。这里根据一个被压垮的Tomcat的堆快照文件,来分析Tomcat在崩溃时的内部情况。

分析过程

size为堆空间的大小

保留大小为16.4MB

图一:

image-20210930100607889

图二:

image-20210930101107460

图三:

sessions对象,占用约17MB空间,而保留空间为16.4M,所以我们怀疑是sessions这个对象出现了问题

image-20210930101213140

图四:

可以看到sessions对象为ConcurrentHashMap,其内部分为16个Segment。从深堆大小看,每个Segment都比较平均,大约为1MB,合计17MB。

image-20210930101545502

图五:

然后探究在sessions中的session的数量,看看到底是什么导致session对象这么大

image-20210930101716115

图六:

当前堆中含有9941个session,并且每一个session的深堆为1592字节,合计约15NB,达到当前堆大N的50%。

然后就是想看看这些session是不是在很短时间内创建,然后导致来不及回收,导致OOM异常

1
SELECT 0BJECTS s from org.apache.catalina.session.StandardSession s
image-20210930102334215

图七:

选择一个session查看详细信息

image-20210930102538142

图八:

image-20210930102628563

根据当前的session总数,可以计算每秒的平均压力为:9941/(1403124677648-1403324645728)*1000=311次/秒。

由此推断,在发生Tomcat堆溢出时,Tomcat在连续30秒的时间内,平均每秒接收了约311次不同客户端的请求,创建了合计9941个session。

补充:内存泄露

概述

什么是内存泄露

image-20210930110446754

可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏)。

如果对象还被使用但是不被需要,那么这就是内存泄露,但是如果还需要则不是内存泄露

  • 是否还被使用?是

  • 是否还被需要?否

内存泄露理解

严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏

但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致 OOM,也可以叫做宽泛意义上的“内存泄漏”。

image-20210930111143440

对象X引用对象Y,X的生命周期比Y的生命周期长;

那么当Y生命周期结束的时候,x依然引用着Y,这时候,垃圾回收期是不会回收对象Y的;

如果对象 X 还引用着生命周期比较短的 A、B、C,对象 A 又引用着对象 a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。

内存泄露与和内存溢出关系

内存泄露(memory leak)

申请了内存用完了不释放,比如一共有 1024M 的内存,分配了 512M 的内存一直不回收,那么可以用的内存只有 512M 了,仿佛泄露掉了一部分;通俗一点讲的话,内存泄漏就是【占着茅坑不拉 shi】

内存溢出(out of memory)

申请内存时,没有足够的内存可以使用;通俗一点儿讲,一个厕所就三个坑,有两个站着茅坑不走的(内存泄漏),剩下最后一个坑,厕所表示接待压力很大,这时候一下子来了两个人,坑位(内存)就不够了,内存泄漏变成内存溢出了。

可见,内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出。

泄漏的分类

  • 经常发生:发生内存泄露的代码会被多次执行,每次执行,泄露一块内存;
  • 偶然发生:在某些特定情况下才会发生
  • 一次性:发生内存泄露的方法只会执行一次;
  • 隐式泄漏:一直占着内存不释放,直到执行结束;严格的说这个不算内存泄漏,因为最终释放掉了,但是如果执行时间特别长,也可能会导致内存耗尽。

Java 中内存泄露的 8 种情况

静态集合类

静态集合类,如 HashMap、LinkedList 等等。如果这些容器为静态的,那么它们的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

1
2
3
4
5
6
7
public class MemoryLeak {
static List list = new ArrayList();
public void oomTests(){
Object obj=new Object();//局部变量
list.add(obj);
}
}

这一点在工作中我们需要注意,因为我们现在的项目都是多人开发,我们需要调用别人的接口,而别人又需要调用我们的接口。假如有着一种情况:你现在写了这个类(这个类比较底层,生命周期特别长),然后把这个一个全局变量List生命为为static,然后提供了对这个变量的一下业务逻辑操作,然后你的同事需要用到你的这个接口,他在用的时候对list一系列操作添加了一些列对象,然后出方法的时候忘记了,没有主动对list这些无用的对象销毁,那么这个时候就造成了内存泄露。

单例模式

单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

内部类持有外部类

内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。

各种连接,如数据库连接、网络连接和 IO 连接等

在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用 close 方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象,因为GC不能示释放非托管资源(各种连接),不会帮你主动关闭。

否则,如果在访问数据库的过程中,对 Connection、Statement 或 ResultSet 不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
try{
Connection conn =null;
Class.forName("com.mysql.jdbc.Driver");
conn =DriverManager.getConnection("url","","");
Statement stmt =conn.createStatement();
ResultSet rs =stmt.executeQuery("....");
} catch(Exception e){//异常日志
} finally {
// 1.关闭结果集 Statement
// 2.关闭声明的对象 ResultSet
// 3.关闭连接 Connection
}
}

变量不合理的作用域

变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为 null,很有可能导致内存泄漏的发生。

1
2
3
4
5
6
7
8
9
10
public class UsingRandom {
private String msg;
public void receiveMsg(){
// private String msg;
msg = readFromNet();//从网络中接受数据保存到msg中
saveDB(msg);//把msg保存到数据库中
//在使用完 msg 后,把 msg 设置为 null
// msg = null;
}
}

如上面这个伪代码,通过 readFromNet 方法把接受的消息保存在变量 msg 中,然后调用 saveDB 方法把 msg 的内容保存到数据库中,此时 msg 已经就没用了,由于 msg 的生命周期与对象的生命周期相同,此时 msg 还不能回收,因此造成了内存泄漏。

实际上这个 msg 变量可以放在 receiveMsg 方法内部,当方法使用完,那么 msg 的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。

改变哈希值

改变哈希值,当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。

否则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏。

这也是 String 为什么被设置成了不可变类型,我们可以放心地把 String 存入 HashSet,或者把 String 当做 HashMap 的 key 值;

当我们想把自己定义的类保存到散列表的时候,需要保证对象的 hashCode 不可变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package top.lvxiaoyi.three.chapter03.code041;

import java.util.HashSet;

public class ChangeHashCode1 {
public static void main(String[] args) {
HashSet<Point> hs = new HashSet<Point>();
Point cc = new Point();
//hashCode = 41
cc.setX(10);
hs.add(cc);
//hashCode = 51 此行为导致了内存的泄漏
cc.setX(20);
// 删除元素需要hashcode,但是hashcode又和x的值是相关的,所以西面remove失败
System.out.println("hs.remove = " + hs.remove(cc));//false
// 因为hashcode不一样,所以这里能够放进去
hs.add(cc);
// 造成了同一个对象,却能够存放进去hashSet两次
System.out.println("hs.size = " + hs.size());//size = 2
System.out.println(hs);
}

}

class Point {
int x;

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Point other = (Point) obj;
if (x != other.x) {
return false;
}
return true;
}

@Override
public String toString() {
return "Point{" +
"x=" + x +
'}';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package top.lvxiaoyi.three.chapter03.code041;

import java.util.HashSet;

public class ChangeHashCode {
public static void main(String[] args) {
HashSet set = new HashSet();
Person p1 = new Person(1001, "AA");
Person p2 = new Person(1002, "BB");

set.add(p1);
set.add(p2);

p1.name = "CC";//导致了内存的泄漏
set.remove(p1); //删除失败

System.out.println(set);

set.add(new Person(1001, "CC"));
System.out.println(set);

set.add(new Person(1001, "AA"));
System.out.println(set);

}
}

class Person {
int id;
String name;

public Person(int id, String name) {
this.id = id;
this.name = name;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Person)) {
return false;
}

Person person = (Person) o;

if (id != person.id) {
return false;
}
return name != null ? name.equals(person.name) : person.name == null;
}

@Override
public int hashCode() {
int result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}

@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

缓存泄露

内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。

对于这个问题,可以使用 WeakHashMap 代表缓存,此种 Map 的特点是,当除了自身有对 key 的引用外,此 key 没有其他引用那么此 map 会自动丢弃此值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package top.lvxiaoyi.three.chapter03.code041;

import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;

public class MapTest {
// 弱引用
static Map wMap = new WeakHashMap();
static Map map = new HashMap();

public static void main(String[] args) {
init();
testWeakHashMap();
testHashMap();
}

public static void init() {
String ref1 = new String("obejct1");
String ref2 = new String("obejct2");
String ref3 = new String("obejct3");
String ref4 = new String("obejct4");
wMap.put(ref1, "cacheObject1");
wMap.put(ref2, "cacheObject2");
map.put(ref3, "cacheObject3");
map.put(ref4, "cacheObject4");
System.out.println("String引用ref1,ref2,ref3,ref4 消失");

}

public static void testWeakHashMap() {
System.out.println("WeakHashMap GC之前");
for (Object o : wMap.entrySet()) {
System.out.println(o);
}
// gc的时候会主动回收弱引用中的内容
try {
System.gc();
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("WeakHashMap GC之后");
for (Object o : wMap.entrySet()) {
System.out.println(o);
}
}

public static void testHashMap() {
System.out.println("HashMap GC之前");
for (Object o : map.entrySet()) {
System.out.println(o);
}
try {
System.gc();
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("HashMap GC之后");
for (Object o : map.entrySet()) {
System.out.println(o);
}
}

}
image-20210930145355127

上面代码和图示主演演示 WeakHashMap 如何自动释放缓存对象,当 init 函数执行完成后,局部变量字符串引用 weakd1,weakd2,d1,d2 都会消失,此时只有静态 map 中保存中对字符串对象的引用,可以看到,调用 gc 之后,HashMap 的没有被回收,而 WeakHashMap 里面的缓存被回收了。

监听器和其他回调

内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的 API 中注册回调,却没有显示的取消,那么就会积聚。

需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为 WeakHashMap 中的键。

这个在大量的源码中都有这样的案例

内存泄露案例分析

案例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}

public void push(Object e) { //入栈
ensureCapacity();
elements[size++] = e;
}

public Object pop() { //出栈
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
// 应该用这种的,把引用置空
// public Object pop() {
// if (size == 0) {
// throw new EmptyStackException();
// }
// Object result = elements[--size];
// elements[size] = null;
// return result;
// }

private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}

上述程序并没有明显的错误,但是这段程序有一个内存泄漏,随着 GC 活动的增加,或者内存占用的不断增加,程序性能的降低就会表现出来,严重时可导致内存泄漏,但是这种失败情况相对较少。

代码的主要问题在 pop 函数,下面通过这张图示展现。假设这个栈一直增长,增长后如下图所示

image-20210505160114618

当进行大量的 pop 操作时,由于引用未进行置空,gc 是不会释放的,如下图所示

image-20210505160158618

从上图中看以看出,如果栈先增长,再收缩,那么从栈中弹出的对象将不会被当作垃圾回收,即使程序不再使用栈中的这些队象,他们也不会回收,因为栈中仍然保存这对象的引用,俗称过期引用,这个内存泄露很隐蔽。

将代码中的 pop()方法变成如下方法:

1
2
3
4
5
6
7
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}

一旦引用过期,清空这些引用,将引用置空。

image-20210505160423289

案例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package top.lvxiaoyi.three.chapter03.code041;

/**
* @author lvxiaoyi
* @date 2021/9/30 15:18
*/
public class TestActivity extends Activity {
private static final Object key = new Object();

@Override
protected void onCreate(Bundle savedInstancestate) {
super.onCreate(savedInstancestate);

setContentView(R.layout.activity_main);

new Thread() {//匿名线程
@Override
public void run() {
synchronized (key) {
try {
key.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}

图一:

image-20210930153051236

图二:

image-20210930153029077

解决:

  1. 使用线程时,一定要确保线程在周期性对象(如Activity)销毁时能正常结束,如能正常结束,但是Activity销毁后还需执行一段时间,也可能造成泄露,此时可采用weakReference方法来解决,另外在使用Handler的时候,如存在Delay操作,也可以采用weakReference;

  2. 使用Handler + HandlerThread时,记住在周期性对象销毁时调用looper.quit()方法;

补充:使用 OQL 语言查询对象信息

MAT 支持一种类似于 SQL 的查询语言 OQL(Object Query Language)。OQL 使用类 SQL 语法,可以在堆中进行对象的查找和筛选。

SELECT 子句

在 MAT 中,Select 子句的格式与 SQL 基本一致,用于指定要显示的列。Select 子句中可以使用“*”,查看结果对象的引用实例(相当于 outgoing references)。

1
2
3
4
select * from java.util.ArrayList

select v.elementData from java.util.ArrayList v

使用“OBJECTS”关键字,可以将返回结果集中的项以对象的形式显示。

1
2
3
select objects v.elementData from java.util.ArrayList v

SELECT objects s.value from java.lang.String s

在 Select 子句中,使用“AS RETAINED SET”关键字可以得到所得对象的保留集。

1
select as retained set * from top.lvxiaoyi.three.chapter03.Student

“DISTINCT”关键字用于在结果集中去除重复对象。

1
SELECT DISTINCT OBJECTS classof(s) FROM java.lang.String s

FROM 子句

From 子句用于指定查询范围,它可以指定类名、正则表达式或者对象地址。

1
SELECT * FROM java.lang.String s

使用正则表达式,限定搜索范围,输出所有top.lvxiaoyi.three.chapter03包下所有类的实例

1
SELECT * FROM "top\.lvxiaoyi\.three\.chapter03\..*"

使用类的地址进行搜索。使用类的地址的好处是可以区分被不同 ClassLoader 加载的同一种类型。

1
select * from 0x37a0b4d

WHERE 子句

Where 子句用于指定 OQL 的查询条件。OQL 查询将只返回满足 Where 子句指定条件的对象。Where 子句的格式与传统 SQL 极为相似。

返回长度大于 10 的 char 数组。

1
SELECT * FROM char[] s WHERE s.@length > 10

返回包含“java”子字符串的所有字符串,使用“LIKE”操作符,“LIKE”操作符的操作参数为正则表达式。

1
SELECT * FROM java.lang.String s WHERE toString(s) LIKE ".*java.*"

返回所有 value 域不为 null 的字符串,使用“=”操作符。

1
SELECT * FROM java.lang.String s where s.value!=null

返回数组长度大于 15,并且深堆大于 1000 字节的所有 Vector 对象。

1
SELECT * FROM java.util.Vector v WHERE v.elementData.@length>15 AND v.@retainedHeapSize>1000

内置对象与方法

OQL 中可以访问堆内对象的属性,也可以访问堆内代理对象的属性。访问堆内对象的属性时,格式如下,其中 alias 为对象名称:

[ <alias>. ] <field> . <field>. <field>

访问 java.io.File 对象的 path 属性,并进一步访问 path 的 value 属性:

1
SELECT toString(f.path.value) FROM java.io.File f

显示 String 对象的内容、objectid 和 objectAddress。

1
SELECT s.toString(),s.@objectId, s.@objectAddress FROM java.lang.String s

显示 java.util.Vector 内部数组的长度。

1
SELECT v.elementData.@length FROM java.util.Vector v

显示所有的 java.util.Vector 对象及其子类型

1
select * from INSTANCEOF java.util.Vector

补充:OOM

堆溢出

元空间溢出

OOM:GC overhead limit exceeded

98%的时间都在进行垃圾回收,但是回收效率特别低,就提前报了这个异常

线程溢出

JProfiler

概述

介绍

在运行 Java 的时候有时候想测试运行时占用内存情况,这时候就需要使用测试工具查看了。在 eclipse 里面有 Eclipse Memory Analyzer tool(MAT)插件可以测试,而在 IDEA 中也有这么一个插件,就是 JProfiler。

JProfiler 是由 ej-technologies 公司开发的一款 Java 应用性能诊断工具。功能强大,但是收费。

官网地址:https://www.ej-technologies.com/products/jprofiler/overview.html

特点

  • 使用方便、界面操作友好(简单且强大)
  • 对被分析的应用影响小(提供模板)
  • CPU,Thread,Memory 分析功能尤其强大
  • 支持对 jdbc,noSql,jsp,servlet,socket 等进行分析
  • 支持多种模式(离线,在线)的分析
  • 支持监控本地、远程的 JVM
  • 跨平台,拥有多种操作系统的安装版本

主要功能

  • 方法调用:对方法调用的分析可以帮助您了解应用程序正在做什么,并找到提高其性能的方法
  • 内存分配:通过分析堆上对象、引用链和垃圾收集能帮您修复内存泄露问题,优化内存使用
  • 线程和锁:JProfiler 提供多种针对线程和锁的分析视图助您发现多线程问题
  • 高级子系统:许多性能问题都发生在更高的语义级别上。例如,对于 JDBC 调用,您可能希望找出执行最慢的 SQL 语句。JProfiler 支持对这些子系统进行集成分析

下载与安装

下载

https://www.ej-technologies.com/download/jprofiler/files

JProfile中配置IDEA

image-20210930162308657

点击Integrate(合并)

image-20210930162337456

image-20210930163423373

idea集成JProfiler

image-20210930162730772 image-20210930162830076

OOMTest测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JProfiler> Protocol version 63
JProfiler> Java 8 detected.
JProfiler> 64-bit library
JProfiler> Listening on port: 34877.
JProfiler> Enabling native methods instrumentation.
JProfiler> Can retransform classes.
JProfiler> Can retransform any class.
JProfiler> Native library initialized
JProfiler> VM initialized
JProfiler> Waiting for a connection from the JProfiler GUI ...
JProfiler> Using sampling (5 ms)
JProfiler> Time measurement: elapsed time
JProfiler> CPU profiling enabled

image-20210930163828055

具体使用

image-20210930164430916

Open session:分析一个保存的文件

Quick Attach:分析正在运行的java进程(jps)

数据采集方式:

JProfier 数据采集方式分为两种:Sampling(样本采集)和 Instrumentation(重构模式)

Instrumentation:这是 JProfiler 全功能模式。在 class 加载之前,JProfier 把相关功能代码写入到需要分析的 class 的 bytecode 中,对正在运行的 jvm 有一定影响。

  • 优点:功能强大。在此设置中,调用堆栈信息是准确的。
  • 缺点:若要分析的 class 较多,则对应用的性能影响较大,CPU 开销可能很高(取决于 Filter 的控制)。因此使用此模式一般配合 Filter 使用,只对特定的类或包进行分析

Sampling(推荐):类似于样本统计,每隔一定时间(5ms)将每个线程栈中方法栈中的信息统计出来。

  • 优点:对 CPU 的开销非常低,对应用影响小(即使你不配置任何 Filter)
  • 缺点:一些数据/特性不能提供(例如:方法的调用次数、执行时间)

注:JProfiler 本身没有指出数据的采集类型,这里的采集类型是针对方法调用的采集类型。因为 JProfiler 的绝大多数核心功能都依赖方法调用采集的数据,所以可以直接认为是 JProfiler 的数据采集类型。

遥感监测 Telemetries

图示一:Overview

image-20210930170232606

图示二:Memory

image-20210930170253181

图示三:Recorded Objects

image-20210930170416808

图示四:Recorded Throughput

image-20210930170454509

图示五:GC Activity

image-20210930170552263

图示六:

image-20210930170621339

图示七:Threads

image-20210930170649546

图示八:CPU Load

image-20210930170720803

内存视图 Live Memory

Live memory 内存剖析:class/class instance 的相关信息。例如对象的个数,大小,对象创建的方法执行栈,对象创建的热点。

所有对象 All Objects

显示所有加载的类的列表和在堆上分配的实例数。只有 Java 1.5(JVMTI)才会显示此视图。

image-20210930171435035

分析:内存中的对象的情况

  • 频繁创建的java对象:死循环、循环次数过多
  • 存在大的对象:读取文件时,byte[]应该边读边写
记录对象 Record Objects

查看特定时间段对象的分配,并记录分配的调用堆栈。

判断内存泄露的时候才会使用的功能,内次GC后的剩余内存都在向上增长,说明gc不可回收的对象越来越多,发生了内存泄露

image-20210930203148388

可以选择存活的对象,垃圾回收过的对象和存活且被垃圾回收过的对象

image-20210930203431039

分配访问树 Allocation Call Tree

显示一棵请求树或者方法、类、包或对已选择类有带注释的分配信息的 J2EE 组件。

分配热点 Allocation Hot Spots

显示一个列表,包括方法、类、包或分配已选类的 J2EE 组件。你可以标注当前值并且显示差异值。对于每个热点都可以显示它的跟踪记录树。

类追踪器 Class Tracker

类跟踪视图可以包含任意数量的图表,显示选定的类和包的实例与时间。

堆遍历 heap walker

查看references

image-20210930210100718

查看对象graph引用

image-20210930210155941

离线分析heap快照

image-20210930205937579

cpu 视图 cpu views

JProfiler 提供不同的方法来记录访问树以优化性能和细节。线程或者线程组以及线程状况可以被所有的视图选择。所有的视图都可以聚集到方法、类、包或 J2EE 组件等不同层上。

  • 访问树 Call Tree:显示一个积累的自顶向下的树,树中包含所有在 JVM 中已记录的访问队列。JDBC,JMS 和 JNDI 服务请求都被注释在请求树中。请求树可以根据 Servlet 和 JSP 对 URL 的不同需要进行拆分。

    方法执行越长说明对cpu的占用越长

    image-20210930210910878

    image-20210930211146019

  • 热点 Hot Spots:显示消耗时间最多的方法的列表。对每个热点都能够显示回溯树。该热点可以按照方法请求,JDBC,JMS 和 JNDI 服务请求以及按照 URL 请求来进行计算。

  • 访问图 Call Graph:显示一个从已选方法、类、包或 J2EE 组件开始的访问队列的图。

  • 方法统计 Method Statistis:显示一段时间内记录的方法的调用时间细节。

新版本使用Outliner Detection(异常检测)

image-20210930211457206

线程视图 threads

JProfiler 通过对线程历史的监控判断其运行状态,并监控是否有线程阻塞产生,还能将一个线程所管理的方法以树状形式呈现。对线程剖析。

  • 线程历史 Thread History:显示一个与线程活动和线程状态在一起的活动时间表。
  • 线程监控 Thread Monitor:显示一个列表,包括所有的活动线程以及它们目前的活动状况。
  • 线程转储 Thread Dumps:显示所有线程的堆栈跟踪。

线程分析主要关心三个方面:

  • 1.web 容器的线程最大数。比如:Tomcat 的线程容量应该略大于最大并发数。
  • 2.线程阻塞
  • 3.线程死锁
image-20210930212238474

监控和锁 Monitors &Locks

所有线程持有锁的情况以及锁的信息。观察 JVM 的内部线程并查看状态:

  • 死锁探测图表 Current Locking Graph:显示 JVM 中的当前死锁图表。
  • 目前使用的监测器 Current Monitors:显示目前使用的监测器并且包括它们的关联线程。
  • 锁定历史图表 Locking History Graph:显示记录在 JVM 中的锁定历史。
  • 历史检测记录 Monitor History:显示重大的等待事件和阻塞事件的历史记录。
  • 监控器使用统计 Monitor Usage Statistics:显示分组监测,线程和监测类的统计监测数据

案例分析

案例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package top.lvxiaoyi.three.chapter03.code05;

import java.util.ArrayList;

/**
* @author lvxiaoyi
* @date 2021/9/30 21:25
*/
public class JProfilerTest {
public static void main(String[] args) {
while (true) {
ArrayList<Data> list = new ArrayList<>();
for (int i = 0; i < 500; i++) {
Data data = new Data();
list.add(data);
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class Data {
private int size = 10;
private byte[] buffer = new byte[1204 * 1024];
private String info = "hello, lvxiaoyi";
}

image-20210930213349340

案例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package top.lvxiaoyi.three.chapter03.code05;

import java.util.ArrayList;

/**
* @author lvxiaoyi
* @date 2021/9/30 21:35
*/
public class MemoryLeak {
public static void main(String[] args) {
while (true) {
ArrayList<Object> beanlist = new ArrayList<>();
for (int i = 0; i < 500; i++) {
// Bean data = new Bean();
Bean.list.add(new byte[1204 * 10]);
data.list.add(new byte[1204 * 10]);
beanlist.add(data);
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class Bean {
int size = 10;
String info = "hello,lvxiaoyi";
static ArrayList list = new ArrayList();
// ArrayList list = new ArrayList();
}

image-20210930215739784

image-20210930214843517

image-20210930214859453

修改后:

image-20210930215250926

image-20210930215350778

Arthas

概述

背景

前面,我们介绍了jdk自带的jvisualvm等免费工具,以及商业化工具JProfiler、jvisualvm界面。

这两款工具在业界知名度也比较高,他们的优点是可以图形界面上看到各维度的性能数据,使用者根据这些数据进行综合分析,然后判断哪里出现了性能问题。

但是这两款工具也有个缺点,上述工具都必须在服务端项目进程中配置相关的监控参数,然后工具通过远程连接到项目进程,获取相关的数据。这样就会带来一些不便,比如线上环境的网络是隔离的,本地的监控工具根本连不上线上环境。并且类似于 Jprofiler 这样的商业工具,是需要付费的。

那么有没有一款工具不需要远程连接,也不需要配置监控参数,同时也提供了丰富的性能监控数据呢?

阿里巴巴开源的性能分析神器 Arthas 应运而生。

Arthas概述

Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪 Java 代码;实时监控 JVM 状态。

Arthas 支持 JDK 6 +,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。

Arthas 可以帮助你解决:

  • 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  • 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  • 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  • 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  • 是否有一个全局视角来查看系统的运行状况?
  • 有什么办法可以监控到 JVM 的实时运行状态?
  • 怎么快速定位应用的热点,生成火焰图?

基于工具

greys-anatomy:Arthas代码基于Greys二次开发而来,非常感谢Greys之前所有的工作,以及Greys原作者对Arthas提出的意见和建议!

termd:Arthas的命令行实现基于termd开发,是一款优秀的命令行程序开发框架,感谢termd提供了优秀的框架。

crash:Arthas的文本渲染功能基于crash中的文本渲染功能开发,可以从这里看到源码,感谢crash在这方面所做的优秀工作。

cli:Arthas的命令行界面基于vert.x提供的cli库进行开发,感谢vert.x在这方面做的优秀工作。

compiler Arthas里的内存编绎器代码来源

Apache Commons Net Arthas里的Telnet client代码来源

JavaAgent:运行在main方法之前的拦截器,它内定的方法名叫 premain ,也就是说先执行premain方法然后再执行main方法

ASM:一个通用的Java字节码操作和分析框架。它可以用于修改现有的类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从它们构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但是主要关注性能。因为它被设计和实现得尽可能小和快,所以非常适合在动态系统中使用(当然也可以以静态方式使用,例如在编译器中)

官方地址:https://arthas.aliyun.com/zh-cn/

安装

https://arthas.aliyun.com/doc/install-detail.html

安装方式一

可以直接在linux上通过命令下载

可以在官方github上进行下载,如果速度较慢,可以尝试国内的码云下载。

安装方式二:

也可以在浏览器直接访问https://alibaba.github.io/arthas/arthas-boot.jar,等待下载成功后,上传到Linux服务器上。

在系统启动文件中添加参数-XX:+StartAttachListener

卸载

在 Linux/Unix/Mac平台

删除下面文件:

1
2
3
-m -rf ~/.arthas/

rm -rf ~/logs/arthas

windows平台直接删除user home下面的.arthas和logs/arthas目录

安装目录

arthas-agent:基于JavaAgent技术的代理

bin:一些启动脚本

arthas-boot: Java版本的一键安装启动脚本

arthas-client: telnet client代码

arthas-common:一些共用的工具类和枚举类

arthas-core:核心库,各种arthas命令的交互和实现

arthas-demo:示例代码

arthas-memorycompiler:内存编绎器代码,Fork from https://github.com/skalogs/SkaETL/tree/master/compiler

erarthas-packaging:maven打包相关的

arthas-site:arthas站点

arthas-spy:编织到目标类中的各个切面

static:静态资源

arthas-testcase:测试

启动

Arthas只是一个java程序,所以可以直接用java -jar运行。

执行成功后,arthas提供了一种命令行方式的交互方式,arthas会检测当前服务器上的Java进程,并将进程列表展示出来,用户输入对应的编号(1、2、3、4…)进行选择,然后回车。

比如:方式1:

1
java -jar arthas-boot.jar
  • 选择进程(输入[]内编号(不是PID)回车)

方式2:运行时选择Java进程 PID

1
java -jar arthas-boot.jar [PID]
案例一

这里没有在linux中安装图像化界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 这里编译文件的时候,要把文件中的包去掉
[root@VM-0-9-centos javatest]# java -Xms600m -Xmx600m -XX:SurvivorRatio=8 OOMTest.java
Error: Could not find or load main class OOMTest.java
[root@VM-0-9-centos javatest]# java OOMTest
^C[root@VM-0-9-centos javatest]# javac OOMTest.java
# 后台运行,要不然运行arthas-boot一直报错
[root@VM-0-9-centos javatest]# nohup java OOMTest &
[1] 19156
[root@VM-0-9-centos javatest]# nohup: ignoring input and appending output to ‘nohup.out’

[root@VM-0-9-centos javatest]# jps
19156 OOMTest
19405 Jps
[root@VM-0-9-centos javatest]# cd ../
[root@VM-0-9-centos arthas]# java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.5.3
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 19156 OOMTest
1
[INFO] arthas home: /root/.arthas/lib/3.5.4/arthas
[INFO] Try to attach process 19156
[INFO] Attach process 19156 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'


wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 3.5.4
main_class
pid 19156
time 2021-10-01 14:05:27

[arthas@19156]$

查看进程

jps

查看日志

1
2
3
4
5
6
[root@VM-0-9-centos arthas]# cat ~/logs/arthas/arthas.log
Arthas server agent start...
2021-10-01 11:28:06 [arthas-binding-thread] INFO c.t.arthas.core.util.ArthasBanner -Current arthas version: 3.5.4, recommend latest version: 3.5.4
2021-10-01 11:28:06 [arthas-binding-thread] INFO c.t.arthas.core.util.ArthasBanner -Current arthas version: 3.5.4, recommend latest version: 3.5.4
2021-10-01 11:28:06 [arthas-binding-thread] INFO c.t.a.core.server.ArthasBootstrap -try to bind telnet server, host: 127.0.0.1, port: 3658.
...

查看帮助

1
java -jar arthas-boot.jar -h

web console

除了在命令行查看外,Arthas目前还支持 web Console。在成功启动连接进程之后就已经自动启动,可以直接访问http://127.0.0.1:8563/访问,页面上的操作模式和控制台完全一样。

linux使用curl指令验证

1
2
3
4
5
6
7
8
9
[root@VM-0-9-centos ~]# curl 127.0.0.1:8563
<!doctype html>
<html lang="en">

<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
...

退出

最后一行[arthas@7457]$,说明打开进入了监控客户端,在这里就可以执行相关命令进行查看了。

  • 使用quit`exit`:退出当前客户端
  • 使用stop`shutdown`:关闭arthas服务端,并退出所有客户端。

相关诊断指令

基础指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
quit/exit 退出当前 Arthas客户端,其他 Arthas喜户端不受影响
stop/shutdown 关闭 Arthas服务端,所有 Arthas客户端全部退出
help 查看命令帮助信息
cat 打印文件内容,和linux里的cat命令类似
echo 打印参数,和linux里的echo命令类似
grep 匹配查找,和linux里的gep命令类似
tee 复制标隹输入到标准输出和指定的文件,和linux里的tee命令类似
pwd 返回当前的工作目录,和linux命令类似
cs 清空当前屏幕区域
session 查看当前会话的信息
reset 重置增强类,将被 Arthas增强过的类全部还原, Arthas服务端关闭时会重置所有增强过的类
version 输出当前目标Java进程所加载的 Arthas版本号
history 打印命令历史
keymap Arthas快捷键列表及自定义快捷键
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@VM-0-9-centos javatest]# nohup java -Xms600m -Xmx600m -XX:SurvivorRatio=8 OOMTest  &
[1] 27496
[root@VM-0-9-centos javatest]# nohup: ignoring input and appending output to ‘nohup.out’

[root@VM-0-9-centos javatest]# jps
27496 OOMTest
27518 Jps
[root@VM-0-9-centos javatest]# jinfo -flags 27496
Attaching to process ID 27496, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.144-b01
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=629145600 -XX:MaxHeapSize=629145600 -XX:MaxNewSize=209715200 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=209715200 -XX:OldSize=419430400 -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
Command line: -Xms600m -Xmx600m -XX:SurvivorRatio=8
[root@VM-0-9-centos arthas]# javar -jar arthas-boot.jar

1

jvm 相关

1
2
3
4
5
6
7
8
9
10
11
12
dashboard 当前系统的实时数据面板
thread 查看当前JVM的线程堆栈信息
jvm 查看当前JVM的信息
sysprop 查看和修改JVM的系统属性
sysem 查看JVM的环境变量
vmoption 查看和修改JVM里诊断相关的option
perfcounter 查看当前JVM的 Perf Counter信息
logger 查看和修改logger
getstatic 查看类的静态属性
ognl 执行ognl表达式
mbean 查看 Mbean的信息
heapdump dump java heap,类似jmap命令的 heap dump功能

class/classloader 相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sc 查看JVM已加载的类信息
-d 输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的Classloader等详细信息。如果一个类被多个Classloader所加载,则会出现多次
-E 开启正则表达式匹配,默认为通配符匹配
-f 输出当前类的成员变量信息(需要配合参数-d一起使用)
-X 指定输出静态变量时属性的遍历深度,默认为0,即直接使用toString输出
sm 查看已加载类的方法信息
-d 展示每个方法的详细信息
-E 开启正则表达式匹配,默认为通配符匹配
jad 反编译指定已加载类的源码
mc 内存编译器,内存编译.java文件为.class文件
retransform 加载外部的.class文件, retransform到JVM里
redefine 加载外部的.class文件,redefine到JVM里
dump dump已加载类的byte code到特定目录
classloader 查看classloader的继承树,urts,类加载信息,使用classloader去getResource
-t 查看classloader的继承树
-l 按类加载实例查看统计信息
-c 用classloader对应的hashcode来查看对应的 Jar urls
1
[arthas@31533]$ sc -d Picture

monitor/watch/trace 相关

1
2
3
4
5
6
7
8
9
10
11
12
monitor 方法执行监控,调用次数、执行时间、失败率
-c 统计周期,默认值为120秒
watch 方法执行观测,能观察到的范围为:返回值、抛出异常、入参,通过编写groovy表达式进行对应变量的查看
-b 在方法调用之前观察(默认关闭)
-e 在方法异常之后观察(默认关闭)
-s 在方法返回之后观察(默认关闭)
-f 在方法结束之后(正常返回和异常返回)观察(默认开启)
-x 指定输岀结果的属性遍历深度,默认为0
trace 方法内部调用路径,并输出方法路径上的每个节点上耗时
-n 执行次数限制
stack 输出当前方法被调用的调用路径
tt 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
1
2
3
4
stack Picture <init>

watch Picture <init> "{params,returnObj}" -x 2

其他

1
2
3
4
5
6
7
8
9
jobs 列出所有job
kill 强制终止任务
fg 将暂停的任务拉到前台执行
bg 将暂停的任务放到后台执行
grep 搜索满足条件的结果
plaintext 将命令的结果去除ANSI颜色
wc 按行统计输出结果
options 查看或设置Arthas全局开关
profiler 使用async-profiler对应用采样,生成火焰图
1
2
3
4
5
[arthas@9226]$ profiler start
Started [cpu] profiling
[arthas@9226]$ profiler getSamples
8

Java Misssion Control

历史

在 Oracle 收购 Sun 之前,Oracle 的 JRockit 虚拟机提供了一款叫做 JRockit Mission Control 的虚拟机诊断工具。

在 Oracle 收购 sun 之后,Oracle 公司同时拥有了 Hotspot 和 JRockit 两款虚拟机。根据 Oracle 对于 Java 的战略,在今后的发展中,会将 JRokit 的优秀特性移植到 Hotspot 上。其中一个重要的改进就是在 Sun 的 JDK 中加入了 JRockit 的支持。

在 Oracle JDK 7u40 之后,Mission Control 这款工具己经绑定在 Oracle JDK 中发布。

自 Java11 开始,本节介绍的 JFR 己经开源。但在之前的 Java 版本,JFR 属于 Commercial Feature 通过 Java 虚拟机参数-XX:+UnlockCommercialFeatures 开启。

官方地址:https://github.com/JDKMissionControl/jmc

image-20210505184358041

启动

jdk安装位置/bin/jmc.exe

概述

Java Mission Control(简称 JMC) , Java 官方提供的性能强劲的工具,是一个用于对 Java 应用程序进行管理、监视、概要分析和故障排除的工具套件。它包含一个 GUI 客户端以及众多用来收集 Java 虚拟机性能数据的插件如 JMX Console(能够访问用来存放虚拟机齐个于系统运行数据的 MXBeans)以及虚拟机内置的高效 profiling 工具 Java Flight Recorder(JFR)。

JMC 的另一个优点就是:采用取样,而不是传统的代码植入技术,对应用性能的影响非常非常小,完全可以开着 JMC 来做压测(唯一影响可能是 full gc 多了)。

功能

如果是远程服务器,使用前要开JMX。

1
2
3
4
5
-Dcom.sun.management.jmxremote.port=${YOUR PORT}
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-Djava.rmi.server.hostname=${YOUR HOST/IP}

文件->连接->创建新连接,填入上面JMX参数的 host 和 port

Java Flight Recorder

事件类型

Java Flight Recorder 是 JMC 的其中一个组件,能够以极低的性能开销收集 Java 虚拟机的性能数据。与其他工具相比,JFR 的性能开销很小,在默认配置下平均低于 1%。JFR 能够直接访问虚拟机内的数据并且不会影响虚拟机的优化。因此它非常适用于生产环境下满负荷运行的 Java 程序。

Java Flight Recorder 和 JDK Mission Control 共同创建了一个完整的工具链。JDK Mission Control 可对 Java Flight Recorder 连续收集低水平和详细的运行时信息进行高效、详细的分析。

当启用时 JFR 将记录运行过程中发生的一系列事件。其中包括 Java 层面的事件如线程事件、锁事件,以及 Java 虚拟机内部的事件,如新建对象,垃圾回收和即时编译事件。按照发生时机以及持续时间来划分,JFR 的事件共有四种类型,它们分别为以下四种:

  • 瞬时事件(Instant Event) ,用户关心的是它们发生与否,例如异常、线程启动事件。
  • 持续事件(Duration Event) ,用户关心的是它们的持续时间,例如垃圾回收事件。
  • 计时事件(Timed Event) ,是时长超出指定阈值的持续事件。
  • 取样事件(Sample Event),是周期性取样的事件。

取样事件的其中一个常见例子便是方法抽样(Method Sampling),即每隔一段时问统计各个线程的栈轨迹。如果在这些抽样取得的栈轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法

启动方式

方式1:使用-XX:StartFlightRecording=参数

第一种是在运行目标 Java程序时添加-XX:StartFlightRecording=参数。

比如:下面命令中,JFR将会在Java 虚拟机启动5s 后(对应delay=5s)收集数据,持续20s (对应duration=20s)。当收集完毕后,JFR会将收集得到的数据保存至指定的文件中(对应filename=myrecording.jfr)

java-XX:StartFlightRecording=delay=5s,duration=20s,filename=myrecording.jfr,settings=profile MyApp
由于JFR将持续收集数据,如果不加以限制,那么JFR可能会填满硬盘的所有空间。因此,我们有必要对这种模式下所收集的数据进行限制。
比如:
java -xX:StartFlightRecording=maxage=10m,maxsize=100m, name=SomeLabel MyApp

方式2:使用jcmd的JFR.*子命令

通过jcmd来让JFR开始收集数据、停止收集数据,或者保存所收集的数据,对应的子命令分别为FR.start,JFR.stop,以及FR.dump。
jcmd <PID〉JFR.start settings=profile maxage=10m maxsize=150m name=SomeLabel
上述命令运行过后,目标进程中的JFR已经开始收集数据。此时,我们可以通过下述命令来导出已,经收集到的数据:
jcmd JFR.dump name=SomeLabel filename=myrecording.jfr
最后,我们可以通过下述命令关闭目标进程中的JFR:
jcmd JFR.stop name=SomeLabel

方式3:JMC的JFR插件

取样分析

要采用取样,必须先添加参数:
-XX:+UnlockCommercialFeatures· -XX:+FlightRecorder
否则:

取样时间默认1分钟,可自行按需调整,事件设置选为 profiling,然后可以设置取样 profile哪些信息,比如:
·加上对象数量的统计:Java Virtual Machine -> 6c -> Detailed -> 0bject
Count/0bject Count after Gc
·方法调用采样的间隔从 10ms改为1ms(但不能低于1ms,否则会影响性能了):JavaVirtual Machine -> Profizing ->Method Profiling Sample/Method SamplingInformation
Socket 与 File采样,10ms太久,但即使改为 1ms 也未必能抓住什么,可以干脆取消掉:Java Application->File Read/Filewrite/Socket Read/Socket write

然后就开始 Profile,到时间后 Profile结束,会自动把记录下载回来,在JNC 中展示。

从展示信息中,我们大致可以读到内存和CPU信息、代码、线程和Io等比较重要的信息展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* -Xms600m -Xmx600m -XX:SurvivorRatio=8
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while(true){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(100 * 50)));
}
}
}

class Picture{
private byte[] pixels;

public Picture(int length) {
this.pixels = new byte[length];
}

public byte[] getPixels() {
return pixels;
}

public void setPixels(byte[] pixels) {
this.pixels = pixels;
}
}

其他工具

Flame Graphs(火焰图)

在追求极致性能的场景下,了解你的程序运行过程中 cpu 在干什么很重要,火焰图就是一种非常直观的展示 CPU 在程序整个生命周期过程中时间分配的工具。火焰图对于现代的程序员不应该陌生,这个工具可以非常直观的显示出调用找中的 CPU 消耗瓶颈

网上的关于 Java 火焰图的讲解大部分来自于 Brenden Gregg 的博客 http://new.brendangregg.com/flamegraphs.html

image-20210505190823214

火焰图,简单通过 x 轴横条宽度来度量时间指标,y 轴代表线程栈的层次。

Tprofiler

案例: 使用 JDK 自身提供的工具进行 JVM 调优可以将下 TPS 由 2.5 提升到 20(提升了 7 倍),并准确 定位系统瓶颈。

系统瓶颈有:应用里释态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题。

那么,如何在海量业务代码里边准确定位这些性能代码?这里使用阿里开源工具 Tprofiler 来定位 这些性能代码,成功解决掉了 GC 过于频繁的性能瓶预,并最终在上次优化的基础上将 TPS 再提升了 4 倍,即提升到 100。

  • Tprofiler 配置部署、远程操作、 日志阅谈都不太复杂,操作还是很简单的。但是其却是能够 起到一针见血、立竿见影的效果,帮我们解决了 GC 过于频繁的性能瓶预。
  • Tprofiler 最重要的特性就是能够统汁出你指定时间段内 JVM 的 top method 这些 top method 极有可能就是造成你 JVM 性能瓶颈的元凶。这是其他大多数 JVM 调优工具所不具备的,包括 JRockit Mission Control。JRokit 首席开发者 Marcus Hirt 在其私人博客《 Lom Overhead Method Profiling cith Java Mission Control》下的评论中曾明确指出 JRMC 井不支持 TOP 方法的统计。

官方地址:http://github.com/alibaba/Tprofiler

Btrace

常见的动态追踪工具有 BTrace、HouseHD(该项目己经停止开发)、Greys-Anatomy(国人开发 个人开发者)、Byteman(JBoss 出品),注意 Java 运行时追踪工具井不限干这几种,但是这几个是相对比较常用的。

BTrace 是 SUN Kenai 云计算开发平台下的一个开源项目,旨在为 java 提供安全可靠的动态跟踪分析工具。先看一卜日 Trace 的官方定义:

image-20210505192042974

大概意思是一个 Java 平台的安全的动态追踪工具,可以用来动态地追踪一个运行的 Java 程序。BTrace 动态调整目标应用程序的类以注入跟踪代码(“字节码跟踪“)。

YourKit

JProbe

Spring Insight

JVM 运行时参数

JVM 参数选项

官网地址:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

类型一:标准参数选项

特点:比较稳定,后续版本基本不会变化

以-开头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
> 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>
使用指定的图像显示启动屏幕
有关详细信息, 请参阅 http://www.oracle.com/technetwork/java/javase/documentation/index.html。

Server 模式和 Client 模式

Hotspot JVM 有两种模式,分别是 server 和 client,分别通过-server 和-client 模式设置

  • 32 位系统上,默认使用 Client 类型的 JVM。要想使用 Server 模式,机器配置至少有 2 个以上的 CPU 和 2G 以上的物理内存。client 模式适用于对内存要求较小的桌面应用程序,默认使用 Serial 串行垃圾收集器

  • 64 位系统上,只支持 server 模式的 JVM,适用于需要大内存的应用程序,默认使用并行垃圾收集器

    C1模式和C2模式,C1模式比较简单运行时间少,C2模式比较复杂运行时间长,但是C2对字节码的优化比较好

官网地址:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/server-class.html

Architecture OS Default client VM if server-class, server VM; otherwise, client VM Default server VM
SPARC 32-bit Solaris X
i586 Solaris X
i586 Linux X
i586 Microsoft Windows X
SPARC 64-bit Solaris X
AMD64 Solaris X
AMD64 Linux X
AMD64 Microsoft Windows X

如何知道系统默认使用的是那种模式呢?

通过 java -version 命令:可以看到 Server VM 字样,代表当前系统使用是 Server 模式

1
2
3
4
C:\Users\Think>java -version
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)

类型二:-X 参数选项

特点:

非标准化参数

功能还是比较稳定的。但官方说后续版本可能会变更

以-X开头

选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
C:\Users\Think>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 选项是非标准选项, 如有更改, 恕不另行通知。

如何知道 JVM 默认使用的是混合模式呢?

同样地,通过 java -version 命令:可以看到 mixed mode 字样,代表当前系统使用的是混合模式

JVM的JIT编译模式相关的选项

1
2
3
4
5
-Xint			只使用解释器:所有字节码都被解释执行,这个模式的速度是很慢的

-Xcomp 只使用编译器:所有字节码第一次使用就被编译成本地代码,然后在执行

-Xmixed 混合模式:这是默认模式,刚开始的时候使用解释器慢慢解释执行,后来让JIT即时编译器根据程序运行的情况,有选择地将某些热点代码提前编译并缓存在本地,在执行的时候效率就非常高了

特别的

-Xmx -Xms -Xss属于XX参数?

1
2
3
4
5
-Xms<size>       设置初始Java堆大小,等价于-XX:InitialHeapSize

-Xmx<size> 设置最大Java堆大小,等价于-XX:MaxHeapSize

-Xss<size> 设置Java线程堆栈大小,等价于-XX:ThreadStackSize

类型三:-XX 参数选项

特点

非标准化参数

使用的最多的参数类型

这类选项属于实验性,不稳定

以-XX开头

作用

用于开发和调试JVM

分类

Boolean 类型格式
1
2
-XX:+<option>  启用option属性
-XX:-<option> 禁用option属性
1
2
3
-XX:+UseParallelGC           选择垃圾收集器为并行收集器
-XX:+UseG1GC 表示启用G1收集器
-XX:+UseAdaptiveSizePolicy 自动选择年轻代区大小和相应的Survivor区比例

说明:因为有的指令默认是开启的,所以可以使用-关闭

非 Boolean 类型格式(key value)
1
-XX:<option>=<number>  设置option数值,可以带单位如k/K/m/M/g/G

number表示数值,number可以带上单位,比如: ‘m’、‘’M’表示兆,‘k ’、’K’表示Kb,’g’、’G’表示g(例如32k跟32768是一样的效果)
例如:
-XX:NewSize=1024m表示设置新生代初始大小为1024兆

-XX:MaxGCPauseMillis=500表示设置Gc停顿时间:5日0毫秒

-XX:GCTimeRatio=19表示设置吞吐量

-XX:NewRatio=2 表示新生代与老年代的比例

1
-XX:<option>=<string>  设置option字符值

-XX:HeapDumpPath=/usr/local/heapdump.hprof 用来指定heap转存文件的存储路径。

特别的

1
-XX:+PrintFlagsFinal

输出所有参数的名称和默认值

默认不包括Diagnostic和Experimental的参数

可以配合-XX:+UnlockDiagnosticVMOptions和-XX:UnlockExperimentalVMOptions使用

全部

==官网地址:==

https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

添加 JVM 参数选项

idea配置:

image-20211002100529728

运行 jar 包

1
java -Xms100m -Xmx100m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -jar demo.jar

Tomcat 运行 war 包

Linux系统下可以在tomcat/bin/catalina.sh中添加类似如下配置:JAVA_OPTS=”-Xms512M -Xmx1024M”

Windows系统下载catalina.bat中添加类似如下配置:
set “JAVA_OPTS=-Xms512M -Xmx1024M”

1
2
3
4
# linux下catalina.sh添加
JAVA_OPTS="-Xms512M -Xmx1024M"
# windows下catalina.bat添加
set "JAVA_OPTS=-Xms512M -Xmx1024M"

程序运行中

1
2
3
4
# 设置Boolean类型参数
jinfo -flag [+|-]<name> <pid>
# 设置非Boolean类型参数
jinfo -flag <name>=<value> <pid>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
C:\Users\Think>jps
27968 Jps
33616 Eclipse
13780 OOMTest
6628 KotlinCompileDaemon
31608 Launcher
39784

C:\Users\Think>jinfo -flag InitialHeapSize 13780
-XX:InitialHeapSize=629145600

C:\Users\Think>jinfo -flag MaxHeapSize 13780
-XX:MaxHeapSize=629145600

C:\Users\Think>jinfo -flag MaxHeapSize=14780 13780
Exception in thread "main" com.sun.tools.attach.AttachOperationFailedException: flag 'MaxHeapSize' cannot be changed

at sun.tools.attach.WindowsVirtualMachine.execute(WindowsVirtualMachine.java:117)
at sun.tools.attach.HotSpotVirtualMachine.executeCommand(HotSpotVirtualMachine.java:261)
at sun.tools.attach.HotSpotVirtualMachine.setFlag(HotSpotVirtualMachine.java:234)
at sun.tools.jinfo.JInfo.flag(JInfo.java:134)
at sun.tools.jinfo.JInfo.main(JInfo.java:81)


C:\Users\Think>jinfo -flag HeapDumpAfterFullGC 13780
-XX:-HeapDumpAfterFullGC

C:\Users\Think>jinfo -flag +HeapDumpAfterFullGC 13780

C:\Users\Think>jinfo -flag HeapDumpAfterFullGC 13780
-XX:+HeapDumpAfterFullGC

常用的 JVM 参数选项

打印设置的 XX 选项及值

1
2
3
4
-XX:+PrintCommandLineFlags 程序运行时JVM默认设置或用户手动设置的XX选项
-XX:+PrintFlagsInitial 打印所有XX选项的默认值
-XX:+PrintFlagsFinal 打印所有XX选项的实际值
-XX:+PrintVMOptions 打印JVM的参数

堆、栈、方法区等内存大小设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 栈
-Xss128k <==> -XX:ThreadStackSize=128k 设置线程栈的大小为128K

# 堆
-Xms2048m <==> -XX:InitialHeapSize=2048m 设置JVM初始堆内存为2048M,默认为内存的1/64
-Xmx2048m <==> -XX:MaxHeapSize=2048m 设置JVM最大堆内存为2048M,默认为内存的1/4
-Xmn2g <==> -XX:NewSize=2g -XX:MaxNewSize=2g 设置年轻代大小为2G
-XX:SurvivorRatio=8 设置Eden区与Survivor区的比值,默认为8
-XX:NewRatio=2 设置老年代与年轻代的比例,默认为2
-XX:+UseAdaptiveSizePolicy 设置大小比例自适应,默认开启,默认自适应(6:1:1)后SurvivorRatio就会失效
-XX:PretenureSizeThreadshold=1024 设置让大于此阈值的对象直接分配在老年代,只对Serial、ParNew收集器有效
-XX:MaxTenuringThreshold=15 设置新生代晋升老年代的年龄限制,默认为15
-XX:TargetSurvivorRatio 设置MinorGC结束后Survivor区占用空间的期望比例

# 方法区
-XX:MetaspaceSize / -XX:PermSize=256m 设置元空间/永久代初始值为256M
-XX:MaxMetaspaceSize / -XX:MaxPermSize=256m 设置元空间/永久代最大值为256M
-XX:+UseCompressedOops 使用压缩对象
-XX:+UseCompressedClassPointers 使用压缩类指针
-XX:CompressedClassSpaceSize 设置Klass Metaspace的大小,默认1G

# 直接内存
-XX:MaxDirectMemorySize 指定DirectMemory容量,默认等于Java堆最大值

OutOfMemory 相关的选项

-XX:+HeapDumpOnOutMemoryError(在出现OOM的时候生成dump文件)和-XX:+HeapDumpBeforeFullGC(在出现Full GC的时候生成dump文件)只能设置1个,如果不没置-XX:HeapDumpPath=<path>,那么将会在当前目录下生成dump文件,如果设置的话,将会在指定位置生成dump文件

1
2
3
4
5
-XX:+HeapDumpOnOutMemoryError 	内存出现OOM时生成Heap转储文件,两者互斥
-XX:+HeapDumpBeforeFullGC 出现FullGC时生成Heap转储文件,两者互斥

-XX:HeapDumpPath=<path> 指定heap转储文件的存储路径,默认当前目录
-XX:OnOutOfMemoryError=<path> 指定可行性程序或脚本的路径,当发生OOM时执行脚本

对OnoutOfMemoryError的运维处理:

以部署在linux系统/opt/Server目录下的Server.jar为例

  1. 在run.sh启动脚本中添加jvm参数:
1
-XX:OnOutOfMemoryError=/opt/Server/restart.sh
  1. restart.sh脚本
  2. linux环境:
1
2
3
4
5
#!/bin/bash
pid=$(ps -ef|grep Server.jar|awk '{if($8=="java") {print $2}}')
kill -9 $pid
cd /opt/Server/
sh run.sh

windows环境:

1
2
3
4
echo off
wmic process where Name='java.exe' delete
cd D:\Server
start run.bat

垃圾收集器相关选项

首先需了解垃圾收集器之间的搭配使用关系

  • 红色虚线表示在 jdk8 时被 Deprecate,jdk9 时被删除
  • 绿色虚线表示在 jdk14 时被 Deprecate
  • 绿色虚框表示在 jdk9 时被 Deprecate,jdk14 时被删除

image-20210506182458663

查看默认垃圾回收器

1
2
3
-XX:+PrintCommandLineFlags 			查看命令行相关参数(包含使用的垃圾收集器)

jinfo -flag 相关垃圾回收器参数 进程ID 使用命令行指令

以上两种方式都可以查看默认使用的垃圾回收器,第一种方式更加准备,但是需要程序的支持;第二种方式需要去尝试,如果使用了,返回的值中有+号,否则就是-号

Serial回收器

SeriaI收集器作为HotSpot中client模式下的默认新生代垃圾收集器。Serial old是运行在client模式下默认的老年代的垃圾回收器。

1
2
# Serial回收器
-XX:+UseSerialGC 年轻代使用Serial GC, 老年代使用Serial Old GC

指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial old GC。可以获得最高的单线程收集效率。

ParNew回收器

1
-XX:+UseParNewGC

手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。

1
-XX:ParallelGCThreads=N

设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。

  • 在默认情况下,当CPU 数量小于8个,ParallelGCThreads 的值等于CPU 数量。
  • 当CPU数量大于8个,ParallelGCThreads 的值等于3+[5*CPu_Count]/8]。

在jdk14后,ParNew没有组合关系,相当于被废弃了,但是jdk14之前还能继续使用

Parallel回收器

==吞吐量优先,主打服务器端==

1
-XX:+UseParallelGC

年轻代使用 Parallel Scavenge GC,互相激活

1
-XX:+UseParallelOldGC

老年代使用 Parallel Old GC,互相激活

1
-XX:ParallelGCThreads

设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
* 在默认情况下,当CPU 数量小于8个,ParallelGCThreads 的值等于CPU 数量。
* 当CPU数量大于8个,ParallelGCThreads 的值等于3+[5*CPu_Count]/8]。

1
-XX:MaxGCPauseMillis

设置垃圾收集器最大停顿时间(即STW的时间),单位是毫秒。

  • 为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数。
  • 对于用户来讲,停顿时间越短体验越好;但是服务器端注重高并发,整体的吞吐量。
  • 所以服务器端适合Parallel,进行控制。
  • ==该参数使用需谨慎==
1
-XX:GCTimeRatio

垃圾收集时间占总时间的比例(1 / (N+1)),用于衡量吞吐量的大小
* 取值范围(0,100),默认值99,也就是垃圾回收时间不超过1%。
* 与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。

1
-XX:+UseAdaptiveSizePolicy

设置Parallel Scavenge收集器具有自适应调节策略。

* 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
* 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。

CMS回收器

年轻代使用CMS GC。

1
-XX:+UseConcMarkSweepGC

手动指定使用CMS收集器执行内存回收任务

​ * 开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区)+ CMS(Old区)+ Serial Old的组合

1
-XX:CMSInitiatingOccupanyFraction  

设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。

  • JDK5及以前版本的默认值为68,DK6及以上版本默认值为92%。
  • 如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低FullGC的执行次数。
1
-XX:+UseCMSInitiatingOccupancyOnly

是否动态可调,使CMS一直按CMSInitiatingOccupancyFraction设定的值启动

1
-XX:+UseCMSCompactAtFullCollection

用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。

1
-XX:CMSFullGCsBeforeCompaction

设置在执行多少次Full GC后对内存空间进行压缩整理。

1
-XX:ParallelCMSThreads

设置CMS的线程数量。

* CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数。
* 当CPU 资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。
1
-XX:ConcGCThreads

设置并发垃圾收集的线程数,默认该值是基于ParallelGCThreads计算出来的

1
-XX:+CMSScavengeBeforeRemark

强制hotspot在cms remark阶段之前做一次minor gc,用于提高remark阶段的速度

1
-XX:+CMSClassUnloadingEnable

如果有的话,启用回收Perm 区(JDK8之前)

1
-XX:+CMSParallelInitialEnabled

用于开启CMS initial-mark阶段采用多线程的方式进行标记

  • 用于提高标记速度,在Java8开始已经默认开启
1
2
3
4
5
6
7
8
9
# CMS回收器

用于开启CMS initial-mark阶段采用多线程的方式进行标记
用于提高标记速度,在Java8开始已经默认开启
-XX:+CMSParallelRemarkEnabled 用户开启CMS remark阶段采用多线程的方式进行重新标记,默认开启
-XX:+ExplicitGCInvokesConcurrent
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
这两个参数用户指定hotspot虚拟在执行System.gc()时使用CMS周期
-XX:+CMSPrecleaningEnabled 指定CMS是否需要进行Pre cleaning阶段

-XX:ParallelCMSThreads和ParallelGCThreads有关系,ParallelGCThreads在上面Parnew回收器中有提到

G1回收器

1
2
3
4
5
6
7
8
9
10
11
# G1回收器
-XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务。
-XX:G1HeapRegionSize 设置每个Region的大小。
值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。
-XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
-XX:ParallelGCThread 设置STW时GC线程数的值。最多设置为8
-XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
-XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
-XX:G1NewSizePercent 新生代占用整个堆内存的最小百分比(默认5%)
-XX:G1MaxNewSizePercent 新生代占用整个堆内存的最大百分比(默认60%)
-XX:G1ReservePercent=10 保留内存区域,防止 to space(Survivor中的to区)溢出

怎么选择

  • 优先让 JVM 自适应,调整堆的大小
  • 串行收集器:内存小于 100M;单核、单机程序,并且没有停顿时间的要求
  • 并行收集器:多 CPU、高吞吐量、允许停顿时间超过 1 秒
  • 并发收集器:多 CPU、追求低停顿时间、快速响应(比如延迟不能超过 1 秒,如互联网应用)
  • 官方推荐 G1,性能高。现在互联网的项目,基本都是使用 G1

特别说明:

  • 没有最好的收集器,更没有万能的收集器
  • 调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器

GC 日志相关选项

1
2
3
4
5
6
-XX:+PrintGC <==> -verbose:gc  打印简要日志信息
-XX:+PrintGCDetails 打印详细日志信息
-XX:+PrintGCTimeStamps 打印程序启动到GC发生的时间,搭配-XX:+PrintGCDetails使用
-XX:+PrintGCDateStamps 打印GC发生时的时间戳,搭配-XX:+PrintGCDetails使用
-XX:+PrintHeapAtGC 打印GC前后的堆信息,如下图
-Xloggc:<file> 输出GC导指定路径下的文件中

image-20210506195156935

1
2
3
4
5
6
7
8
-XX:+TraceClassLoading  监控类的加载
-XX:+PrintGCApplicationStoppedTime 打印GC时线程的停顿时间
-XX:+PrintGCApplicationConcurrentTime 打印垃圾收集之前应用未中断的执行时间
-XX:+PrintReferenceGC 打印回收了多少种不同引用类型的引用
-XX:+PrintTenuringDistribution 打印JVM在每次MinorGC后当前使用的Survivor中对象的年龄分布
-XX:+UseGCLogFileRotation 启用GC日志文件的自动转储
-XX:NumberOfGCLogFiles=1 设置GC日志文件的循环数目
-XX:GCLogFileSize=1M 设置GC日志文件的大小

其他参数

1
2
3
4
5
6
7
8
-XX:+DisableExplicitGC  禁用hotspot执行System.gc(),默认禁用
-XX:ReservedCodeCacheSize=<n>[g|m|k]、-XX:InitialCodeCacheSize=<n>[g|m|k] 指定代码缓存的大小
-XX:+UseCodeCacheFlushing 放弃一些被编译的代码,避免代码缓存被占满时JVM切换到interpreted-only的情况
-XX:+DoEscapeAnalysis 开启逃逸分析
-XX:+UseBiasedLocking 开启偏向锁
-XX:+UseLargePages 开启使用大页面
-XX:+PrintTLAB 打印TLAB的使用情况
-XX:TLABSize 设置TLAB大小

通过 Java 代码获取 JVM 参数

Java 提供了 java.lang.management 包用于监视和管理 Java 虚拟机和 Java 运行时中的其他组件,它允许本地或远程监控和管理运行的 Java 虚拟机。其中 ManagementFactory 类较为常用,另外 Runtime 类可获取内存、CPU 核数等相关的数据。通过使用这些 api,可以监控应用服务器的堆内存使用情况,设置一些阈值进行报警等处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MemoryMonitor {
public static void main(String[] args) {
MemoryMXBean memorymbean = ManagementFactory.getMemoryMXBean();
MemoryUsage usage = memorymbean.getHeapMemoryUsage();
System.out.println("INIT HEAP: " + usage.getInit() / 1024 / 1024 + "m");
System.out.println("MAX HEAP: " + usage.getMax() / 1024 / 1024 + "m");
System.out.println("USE HEAP: " + usage.getUsed() / 1024 / 1024 + "m");
System.out.println("\nFull Information:");
System.out.println("Heap Memory Usage: " + memorymbean.getHeapMemoryUsage());
System.out.println("Non-Heap Memory Usage: " + memorymbean.getNonHeapMemoryUsage());

System.out.println("=======================通过java来获取相关系统状态============================ ");
System.out.println("当前堆内存大小totalMemory " + (int) Runtime.getRuntime().totalMemory() / 1024 / 1024 + "m");// 当前堆内存大小
System.out.println("空闲堆内存大小freeMemory " + (int) Runtime.getRuntime().freeMemory() / 1024 / 1024 + "m");// 空闲堆内存大小
System.out.println("最大可用总堆内存maxMemory " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "m");// 最大可用总堆内存大小

}
}

分析 GC 日志

GC 分类

针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)

  • 部分收集(Partial GC):不是完整收集整个 Java 堆的垃圾收集。其中又分为:
    • 新生代收集(Minor GC / Young GC):只是新生代(Eden / S0, S1)的垃圾收集
    • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有 CMS GC 会有单独收集老年代的行为。注意,很多时候 Major GC 会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。
  • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有 G1 GC 会有这种行为
  • 整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集。

GC 日志分类

MinorGC

MinorGC(或 young GC 或 YGC)日志:

1
[GC (Allocation Failure) [PSYoungGen: 31744K->2192K (36864K) ] 31744K->2200K (121856K), 0.0139308 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]

image-20210506202126562

image-20210506202156090

FullGC

1
[Full GC (Metadata GC Threshold) [PSYoungGen: 5104K->0K (132096K) ] [Par01dGen: 416K->5453K (50176K) ]5520K->5453K (182272K), [Metaspace: 20637K->20637K (1067008K) ], 0.0245883 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]

image-20210506202330868

image-20210506202349072

GC 日志结构剖析

透过日志看垃圾收集器

  • Serial 收集器:新生代显示 “[DefNew”,即 Default New Generation
  • ParNew 收集器:新生代显示 “[ParNew”,即 Parallel New Generation
  • Parallel Scavenge 收集器:新生代显示”[PSYoungGen”,JDK1.7 使用的即 PSYoungGen
  • Parallel Old 收集器:老年代显示”[ParoldGen”
  • G1 收集器:显示”garbage-first heap“

透过日志看 GC 原因

  • Allocation Failure:表明本次引起 GC 的原因是因为新生代中没有足够的区域存放需要分配的数据
  • Metadata GCThreshold:Metaspace 区不够用了
  • FErgonomics:JVM 自适应调整导致的 GC
  • System:调用了 System.gc()方法

透过日志看 GC 前后情况

通过图示,我们可以发现 GC 日志格式的规律一般都是:GC 前内存占用-> GC 后内存占用(该区域内存总大小)

1
[PSYoungGen: 5986K->696K (8704K) ] 5986K->704K (9216K)
  • 中括号内:GC 回收前年轻代堆大小,回收后大小,(年轻代堆总大小)
  • 括号外:GC 回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)

注意:Minor GC 堆内存总容量 = 9/10 年轻代 + 老年代。原因是 Survivor 区只计算 from 部分,而 JVM 默认年轻代中 Eden 区和 Survivor 区的比例关系,Eden:S0:S1=8:1:1。

透过日志看 GC 时间

GC 日志中有三个时间:user,sys 和 real

  • user:进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际 CPU 时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示 GC 线程执行所使用的 CPU 总时间。
  • sys:进程在内核态消耗的 CPU 时间,即在内核执行系统调用或等待系统事件所使用的 CPU 时间
  • real:程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。对于并行 gc,这个数字应该接近(用户时间+系统时间)除以垃圾收集器使用的线程数。

由于多核的原因,一般的 GC 事件中,real time 是小于 sys time + user time 的,因为一般是多个线程并发的去做 GC,所以 real time 是要小于 sys + user time 的。如果 real > sys + user 的话,则你的应用可能存在下列问题:IO 负载非常重或 CPU 不够用。

GC 日志分析工具

GCEasy

GCEasy 是一款在线的 GC 日志分析器,可以通过 GC 日志分析进行内存泄露检测、GC 暂停原因分析、JVM 配置建议优化等功能,大多数功能是免费的。

官网地址:https://gceasy.io/

GCViewer

GCViewer 是一款离线的 GC 日志分析器,用于可视化 Java VM 选项 -verbose:gc 和 .NET 生成的数据 -Xloggc:。还可以计算与垃圾回收相关的性能指标(吞吐量、累积的暂停、最长的暂停等)。当通过更改世代大小或设置初始堆大小来调整特定应用程序的垃圾回收时,此功能非常有用。

源码下载:https://github.com/chewiebug/GCViewer

运行版本下载:https://github.com/chewiebug/GCViewer/wiki/Changelog

GChisto

  • 官网上没有下载的地方,需要自己从 SVN 上拉下来编译
  • 不过这个工具似乎没怎么维护了,存在不少 bug

HPjmeter

  • 工具很强大,但是只能打开由以下参数生成的 GC log,-verbose:gc -Xloggc:gc.log。添加其他参数生成的 gc.log 无法打开
  • HPjmeter 集成了以前的 HPjtune 功能,可以分析在 HP 机器上产生的垃圾回收日志文件