JVM系列-22-3、性能监控与调优篇---性能调优概述、部分命令行与GUI监控与诊断工具、再谈内存泄漏
性能调优概述
大厂面试题
支付宝:
1 |
|
小米:
1 |
|
蚂蚁金服:
1 |
|
阿里:
1 |
|
字节跳动:
1 |
|
拼多多:
1 |
|
京东:
1 |
|
背景说明
生产环境中的问题
- 生产环境繁盛了内存溢出该如何处理?
- 生产环境应该给服务器分配多少内存合适?
- 如何对垃圾回收器的性能进行调优?
- 生产环境 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 微妙 |
在垃圾回收环节中:
1 |
|
吞吐量
对单位时间内完成的工作量(请求)的量度。在 GC 中:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间 + 内存回收的时间)。
吞吐量为 1-1/(1+n)。-XX:GCTimeRatio=n
。
并发数
同一时刻,对服务器有实际交互的请求数。
内存占用
Java 堆区所占的内存大小。
相互间的关系
以高速公路通行状况为例:
吞吐量:每天通过高速公路收费站的车辆的数据
并发数:高速公路上正在行驶的车辆的数目
响应时间:车速
JVM监控及诊断工具-命令行篇
概述
性能诊断是软件工程师在日常工作中需要经常面对和解决的问题,在用户体验至上的今天,解决好应用的性能问题能带来非常大的收益。
Java 作为最流行的编程语言之一,其应用性能诊断一直收到业界广泛关注。可能造成 Java 应用出现性能问题的因素非常多,例如线程控制、磁盘读写、数据库访问、网络 I/O、垃圾收集等。想要定位这些问题,一款非常优秀的性能诊断工具必不可少。
体会一:使用数据说明问题,使用知识分析问题,使用工具处理问题。
体会二:无监控、不调优。
简单命令行工具 javac、java
在我们刚接触 Java 学习的时候,大家肯定最先了解的两个命令就是 javac、java,那么除此之外,还有没有其他的命令可以供我们使用呢?我们进入到安装 JDK 的 bin 目录,发现还有一系列辅助工具。这些辅助工具用来获取目标 JVM 不同方面、不同层次的信息,帮助开发人员很好地解决 Java 应用程序的一些疑难杂症。
jps:查看正在运行的 Java 进程
基本情况
jps(Java Process Status):显示指定系统内所有的 HotSpot 虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。
说明:对于本地虚拟机进程来说,进程的本地虚拟机 ID 与操作系统的进行 ID 是一致的,是唯一的。
基本语法
jps [options] [hostid]
options 参数
1 |
|
hostid 参数
RMI 注册表中注册的主机名。 如果想要远程监控主机上的 Java 程序,需要安装 jstatd。
对于具有更严格的安全实践的网络场所而言,可能使用一个自定义的策略文件来显示对特定的可信主机或网络访问,尽管 这种技术容易受到 IP 地址欺诈攻击。
如果安全问题无法使用一个定制的策略文件来处理,那么最安全的操作是不运行 jstatd 服务器,而是在本地使用 jstat 和 jps 工具。
jstat:查看 JVM 统计信息
基本情况
jstat(JVM Statistics Monitoring Tool):用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据。
在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。
基本语法
jstat -<option> [t] [-h<lines>] <vmid> [<interval> [<count>]]
查看命令相关参数:jstat -h
或jstat -help
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:显示永久代使用到的最大、最小空间。
- JIT 相关的
- -compiler:显示 JIT 编译器编译过的方法、耗时等信息。
- -printcomilation:输出已经被 JIT 编译的方法。
新生代相关:
- S0C 是第一个幸存者区的大小(字节)
- S1C 是第二个幸存者区的大小(字节)
- S0U 是第一个幸存者区已使用的大小(字节)
- S1U 是第二个幸存者区已使用的大小(字节)
- EC 是 Eden 空间的大小(字节)
- EU 是 Eden 空间已使用大小(字节)
老年代相关:
- OC 是老年代的大小(字节)
- OU 是老年代已使用的大小(字节)
方法区(元空间)相关:
- MC 是方法区的大小
- MU 是方法区已使用的大小
- CCSC 是压缩类空间的大小
- CCSU 是压缩类空间已使用的大小
其他:
- YGC 是指从应用程序启动到采样时 Young GC 次数
- YGCT 是指从应用程序启动到采样时 Young GC 消耗的时间(秒)
- FGC 是指从应用程序启动到采样时 Full GC 次数
- FGCT 是指从应用程序启动到采样时 Full GC 消耗的时间(秒)
- GCT 是指从应用程序启动到采样时 GC 的总时间
interval 参数
用于指定输出统计数据的周期,单位为毫秒。即:查询间隔。
count 参数
用于指定查询的总次数。
-t 参数
可以在输出信息前加上一个 Timestamp 列,显示程序的运行时间。单位:秒。
1 |
|
-h 参数
可以在周期性数据输出时,输出多少行数据后输出一个表头信息。
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
基本语法
jinfo [options] pid
说明:Java 进程 ID 必须要加上。
选项 | 选项说明 |
---|---|
no option | 输出全部的参数和系统属性 |
-flag name | 输出对应名称的参数 |
-flag [+-] name | 开启或关闭对应名称的参数,只有被标记为 manageable 的参数才可以被动态修改 |
-flag name=value | 设定对应名称的参数 |
-flags | 输出全部的参数 |
-sysprops | 输出系统属性 |
查看
1 |
|
修改
jinfo 不仅可以查看运行时某一个 Java 虚拟机参数的实际取值,甚至可以在运行时修改部分参数,并使之立即生效。但是,并非所有参数都支持动态修改。参数只有被标记为 manageable 的 flag 可以被实时修改。其实,这个修改能力是极其有限的。
1 |
|
拓展
java -XX:+PrintFlagsInitial:查看所有 JVM 参数启动的初始值。
java -XX:+PrintFlagsFinal:查看所有 JVM 参数的最终值。
java -XX:+PrintCommandLineFlags:查看那些已经被用户或者 JVM 设置过的详细的 XX 参数的名称和值。
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>
其中 option 包括:
选项 | 作用 |
---|---|
-dump | 生成 dump 文件 |
-finalizerinfo | 以 ClassLoader 为统计口径输出永久代的内存状态信息 |
-heap | 输出整个堆空间的详细信息,包括 GC 的使用、堆配置信息、以及内存的使用信息等 |
-histo | 输出堆空间中对象的统计信息,包括类、实例数量和合计容量 |
-permstat | 以 ClassLoader 为统计口径输出永久代的内存状态信息 |
-F | 当虚拟机进程对 -dump 选项没有任何响应时,强制执行生成 dump 文件 |
说明:这些参数和 Linux 下输入显示的命令多少会有不同,包括也受 JDK 版本的影响。
1 |
|
使用 1:导出内存映像文件
一般来说,使用 jmap 指令生成 dump 文件的操作算得上是最常用的 jmap 命令之一,将堆中所有存货对象导出至一个文件之中。
Heap Dump 又叫做堆转储文件,指一个 Java 进程在某个时间点的内存快照。Heap Dump 在触发内存快照的时候会保存此刻的信息如下:
All Object
:Class, fields, primitive, values and reference
All Classes
:ClassLoader, name, super class, static fields
Garbage Collection Roots
:Objects 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-frame information about local objects
说明:
- 通常在写 Heap Dump 文件前会触发一次 Full GC,所以 Heap Dump 文件里保存的都是 Full GC 后留下的对象信息。
- 由于生成 dump 文件比价耗时,因此大家需要耐心等待,尤其是大内存镜像生成 dump 文件则需要耗费更长的时间来完成。
- 手动的方式
1 |
|
- 自动的方式
当程序发生 OOM 退出系统时,一些瞬时信息都随着程序的终止而消失,而重现 OOM 问题往往比较困难或者耗时。此时若能在 OOM 时,自动导出 dump 文件就显得十分迫切。
这里介绍一种比较常用的取得堆快照文件的方法,即使用:
1 |
|
使用 2:显示堆内存相关信息
1 |
|
使用 3:其他作用
1 |
|
小结
由于 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 命令在 JDK 9、JDK 10 中已经被删除,官方建议使用 VisualVM 代替。
基本语法
1 |
|
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
基本语法
它的基本使用语法为:
jstack option PID
1 |
|
1 |
|
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 中的下述功能,看看有没有适合你项目的监控项:
1 |
|
jstatd:远程主机信息收集
之前的指令只涉及到监控本机的 Java 应用程序,而在这些工具中,一些监控工具也支持对远程计算机的监控(如 jps、jstat)。为了启用远程监控,则需要配合使用 jstatd 工具。
命令 jstatd 是一个 RMI 服务端程序,它的作用相当于代理服务器,建立本地计算机与远程监控工具的通信。jstatd 服务器将本机的 Java 应用程序信息传递到远程计算机。
JVM监控及诊断工具-GUI篇
工具概述
使用上一章命令行工具或组合能帮您获取目标 Java 应用性能相关的基础信息,但它们存在下列局限:
- 无法获取方法级别的分析数据,如方法间的调用关系,各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)。
- 要求用户登录到目标 Java 应用所在的宿主机上,使用起来不是很方便。
- 分析数据通过终端输出,结果展示不够直观。
为此,JDK 提供了一些内存泄漏的分析工具,如 JConsole、jVisual VM 等,用于辅助开发人员定位问题,但这些工具很多时候并不足以满足快速定位的需求,所以这里我们介绍的工具相对多一些、丰富一些。
图形化综合诊断工具:
JDK 自带的工具
JConsole:JDK 自带的可视化监控工具、查看 Java 应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等。
位置:jdk.exe
Visual VM:Visual VM 是一个工具,它提供了一个可视界面,用于查看 Java 虚拟机上运行的基于 Java 技术的应用程序的详细信息。
位置:jdk.exe
JVM:Java Mission Control,内置 Java Flight Recorder,能够以极低的性能开销收集 Java 虚拟机的性能数据。
第三方工具
- MAT:MAT(Memory Analyzer Tool)是基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 Java Heap 分析工具,它可以帮助我们查看内存泄漏和减少内存消耗。
- JProfiler:商业软件,需要付费,功能强大。
- Arthas:Alibaba 开源的 Java 诊断工具。
- Btrace:Java 运行时追踪工具,可以在不停机的情况下,跟踪指定的方法调用、构造函数调用和系统内存等信息。
JConsole
基本概述
- 从 Java 5 开始,在 JDK 中自带的 Java 监控和管理控制台。
- 用于对 JVM 中内存、线程和类等的监控,是一个基于 JMX(Java Management Extensions)的 GUI 性能监控工具。
官方教程:https://docs.oracle.com/javase/7/docs/technotes/guides/management/JConsole.html
启动
jdk/bin 目录下,启动 JConsole.exe 即可,不需要使用 jps 命令来查询。
三种连接方式
- 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 代理,或者是一个使用 JDK 1.4 的实现了 JMX 和 JMX Remote 的应用。
JConsole主要作用
- 监控内存
- 监控线程
- 监控死锁
- 类加载与虚拟机信息
Visual VM
基本概述
Visual VM 是一个功能强大的多合一故障诊断和性能监控的可视化工具。
它继承了多个 JDK 命令行工具,使用 Visual VM 可用于显示虚拟机进程及进程的配置和环境信息(jps、jinfo),监视应用程序的 CPU、GC、堆、方法区及线程的信息(jstat、jstack)等,甚至代替 JConsole。
在 JDK 6 update 7 以后,Visual VM 便作为 JDK 的一部分发布(Visual VM 在 JDK/bin 目录下)。
此外,Visual VM 也可以作为独立的软件安装。首页:https://visualvm.github.io/index.html
插件的安装
Visual VM 的一大特点是支持插件扩展,并且插件安装非常方便。我们既可以通过离线下载插件文件 *.nbm,然后在 Plugin 对话框的已下载页面下,添加已下载的插件。也可以在可用插件页面下,在线安装插件。插件地址:https://visualvm.github.io/pluginscenters.html
IDEA 安装 Visual VM Launcher 插件:
1 |
|
连接方式
本地连接:监控本地 Java 进程的 CPU、类、线程等。
远程连接:
- 确定远程服务器的 IP 地址
- 添加 JMX(通过 JMX 技术具体监控远端服务器哪个 Java 进程)
- 修改 bin/catalina.sh 文件,连接远程的 Tomcat
- 在 ../conf 中添加 jmxremote.access 和 jmxremote.password 文件
- 将服务器地址改为公网 IP 地址
- 设置阿里云安全策略和防火墙策略
- 启动 Tomcat,查看 Tomcat 启动日志和端口监听
- JMX 中输入端口号、用户名、密码登录
Visual VM主要功能
- 生成/读取堆内存快照
- 查看 JVM 参数和系统属性
- 查看运行中的虚拟机进程
- 生成/读取线程快照
- 程序资源的实时监控
- 其他功能:
- JMX 代理连接
- 远程环境监控
- CPU 分析和内存分析
Eclipse MAT
基本概述
MAT(Memory Analyzer Tool)工具是一款功能强大的 Java 堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。
MAT 是基于 Eclipse 开发的,不仅可以单独使用,还可以作为插件的形式嵌入在 Eclipse 中使用,是一款免费的性能分析工具,使用起来非常方便。可以在 https://www.eclipse.org/mat/downloads.php 下载并使用 MAT。
获取堆 dump 文件
dump 文件内容
MAT 可以分析 Heap dump 文件。在进行内存分析时,只要获得了反映当前设备内存映像的 hprof 文件,通过 MAT 打开就可以直观地看到当前的内存信息。
一般来说:这些内存信息包含:
- 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
- 所有的类信息,包括 ClassLoader、类名称、父类、静态变量等。
- GCRoot 到所有的这些对象的引用路径。
- 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)。
两点说明
说明 1:
MAT 不是一个万能的工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如 Sun、HP、SAP 所采用的 HPROF 二进制堆存储文件。以及 IBM 的 PHD 堆存储文件等都能被很好地解析。
说明 2:
最吸引人的还是能够快速为开发人员生成内存泄漏报表,方便定位问题和分析问题。虽然 MAT 有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从 MAT 展现给我们的信息当中通过经验和直觉来判断才能发现。
获取并分析堆dump文件
1、 获取 dump 文件
方法一:通过前一章介绍的 jmap 工具生成,可以生成任意一个 Java 进程的 dump 文件;
方法二:通过配置 JVM 参数生成
- 选项"
-XX:+HeapDumpOnOutOfMemoryError
"或"-XX:+HeapDumpBeforeFullGC
" - 选项"
-XX:HeapDumpPath
"所代表的含义就是当程序出现 OutOfMemory 时,将会在相应的目录下生成一份 dump 文件。如果不指定选项"XX:HeapDumpPath
"则在当前目录下生成 dump 文件。
对比:考虑到生产环境中几乎不可能在线对其进行分析,大家都是采用离线分析,因此使用 jmap+MAT 工具是最常见的组合。
方法三:使用 Visual VM 可以导出堆 dump 文件。
方法四:使用 MAT 既可以打开一个已有的堆快照,也可以通过 MAT 直接从活动 Java 程序中导出堆快照。该功能将借助 jps 列出当前正在运行的 Java 进程,以供选择并获取快照。
2、分析堆 dump 文件
histogram
展示了各个类的实例数目以及这些实例的 Shallow Heap 或 Retained Heap 的总和。
MAT 的直方图和 jmap 的 -histo 子命令一样,都能够展示各个类的实例数目以及这些实例的 Shallow Heap 总和。但是,MAT 的直方图还能够计算 Retained Heap,并支持基于实例数目或 Retained Heap 的排序方式(默认为 Shallow Heap)。
此外,MAT 还可以将直方图中的类按照超类、类加载器或者包名分组。
当选中某个类时,MAT 界面左上角的 Inspector 窗口将展示该类的 Class 实例的相关信息,如类加载器等。
Thread Overview
- 查看系统中的 Java 线程
- 查看局部变量的信息
获得对象相互引用的关系
- with outgoing references
- with incoming references
浅堆与深堆
Shallow Heap
浅堆(Shallow Heap)是指一个对象所消耗的内存。在 32 位系统中,一个对象引用会占据 4 个字节,一个 int 类型会占据 4 个字节,long 型变量会占据 8 个字节,每个对象头需要占用 8 个字节。根据堆快照格式不同,对象的大小可能会向 8 字节进行对齐。
以 String 为例:2 个 int 值共占 8 字节,对象引用占用 4 字节,对象头 8 字节,合计 20 字节,向 8 字节对齐,故占 24 字节。(JDK 7 中)
这 24 字节为 String 对象的浅堆大小。它与 String 的 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节。
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 的深堆范围内。
练习
看图理解 Retained Size
上图中,GC Roots 直接引用了 A 和 B 两个对象。
A 对象的 Retained Size=A 对象的 Shallow Size
B 对象的 Retained Size=B 对象的 Shallow Size + C 对象的 Shallow Size
这里不包括 D 对象,因为 D 对象被 GC Roots 直接引用。
支配树(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。
同理,对象 E 支配对象 G。到达对象 H 的可以通过对象 D,也可以通过对象 E,因此对象 D 和 E 都不能支配对象 H,而经过对象 C 既可以到达 D 也可以到达 E,因此对象 C 为对象 H 的直接支配者。
在 MAT 中,单击工具栏上的对象支配树按钮,可以打开对象支配树视图。
下图显示了对象支配树视图的一部分。该截图显示部分 Lily 学生的 history 队列的直接支配对象。即当 Lily 对象被回收,也会一并回收的所有对象。显然能被 3 或者 5 整除的网页不会出现在该列表中,因为它们同时被另外两名学生对象引用。
支持使用 OQL 语言查询对象信息
MAT 支持一种类似于 SQL 的查询语言 OQL(Object Query Language),OQL 使用类 SQL 语法,可以在堆中进行对象的查找和筛选。
SELECT 子句
在 MAT 中,SELECT 子句的格式与 SQL 基本一致,用于指定要显示的列,SELECT 子句中可以使用"*",查看结果对象的引用实例(相当于 outgoing reference)。
1 |
|
使用"OBJECTS"关键字,可以将返回结果集中的项以对象的形式显示。
1 |
|
在 SELECT 子句中,使用"AS RETAINED SET"关键字可以得到所得对象的保留集。
1 |
|
"DISTINCT" 关键字用于在结果集中去除重复对象。
1 |
|
FROM 子句
FROM 子句用于指定查询范围,它可以指定类名、正则表达式或者对象地址。
1 |
|
下列使用正则表达式,限定搜索范围,输出所有 com.atguigu 包下所有类的实例
1 |
|
也可以直接使用类的地址进行搜索。使用类的地址的好处是可以区分被不同 ClassLoader 加载的同一种类型。
1 |
|
WHERE 子句
WHERE 子句用于指定 OQL 的查询条件。OQL 查询将只返回满足 WHERE 子句指定条件的对象。WHERE 子句的格式与传统 SQL 极为相似。
下例返回长度大于 10 的 char 数组。
1 |
|
下例返回所有 value 域不为 null 的字符串,使用"="操作符。
1 |
|
WHERE 子句支持多个条件的 AND、OR 运算。下例返回数组长度大于 15,并且深堆大于 1000 字节的所有 Vector 对象。
1 |
|
内置对象与方法
OQL 中可以访问堆内对象的属性,也可以访问堆内代理对象的属性,访问堆内对象的属性时,格式如下:
[<alias>.] <field>.<field>.<field>
,其中 alias 为对象名称。
访问 java.io.File 对象的 path 属性,并进一步访问 path 的 value 属性。
1 |
|
下例显示了 String 对象的内容、objectId 和 objectAddress:
1 |
|
下例显示了 java.util.Vector 内部数组的长度:
1 |
|
下例显示了所有的 java.util.Vector 对象及其子类型:
1 |
|
再谈内存泄漏
内存泄露的理解与分类
何为内存泄漏(Memory Leak)
可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况,由于代码的实现不同就会出现很多种内存泄漏问题(让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏)。
内存泄漏(Memory Leak)的理解
严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏。但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致 OOM,也可以叫做宽泛意义上的"内存泄漏"。
对象 X 引用对象 Y,X 的生命周期比 Y 的生命周期长,那么当 Y 生命周期结束的时候,X 依然引用着 Y,这时候,垃圾回收器是不会回收对象 Y 的;如果对象 X 还引用着生命周期比较短的 A、B、C,对象 A 又引用着对象 A、B、C,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。
内存泄漏和内存溢出的关系
内存泄漏(Memory Leak)
申请了内存用完了不释放,比如一共有 1024M 的内存,分配了 521M 的内存一直不回收,那么可以用的内存只有 521M 了,仿佛泄漏掉了一部分。
内存溢出(Out Of Memory)
申请内存时,没有足够的内存可以使用。
可见,内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出。
泄漏的分类
经常发生:发生内存泄露的代码会被多次执行,每次执行,泄漏一块内存;
偶尔发生:在某些特定情况下才会发生;
一次性:发生内存泄漏的方法只会执行一次;
隐式泄漏:一直占着内存不释放,直到执行结束;严格地说这个不算内存泄漏,因为最终释放掉了,但是如果执行时间特别长,也可能会导致内存耗尽。
Java 中内存泄漏的 8 种情况
静态集合类
静态集合类,如 HashMap、LinkedList 等等。如果这些容器为静态的,那么它们的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
1
2
3
4
5
6
7
8public class MemoryLeak {
static List list = new ArrayList();
public void comTests() {
Object obj = new Object();
list.add(obj);
}
}单例模式
单例模式,和静态集合导致内存泄漏的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部独享也不会被回收,那么就会造成内存泄漏。
内部类持有外部类
内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
各种连接,如数据库连接、网络连接和 IO 连接等
在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用 close 方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。
否则,如果在访问数据库的过程中,对 Connection、Statement 或 ResultSet 不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public 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
8public class UsingRandom {
private String msg;
public void receiveMsg() {
readFromNet(); // 从网络中接受数据保存到 msg 中
saveDB(); // 把 msg 保存到数据库中
}
}如果上面这个伪代码,通过 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 不可改变。
缓存泄露
内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,它就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。
对于这个问题,可以使用 WeakHashMap 代表缓存,此种 Map 的特点是,当除了自身有对 key 的引用外,此 key 没有其他引用那么此 map 会自动丢弃此值。
监听器和回调
内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的 API 中注册回调,却没有显示的取消,那么就会积聚。
需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为 WeakHashMap 中的键。