learn-tech/专栏/JVM核心技术32讲(完)/26面临复杂问题时的几个高级工具:它山之石,可以攻玉.md
2024-10-16 00:01:16 +08:00

654 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
26 面临复杂问题时的几个高级工具:它山之石,可以攻玉
前面提到了很多 JVM 的分析工具,本节里我们会再介绍几种有用的工具,大家可以在需要的时候按需使用。
OOM Killer
在前面的章节,我们简单提及过 Linux 系统上的 OOM KillerOut Of Memory killerOOM 终结者。假如物理内存不足Linux 会找出“一头比较壮的进程”来杀掉。
OOM Killer 参数调优
Java 的堆内存溢出OOM是指堆内存用满了GC 没法回收导致分配不了新的对象。
而操作系统的内存溢出OOM则是指计算机所有的内存物理内存 + 交换空间),都被使用满了。
这种情况下,默认配置会导致系统报警,并停止正常运行。当然,将 /proc/sys/vm/panic_on_oom 参数设置为 0 之后,则系统内核会在发生内存溢出时,自动调用 OOM Killer 功能来杀掉最壮实的那头进程Rogue Process流氓进程这样系统也许就可以继续运行了。
以下参数可以基于单个进程进行设置,以手工控制哪些进程可以被 OOM Killer 终结。这些参数位于 proc 文件系统中的 /proc/pid/ 目录下,其中 pid 是指进程的 ID。
oom*adj正常范围是 -16 到 15用于计算一个进程的 OOM 评分oom*score。这个分值越高该进程越有可能被 OOM Killer 给干掉。如果设置为 -17则禁止 OOM Killer 杀死该进程。
proc 文件系统是虚拟文件系统,某个进程被杀掉,则 /proc/pid/ 目录也就被销毁了。
OOM Killer 参数调整示例
例如进程的 pid=12884root 用户执行:
$ cat /proc/12884/oom_adj
0
# 查看最终得分
$ cat /proc/12884/oom_score
161
$ cat /proc/12884/oom_score_adj
0
# 修改分值 ...
$ echo -17 > /proc/12884/oom_adj
$ cat /proc/12884/oom_adj
-17
$ cat /proc/12884/oom_score
0
# 查看分值修正值
$ cat /proc/12884/oom_score_adj
-1000
# 修改分值
$ echo 15 > /proc/12884/oom_adj
$ cat /proc/12884/oom_adj
15
$ cat /proc/12884/oom_score
1160
$ cat /proc/12884/oom_score_adj
1000
这样配置之后,就允许某个占用了最多资源的进程,在操作系统内存不足时,也不会杀掉他,而是先去杀别的进程。
案例
我们通过以下这个案例来展示 OOM Killer。
1. 问题描述
某个 Java 应用经常挂掉,原因疑似 Java 进程被杀死。
2. 配置信息
配置如下:
服务器:阿里云 ECS
IP 地址192.168.1.52
CPU4 核,虚拟 CPU Intel Xeon E5-2650 2.60GHz
物理内存8GB
3. 可用内存
内存不足4 个 Java 进程2.1+1.7+1.7+1.3=6.8G,已占用绝大部分内存。
4. 查看日志
Linux 系统的 OOM Killer 日志:
sudo cat /var/log/messages | grep killer -A 2 -B 2
经排查发现,具有如下日志:
$ sudo cat /var/log/messages | grep killer -A 2 -B 2
May 21 09:55:01 web1 systemd: Started Session 500687 of user root.
May 21 09:55:02 web1 systemd: Starting Session 500687 of user root.
May 21 09:55:23 web1 kernel: java invoked oom-killer: gfp_mask=0x201daorder=0oom_score_adj=0
May 21 09:55:24 web1 kernel: java cpuset=/ mems_allowed=0
May 21 09:55:24 web1 kernel: CPU: 3 PID: 25434 Comm: java Not tainted 3.10.0-514.6.2.el7.x86_64 #1
--
May 21 12:05:01 web1 systemd: Started Session 500843 of user root.
May 21 12:05:01 web1 systemd: Starting Session 500843 of user root.
May 21 12:05:22 web1 kernel: jstatd invoked oom-killer: gfp_mask=0x201daorder=0oom_score_adj=0
May 21 12:05:22 web1 kernel: jstatd cpuset=/ mems_allowed=0
May 21 12:05:23 web1 kernel: CPU: 2 PID: 10467 Comm: jstatd Not tainted 3.10.0-514.6.2.el7.x86_64 #1
可以确定,确实是物理内存不足引起的。
注意:所有 Java 进程的 -Xmx 加起来,如果大于系统的剩余内存,就可能发生这种情况。
查询系统所有进程的 oom_score
ps -eo pid,comm,pmem --sort -rss | awk '{"cat /proc/"$1"/oom_score" | getline oom; print $0"\t"oom}'
重要提示:
如果调整过某个进程的 oom_adj 配置,那么由该进程创建的所有进程,都会继承 oom_score 分值。例如,假设某个 sshd 进程受 OOM Killer 的保护,则所有的 SSH 会话也将受到保护。这样的配置,如果发生 OOM有可能会影响 OOM Killer 拯救系统的功能。
我们现在设想一个场景,假如我们想要随时调试跟踪线上运行的系统,需要用什么样的工具呢?下面就介绍 2 款这样的工具。
BTrace 诊断分析工具
BTrace 是基于 Java 语言的一款动态追踪工具,可用于辅助问题诊断和分析。
BTrace 项目地址:
https://github.com/btraceio/btrace/
在 Wiki 页面 中有一些简单的介绍:
BTrace 基于 ASM、Java Attach API、Instruments 开发,提供很多注解。通过这些注解,可以通过 Java 代码来编写 BTrace 脚本进行只读监控,而无需深入了解 ASM 对字节码的操纵。
下面我们来实际操作一下。
BTrace 下载
找到 Release 页面,找到最新的压缩包下载:
btrace-bin.tar.gz
btrace-bin.zip
下载完成后解压即可使用:
可以看到bin 目录下是可执行文件samples 目录下是脚本示例。
示例程序
我们先编写一个有入参有返回值的方法,示例如下:
package demo.jvm0209;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class RandomSample {
public static void main(String[] args) throws Exception {
//
int count = 10000;
int seed = 0;
for (int i = 0; i < count; i++) {
seed = randomHash(seed);
TimeUnit.SECONDS.sleep(2);
}
}
public static int randomHash(Integer seed) {
String uuid = UUID.randomUUID().toString();
int hashCode = uuid.hashCode();
System.out.println("prev.seed=" + seed);
return hashCode;
}
}
这个示例程序很简单,循环很多次调用某个方法,使用其他程序也是一样的。
然后运行程序,可以看到控制台每隔一段时间就有一些输出:
prev.seed=1601831031
...
BTrace 提供了命令行工具,但使用起不如在 JVisualVM 中方便,下面通过 JVisualVM 中集成 BTrace 插件进行简单的演示。
JVisualVM 环境中使用 BTrace
安装 JVisualVM 插件的操作,我们在前面的章节《[JDK 内置图形界面工具]》中介绍过。
细心的同学可能已经发现,在安装 JVisualVM 的插件时有一款插件叫做“BTrace Workbench”。安装这款插件之后在对应的 JVM 实例上点右键,就可以进入 BTrace 的操作界面。
1. BTrace 插件安装
打开 VisualVM选择菜单“工具插件(G)”:
然后在插件安装界面中,找到“可用插件”:
勾选“BTrace Workbench”之后点击“安装(I)”按钮。
如果插件不显示,请更新 JDK 到最新版。
按照引导和提示,继续安装即可。
接受协议,并点击安装。
等待安装完成:
点击“完成”按钮即可。
BTrace 插件使用
打开后默认的界面如下:
可以看到这是一个 Java 文件的样子。然后我们参考官方文档,加一些脚本进去。
BTrace 脚本示例
我们下载的 BTrace 项目中samples 目录下有一些脚本示例。 参照这些示例,编写一个简单的 BTrace 脚本:
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class TracingScript {
@OnMethod(
clazz = "/demo.jvm0209.*/",
method = "/.*/"
)
// 方法进入时
public static void simple(
@ProbeClassName String probeClass,
@ProbeMethodName String probeMethod) {
print("entered " + probeClass);
println("." + probeMethod);
}
@OnMethod(clazz = "demo.jvm0209.RandomSample",
method = "randomHash",
location = @Location(Kind.RETURN)
)
// 方法返回时
public static void onMethodReturn(
@ProbeClassName String probeClass,
@ProbeMethodName String probeMethod,
@Duration long duration,
@Return int returnValue) {
print(probeClass + "." + probeMethod);
print(Strings.strcat("(), duration=", duration+"ns;"));
println(Strings.strcat(" return: ", ""+returnValue));
}
}
执行结果
可以看到输出了简单的执行结果
可以和示例程序的控制台输出比对一下
更多示例
BTrace 提供了很多示例照着改一改就能执行简单的监控
BTrace 简介
Btrace 入门到熟练小工完全指南
BTrace 用户指南
Java 动态追踪技术探究
Arthas 诊断分析工具
上面介绍的 BTrace 功能非常强大但是实用限制也不少环境问题也很多那么有没有更好的类似工具呢
本节我们就来介绍一下 Arthas 诊断分析工具Arthas 项目首页
https://github.com/alibaba/arthas
Arthas 简介
Arthas阿尔萨斯是阿里巴巴推出了一款开源的 Java 诊断工具深受开发者喜爱为什么这么说呢
Arthas 支持 JDK 6 以及更高版本的 JDK
支持 Linux/Mac/Winodws 操作系统
采用命令行交互模式同时提供丰富的 Tab 自动补全功能方便进行问题的定位和诊断
支持 WebConsole在某些复杂的情况下打通 HTTP 路由就可以访问
当我们遇到以下类似问题而束手无策时可以使用 Arthas 来帮助我们解决
这个类从哪个 jar 包加载的为什么会报各种类相关的 Exception
我改的代码为什么没有执行到难道是我没 commit分支搞错了
遇到问题无法在线上 Debug难道只能通过加日志再重新发布吗
线上遇到某个用户的数据处理有问题但线上同样无法 debug线下无法重现
是否有一个全局视角来查看系统的运行状况
有什么办法可以监控到 JVM 的实时运行状态
怎么快速定位应用的热点生成火焰图
Arthas 官方提供了基础教程和进阶教程以及网页版的命令行模拟器跟着执行一遍很容易了解相关的功能
下面我们跟着教程来体验一下
下载与安装
首先我们来安装 Arthas
# 准备目录
mkdir -p /usr/local/tools/arthas
cd /usr/local/tools/arthas
# 执行安装脚本
curl -L https://alibaba.github.io/arthas/install.sh | sh
······
命令行启动
# 启动
./as.sh -h
Error: telnet is not installed. Try to use java -jar arthas-boot.jar
# 安装telnet
# yum -y install telnet
# brew install telnet
# 再次执行
./as.sh
通过 jar 启动
# 进入jar目录
cd ~/.arthas/lib/3.1.7/arthas/
# 通过jar启动 arthas
java -jar arthas-boot.jar
使用示例
启动之后显示的信息大致如下图所示
然后我们输入需要连接Attach JVM 进程例如 1然后回车
如果需要退出输入 exit 即可
接着我们输入 help 命令查看帮助返回的信息大致如下
[arthas@27350]$ help
NAME DESCRIPTION
help Display Arthas Help
keymap Display all the available keymap for the specified connection.
sc Search all the classes loaded by JVM
sm Search the method of classes loaded by JVM
classloader Show classloader info
jad Decompile class
getstatic Show the static field of a class
monitor Monitor method execution statistics, e.g. total/success/failure count, average rt, fail rate, etc.
stack Display the stack trace for the specified class and method
thread Display thread info, thread stack
trace Trace the execution time of specified method invocation.
watch Display the input/output parameter, return object, and thrown exception of specified method invocation
tt Time Tunnel
jvm Display the target JVM information
ognl Execute ognl expression.
mc Memory compiler, compiles java files into bytecode and class files in memory.
redefine Redefine classes. @see Instrumentation#redefineClasses(ClassDefinition...)
dashboard Overview of target jvm's thread, memory, gc, vm, tomcat info.
dump Dump class byte array from JVM
heapdump Heap dump
options View and change various Arthas options
cls Clear the screen
reset Reset all the enhanced classes
version Display Arthas version
shutdown Shutdown Arthas server and exit the console
stop Stop/Shutdown Arthas server and exit the console. Alias for shutdown.
session Display current session information
sysprop Display, and change the system properties.
sysenv Display the system env.
vmoption Display, and update the vm diagnostic options.
logger Print logger info, and update the logger level
history Display command history
cat Concatenate and print files
pwd Return working directory name
mbean Display the mbean information
grep grep command for pipes.
profiler Async Profiler. https://github.com/jvm-profiling-tools/async-profiler
这里列出了支持的命令如果要查看某个命令的帮助信息怎么办呢 可以使用 help xxx 的形式例如
help thread
如果查看 JVM 信息输入命令 jvm 即可
环境变量 sysenv
查看线程信息输入命令 thread
查看某个线程的信息
[arthas@27350]$ thread 1
"main" Id=1 TIMED_WAITING
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at demo.jvm0209.RandomSample.main(Unknown Source)
查看 JVM 选项 vmoption
某些选项可以设置这里给出了示例 vmoption PrintGCDetails true
查找类 sc
反编译代码 jad
堆内存转储 heapdump
跟踪方法执行时间 trace
观察方法执行 watch
可以看到支持条件表达式类似于代码调试中的条件断点 功能非常强大并且作为一个 JVM 分析的集成环境使用起来也比一般工具方便更多功能请参考 Arthas 用户文档
抽样分析器Profilers
下面介绍分析器profilersOracle 官方翻译是抽样器”)。
相对于前面的工具分析器只关心 GC 中的一部分领域本节我们也只简单介绍分析器相关的 GC 功能
需要注意不要认为分析器适用于所有的场景分析器有时确实作用很大比如检测代码中的 CPU 热点时但某些情况使用分析器不一定是个好方案
GC 调优来说也是一样的要检测是否因为 GC 而引起延迟或吞吐量问题时不需要使用分析器前面提到的工具jstat 或原生/可视化 GC 日志就能更好更快地检测出是否存在 GC 问题.。特别是从生产环境中收集性能数据时最好不要使用分析器因为性能开销非常大对正在运行的生产系统会有影响
如果确实需要对 GC 进行优化那么分析器就可以派上用场了可以对 Object 的创建信息一目了然换个角度看如果 GC 暂停的原因不在某个内存池中那就只会是因为创建对象太多了所有分析器都能够跟踪对象分配via allocation profiling根据内存分配的轨迹让你知道 实际驻留在内存中的是哪些对象
分配分析能定位到在哪个地方创建了大量的对象使用分析器辅助进行 GC 调优的好处是能确定哪种类型的对象最占用内存以及哪些线程创建了最多的对象
下面我们通过实例介绍 3 种分配分析器hprofJVisualVM AProf实际上还有很多分析器可供选择有商业产品也有免费工具但其功能和应用基本上都是类似的
hprof
hprof 分析器内置于 JDK 之中在各种环境下都可以使用一般优先使用这款工具
性能分析工具——HPROF 简介
https://github.com/cncounter/translation/blob/master/tiemao*201720*hprof/20_hprof.md
HPROF 参考文档
https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr008.html
要让 hprof 和程序一起运行需要修改启动脚本类似这样
java -agentlib:hprof=heap=sites com.yourcompany.YourApplication
在程序退出时会将分配信息 dump转储到工作目录下的 java.hprof.txt 文件中使用文本编辑器打开并搜索SITES BEGIN关键字可以看到
SITES BEGIN (ordered by live bytes) Tue Dec 8 11:16:15 2015
percent live alloc'ed stack class
rank self accum bytes objs bytes objs trace name
1 64.43% 4.43% 8370336 20121 27513408 66138 302116 int[]
2 3.26% 88.49% 482976 20124 1587696 66154 302104 java.util.ArrayList
3 1.76% 88.74% 241704 20121 1587312 66138 302115 eu.plumbr.demo.largeheap.ClonableClass0006
... 部分省略 ...
SITES END
从以上片段可以看到allocations 是根据每次创建的对象数量来排序的第一行显示所有对象中有 64.43% 的对象是整型数组 (int[])在标识为 302116 的位置创建搜索TRACE 302116可以看到
TRACE 302116:
eu.plumbr.demo.largeheap.ClonableClass0006.<init>(GeneratorClass.java:11)
sun.reflect.GeneratedConstructorAccessor7.newInstance(<Unknown Source>:Unknown line)
sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
java.lang.reflect.Constructor.newInstance(Constructor.java:422)
现在,知道有 64.43% 的对象是整数数组,在 ClonableClass0006 类的构造函数中,第 11 行的位置,接下来就可以优化代码,以减少 GC 的压力。
Java VisualVM
本章前面的第一部分,在监控 JVM 的 GC 行为工具时介绍了 JVisualVM本节介绍其在分配分析上的应用。
JVisualVM 通过 GUI 的方式连接到正在运行的 JVM。连接上目标 JVM 之后:
打开“工具”–>“选项”菜单,点击 性能分析Profiler标签新增配置选择 Profiler 内存确保勾选了“Record allocations stack traces”记录分配栈跟踪
勾选“Settings”设置复选框在内存设置标签下修改预设配置。
点击“Memory”内存按钮开始进行内存分析。
让程序运行一段时间,以收集关于对象分配的足够信息。
单击下方的“Snapshot”快照按钮可以获取收集到的快照信息。
完成上面的步骤后,可以得到类似这样的信息:
上图按照每个类被创建的对象数量多少来排序。看第一行可以知道,创建的最多的对象是 int[] 数组。鼠标右键单击这行,就可以看到这些对象都在哪些地方创建的:
与 hprof 相比JVisualVM 更加容易使用 —— 比如上面的截图中,在一个地方就可以看到所有 int[] 的分配信息,所以多次在同一处代码进行分配的情况就很容易发现。
AProf
AProf 是一款重要的分析器,是由 Devexperts 开发的 AProf。内存分配分析器 AProf 也被打包为 Java agent 的形式。
用 AProf 分析应用程序,需要修改 JVM 启动脚本,类似这样:
java -javaagent:/path-to/aprof.jar com.yourcompany.YourApplication
重启应用之后,工作目录下会生成一个 aprof.txt 文件。此文件每分钟更新一次,包含这样的信息:
========================================================================================================================
TOTAL allocation dump for 91,289 ms (0h01m31s)
Allocated 1,769,670,584 bytes in 24,868,088 objects of 425 classes in 2,127 locations
========================================================================================================================
Top allocation-inducing locations with the data types allocated from them
------------------------------------------------------------------------------------------------------------------------
eu.plumbr.demo.largeheap.ManyTargetsGarbageProducer.newRandomClassObject: 1,423,675,776 (80.44%) bytes in 17,113,721 (68.81%) objects (avg size 83 bytes)
int[]: 711,322,976 (40.19%) bytes in 1,709,911 (6.87%) objects (avg size 416 bytes)
char[]: 369,550,816 (20.88%) bytes in 5,132,759 (20.63%) objects (avg size 72 bytes)
java.lang.reflect.Constructor: 136,800,000 (7.73%) bytes in 1,710,000 (6.87%) objects (avg size 80 bytes)
java.lang.Object[]: 41,079,872 (2.32%) bytes in 1,710,712 (6.87%) objects (avg size 24 bytes)
java.lang.String: 41,063,496 (2.32%) bytes in 1,710,979 (6.88%) objects (avg size 24 bytes)
java.util.ArrayList: 41,050,680 (2.31%) bytes in 1,710,445 (6.87%) objects (avg size 24 bytes)
... cut for brevity ...
上面的输出是按照 size 进行排序的。可以看出80.44% 的 bytes 和 68.81% 的 objects 是在 ManyTargetsGarbageProducer.newRandomClassObject() 方法中分配的。其中int[] 数组占用了 40.19% 的内存,是最大的一个。
继续往下看,会发现 allocation traces分配痕迹相关的内容也是以 allocation size 排序的:
Top allocated data types with reverse location traces
------------------------------------------------------------------------------------------------------------------------
int[]: 725,306,304 (40.98%) bytes in 1,954,234 (7.85%) objects (avg size 371 bytes)
eu.plumbr.demo.largeheap.ClonableClass0006.: 38,357,696 (2.16%) bytes in 92,206 (0.37%) objects (avg size 416 bytes)
java.lang.reflect.Constructor.newInstance: 38,357,696 (2.16%) bytes in 92,206 (0.37%) objects (avg size 416 bytes)
eu.plumbr.demo.largeheap.ManyTargetsGarbageProducer.newRandomClassObject: 38,357,280 (2.16%) bytes in 92,205 (0.37%) objects (avg size 416 bytes)
java.lang.reflect.Constructor.newInstance: 416 (0.00%) bytes in 1 (0.00%) objects (avg size 416 bytes)
... cut for brevity ...
可以看到int[] 数组的分配,在 ClonableClass0006 构造函数中继续增大。
和其他工具一样AProf 揭露了 分配的大小以及位置信息allocation size and locations从而能够快速找到最耗内存的部分。
在我们看来AProf 是非常有用的分配分析器,因为它只专注于内存分配,所以做得最好。同时这款工具是开源免费的,资源开销也最小。
参考链接
Linux 内核 OOM 机制的详细分析
Linux 找出最有可能被 OOM Killer 杀掉的进程
性能分析工具——HPROF 简介
HPROF 参考文档