基础
简述
JVM 全称Java Virtual Machine,翻译为中文”Java 虚拟机”。本文中的JVM 主要指的是Oracle 公司的HotSpotVM,版本是Java 8。
Java虚拟机主要分为五大模块:
- 类装载器子系统。
- 运行时数据区。
- 执行引擎。
- 本地方法接口。
- 垃圾收集模块。
其中垃圾收集模块在Java 虚拟机规范中并没有要求Java 虚拟机垃圾收集,但是在没有发明无限的内存之前,大多数JVM实现都是有垃圾收集的。而运行时数据区都会以某种形式存在于每一个Java 虚拟机实例中,但是Java 虚拟机规范对它的描述却是相当抽象。这些运行时数据结构上的细节,大多数都由具体实现的设计者决定。
Java 虚拟机不是真实的物理机,它没有寄存器,所以指令集是使用Java 栈来存储中间数据,这样做的目的就是为了保持Java 虚拟机的指令集尽量的紧凑,同时也便于Java 虚拟机在那些只有很少通用寄存器的平台上实现。
另外,Java虚拟机的这种基于栈的体系结构,有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。
环境准备和相关设置
安装JDK
JDK 可以从Oracle 官网下载,下载自己所需的版本即可。
安装完成后,还需要设置系统变量JAVA_HOME 和PATH,之后验证一下Java 是否安装完成:
1 | java -version |
其他准备工作
按照官方文档里的说法,在诊断问题之前需要做这么几个准备工作:
- 取消core 文件限制。
操作系统提供限制可使用资源的方式,这可能对印象Java 应用程序。 - 开启自动内存dump 选项。
增加启动参数-XX:+HeapDumpOnOutOfMemoryError,在内存溢出时自动转储。 - 可以生成JFR 记录。
- 开启GC 日志。
- 确定JVM 版本以及启动参数。
- 允许JMX 监控信息。
JMX 支持远程监控,通过设置属性来启用。
性能指标
需要解决的问题
- 程序BUG。
- 系统性能问题。
衡量系统维度
- 延迟(Latency)。响应时间。
- 吞吐量(Throughtput)。每秒处理的业务数(TPS)。
- 系统容量(Capacity)。也叫设计容量,可以理解为硬件配置,成本约束。
性能指标
- 业务需求指标:如吞吐量(QPS、 TPS)、响应时间(RT)、并发数、业务成功率等。
- 资源约束指标:如CPU、内存、I/O等资源的消耗情况。
JVM
JVM 背景知识
Java 虚拟机有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
Java 虚拟机本质是就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。
Java 语言的可移植性正是建立在Java 虚拟机的基础上。任何平台只要装有针对于该平台的Java 虚拟机,字节码文件(.class)就可以在该平台上运行。
这就是“一次编译,多次运行”。
Java 虚拟机不仅是一种跨平台的语言,而且是一种新的网络计算平台。该平台包括许多相关的技术,如符合开放接口标准的各种API、优化技术等。Java 技术使同一种应用可以运行在不同的平台上。
Java 平台可分为两部分,即Java 虚拟机(Java virtual machine,JVM)和Java API 类库。
JVM 内存结构
JVM 启动参数
启动Java 程序的格式为:
1 | java [options] classname [args] |
[options] 部分被称为”JVM 选项”,对应IDE 中的VM options,可用jps -V 查看。
[args] 部分是指”传给main 函数的参数”。
agent
agent 是JVM 中的一项黑科技,可以通过无侵入方式来做很多事情,比如注入AOP 代码、执行统计等。
设置agent 的语法如下:
- -agentlib:libname[=options]启用native 方式的agent。
- -agentpath:pathname[=options]启用native 方式的agent。
- -javaagent:jarpath[=options]启用外部的agent 库。
- -Xnoagent 则是禁用所有的agent。
我的一篇博客关于Agent 的详细介绍
设置堆内存
JVM 总内存 = 堆 + 栈 + 非堆 + 堆外内存。
参数:
- -Xmx: 指定最大堆内存。
- -Xms: 指定堆内存空间的起始值。
- -Xmn: 等价于 -XX:NewSize ,使用G1 垃圾收集器不应该设置此项,在某些业务场景下可以设置。
- -XX:MaxPermSize=size: JDK 7 之前设置的,JDK 8之后默认允许的Meta 空间无限大。
- -XX:MaxMetaspaceSize=size: JDK 8默认不限制大小,一般不设置。
设置栈内存
- -Xss: 设置每个线程栈的字节数。
- -XX:ThreadStackSize=1m,和-Xss1m等价。
GC 日志相关
- -verbose:gc 参数
和其他的GC 参数组合使用,在GC 日志中输出详细的GC 信息。包括每次GC 前后各个内存池的大小,堆内存的大小,提升到老年代的大小,以及消耗的时间。 - -XX:+PrintGCDetails 和-XX:PrintGCTimeStamps,打印GC 细节与发生时间。
指定垃圾收集器
指定具体的垃圾收集器:
- -XX:+useG1GC
- -XX:+useConcMarkSweepGC
- -XX:+useSerialGC
- -XX:+useParallelGC
特殊情况执行脚本
- -XX:+-HeapDumpOnOutOfMemoryError 选项,当OutOfMemoryError 产生,即内存溢出时,自动Dump 堆内存。因为在运行时并没有什么开销,所以在生产机器上是可以使用的。
- -XX:HeapDumpPath 选项,与HeapDumpOnOutOfMemoryError 搭配使用,指定内存溢出时Dump 文件的目录。如果没有则指定默认为启动Java 程序的工作目录。
- -XX:OnError 选项,发生致命错误时执行的脚本。
- -XX:OnOutOfMemoryError 选项,抛出OutOfMemoryError 错误时执行的脚本。
- -XX:ErrorFile=filename 选项,致命错误的日志文件名、绝对路径或者相对路径。
JDK 内置工具
jps - 进程
我们知道,操作系统提供了一个工具叫做ps ,用于显示进程状态(Process Status)。
Java 也提供了类似的命令行工具,叫做jps ,用于展示Java 进程信息(列表)。
jps 只能展示当前用户可看见的Java 进程,如果看不见可能需要sudo、su 之类的命令辅助。
查看帮助信息:
1 | jps -help |
在知道JVM 进程的PID 之后,就可以使用其他工具来进行诊断了。
jstat - 内存、GC
jstat 用来监控JVM 内置的各种统计信息,主要查看内存和GC 相关的信息。
查看jstat 的帮助信息:
1 | jstat -help |
再看看options 部分支持哪些选项:
- -class: 类加载信息统计。
- -compiler: JIT 即时编译器相关的统计信息。
- -gc: GC 相关的堆内存信息。
- -gccapacity: 各个内存池分代空间的容量。
- -gccause: 看上次GC、本次GC 的原因,其他输出和-gcutil 一致。
- -gcmetacapacity: meta 区大小统计。
- -gcnew: 年轻代的统计信息。
- -gcnewcapacity: 年轻代空间大小统计。
- -gcold: 老年代和元数据区的行为统计。
- -gcoldcapacity: old 空间大小统计。
- -gcutil: GC 相关区域的使用率统计。
- -printcompilation: 打印JVM 编译统计信息。
实例:
-t 选项的位置是固定的,不能在前也不能在后,用于显示时间戳,即JVM 启动到现在的秒数。
简单分析:
- Timestamp: JVM 启动时间。
- S0: 0号存活区的使用率。0% 很正常,因为S0 和S1 随时有一个是空的。
- S1: 就是1号存活区的使用率。
- E: Eden 区,新生代的使用率。
- O: Old 区,老年代的使用率。
- M: Meta 区,元数据区的使用率。
- CCS: 压缩Class 空间的使用率。
- YGC: 年轻代GC 的次数。
- YGCT: 年轻代GC 消耗的总时间。
- FGC: FullGc 的次数。
- FGCT: FullGC 的总时间。
- GCT: 即所有GC 加起来消耗的总时间。
jmap - 堆
面视最常问的就是jmap 。jmap 主要用来dump 堆内存,当然也支持输出统计信息。
查看jmap 帮助信息:
Dump 堆内存:
1 | jmap -dump:format=b,file=682.hprof 682 |
查看堆内存统计信息:
1 | java -heap 682 |
1 | Attaching to process ID 682, please wait... |
查看直方图的样式:
1 | java -histo 682 |
jcmd - All
jcmd 是JDk 8推出的一款本地诊断工具,只支持连接本机上同一个用户空间下的JVM 进程。
查看帮助信息:
jstack - 线程
jstack 是JDK 自带的线程堆栈分析工具,使用该命令可以查看或导出Java 应用程序中线程堆栈信息。
查看帮助信息:
查看Java 进程的堆栈信息:
1 | jstack 7923 |
1 | 2019-07-25 18:50:31 |
jinfo - JVM
jinfo 全称Java Configuration Info,它的主要作用就是实时查看和调整JVM 配置参数。
查看帮助信息:
查看当前Java 进程的所有参数和系统属性:
1 | jinfo 13164 |
1 | Attaching to process ID 13164, please wait... |
jvisualvm - 本地远程
Java VisualVM 可以对应用程序进行故障排除,并监视和改进应用程序的性能。
Java VisualVM 将多个监视、故障排除和分析实用程序组合到一个工具中。
Java VisualVM 能够生成和分析堆转储,跟踪内存泄漏,执行和监视垃圾收集,以及执行轻量级内存和CPU 分析。
命令行输入jvisualvm 即可打开!
jmc - JVM
使用Java 任务控制器Java Mission Control(JMC) 深入分析Java 应用程序的性能,启动JMC 后将会显示当前机器中的所运行的JVM 进程信息,当然也可以选择添加更多的JVM 进程进行监控。
通过自定义设置上方仪表盘中显示的信息,既可以查看被监控JVM 的详细信息,例如垃圾回收、类的加载、线程的使用、内存堆的使用率等等。也可以查看指定操作系统信息,例如系统的CPU 和内存的使用率、磁盘的交换信息、平均负载等相关信息。
JFR(Java Flight Recorder)
JFR 是JMC 中一个非常关键的功能。它记录了JVM 所有事件的历史数据,通过这些数据,性能分析人员可以结合以往的历史数据对JVM 性能瓶颈进行分析诊断。
JFR 的基本操作是开启一系列的事件。当某个事件发生时,这个事件的所有数据将会被保存至内存或者一个文件当中。数据流被保留在一个环形缓存中,所以只有最近发生的事件的数据才是可用的。JMC 可以从JVM 或者文件中读取并展示这些事件数据,通过这些数据,可以进行性能分析。
通过 JVM 参数,JMC 用户界面以jcmd 命令,可以指定上文中提到的事件类型、环形缓存的大小、数据存储的位置等信息。
在默认设置下,JMC 对被监控应用的影响非常小。但是随着越来越多的事件被开启,以及触发事件的阀值的改变,JMC 的影响也在变化。
展示JFR 的用户界面:
- JFR 概述
- JFR 内存视图
- JFR 代码视图
- JFR 事件概述
命令行输入jmc 即可打开!
jstatd - 服务器
jhat - 堆
jhat 是用来分析Java 堆的命令,可以将堆中的对象以html 的形式显示出来,包括对象的数量、大小等等,并支持对象查询语言(OQL) 。
查看帮助信息:
1 | jhat -h |
1 | Usage: jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file> |
分析dump 堆转储文件:
1 | jhat -J-Xmx512m test # 设置堆空间大小,防止堆空间不足 |
JDWP
这是我的另一篇博客
https://vgbhfive.cn/Java-JDWP协议探究/
JMX
这也是我的另一篇博客
https://vgbhfive.cn/Java-JMX框架探究/
内存回收机制
GC
JAVA 的内存回收机制(Garbage Collector, GC),内存空间中垃圾回收的工作由垃圾回收器 (Garbage Collector,GC) 完成的。
它的核心思想是:对虚拟机可用内存空间,即堆空间中的对象进行识别,如果对象正在被引用,那么称其为存活对象,反之,如果对象不再被引用,则为垃圾对象,可以回收其占据的空间,用于再分配。
在垃圾回收机制中有一组元素被称为根元素集合,它们是一组被虚拟机直接引用的对象。比如,正在运行的线程对象,系统调用栈里面的对象以及被 system class loader 所加载的那些对象。
堆空间中的每个对象都是由一个根元素为起点被层层调用的。因此,一个对象还被某一个存活的根元素所引用,就会被认为是存活对象,不能被回收,进行内存释放。
我们可以通过分析一个对象到根元素的引用路径来分析为什么该对象不能被顺利回收。如果说一个对象已经不被任何程序逻辑所需要但是还存在被根元素引用的情况,我们可以说这里存在内存泄露。
日志解读与分析
使用下面的启动参数让 JVM 打印出详细的GC日志:
1 | -XX:+PrintGCDetails |
配置之后,再发生GC 时就会显示下面这样的样子:
1 | 2015-05-26T14:45:37.987-0200: 151.126: |
由上看GC 日志暴露了JVM中 的一些信息,这是一次发生在年轻代中的小型GC:
- 2015-05-26T14:45:37.987-0200: GC事件(GC event)开始的时间点。
- 151.126: GC事件的开始时间,相对于JVM的启动时间,单位是秒(Measured in seconds)。
- GC: 用来区分(distinguish)是 Minor GC 还是 Full GC 的标志(Flag). 这里的 GC 表明本次发生的是 Minor GC 。
- Allocation Failure: 引起垃圾回收的原因. 本次GC是因为年轻代中没有任何合适的区域能够存放需要分配的数据结构而触发的。
- DefNew: 使用的垃圾收集器的名字. DefNew 这个名字代表的是: 单线程(single-threaded), 采用标记复制(mark-copy)算法的, 使整个JVM暂停运行(stop-the-world)的年轻代(Young generation) 垃圾收集器(garbage collector)。
- 629119K->69888K: 在本次垃圾收集之前和之后的年轻代内存使用情况(Usage)。
- (629120K): 年轻代的总的大小(Total size)。
- 1619346K->1273247K: 在本次垃圾收集之前和之后整个堆内存的使用情况(Total used heap)。
- (2027264K): 总的可用的堆内存(Total available heap)。
- 0.0585007 secs: GC事件的持续时间(Duration),单位是秒。
GCViewer 工具
这个工具只能在1.5 以下的版本中运行,1.6 以后没有对应。
内存dump
首先我们需要获得一个dump 文件,JMap 获取Java 进程的堆转储文件:
1 | jmap -dump:format=b,file=test <pid> |
MAT
MAT 支持两种版本的工具,“联机版”和“脱机版”。具体的安装方法可以参考其他博客。
安装完成之后就需要配置环境参数,因为需要分析堆转储文件,所以需要消耗很多的堆空间,为了保证效率,建议多分配一点内存。
修改配置参数的方法:
- 修改启动参数 MemoryAnalyzer.exe -vmargs -Xmx4g
- 编辑文件 MemoryAnalyzer.ini,在里面添加类似信息 -vmargs – Xmx4g
配置文件完成后,就可以使用了。
在获得dump 堆转储文件之前,可以通过设置**-XX:+HeapDumpOnOutOfMemoryError** 自动在出现内存泄漏时,就可以获得当时的堆转储文件。
分析报告
在获得dump 堆转储文件之后,就要开始分析堆转储文件了。
启动Memory Analyzer tool 工具,然后加载dump 堆转储文件,文件加载完之后就可以展示内存的大体使用情况了。
通常,会采用三步来分析内存泄漏问题:
- 对问题发生时刻的系统内存状态获取一个整体印象。
- 找到最可能的内存泄漏元凶,通常就是指消耗内存最多的对象。
我们可以通过分析一个对象到根元素的引用路径来分析为什么该对象不能被顺利回收。如果说一个对象已经不被任何程序逻辑所需要但是还存在被根元素引用的情况,我们可以说这里存在内存泄露。 - 查看这个内存消耗大户的具体情况,有没有什么异常的行为。
总结
使用 MAT 来进行堆转储文件分析,寻找内存泄露非常简单。
但MAT 绝对不是一个傻瓜式的工具,他还提供了很多高级功能,比如MAT 支持OQL(OBject Query Language) 对heap dump 中的对象进行查询,支持线程的分析等。
引用
https://www.ibm.com/developerworks/cn/opensource/os-cn-ecl-ma/index.html
高级工具
Arthas
Arthas(阿尔萨斯)是Alibaba 开源的Java 诊断工具,操作简单,亲自体验过后,感觉非常好用。
在线教程
官方文档
BTrace
简介
BTrace 是检查和解决线上的问题的杀器,BTrace 可以通过编写脚本的方式,获取程序执行过程中的一切信息,不用重启服务。写好脚本,直接用命令执行即可,不用动原程序的代码。
总体来说,BTrace 是基于动态字节码修改技术(Hotswap )来实现运行时Java 程序的跟踪和替换。
大体的原理可以用下面的公式描述:Client(Java compile api + attach api) + Agent(脚本解析引擎 + ASM + JDK6 Instumentation) + Socket。其实BTrace 就是使用了java attach api 附加agent.jar ,然后使用脚本解析引擎+asm 来重写指定类的字节码,再使用instrument 实现对原有类的替换。
安装
- 从Github 上下载源码。
- 解压源码到文件夹下,并配置对应的环境变量JAVA_HOME、 BTRACE_HOME 即可。
使用场景
BTrace 是一个事后工具,所谓事后工具就是指在服务已经上线,但是发现存在以下问题的时候,可以用 BTrace:
- 比如哪些方法执行太慢。例如监控执行时间超过1s的方法。
- 查看哪些方法调用了System.gc() ,调用栈是怎样的。
- 查看方法参数或对象属性。
- 哪些方法发生了异常。
为了保证trace 语句只读,最小化对被检测程序造成影响,BTrace 对trace 脚本有一些限制:
- BTrace class 不能新建类,新建数组,抛异常,捕获异常。
- 不能调用实例方法以及静态方法。(com.sun.btrace.BTraceUtils除外)
- 不能将目标程序和对象赋值给BTrace 的实例和静态field 。
- 不能定义外部、内部、匿名、本地类。
- 不能有同步块和方法。
- 不能有循环。
- 不能实现接口,不能扩展类。
- 不能使用assert语句,不能使用class 字面值。
简单示例
BTrace 的语法很简单,只需要知道需要探测的Java 程序的PID 即可,然后编写一个探测脚本。
写一个常驻内存的Java 程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* @auther Vgbh
*
*/
public class b {
public static void main(String[] args) {
new b().hello(100000);
}
public void hello(Integer x) {
for (int i = 0; i < x; i++) {
if ((i%100) == 0) {
Object obj = new Object();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i + " Hello World!");
}
}
}获取Java 程序的进程ID
1
2
3
4
5C:\Users\Vgbh>jps
18048
6736 b
18760 Jps
21420编写BTrace 脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.Strings.strcat;
import static com.sun.btrace.BTraceUtils.jstack;
import static com.sun.btrace.BTraceUtils.println;
import static com.sun.btrace.BTraceUtils.str;
/**
* @auther Vgbh
*
*/
public class BTraceTest {
public static void func(int entry) {
println("BTrace ---------------");
println("Entry: " + str(entry));
jstack();
}
}预编译脚本,检查脚本的正确性
1
btracec BTraceTest.java
调用命令执行
1
btrace 6736 BTraceTest.java > log.log
退出
按Ctrl + C 页面会给出提示,再按1 即可退出。
文档
具体的开发请看官方文档
OOM Killer
自己看了两三遍,看懂了一半,emmmmmm……..
自己看吧
容器化技术
在如今的时代,容器的使用越来越普及,成为很多大规模集群的基石。 在容器环境下,要直接进行调试并不容易。我们更多的是进行应用性能指标的采集和监控,并构建预警机制。而这需要架构师、开发、测试、运维人员的协作。 但监控领域的工具, 又多又杂, 而且在持续发展和迭代中。
最早期的监控, 只在系统发布时检查服务器相关的参数,并将这些参数用作系统运行状况的指标。而监控服务器的健康状况,与用户体验之间紧密相关,悲剧在于那些年代发生的问题比实际检测到的要多很多。
随着时间推移,日志管理、预警、遥测以及系统报告领域持续发力。其中有很多有效的措施,诸如安全事件、有效警报、记录资源使用量等等, 但前提是我们需要有一个清晰的策略,进行用户访问链路跟踪。 比如 Zabbix、Nagios,以及 Prometheus 等工具在生产环境中被广泛使用。
性能问题的关键是人, 也就是我们的用户。但已有的这些工具并没有实现真正的用户体验监控。仅仅使用这些软件也不能缓解性能问题, 我们还需要采取各种措施, 在勇敢和专注下不懈地努力。
Web系统的问题诊断和性能调优,是一件意义重大的事情。需要严格把控,也需要付出很多精力。当然, 成功实施这些工作对企业的回报也是巨大的!
Spring是Java领域事实上的标准,SpringBoot提供了一款应用指标收集器:
- Micrometer
官方文档连接: https://micrometer.io/docs
支持直接将数据上报给 Elasticsearch, Datadog, InfluxData等各种流行的监控系统。自动采集最大延迟,平均延迟,95%线,吞吐量,内存使用量等指标。此外,在小规模集群中,我们还可以使用Pinpoint 、Skywalking等开源APM工具。
容器技术的发展历程简介
https://www.infoq.cn/article/R1p3H3_29f4TYImExsyw
案例分析
OOM故障分析记录:HeapByteBuffer byte[] 占用了大量内存
现象
版本上测试环境测试,结果遇到服务启动一段时间就会OOM,并且运行会非常卡顿。
分析
- 分析可能是内存不够使用,查看了该服务的内存使用情况和GC 使用,通过与生产环境比较,没有发现什么问题。
- 分析本次上线测试代码,发现了几处容易内存泄漏的代码,修改后并上测试环境测试,还是会出现OOM 问题。
- 保存OOM 是的dump文件,下载到本地查看OOM 源头问题。
尝试解决问题步骤
获取测试环境OOM 的dump文件
1
jmap -dump:format=b,file=<PID>.bin <PID>
用JDk 知道的jvisualVM 打开dump 文件。
查看堆使用情况,并找到使用堆最多的类。
进一步查看占用情况,获取到占用堆最大文件的内容保存到本地,通过Sublime 查看内容。
这时发现保存到本地的文件大小和max-http-header-size 一样大,因此判断出可能是Tomcat 的配置有问题。
查看Tomcat 配置文件,果不其然,并且在跟生产环境比对后,也是不一致的。至此,将属性值修改为跟生产一致的内容即可。
总结
问了测试,可能是之前配置的测试其他功能未修改回来导致的问题。
引用
https://gitbook.cn/gitchat/activity/5d1c6a069c31912c0f9f9f0b 我没有打广告,真的很好!
个人备注
此博客内容均为作者学习所做笔记,侵删!
若转作其他用途,请注明来源!