前言
基本上静态分析可以做的事情,arthas 也做到了,athars 运行后也会提供一些metrics 数据,可以采集后进行动态分析。
线上常见问题排查手册 arthas idea plugin 这个解决问题的创新、死磕精神特别牛逼。如何使用Arthas提高日常开发效率?
JVM调优好用的内存分析工具 性能优化思路及常用工具及手段 非常经典。 三万字长文:JVM内存问题排查Cookbook
dashboard 与 JVM 运行指标
https://qiyeyun.gitbook.io/yyydata/jvm/jvm-yun-hang-zhi-biao
分析cpu问题靠火焰图。PS:一些大厂的工具可以给出java应用的cpu火焰图。
常见的 JVM 内存热点产生原因主要包括以下几类,每种原因背后都隐藏着复杂的机制。
- 对象创建过于频繁:如果存在大量短生命周期的对象被频繁地创建与销毁,这将导致垃圾回收器(Garbage Collector, GC)频繁工作以清理不再使用的对象空间。这种情况下,即使GC算法本身效率很高,但由于其执行频率过高,仍然会对系统性能造成显著影响。例如,在循环体内部创建临时变量而不进行复用。为了缓解这一问题,可以考虑使用对象池技术或尽量减少不必要的对象实例化操作。还有一种情况是上游系统请求流量飙升,常见于各类促销/秒杀活动,此时可以考虑添加机器资源,或者做限流降级。
- 大对象分配:当应用程序中申请大对象时(如大型数组),通常会被直接分配到老年代而非新生代区域。虽然这样做可以避免短期内因这些大对象而触发 YoungGC,但如果此类对象数量较多,则可能会迅速填满老年代空间,进而迫使Full GC发生。Full GC会暂停所有用户线程并扫描整个堆区,因此对应用性能的影响尤为严重。针对这种情况,建议评估是否真的需要如此大的数据结构,并探索更高效的数据表示方式。
- 内存泄漏:尽管Java具有自动内存管理功能,但不当的设计模式或编程习惯仍可能导致内存泄露问题。比如,静态集合类持有外部引用、未关闭的数据库连接等都是常见场景。随着时间推移,这些无法被正常回收的对象逐渐积累起来,最终耗尽可用堆空间。解决之道,首先通过一些监控分析工具定界不断增长的内存位置来源,判断内存泄露是发生在堆内还是堆外,如果是堆内可以借助诸如jmap等工具下载内存快照,检查堆内占比高的内存对象,并结合代码分析根因。如果是堆外部分出现了内存稳定增长,此时需要借助一些外部诊断工具,比如 NMT(Native Memory Tracking)等对堆外内存申请情况进行监测,分析可能的原因。
- 不合理的堆大小设置:JVM启动参数中的-Xms(初始堆大小)和-Xmx(最大堆大小)对于控制内存使用至关重要。如果这两个值设置得过低,则可能因为频繁的GC活动而降低程序性能;反之,若设定得过高,则又会浪费宝贵的物理内存资源。理想状态下,应根据实际业务需求及硬件配置情况合理调整这两个参数,一般设置为总内存大小的1/2左右,然后留1/2给非堆部分使用。此外,-XX:NewRatio等选项的设置也很重要,需要基于其去平衡新生代与老年代的比例关系,从而达到最佳性能状态。
- 加载的 class 数目太多或体积太大:永久代(Permanent Generation,JDK 1.8 使用 Metaspace 替换)的使用量与加载到内存的 class 的数量/大小正相关。当加载的 class 数目太多或体积太大时,会导致 永久代用满,从而导致内存溢出报错。可以通过 -XX:MaxMetaspaceSize / -XX:MaxPermSize 上调永久代大小。
生产环境需要常态化跟踪 JVM 内存变化,如何第一时间发现 JVM 内存问题,并快速定位止血,整体思路与 CPU 热点优化类似,主要包括以下步骤:
- 通过 JVM 监控/告警发现内存或 GC 异常,分析新生代、老年代、Metaspace、DirectBuffer 等内存变化。
- 通过持续剖析-内存热点功能,常态化记录每个方法的内存对象分配占比火焰图,比如下图中AllocMemoryAction.runBusiness() 方法消耗了 99.92% 的内存对象分配。
- 内存快照记录了相关时刻的堆内存对象占用和进程类加载等信息。阿里云 ARMS 提供了一种开箱即用的内存快照白屏化操作功能,让快照创建、获取和分析更加简单便捷。结合阿里云 ATP 分析工具,实现了 JVM 内存对象与引用关系的深入分析和诊断。
热更新代码
Step1 jad命令反编译到磁盘文件
jad --source-only demo.MathGame > /tmp/MathGame.java
Step2 使用文本编辑器修改代码
vi /tmp/MathGame.java
public static void print(int number, List<Integer> primeFactors) {
StringBuffer sb = new StringBuffer("" + number + "=");
Iterator<Integer> iterator = primeFactors.iterator();
while (iterator.hasNext()) {
int factor = iterator.next();
sb.append(factor).append('*');
}
if (sb.charAt(sb.length() - 1) == '*') {
sb.deleteCharAt(sb.length() - 1);
}
System.out.println("MyTest.......");
}
Step3 mc命令来内存编译修改过的代码
$ mc /tmp/MathGame.java -d /tmp
Memory compiler output:
/tmp/demo/MathGame.class
Step4 用redefine命令加载新的字节码
$ redefine /tmp/demo/MathGame.class
redefine success, size: 1
现在看一下程序日志
illegalArgumentCount:96218, number is: -169877, need >= 2
illegalArgumentCount:96219, number is: -57731, need >= 2
MyTest.......
illegalArgumentCount:96220, number is: -207843, need >= 2
illegalArgumentCount:96221, number is: -193695, need >= 2
MyTest.......
illegalArgumentCount:96222, number is: -19514, need >= 2
illegalArgumentCount:96223, number is: -199441, need >= 2
illegalArgumentCount:96224, number is: -110791, need >= 2
MyTest.......
illegalArgumentCount:96225, number is: -116154, need >= 2
MyTest.......
MyTest.......
MyTest.......
MyTest.......
MyTest.......
MyTest.......
jvm attach 机制
JVM Attach机制实现Attach机制是jvm提供一种jvm进程间通信(这里用的是套接字socket)的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作。
static AttachOperationFunctionInfo funcs[] = {
{ "agentProperties", get_agent_properties },
{ "datadump", data_dump },
{ "dumpheap", dump_heap },
{ "load", JvmtiExport::load_agent_library },
{ "properties", get_system_properties },
{ "threaddump", thread_dump },
{ "inspectheap", heap_inspection },
{ "setflag", set_flag },
{ "printflag", print_flag },
{ "jcmd", jcmd },
{ NULL, NULL }
};
Attach_listener 线程的逻辑
static void Attach_listener_thread_entry(JavaThread* thread, TRAPS) {
...
for (;;) {
AttachOperation* op = AttachListener::dequeue();
...
// find the function to dispatch too
AttachOperationFunctionInfo* info = NULL;
for (int i=0; funcs[i].name != NULL; i++) {
const char* name = funcs[i].name;
assert(strlen(name) <= AttachOperation::name_length_max, "operation <= name_length_max");
if (strcmp(op->name(), name) == 0) {
info = &(funcs[i]);
break;
}
}
// check for platform dependent Attach operation
if (info == NULL) {
info = AttachListener::pd_find_operation(op->name());
}
if (info != NULL) {
// dispatch to the function that implements this operation
res = (info->func)(op, &st);
} else {
st.print("Operation %s not recognized!", op->name());
res = JNI_ERR;
}
// operation complete - send result and output to client
op->complete(res, &st);
}
}
- 从队列里不断取AttachOperation
- 根据 AttachOperation 得到 AttachOperationFunctionInfo
- 执行AttachOperationFunctionInfo 对应的方法并返回结果
排查示例
使用 Arthas 排查 SpringBoot 诡异耗时的 Bug
一个网络问题排查
现象: rpc客户端read timeout。 那么问题可能出在网络层、rpc框架层和上层业务方
监控本机 eth0 网卡与目标主机的往来数据包tcpdump -i eth0 -nn 'host 目标主机ip'
可以观察到 在客户端数据发出后,服务端很快回复了ack,说明数据包顺利送达到了 服务端。但服务端的响应在很长时间之后才返回。 所以初步定位是服务端处理的问题
观察服务端日志,已知的业务日志收到请求的时间与 网络抓包的时间间隔很长(这里值得学习的一点就是网络抓包时间与 服务日志时间放在一起比对,以前没这么想过),基本可以判断问题出在 接收数据包与 框架调用业务逻辑之间,即出在框架层。
然后再使用arthas trace 指令跟踪框架层入口方法的执行逻辑,即可查看哪一个步骤执行的耗时时间最长。
启动/类加载失败
开发报错:java.lang.IllegalStateException: Failed to introspect Class [类全名] from ClassLoader xx
,
怀疑是 classpath 下有多个jar 包含该类,通过 arthas sc -d 类全名
找到此次加载类所用的 jar 名,到classpath 下检索,jar -vtf jar名称 | grep 类全名
发现了相关的多个jar 都包含 该类。
tomcat 假死
arthas thread 可以查看jvm 所有线程的状态:
thread Threads Total: 512, NEW: 0, RUNNABLE: 175, BLOCKED: 0, WAITING: 229, TIMED_WAITING: 101, TERMINATED: 0, Internal threads: 7
发现 WAITING 和 TIMED_WAITING 线程较多,猜测有可能是 tomcat 线程池耗尽进而无法接受新的请求。根据线程 stack 发现这些线程都执行了 xx.park
,进而可以确定引起 park的位置。