# Java进阶JVM
# 1. JVM 基础 - 类字节码详解
# 1.1 什么是字节码?
可以把Java程序想象成一个菜谱,源代码(.java文件)就是人类能看懂的菜谱文字,而字节码(.class文件)则是厨房机器能直接执行的指令码。
// 源代码示例
public class Hello {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
System.out.println(c);
}
}
2
3
4
5
6
7
8
9
编译后使用 javap -c Hello.class 查看字节码:
public static void main(java.lang.String[]);
Code:
0: iconst_1 // 将int型1推送至栈顶
1: istore_1 // 将栈顶int值存入局部变量1
2: iconst_2 // 将int型2推送至栈顶
3: istore_2 // 将栈顶int值存入局部变量2
4: iload_1 // 从局部变量1装载int值到栈顶
5: iload_2 // 从局部变量2装载int值到栈顶
6: iadd // 栈顶两个int值相加,结果入栈
7: istore_3 // 将栈顶int值存入局部变量3
8: getstatic #2 // 获取System.out静态字段
11: iload_3 // 加载局部变量3到栈顶
12: invokevirtual #3 // 调用println方法
15: return
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.2 字节码文件结构
.class文件就像是一个精心设计的集装箱,内部结构非常规整:
ClassFile {
u4 magic; // 魔数:0xCAFEBABE
u2 minor_version; // 次版本号
u2 major_version; // 主版本号
u2 constant_pool_count; // 常量池大小
cp_info constant_pool[constant_pool_count-1]; // 常量池
u2 access_flags; // 访问标志
u2 this_class; // 类索引
u2 super_class; // 父类索引
u2 interfaces_count; // 接口数量
u2 interfaces[interfaces_count]; // 接口索引
u2 fields_count; // 字段数量
field_info fields[fields_count]; // 字段表
u2 methods_count; // 方法数量
method_info methods[methods_count]; // 方法表
u2 attributes_count; // 属性数量
attribute_info attributes[attributes_count]; // 属性表
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
常量池示例:
常量池(像字典一样存储所有符号引用):
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #23.#24 // java/io/PrintStream.println:(I)V
#4 = Class #25 // Hello
#5 = Class #26 // java/lang/Object
#6 = Utf8 <init>
...
#20 = Utf8 ()V
2
3
4
5
6
7
8
9
# 1.3 字节码指令分类
| 指令类别 | 助记符前缀 | 功能说明 | 示例指令 |
|---|---|---|---|
| 加载存储 | i/l/f/d/a load/store | 局部变量与操作数栈间传输 | iload_1, astore_2 |
| 算术运算 | i/l/f/d add/sub/mul/div | 算术运算 | iadd, fmul |
| 类型转换 | i2l, f2d, d2i | 数值类型转换 | i2l (int转long) |
| 对象操作 | new, getfield, putfield | 对象创建与字段访问 | new, getstatic |
| 数组操作 | newarray, arraylength | 数组操作 | newarray, aaload |
| 控制转移 | if_icmpeq, goto | 条件与无条件跳转 | if_icmpeq, goto |
| 方法调用 | invokevirtual/invokestatic | 方法调用 | invokevirtual |
| 栈操作 | pop, dup, swap | 操作数栈管理 | pop, dup_x1 |
# 2. JVM 基础 - 字节码的增强技术
# 2.1 字节码增强的三种时机
字节码增强的三个时机:
1. 编译期增强(AspectJ编译器)
.java ───编译时织入──→ .class
↓
增强后的字节码
2. 类加载期增强(Java Agent)
.class ───ClassLoader──→ 内存
↓ ↑
└─Java Agent拦截─┘
3. 运行时增强(CGLib/ASM)
已加载的类 ───动态修改──→ 增强类
↓
运行时创建子类
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2.2 ASM 字节码操作示例
ASM就像外科手术刀,可以直接操作字节码:
import org.objectweb.asm.*;
public class MethodMonitorVisitor extends ClassVisitor {
public MethodMonitorVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc,
signature, exceptions);
if (!name.equals("<init>") && mv != null) {
// 为每个方法添加监控逻辑
mv = new MethodMonitorAdapter(mv, name);
}
return mv;
}
class MethodMonitorAdapter extends MethodVisitor {
private String methodName;
public MethodMonitorAdapter(MethodVisitor mv, String methodName) {
super(Opcodes.ASM9, mv);
this.methodName = methodName;
}
@Override
public void visitCode() {
// 方法开始处插入代码
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"java/lang/System", "nanoTime", "()J", false);
mv.visitVarInsn(Opcodes.LSTORE, 100); // 存储开始时间
super.visitCode();
}
@Override
public void visitInsn(int opcode) {
// 在返回指令前插入代码
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
|| opcode == Opcodes.ATHROW) {
// 计算耗时并打印
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"java/lang/System", "nanoTime", "()J", false);
mv.visitVarInsn(Opcodes.LLOAD, 100);
mv.visitInsn(Opcodes.LSUB);
mv.visitVarInsn(Opcodes.LSTORE, 101); // 存储耗时
// 调用监控方法
mv.visitLdcInsn(methodName);
mv.visitVarInsn(Opcodes.LLOAD, 101);
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"Monitor", "record", "(Ljava/lang/String;J)V", false);
}
super.visitInsn(opcode);
}
}
}
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
# 2.3 字节码增强应用场景
应用场景示例 - 方法监控:
原始方法:
┌────────────────────────────────┐
│ public void doBusiness() { │
│ // 业务逻辑 │
│ System.out.println("Done");│
│ } │
└────────────────────────────────┘
增强后的字节码:
┌────────────────────────────────────────────┐
│ public void doBusiness() { │
│ long start = System.nanoTime(); │
│ // 业务逻辑 │
│ System.out.println("Done"); │
│ long cost = System.nanoTime() - start; │
│ Monitor.record("doBusiness", cost); │
│ } │
└────────────────────────────────────────────┘
实际执行的指令流:
iconst_0
istore_1
aload_0
invokevirtual #5 // 业务方法
getstatic #6 // System.out
ldc #7 // "Done"
invokevirtual #8 // println
return
↓ 增强后 ↓
invokestatic #9 // System.nanoTime()
lstore 100
iconst_0
istore_1
aload_0
invokevirtual #5
getstatic #6
ldc #7
invokevirtual #8
invokestatic #9 // System.nanoTime()
lload 100
lsub
lstore 101
ldc #10 // "doBusiness"
lload 101
invokestatic #11 // Monitor.record
return
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
# 3. JVM 基础 - Java 类加载机制
# 3.1 类加载的完整过程
把类加载想象成工厂的生产流水线:
类加载流水线:
[ 加载 Loading ] → [ 验证 Verification ] → [ 准备 Preparation ]
↓ ↓ ↓
找.class文件 检查格式安全 分配内存设默认值
↓ ↓ ↓
[ 解析 Resolution ] → [ 初始化 Initialization ] → [ 使用 Using ]
↓ ↓ ↓
符号引用转直接引用 执行<clinit>() 正常使用
↓ ↓
链接完成 静态块执行
2
3
4
5
6
7
8
9
10
# 3.2 双亲委派模型
类加载器的层次关系就像一个公司的汇报体系:
双亲委派模型层级:
Bootstrap ClassLoader (董事长)
↑
Extension ClassLoader (副总裁)
↑
Application ClassLoader (部门经理)
↑
Custom ClassLoader (普通员工)
工作流程示例:CustomClassLoader加载com.example.MyClass
1. CustomClassLoader → 问Application: "你加载过吗?"
2. Application → 问Extension: "你加载过吗?"
3. Extension → 问Bootstrap: "你加载过吗?"
4. Bootstrap: "没有,你加载吧"
5. Extension: "没有,你加载吧"
6. Application: "没有,我试试...找到了!"
7. Application加载并返回,CustomClassLoader直接使用
为什么要这样设计?
1. 避免重复加载:父亲加载过了,儿子直接用
2. 安全隔离:核心类不会被自定义类篡改
3. 保证唯一性:全限定名相同的类只加载一次
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 3.3 自定义类加载器实现
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 1. 读取字节码文件
byte[] data = loadClassData(name);
// 2. 定义类
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
private byte[] loadClassData(String className) throws IOException {
// 转换类名到文件路径:com.example.Test → com/example/Test.class
String path = className.replace('.', '/') + ".class";
String fullPath = classPath + "/" + path;
try (InputStream is = new FileInputStream(fullPath);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
return baos.toByteArray();
}
}
// 打破双亲委派:重写loadClass方法
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
// 2. 对于某些包下的类,先自己尝试加载
if (name.startsWith("com.example.")) {
try {
c = findClass(name);
} catch (ClassNotFoundException e) {
// 自己找不到,再委托给父加载器
}
}
// 3. 其他类使用双亲委派
if (c == null) {
if (getParent() != null) {
c = getParent().loadClass(name);
} else {
c = findBootstrapClassOrNull(name);
}
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
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
# 4. JVM 基础 - JVM 内存结构
# 4.1 JVM 内存布局全景图
把JVM内存想象成一个现代化工厂的布局:
JVM 内存工厂布局:
┌─────────────────────────────────────────────────────────┐
│ JVM Process │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌─────────────────────────────┐ │
│ │ Metaspace │ │ Heap │ │
│ │ (仓库区) │ │ (生产车间) │ │
│ │ │ │ ┌─────────┐ ┌──────────┐ │ │
│ │ Class信息 │ │ │ Young │ │ Old │ │ │
│ │ 方法区元数据 │ │ │ Gen │ │ Gen │ │ │
│ │ 运行时常量池 │ │ │ Eden │ │ │ │ │
│ │ │ │ │ S0 S1 │ │ │ │ │
│ └──────────────┘ │ └─────────┘ └──────────┘ │ │
│ └─────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Thread 1 Thread 2 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ 栈内存 │ │ 栈内存 │ │ │
│ │ │ (操作台) │ │ (操作台) │ │ │
│ │ │ ┌──────────┐│ │ ┌──────────┐│ │ │
│ │ │ │ 栈帧 ││ │ │ 栈帧 ││ │ │
│ │ │ │局部变量表 ││ │ │局部变量表 ││ │ │
│ │ │ │操作数栈 ││ │ │操作数栈 ││ │ │
│ │ │ │动态链接 ││ │ │动态链接 ││ │ │
│ │ │ │返回地址 ││ │ │返回地址 ││ │ │
│ │ │ └──────────┘│ │ └──────────┘│ │ │
│ │ │ ... │ │ ... │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ │ ┌──────────────────────────────────────────────┐│ │
│ │ │ 程序计数器 (PC Register) ││ │
│ │ │ (生产线当前位置指示器) ││ │
│ │ └──────────────────────────────────────────────┘│ │
│ │ ┌──────────────────────────────────────────────┐│ │
│ │ │ 本地方法栈 ││ │
│ │ │ (特殊设备操作区) ││ │
│ │ └──────────────────────────────────────────────┘│ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
关键区域说明:
1. 堆(Heap):对象的生产和存储车间,GC主要工作区
2. 方法区(Metaspace):存放设计图纸(类信息)和标准件(常量)
3. 栈(Stack):每个工人的操作台,存放临时工具和半成品
4. 程序计数器:每个工人的工作进度指示器
5. 本地方法栈:操作特殊设备(C函数)的专用区域
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
# 4.2 栈帧详细结构
栈帧是方法调用的基本单位,就像工人在操作台上的一次操作:
一个完整的栈帧结构:
┌─────────────────────────────────────────────┐
│ 栈帧 (Stack Frame) │
├─────────────────────────────────────────────┤
│ 局部变量表 (Local Variable Array) │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │...│ │
│ └───┴───┴───┴───┴───┴───┴───┴───┘ │
│ 索引: 0-this, 1-arg1, 2-arg2, 3-local1 │
├─────────────────────────────────────────────┤
│ 操作数栈 (Operand Stack) │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ │ │ │ │ │ │ │ │ │
│ └───┴───┴───┴───┴───┴───┴───┴───┘ │
│ 顶部→ │
├─────────────────────────────────────────────┤
│ 动态链接 (Dynamic Linking) │
│ 指向运行时常量池中该栈帧所属方法的引用 │
├─────────────────────────────────────────────┤
│ 方法返回地址 (Return Address) │
│ 方法正常退出或异常退出的地址 │
└─────────────────────────────────────────────┘
示例:执行 a = b + c 时栈帧的变化
初始状态:
局部变量表:[this, b=10, c=20, a=?]
操作数栈:[]
执行 iload_1 (加载b):
局部变量表:[this, b=10, c=20, a=?]
操作数栈:[10]
执行 iload_2 (加载c):
局部变量表:[this, b=10, c=20, a=?]
操作数栈:[10, 20]
执行 iadd (相加):
局部变量表:[this, b=10, c=20, a=?]
操作数栈:[30]
执行 istore_3 (存储结果):
局部变量表:[this, b=10, c=20, a=30]
操作数栈:[]
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
# 4.3 堆内存分代结构详解
堆内存采用分代设计,就像工厂的不同生产区域:
堆内存分代布局 (以G1之前的传统分代为例):
┌─────────────────────────────────────────────────────┐
│ Heap (堆) │
├─────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Young Generation │ │
│ │ (新产品试产车间) │ │
│ │ ┌─────────────┬─────────────┬─────────────┐ │ │
│ │ │ Eden │ Survivor │ Survivor │ │ │
│ │ │ (新品区) │ S0 │ S1 │ │ │
│ │ │ │ (质检区) │ (质检区) │ │ │
│ │ │ 新对象出生地│ 存活对象暂存│ 存活对象暂存│ │ │
│ │ │ 90%新对象 │ 10% │ 10% │ │ │
│ │ └─────────────┴─────────────┴─────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Old Generation │ │
│ │ (成熟产品仓库) │ │
│ │ │ │
│ │ 长期存活对象存放区 │ │
│ │ 大对象直接分配(避免复制开销) │ │
│ │ │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Permanent Generation │ │
│ │ (JDK8之前,现为Metaspace) │ │
│ │ 存放类元数据、常量池等 │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
对象生命周期流程:
1. 新对象诞生 → 分配在Eden区
2. Minor GC时 → 存活对象复制到Survivor S0
3. 再次Minor GC → S0存活对象复制到S1,年龄+1
4. 年龄达到阈值(默认15) → 晋升到Old Generation
5. Major GC/Full GC → 清理Old Generation
内存分配示例:
Object obj1 = new Object(); // 分配在Eden
Object obj2 = new Object(); // 分配在Eden
// Eden区满了,触发Minor GC
// obj1存活,复制到S0,年龄=1
// obj2死亡,被回收
// 继续分配新对象...
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
# 5. JVM 基础 - Java 内存模型详解
# 5.1 JMM与硬件内存架构的关系
Java内存模型是抽象规范,硬件内存是具体实现:
Java内存模型 vs 硬件内存架构:
┌─────────────────────────────────────────────────────┐
│ Java Memory Model (JMM) │
│ (程序员视角的逻辑内存模型) │
├─────────────────────────────────────────────────────┤
│ 每个线程有自己的工作内存,共享主内存 │
│ │
│ Thread 1 Thread 2 Thread N │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 工作内存 │ │ 工作内存 │ │ 工作内存 │ │
│ │ (缓存) │ │ (缓存) │ │ (缓存) │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └────────────────┴─────────────────┘ │
│ │ │
│ ┌───┴───┐ │
│ │ 主内存 │ │
│ │(共享) │ │
│ └───────┘ │
└─────────────────────────────────────────────────────┘
对应的硬件架构:
┌─────────────────────────────────────────────────────┐
│ Hardware Memory Architecture │
│ (物理硬件内存结构) │
├─────────────────────────────────────────────────────┤
│ │
│ CPU Core 1 CPU Core 2 CPU Core N │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ L1 Cache│ │ L1 Cache│ │ L1 Cache│ │
│ │ L2 Cache│ │ L2 Cache│ │ L2 Cache│ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └────────────────┴─────────────────┘ │
│ │ │
│ ┌───┴───┐ │
│ │ L3 Cache│ │
│ │ (共享) │ │
│ └────┬───┘ │
│ │ │
│ ┌────┴────┐ │
│ │ RAM │ │
│ │ (主内存) │ │
│ └─────────┘ │
└─────────────────────────────────────────────────────┘
内存访问示例:两个线程读写共享变量
初始状态:主内存中 x = 0
Thread 1执行:x = 1
1. 从主内存读取 x=0 到工作内存
2. 在工作内存修改 x=1
3. 写回主内存 x=1 (时间点T1)
Thread 2执行:读取 x
1. 从主内存读取 x 到工作内存
2. 如果读取发生在T1后,看到 x=1
3. 如果读取发生在T1前,看到 x=0 (可见性问题)
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
# 5.2 happens-before 规则
happens-before就像法律条文,规定了操作间的可见性保证:
happens-before关系(如果A happens-before B,那么A的操作对B可见):
┌─────────────────────────────────────────────────────────┐
│ happens-before 规则 │
├─────────────────────────────────────────────────────────┤
│ 1. 程序顺序规则:同一线程中的每个操作都happens-before于 │
│ 该线程中任意的后续操作 │
│ Thread内:操作1 → 操作2 → 操作3 │
│ 可见性保证:1对2、3可见,2对3可见 │
│ │
│ 2. 监视器锁规则:对一个锁的解锁happens-before于随后对 │
│ 这个锁的加锁 │
│ unlock(锁A) → lock(锁A) │
│ 解锁前的写操作 → 对加锁线程可见 │
│ │
│ 3. volatile变量规则:对一个volatile域的写happens-before│
│ 于任意后续对这个volatile域的读 │
│ write volatile v = 1 → read volatile v │
│ 写操作 → 对所有读线程可见 │
│ │
│ 4. 传递性规则:如果A happens-before B,且B happens- │
│ before C,那么A happens-before C │
│ A → B → C ⇒ A → C │
│ │
│ 5. 线程启动规则:Thread.start() happens-before于 │
│ 线程中的任何操作 │
│ main: thread.start() → thread: run() │
│ main线程的start前操作 → 对子线程可见 │
│ │
│ 6. 线程终止规则:线程中的所有操作都happens-before于 │
│ 其他线程检测到该线程已经终止 │
│ thread: 所有操作 → main: thread.join()返回 │
│ 子线程的操作 → 对join线程可见 │
└─────────────────────────────────────────────────────────┘
示例:volatile的happens-before保证
初始:boolean flag = false; // 非volatile
int value = 0;
Thread 1: Thread 2:
value = 42; while (!flag) {
flag = true; // 写 // 忙等待
}
print(value);
问题:Thread 2可能打印0,因为flag非volatile,value=42可能不可见
改为volatile:
volatile boolean flag = false;
Thread 1: Thread 2:
value = 42; while (!flag) {
flag = true; // volatile写
}
print(value);
保证:打印42,因为根据规则3和1:
1. value=42 happens-before flag=true (程序顺序规则)
2. flag=true happens-before flag读取 (volatile规则)
3. 传递性:value=42 happens-before flag读取
4. 所以value=42对Thread 2可见
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
# 6. GC - Java 垃圾回收基础知识
# 6.1 对象存活性判断
判断对象是否存活就像判断仓库里的物品是否还需要:
对象存活性判断方法:
1. 引用计数法(Python使用,Java不采用):
每个对象有个计数器,记录被引用次数
对象A (count=1) 对象B (count=2)
↑ ↑ ↑
引用1 引用2 引用3
问题:循环引用无法解决
对象A ↔ 对象B
count=1 count=1
但实际已无法访问,应被回收但计数不为0
2. 可达性分析法(Java使用):
从GC Roots出发,遍历引用链
GC Roots包括:
┌─ 栈中引用的对象 (局部变量表)
│ └─ Thread 1栈帧 → 对象A
├─ 静态属性引用的对象
│ └─ static Object → 对象B
├─ 常量引用的对象
│ └─ final static → 对象C
├─ JNI引用的Native对象
└─ 活跃线程引用的对象
可达性分析过程:
GC Roots集合: {R1, R2, R3}
↓
R1 → 对象A → 对象B → 对象C
↓
R2 → 对象D → 对象E
↓
R3 → 对象F (孤立)
标记结果:
存活对象:A, B, C, D, E (从Roots可达)
死亡对象:F (不可达)
3. 实际Java中的引用类型:
┌──────────────┬─────────────────────────────┐
│ 引用类型 │ 特点与GC行为 │
├──────────────┼─────────────────────────────┤
│ 强引用 │ Object obj = new Object() │
│ │ 永不回收,除非无强引用可达 │
├──────────────┼─────────────────────────────┤
│ 软引用 │ SoftReference<Object> ref │
│ │ 内存不足时回收,适合缓存 │
├──────────────┼─────────────────────────────┤
│ 弱引用 │ WeakReference<Object> ref │
│ │ 下次GC一定回收,适合缓存 │
├──────────────┼─────────────────────────────┤
│ 虚引用 │ PhantomReference<Object> ref│
│ │ 随时可能回收,用于跟踪GC │
└──────────────┴─────────────────────────────┘
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
# 6.2 垃圾回收算法
三大经典GC算法对比:
垃圾回收算法对比:
1. 标记-清除 (Mark-Sweep) - 老式清理法
步骤:
标记阶段:从GC Roots遍历,标记存活对象
[A][B][C][D][E][F][G]
✓ ✓ ✓ ✓ (存活标记)
清除阶段:清除未标记的对象,产生内存碎片
[A][B][ ][D][ ][F][ ]
空闲 空闲 空闲
优点:简单,老对象不用移动
缺点:碎片化,效率低
2. 复制 (Copying) - 搬家式清理
步骤:
内存分为两半:From空间和To空间
初始状态:
From空间:[A][B][C][D][ ][ ][ ][ ]
To空间: [ ][ ][ ][ ][ ][ ][ ][ ]
标记阶段:标记From空间的存活对象
[A][B][C][D] (A,C存活)
复制阶段:把存活对象复制到To空间
From空间:[A][ ][C][ ][ ][ ][ ][ ]
To空间: [A][C][ ][ ][ ][ ][ ][ ]
清理阶段:清空整个From空间
From空间:[ ][ ][ ][ ][ ][ ][ ][ ]
优点:无碎片,分配简单(指针碰撞)
缺点:浪费一半空间
3. 标记-整理 (Mark-Compact) - 压缩式清理
步骤:
标记阶段:标记存活对象
[A][B][C][D][E][F][G][H]
✓ ✓ ✓ ✓ (A,C,D,F存活)
整理阶段:存活对象向左移动,保持顺序
[A][C][D][F][ ][ ][ ][ ]
清理阶段:清理右边剩余空间
[A][C][D][F][ ][ ][ ][ ]
优点:无碎片,空间利用率高
缺点:移动对象成本高
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
# 7. GC - Java 垃圾回收器之G1详解
# 7.1 G1 整体架构
G1 (Garbage-First) 是一个面向服务端应用的垃圾回收器:
G1 堆内存布局:
┌─────────────────────────────────────────────────────────┐
│ G1 Heap (逻辑视图) │
├─────────────────────────────────────────────────────────┤
│ │
│ 整个堆被划分为多个大小相等的Region(默认2048个) │
│ 每个Region可以是以下类型之一: │
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ E │ │ S │ │ O │ │ H │ │ E │ │ S │ ... │
│ │den │ │urv │ │ld │ │umong│ │den │ │urv │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │
│ Region类型: │
│ E - Eden Region (新生代) │
│ S - Survivor Region (幸存者) │
│ O - Old Region (老年代) │
│ H - Humongous Region (大对象,连续多个Region) │
│ │
│ G1工作方式: │
│ 1. 并发标记全局垃圾分布 │
│ 2. 根据停顿时间目标,优先回收垃圾最多的Region │
│ 3. 复制存活对象到空闲Region,整理碎片 │
└─────────────────────────────────────────────────────────┘
G1 内存分配示例:
初始状态:空闲Region列表:[R1, R2, R3, R4, R5, R6]
分配小对象:
1. 线程需要分配新对象 → 从当前Eden Region分配
2. Eden Region满了 → 分配新的Eden Region
3. Region分配记录:
R1: Eden (使用中)
R2: Eden (使用中)
R3: 空闲
分配大对象(>Region一半):
1. 对象大小 > 1MB → 需要Humongous分配
2. 寻找连续空闲Region:[R4, R5, R6]
3. 分配为H Region:R4(H头)+R5(H连续)+R6(H连续)
4. 剩余Region:R3(空闲)
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
# 7.2 G1 工作流程
G1的GC周期就像城市的垃圾清理计划:
G1 GC周期(三色标记算法):
┌─────────────────────────────────────────────────────────┐
│ G1 GC 完整周期 │
├─────────────────────────────────────────────────────────┤
│ │
│ 周期开始 → Young GC (多次) → 并发标记 → Mixed GC → ... │
│ │
│ 详细流程: │
│ │
│ 1. 初始标记 (Initial Mark) - STW │
│ │ 标记GC Roots直接可达的对象 │
│ │ 伴随一次Young GC一起进行 │
│ ↓ │
│ 2. 根区域扫描 (Root Region Scanning) - 并发 │
│ │ 扫描Survivor Region中引用老年代的对象 │
│ │ 为并发标记阶段做准备 │
│ ↓ │
│ 3. 并发标记 (Concurrent Marking) - 并发 │
│ │ 遍历堆,标记所有存活对象 │
│ │ 使用SATB算法处理并发修改 │
│ ↓ │
│ 4. 再次标记 (Remark) - STW │
│ │ 处理SATB记录,完成标记 │
│ │ 统计Region存活数据,生成回收集 │
│ ↓ │
│ 5. 清理阶段 (Cleanup) - STW │
│ │ 识别完全空闲的Region立即回收 │
│ │ 计算每个Region的回收价值(可释放空间/回收时间) │
│ ↓ │
│ 6. 复制/清理 (Copying/Cleanup) - STW │
│ │ 根据停顿时间目标,选择价值高的Region回收 │
│ │ 复制存活对象到新Region,清空旧Region │
└─────────────────────────────────────────────────────────┘
三色标记法图示:
初始状态:所有对象都是白色(未标记)
标记过程:
1. GC Roots直接引用对象涂灰色
┌─ 灰色对象:A, B, C (在扫描队列)
│
A → D (白色)
B → E (白色)
C → F (白色)
2. 扫描灰色对象,将其引用对象涂灰色,自身涂黑色
扫描A:A变黑,D变灰
┌─ 黑色:A
├─ 灰色:B, C, D
└─ 白色:E, F
3. 继续扫描,直到没有灰色对象
扫描B:B变黑,E变灰
扫描C:C变黑,F变灰
扫描D:D变黑(无引用)
扫描E:E变黑(无引用)
扫描F:F变黑(无引用)
4. 最终:所有存活对象都是黑色,白色对象可回收
黑色:A, B, C, D, E, F (全部存活)
白色:无(假设示例中所有对象都可达)
SATB(Snapshot-At-The-Beginning)处理并发修改:
并发标记期间,用户线程可能修改引用关系:
初始快照:A → B
C → D
标记期间:用户线程执行 A.next = null(断开A→B)
用户线程执行 C.next = B(新增C→B)
SATB处理:
1. 记录被断开引用的对象B(加入SATB队列)
2. 标记阶段将B视为存活(保守)
3. 最终:B被保留,避免误回收
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
# 8. GC - Java 垃圾回收器之ZGC详解
# 8.1 ZGC 核心设计
ZGC (Z Garbage Collector) 是低停顿时间的垃圾回收器:
ZGC 设计理念 - "染色指针" (Colored Pointers):
在64位指针中,借用高位作为标记位(类似于便签贴)
64位指针布局:
┌─────────────────────────────────────────────┐
│ 64位指针 (理论可寻址空间: 16EB) │
├─────────────────────────────────────────────┤
│ 原本:│ 42位地址偏移 │ │
│ └───────────────────────────────────┘
│
│ ZGC借用高16位作为标记位:
│ ┌───┬───┬───┬─────────────────────────────┐
│ │ M0│ M1│Rem│ 42位地址偏移 │
│ │(1)│(1)│(13)│ │
│ └───┴───┴───┴─────────────────────────────┘
│
│ 标记位含义:
│ M0: Marked0 - 标记阶段0的存活对象
│ M1: Marked1 - 标记阶段1的存活对象
│ Remapped: 重映射标记
│
│ 示例:访问对象时检查标记位
│ 指针值: 0x0000 8000 1234 5678
│ ↑↑↑↑
│ 标记位=0x8 (Remapped位被设置)
│ 表示:该对象已从旧地址重映射到新地址
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
# 8.2 ZGC 工作流程
ZGC的并发回收过程:
ZGC 并发标记-转移流程:
┌─────────────────────────────────────────────────────────┐
│ ZGC 并发周期 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 初始标记 (Pause Mark Start) - STW (约1ms) │
│ │ 标记GC Roots直接可达的对象 │
│ │ 设置本次周期使用的标记色(M0或M1) │
│ ↓ │
│ 2. 并发标记 (Concurrent Mark) - 并发 │
│ │ 遍历对象图,标记所有可达对象 │
│ │ 标记方式:设置指针的M0或M1位 │
│ │ 处理引用变化:使用读屏障(Load Barrier) │
│ ↓ │
│ 3. 再标记 (Pause Mark End) - STW (约1ms) │
│ │ 处理遗漏标记,完成标记阶段 │
│ │ 选择回收区域,计算转移集 │
│ ↓ │
│ 4. 并发转移准备 (Concurrent Prepare for Relocate) │
│ │ 整理转移集,确定转移顺序 │
│ ↓ │
│ 5. 初始转移 (Pause Relocate Start) - STW (约1ms) │
│ │ 转移部分Roots直接引用的对象 │
│ ↓ │
│ 6. 并发转移 (Concurrent Relocate) - 并发 │
│ │ 转移存活对象到新区域 │
│ │ 更新指针:旧指针 → 转移表 → 新地址 │
│ │ 使用读屏障处理未转移对象的访问 │
│ ↓ │
│ 7. 重映射 (Concurrent Remap) - 并发 │
│ │ 更新指针到新地址,清除标记位 │
│ │ 回收旧区域 │
└─────────────────────────────────────────────────────────┘
读屏障 (Load Barrier) 示例:
对象访问:Object obj = root.field;
ZGC读屏障伪代码:
Object load_barrier(Object* field) {
// 1. 检查指针标记位
if (is_marked(field)) {
// 2. 如果对象正在转移中,通过转移表获取新地址
if (is_relocating(field)) {
Object* new_addr = forward_table.lookup(field);
// 3. 自愈:更新指针到新地址
*field = new_addr;
return new_addr;
}
// 4. 对象已标记,直接返回
return field;
}
// 5. 对象未标记,需要标记(在标记阶段)
if (in_marking_phase()) {
mark_object(field);
return field;
}
// 6. 正常返回
return field;
}
指针状态转换示例:
初始:指针指向对象A,地址0x1000,无标记
指针值: 0x0000 0000 0000 1000
标记阶段(M0周期):
1. 标记对象A存活 → 设置M0位
指针值: 0x0001 0000 0000 1000 (M0=1)
转移阶段:
2. 转移对象A到新地址0x2000
旧指针: 0x0001 0000 0000 1000 (M0=1, 正在转移)
转移表: 0x1000 → 0x2000
3. 访问对象A时触发读屏障
发现M0=1且正在转移
查询转移表得到新地址0x2000
更新指针为: 0x0000 0000 0000 2000 (无标记)
这就是"自愈":指针被自动修复
重映射阶段:
4. 更新所有引用到新地址
完成后,所有指针都指向新地址,无标记位
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
# 9. GC - Java 垃圾回收器之CMS GC问题分析与解决
# 9.1 CMS 工作原理与问题
CMS (Concurrent Mark Sweep) 是老年代并发收集器:
CMS 工作流程与潜在问题:
┌─────────────────────────────────────────────────────────┐
│ CMS 工作阶段 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 初始标记 (Initial Mark) - STW │
│ │ 标记GC Roots直接可达的老年代对象 │
│ │ 问题:如果年轻代很大,耗时可能较长 │
│ ↓ │
│ 2. 并发标记 (Concurrent Mark) - 并发 │
│ │ 标记所有可达对象 │
│ │ 问题:并发期间用户线程修改引用,产生"浮动垃圾" │
│ ↓ │
│ 3. 重新标记 (Remark) - STW │
│ │ 修正并发标记期间的变动 │
│ │ 问题:如果对象很多,停顿时间可能较长 │
│ ↓ │
│ 4. 并发清除 (Concurrent Sweep) - 并发 │
│ │ 回收未标记的垃圾对象 │
│ │ 问题:产生内存碎片,可能导致Full GC │
└─────────────────────────────────────────────────────────┘
CMS 典型问题场景:
1. 并发模式失败 (Concurrent Mode Failure):
在并发清理完成前,老年代空间不足
时间线:
T0: 老年代使用率68%,启动CMS
T1: 并发标记进行中,用户线程继续分配
T2: 老年代使用率达92%,年轻代晋升需要10%空间
T3: 老年代空间不足,触发"并发模式失败"
T4: 停止并发清理,启用Serial Old进行Full GC (长时间STW)
图示:
┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
│T0 │T1 │T2 │T3 │T4 │ │ │ │ │
├───┼───┼───┼───┼───┼───┼───┼───┼───┤
│CMS│并发│用户│空间│并发│Serial│ │ │
│开始│标记│分配│不足│失败│ Old │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┴───┘
↑ ↑
正常并发 长时间STW
2. 内存碎片问题:
多次CMS后,老年代空间碎片化
老年代使用情况:
初始: [A][B][C][ ][E][ ][G][H][ ][J]
占用 空闲 占用 空闲
多次分配释放后:
[A][ ][C][D][ ][F][G][ ][I][ ]
空闲 占用 空闲 占用 空闲
分配大对象K需要连续3个空间,但最大连续空间只有2
→ 触发Full GC进行内存整理
3. 晋升失败 (Promotion Failed):
年轻代回收时,幸存对象无法晋升到老年代
场景:年轻代Eden区满,触发Minor GC
存活对象需要晋升到老年代
但老年代空间不足或碎片化
→ 晋升失败,触发Full GC
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
# 9.2 CMS 问题解决策略
// CMS 参数优化示例
public class CMSOptimization {
public static void main(String[] args) {
// 1. 避免并发模式失败
// 设置老年代使用率触发CMS的阈值
// -XX:CMSInitiatingOccupancyFraction=68
// 预留空间给浮动垃圾
// -XX:CMSTriggerRatio=68
// 2. 减少碎片化
// 开启内存整理(JDK5之后默认开启)
// -XX:+UseCMSCompactAtFullCollection
// 设置在多少次Full GC后整理一次
// -XX:CMSFullGCsBeforeCompaction=0 (每次Full GC都整理)
// 3. 优化重新标记阶段
// 开启并行重新标记
// -XX:+CMSParallelRemarkEnabled
// 在重新标记前先做一次年轻代GC
// -XX:+CMSScavengeBeforeRemark
// 4. 处理大对象
// 设置大对象阈值,避免大对象直接进入老年代
// -XX:PretenureSizeThreshold=2M
// 使用大对象专属空间
// -XX:+UseCMSInitiatingOccupancyOnly
// 完整的CMS参数配置示例
String cmsOptions =
"-XX:+UseConcMarkSweepGC " + // 启用CMS
"-XX:CMSInitiatingOccupancyFraction=68 " + // 触发阈值68%
"-XX:+UseCMSInitiatingOccupancyOnly " + // 只用阈值触发
"-XX:+CMSScavengeBeforeRemark " + // 重新标记前先YGC
"-XX:+CMSParallelRemarkEnabled " + // 并行重新标记
"-XX:+UseCMSCompactAtFullCollection " + // Full GC时整理
"-XX:CMSFullGCsBeforeCompaction=0 " + // 每次都整理
"-XX:+ExplicitGCInvokesConcurrent " + // System.gc()触发并发GC
"-XX:+ScavengeBeforeFullGC"; // Full GC前先YGC
System.out.println("CMS优化参数: " + cmsOptions);
// 5. 监控CMS状态
// 添加GC日志参数进行监控
String gcLogOptions =
"-Xloggc:gc.log " +
"-XX:+PrintGCDetails " +
"-XX:+PrintGCDateStamps " +
"-XX:+PrintGCTimeStamps " +
"-XX:+PrintGCApplicationStoppedTime " +
"-XX:+PrintPromotionFailure " + // 打印晋升失败
"-XX:+PrintTenuringDistribution"; // 打印年龄分布
System.out.println("GC日志参数: " + gcLogOptions);
}
}
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
# 10. 调试排错 - JVM 调优参数
# 10.1 JVM 参数分类与作用
JVM参数就像汽车的操控面板,不同参数控制不同行为:
JVM 参数分类大全:
┌─────────────────────────────────────────────────────────┐
│ JVM 参数分类 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 标准参数 (-开头,所有JVM实现都支持) │
│ -version 显示版本信息 │
│ -help 显示帮助 │
│ -server 服务器模式 │
│ -client 客户端模式 │
│ │
│ 2. X参数 (-X开头,非标准,不同JVM可能不同) │
│ -Xmx1024m 最大堆内存 │
│ -Xms1024m 初始堆内存 │
│ -Xss256k 线程栈大小 │
│ -Xmn512m 年轻代大小 │
│ -Xloggc:file GC日志路径 │
│ │
│ 3. XX参数 (-XX:开头,不稳定参数,用于调试和调优) │
│ (1) Boolean类型: -XX:+UseG1GC (启用) │
│ -XX:-UseTLAB (禁用) │
│ (2) Key-Value类型: -XX:MaxMetaspaceSize=256m │
│ -XX:NewRatio=2 (老年代:年轻代=2:1)│
│ -XX:SurvivorRatio=8 (Eden:Survivor)│
│ │
│ 4. 常用调优参数组合: │
│ 堆内存: -Xms4g -Xmx4g -Xmn2g │
│ 元空间: -XX:MetaspaceSize=256m │
│ -XX:MaxMetaspaceSize=256m │
│ GC算法: -XX:+UseG1GC │
│ -XX:MaxGCPauseMillis=200 │
│ 线程栈: -Xss1m │
│ 直接内存:-XX:MaxDirectMemorySize=1g │
└─────────────────────────────────────────────────────────┘
参数设置示例与效果:
场景:电商应用,8核16G服务器
推荐配置:
-Xms8g -Xmx8g # 堆内存固定8G,避免动态调整
-Xmn3g # 年轻代3G
-XX:MetaspaceSize=256m # 元空间初始256M
-XX:MaxMetaspaceSize=512m # 元空间最大512M
-XX:+UseG1GC # 使用G1收集器
-XX:MaxGCPauseMillis=200 # 目标停顿200ms
-XX:ParallelGCThreads=8 # 并行GC线程数=CPU数
-XX:ConcGCThreads=4 # 并发GC线程数=CPU数/2
-XX:+PrintGCDetails # 打印GC详情
-XX:+PrintGCDateStamps # 打印GC时间戳
-Xloggc:/logs/gc.log # GC日志路径
参数关系图示:
初始堆(Xms) vs 最大堆(Xmx)
┌─────────────────────────────────────┐
│ 最大堆 (Xmx) │
│ ┌─────────────────────┐ │
│ │ │ │
│ │ 扩展区域 │ │
│ │ │ │
│ ├─────────────────────┤ │
│ │ 当前堆大小 │ │
│ │ │ │
│ ├─────────────────────┤ │
│ │ 初始堆 (Xms) │ │
│ │ │ │
│ └─────────────────────┘ │
└─────────────────────────────────────┘
年轻代(Xmn) vs 老年代
┌─────────────────────────────────────┐
│ 堆 (Xmx) │
├─────────────────────────────────────┤
│ 年轻代 (Xmn) │
│ ┌─────────────┬─────────────┐ │
│ │ Eden │ Survivor │ │
│ │ (80%) │ S0/S1(10% each) │
│ └─────────────┴─────────────┘ │
├─────────────────────────────────────┤
│ 老年代 │
│ (Xmx - Xmn) │
└─────────────────────────────────────┘
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
# 10.2 参数调优实战案例
// 模拟内存泄漏的示例程序
public class MemoryLeakExample {
static class User {
private byte[] data;
private String name;
public User(String name) {
this.name = name;
this.data = new byte[1024 * 1024]; // 每个User对象1MB
}
}
// 内存泄漏的缓存
static Map<String, User> cache = new HashMap<>();
public static void main(String[] args) throws InterruptedException {
System.out.println("程序开始...");
// 模拟不断添加数据到缓存,但从不移除
for (int i = 0; i < 1000; i++) {
cache.put("user_" + i, new User("User" + i));
System.out.println("添加第 " + i + " 个用户");
Thread.sleep(10);
// 每100个打印一次内存情况
if (i % 100 == 0) {
printMemory();
}
}
}
public static void printMemory() {
Runtime runtime = Runtime.getRuntime();
long total = runtime.totalMemory() / 1024 / 1024;
long free = runtime.freeMemory() / 1024 / 1024;
long used = total - free;
long max = runtime.maxMemory() / 1024 / 1024;
System.out.println("\n=== 内存状态 ===");
System.out.println("最大内存: " + max + "MB");
System.out.println("已分配内存: " + total + "MB");
System.out.println("已使用内存: " + used + "MB");
System.out.println("空闲内存: " + free + "MB");
System.out.println("缓存大小: " + cache.size() + " 个用户");
}
}
// 运行此程序时使用不同的JVM参数观察效果
/*
// 测试1:默认参数(可能很快OOM)
java MemoryLeakExample
// 测试2:增加堆内存
java -Xms512m -Xmx1024m MemoryLeakExample
// 测试3:使用G1并设置目标停顿时间
java -Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 MemoryLeakExample
// 测试4:添加GC日志
java -Xms512m -Xmx1024m -Xloggc:gc.log -XX:+PrintGCDetails MemoryLeakExample
// 测试5:添加内存溢出时Dump
java -Xms512m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./heapdump.hprof MemoryLeakExample
*/
// 监控脚本示例(Linux)
/*
#!/bin/bash
# monitor_jvm.sh
PID=$1
INTERVAL=5 # 监控间隔(秒)
echo "监控JVM进程: $PID"
echo "时间戳 堆使用(MB) 元空间(MB) 线程数 YGC次数 YGCT FGC次数 FGCT"
while true; do
# 使用jstat获取GC统计
GC_INFO=$(jstat -gc $PID 2>&1 | tail -1)
# 解析jstat输出
S0C=$(echo $GC_INFO | awk '{print $1/1024}')
S1C=$(echo $GC_INFO | awk '{print $2/1024}')
S0U=$(echo $GC_INFO | awk '{print $3/1024}')
S1U=$(echo $GC_INFO | awk '{print $4/1024}')
EC=$(echo $GC_INFO | awk '{print $5/1024}')
EU=$(echo $GC_INFO | awk '{print $6/1024}')
OC=$(echo $GC_INFO | awk '{print $7/1024}')
OU=$(echo $GC_INFO | awk '{print $8/1024}')
MC=$(echo $GC_INFO | awk '{print $9/1024}')
MU=$(echo $GC_INFO | awk '{print $10/1024}')
YGC=$(echo $GC_INFO | awk '{print $11}')
YGCT=$(echo $GC_INFO | awk '{print $12}')
FGC=$(echo $GC_INFO | awk '{print $13}')
FGCT=$(echo $GC_INFO | awk '{print $14}')
# 计算堆使用量
HEAP_USED=$(echo "$S0U + $S1U + $EU + $OU" | bc)
echo "$(date '+%H:%M:%S') $HEAP_USED $MU $YGC $YGCT $FGC $FGCT"
sleep $INTERVAL
done
*/
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
# 11. 调试排错 - Java 内存分析之堆内存和MetaSpace内存
# 11.1 堆内存分析实战
堆内存分析就像侦探调查仓库使用情况:
堆内存问题排查流程图:
┌─────────────────────────────────────────────────────────┐
│ 堆内存问题排查流程 │
├─────────────────────────────────────────────────────────┤
│ │
│ 症状:应用响应慢、频繁Full GC、OOM │
│ ↓ │
│ 步骤1:检查GC日志 │
│ [GC日志分析] │
│ ↓ │
│ 步骤2:使用jmap获取堆内存快照 │
│ jmap -dump:format=b,file=heap.hprof <pid> │
│ ↓ │
│ 步骤3:使用MAT/Eclipse Memory Analyzer分析 │
│ [分析泄漏嫌疑对象] │
│ ↓ │
│ 步骤4:定位问题代码 │
│ [查看对象引用链] │
│ ↓ │
│ 步骤5:修复与验证 │
└─────────────────────────────────────────────────────────┘
GC日志分析示例:
2023-10-01T10:00:00.123+0800: 1.234:
[GC (Allocation Failure)
[PSYoungGen: 1572864K->262144K(1835008K)]
2097152K->786432K(3932160K),
0.3456789 secs]
[Times: user=1.23 sys=0.12, real=0.35 secs]
日志解析:
时间戳:2023-10-01T10:00:00.123+0800
JVM启动后时间:1.234秒
GC原因:Allocation Failure (分配失败)
年轻代:回收前1572864K → 回收后262144K (容量1835008K)
整个堆:回收前2097152K → 回收后786432K (容量3932160K)
GC耗时:0.3456789秒
CPU时间:用户态1.23秒,内核态0.12秒,墙钟时间0.35秒
关键指标计算:
年轻代回收效率:(1572864-262144)/1572864 = 83.3% (良好)
晋升到老年代大小:786432 - 262144 = 524288K (较大,可能有问题)
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
# 11.2 MetaSpace 内存分析
MetaSpace是存放类元数据的区域,像图书馆的藏书目录:
MetaSpace 内存结构:
┌─────────────────────────────────────────────────────────┐
│ MetaSpace 内存布局 │
├─────────────────────────────────────────────────────────┤
│ │
│ MetaSpace (元空间) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Class Metadata │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 类信息 │ │ 方法信息 │ │ 字段信息 │ ... │ │
│ │ │ Class │ │ Method │ │ Field │ │ │
│ │ │ Klass │ │ Metadata │ │ Metadata │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Constant Pool │ │ │
│ │ │ 运行时常量池(字符串、数字常量等) │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Method Code │ │ │
│ │ │ 方法编译后的字节码 │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 注意:MetaSpace使用本地内存,不在堆中 │
│ 默认无上限,但受物理内存限制 │
└─────────────────────────────────────────────────────────┘
MetaSpace 内存泄漏常见原因:
1. 动态类生成过多:CGLib/ASM频繁生成代理类
2. 类加载器泄漏:Web应用重启后旧ClassLoader未释放
3. 大量反射调用:生成大量MethodAccessor
4. 字符串常量池:大量intern()操作
检测MetaSpace泄漏:
# 1. 查看MetaSpace使用情况
jstat -gc <pid> | awk '{print $9, $10}' # MC, MU列
# 2. 查看类加载统计
jcmd <pid> GC.class_stats
# 3. Dump类加载器信息
jmap -clstats <pid>
# 4. 在OOM时Dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./oom.hprof
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
MetaSpace增长示例:
时间线:应用启动 → 运行 → 热部署 → 内存泄漏
┌─────┬──────┬──────────┬──────────────┬────────────┐
│时间 │ Meta │ 类加载器 │ 加载类数量 │ 现象 │
├─────┼──────┼──────────┼──────────────┼────────────┤
│ T0 │ 64M │ 1个 │ 5000个 │ 正常启动 │
│ T1 │ 128M │ 3个 │ 8000个 │ 运行正常 │
│ T2 │ 256M │ 5个 │ 12000个 │ 第一次部署 │
│ T3 │ 384M │ 7个 │ 15000个 │ 第二次部署 │
│ T4 │ 512M │ 9个 │ 18000个 │ 第三次部署 │
│ T5 │ OOM │ 11个 │ 20000+个 │ 内存溢出 │
└─────┴──────┴──────────┴──────────────┴────────────┘
问题:每次热部署都创建新ClassLoader,旧ClassLoader未释放
包含的类无法卸载 → MetaSpace持续增长 → OOM
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
# 12. 调试排错 - Java 内存分析之堆外内存
# 12.1 堆外内存类型与监控
堆外内存是JVM向操作系统直接申请的内存:
Java 堆外内存分类:
┌─────────────────────────────────────────────────────────┐
│ 堆外内存类型 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. Direct ByteBuffer (直接内存) │
│ ByteBuffer.allocateDirect(1024) │
│ 由 -XX:MaxDirectMemorySize 限制 │
│ │
│ 2. MappedByteBuffer (内存映射文件) │
│ FileChannel.map(MapMode.READ_WRITE, 0, 1024) │
│ 不受MaxDirectMemorySize限制 │
│ │
│ 3. Native Memory (本地内存) │
│ JNI调用分配的内存 │
│ Unsafe.allocateMemory() │
│ │
│ 4. 线程栈 │
│ 由 -Xss 参数控制 │
│ 1000线程 * 1MB = 1GB │
│ │
│ 5. JVM自身开销 │
│ 代码缓存、GC数据结构、符号表等 │
│ │
│ 6. 第三方Native库分配 │
│ Netty、RocksDB等使用的本地内存 │
└─────────────────────────────────────────────────────────┘
堆外内存泄漏检测:
# 1. 查看进程总内存
top -p <pid>
# RES列是实际物理内存,包括堆内+堆外
# 2. 查看堆内存
jmap -heap <pid>
# 关注Heap Usage部分
# 3. 计算堆外内存
堆外内存 ≈ RES - 堆内存 - MetaSpace
# 4. 跟踪DirectByteBuffer
jcmd <pid> VM.native_memory summary
# 查看Internal部分
# 5. 使用Native Memory Tracking (NMT)
启动时:-XX:NativeMemoryTracking=detail
运行时:jcmd <pid> VM.native_memory detail
堆外内存泄漏示例场景:
应用总内存使用(RES): 4GB
堆内存使用: 1.5GB
MetaSpace使用: 256MB
计算堆外内存: 4GB - 1.5GB - 256MB = 2.25GB (异常偏高)
可能原因:
1. Netty的PooledByteBuf未释放
2. 大量未关闭的MappedByteBuffer
3. JNI库内存泄漏
4. 线程创建过多(每个线程有栈内存)
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
# 12.2 Netty 堆外内存泄漏排查
// Netty堆外内存泄漏示例
public class NettyMemoryLeakExample {
public static void main(String[] args) {
// Netty配置
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
// 有问题的处理器:不断累积ByteBuf
ch.pipeline().addLast(new ProblemHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 绑定端口
ChannelFuture f = b.bind(8080).sync();
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
// 有问题的处理器:每次请求都创建ByteBuf但不释放
static class ProblemHandler extends ChannelInboundHandlerAdapter {
// 错误:累积ByteBuf的列表
private List<ByteBuf> bufferList = new ArrayList<>();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
// 错误1:不释放传入的ByteBuf
// 正确做法:ReferenceCountUtil.release(msg);
// 错误2:创建新的ByteBuf加入列表,从不释放
ByteBuf newBuf = ctx.alloc().directBuffer(1024);
newBuf.writeBytes("response".getBytes());
bufferList.add(newBuf); // 泄漏!
// 写入响应但不释放
ctx.write(newBuf);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
// 正确的处理器实现
static class CorrectHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = null;
try {
buf = (ByteBuf) msg;
// 处理数据...
// 创建响应
ByteBuf response = ctx.alloc().buffer();
response.writeBytes("OK".getBytes());
// 写入并添加监听器释放
ctx.writeAndFlush(response).addListener(future -> {
if (!future.isSuccess()) {
response.release(); // 发送失败时释放
}
// 发送成功时,Netty会自动释放
});
} finally {
// 释放接收到的缓冲区
if (buf != null) {
buf.release();
}
}
}
}
}
// 堆外内存泄漏检测工具
public class DirectMemoryMonitor {
public static void main(String[] args) throws Exception {
// 启用NMT
// -XX:NativeMemoryTracking=summary
// 获取DirectMemory使用情况
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); // 1MB
// 方法1:使用反射获取DirectBuffer的cleaner
Field cleanerField = buffer.getClass().getDeclaredField("cleaner");
cleanerField.setAccessible(true);
Object cleaner = cleanerField.get(buffer);
// 方法2:监控DirectMemory使用
monitorDirectMemory();
// 手动触发GC并清理DirectBuffer
System.gc();
Thread.sleep(1000);
// 方法3:使用MXBean
List<BufferPoolMXBean> pools = ManagementFactory.getPlatformMXBeans(
BufferPoolMXBean.class);
for (BufferPoolMXBean pool : pools) {
System.out.println("Buffer Pool: " + pool.getName());
System.out.println(" Count: " + pool.getCount());
System.out.println(" Memory Used: " + pool.getMemoryUsed() / 1024 / 1024 + "MB");
System.out.println(" Total Capacity: " + pool.getTotalCapacity() / 1024 / 1024 + "MB");
}
}
public static void monitorDirectMemory() {
Thread monitorThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// 获取BufferPool统计
BufferPoolMXBean directPool = null;
List<BufferPoolMXBean> pools = ManagementFactory.getPlatformMXBeans(
BufferPoolMXBean.class);
for (BufferPoolMXBean pool : pools) {
if ("direct".equals(pool.getName())) {
directPool = pool;
break;
}
}
if (directPool != null) {
long used = directPool.getMemoryUsed() / 1024 / 1024;
long count = directPool.getCount();
System.out.println("DirectMemory - Used: " + used + "MB, Buffers: " + count);
}
Thread.sleep(5000);
} catch (InterruptedException e) {
break;
}
}
});
monitorThread.setDaemon(true);
monitorThread.start();
}
}
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# 13. 调试排错 - Java 线程分析之线程Dump分析
# 13.1 线程Dump获取与分析
线程Dump就像给JVM的所有线程拍一张瞬间快照:
线程Dump获取方式:
┌─────────────────────────────────────────────────────────┐
│ 线程Dump获取方法 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 命令行工具 │
│ jstack <pid> > thread.dump │
│ kill -3 <pid> (发送QUIT信号) │
│ │
│ 2. Java代码获取 │
│ Thread.getAllStackTraces() │
│ ManagementFactory.getThreadMXBean().dumpAllThreads()│
│ │
│ 3. 图形化工具 │
│ jvisualvm, jmc, arthas │
│ │
│ 4. 容器环境 │
│ kubectl exec <pod> -- jstack <pid> │
│ docker exec <container> jstack <pid> │
└─────────────────────────────────────────────────────────┘
线程Dump结构分析:
示例线程条目:
"http-nio-8080-exec-1" #31 daemon prio=5 os_prio=0 tid=0x00007f8b3820c800 nid=0x4a3f waiting on condition [0x00007f8b0f7f7000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000f0a8b2c8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
解析:
1. 线程名: "http-nio-8080-exec-1"
2. 线程号: #31
3. 守护线程: daemon
4. Java优先级: prio=5
5. 系统优先级: os_prio=0
6. 线程ID: tid=0x00007f8b3820c800
7. 本地线程ID: nid=0x4a3f (十六进制,转十进制19007)
8. 线程状态: waiting on condition
9. 栈地址: [0x00007f8b0f7f7000]
10. Java状态: WAITING (parking)
11. 调用栈: 从下往上读,最下面是线程起点
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
# 13.2 线程状态与问题模式
线程的6种状态及其含义:
Java线程状态及其含义:
┌─────────────────────────────────────────────────────────┐
│ Java线程状态 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. NEW (新建) │
│ 线程刚创建,尚未启动 │
│ Thread t = new Thread(); │
│ │
│ 2. RUNNABLE (可运行) │
│ 线程正在JVM中执行,可能等待CPU调度 │
│ 包括操作系统层面的Running和Ready状态 │
│ │
│ 3. BLOCKED (阻塞) │
│ 等待获取监视器锁(synchronized) │
│ 发生在进入同步块/方法时 │
│ │
│ 4. WAITING (等待) │
│ 无限期等待,需要其他线程唤醒 │
│ 调用Object.wait(), Thread.join(), LockSupport.park()│
│ │
│ 5. TIMED_WAITING (限时等待) │
│ 有时间限制的等待 │
│ Thread.sleep(1000), Object.wait(1000) │
│ │
│ 6. TERMINATED (终止) │
│ 线程已执行完毕 │
└─────────────────────────────────────────────────────────┘
常见线程问题模式:
1. CPU使用率高 - 查找RUNNABLE状态的线程
"cpu-thread" #1 prio=5 os_prio=0 tid=0x00007f8b3820c800 nid=0x4a3f runnable [0x00007f8b0f7f7000]
java.lang.Thread.State: RUNNABLE
at app.Main.busyLoop(Main.java:10) ← 热点方法
at app.Main.lambda$main$0(Main.java:6)
2. 死锁 - 查找BLOCKED状态和持有锁的线程
"Thread-1" #12 prio=5 os_prio=0 tid=0x00007f8b3820d800 nid=0x4a40 waiting for monitor entry [0x00007f8b0e6f6000]
java.lang.Thread.State: BLOCKED (on object monitor)
at app.DeadlockExample.methodB(DeadlockExample.java:30)
- waiting to lock <0x00000000f0a8b1a0> (a java.lang.Object) ← 等待锁A
- locked <0x00000000f0a8b1b0> (a java.lang.Object) ← 持有锁B
"Thread-2" #13 prio=5 os_prio=0 tid=0x00007f8b3820e800 nid=0x4a41 waiting for monitor entry [0x00007f8b0e5f5000]
java.lang.Thread.State: BLOCKED (on object monitor)
at app.DeadlockExample.methodA(DeadlockExample.java:20)
- waiting to lock <0x00000000f0a8b1b0> (a java.lang.Object) ← 等待锁B
- locked <0x00000000f0a8b1a0> (a java.lang.Object) ← 持有锁A
3. 线程饥饿 - 大量线程WAITING/TIMED_WAITING
"pool-1-thread-100" #110 prio=5 os_prio=0 tid=0x00007f8b3820f800 nid=0x4a42 waiting on condition [0x00007f8b0d4f4000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000f0a8b2c8> (a java.util.concurrent.ThreadPoolExecutor)
← 线程池队列过长,线程等待任务
4. 连接池耗尽 - 大量线程等待数据库连接
"http-nio-8080-exec-50" #80 daemon prio=5 os_prio=0 tid=0x00007f8b38210800 nid=0x4a43 waiting on condition [0x00007f8b0c3f3000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
at com.zaxxer.hikari.pool.ProxyConnection.waitForConnection(ProxyConnection.java:100)
← 等待数据库连接,可能是连接池太小或连接泄漏
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
# 13.3 线程Dump分析实战
// 模拟常见线程问题的示例
public class ThreadProblemExamples {
// 1. CPU过高示例
public static void cpuHighExample() {
Thread cpuThread = new Thread(() -> {
while (true) {
// 空循环,消耗CPU
for (int i = 0; i < 1000000; i++) {
Math.sqrt(i);
}
}
}, "cpu-high-thread");
cpuThread.start();
}
// 2. 死锁示例
public static void deadlockExample() {
Object lockA = new Object();
Object lockB = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread1 acquired lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("Thread1 acquired lockB");
}
}
}, "deadlock-thread-1");
Thread thread2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread2 acquired lockB");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("Thread2 acquired lockA");
}
}
}, "deadlock-thread-2");
thread1.start();
thread2.start();
}
// 3. 线程阻塞示例
public static void blockedExample() {
Object lock = new Object();
// 线程1持有锁不释放
Thread holder = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Holder thread got lock, sleeping...");
Thread.sleep(60000); // 持有锁60秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "lock-holder");
// 多个线程等待锁
for (int i = 0; i < 5; i++) {
Thread waiter = new Thread(() -> {
synchronized (lock) {
System.out.println("Waiter thread got lock");
}
}, "lock-waiter-" + i);
waiter.start();
}
holder.start();
}
// 4. 内存泄漏导致线程数增长
public static void threadLeakExample() {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(10);
// 提交任务但不正确处理
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 不关闭executor,线程会一直存在
}
// 线程Dump分析工具
public static void analyzeThreadDump() {
System.out.println("=== 当前线程状态分析 ===");
// 获取所有线程栈
Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();
// 按状态分类统计
Map<Thread.State, List<Thread>> threadsByState = new HashMap<>();
for (Thread.State state : Thread.State.values()) {
threadsByState.put(state, new ArrayList<>());
}
for (Thread thread : allStackTraces.keySet()) {
threadsByState.get(thread.getState()).add(thread);
}
// 打印统计
for (Map.Entry<Thread.State, List<Thread>> entry : threadsByState.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue().size() + " threads");
// 打印每个线程的简单信息
for (Thread thread : entry.getValue()) {
System.out.println(" " + thread.getName() +
" (daemon=" + thread.isDaemon() + ")");
// 如果是死锁嫌疑,打印更多信息
if (thread.getState() == Thread.State.BLOCKED) {
System.out.println(" BLOCKED - 可能参与死锁");
StackTraceElement[] stack = allStackTraces.get(thread);
if (stack.length > 0) {
System.out.println(" 等待位置: " + stack[0]);
}
}
}
}
// 检测死锁
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
System.out.println("\n!!! 检测到死锁 !!!");
for (long threadId : deadlockedThreads) {
ThreadInfo threadInfo = threadBean.getThreadInfo(threadId);
System.out.println("死锁线程: " + threadInfo.getThreadName());
System.out.println(" 等待锁: " + threadInfo.getLockInfo());
System.out.println(" 持有锁: " + threadInfo.getLockOwnerId());
}
}
// 检测CPU使用率高的线程
System.out.println("\n=== CPU时间统计 ===");
long[] allThreadIds = threadBean.getAllThreadIds();
for (long threadId : allThreadIds) {
ThreadInfo info = threadBean.getThreadInfo(threadId);
long cpuTime = threadBean.getThreadCpuTime(threadId);
long userTime = threadBean.getThreadUserTime(threadId);
if (cpuTime > 1000000000L) { // CPU时间超过1秒
System.out.println("高CPU线程: " + info.getThreadName());
System.out.println(" CPU时间: " + (cpuTime / 1000000) + "ms");
System.out.println(" 用户时间: " + (userTime / 1000000) + "ms");
System.out.println(" 状态: " + info.getThreadState());
}
}
}
public static void main(String[] args) throws Exception {
// 运行各种问题示例
cpuHighExample();
deadlockExample();
blockedExample();
threadLeakExample();
// 等待一会让问题发生
Thread.sleep(3000);
// 分析线程状态
analyzeThreadDump();
// 生成线程Dump文件
String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
System.out.println("\nPID: " + pid);
System.out.println("使用 jstack " + pid + " > thread.dump 获取详细线程信息");
}
}
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# 14. 调试排错 - Java 问题排查之Linux命令
# 14.1 系统资源监控命令
Linux命令就像医生的听诊器和血压计:
系统资源监控命令集:
┌─────────────────────────────────────────────────────────┐
│ Linux系统监控命令 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. CPU监控 │
│ top -p <pid> # 实时监控指定进程 │
│ pidstat -p <pid> 1 # 每秒统计进程CPU │
│ vmstat 1 # 系统级CPU、内存、IO统计 │
│ mpstat -P ALL 1 # 每个CPU核心使用率 │
│ │
│ 2. 内存监控 │
│ free -h # 内存使用概况 │
│ pmap -x <pid> # 进程内存映射详情 │
│ cat /proc/<pid>/status | grep -E 'Vm|RSS' # 进程内存 │
│ slabtop # 内核slab内存分配 │
│ │
│ 3. 磁盘IO监控 │
│ iostat -x 1 # 磁盘IO统计 │
│ iotop # 类似top的IO监控 │
│ pidstat -d 1 # 进程级IO统计 │
│ │
│ 4. 网络监控 │
│ netstat -tunlp # 网络连接状态 │
│ ss -tunlp # netstat的替代品 │
│ tcpdump -i eth0 port 8080 -w capture.pcap # 抓包 │
│ sar -n DEV 1 # 网络设备统计 │
│ │
│ 5. 进程监控 │
│ ps aux --sort=-%cpu | head -10 # CPU使用率前10进程 │
│ ps aux --sort=-%mem | head -10 # 内存使用前10进程 │
│ lsof -p <pid> # 进程打开的文件 │
│ pstree -p <pid> # 进程树 │
└─────────────────────────────────────────────────────────┘
top命令输出解析:
top - 10:30:00 up 100 days, 1:00, 1 user, load average: 1.25, 1.10, 1.05
Tasks: 150 total, 1 running, 149 sleeping, 0 stopped, 0 zombie
%Cpu(s): 15.3 us, 5.2 sy, 0.0 ni, 79.2 id, 0.3 wa, 0.0 hi, 0.0 si
MiB Mem : 16000.0 total, 2000.0 free, 8000.0 used, 6000.0 buff/cache
MiB Swap: 2000.0 total, 2000.0 free, 0.0 used. 7000.0 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1234 appuser 20 0 15.2g 8.1g 2.1g S 85.3 50.6 100:30.15 java
1235 appuser 20 0 2.5g 1.2g 0.1g S 5.2 7.5 20:15.30 java
关键指标:
load average: 1.25, 1.10, 1.05 # 1/5/15分钟平均负载
%Cpu(s): us用户态, sy内核态, id空闲, wa等待IO
MiB Mem: total总内存, free空闲, used使用, buff/cache缓存
RES: 实际物理内存, %MEM: 内存使用百分比
S: 进程状态 (R运行, S睡眠, D不可中断睡眠, Z僵尸)
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
# 14.2 JVM进程专项监控
#!/bin/bash
# jvm_monitor.sh - JVM进程综合监控脚本
PID=$1
INTERVAL=5 # 采集间隔(秒)
LOG_FILE="jvm_monitor_$(date +%Y%m%d_%H%M%S).log"
echo "JVM监控开始,PID: $PID,间隔: ${INTERVAL}s,日志: $LOG_FILE"
echo "时间戳,CPU(%),内存(MB),线程数,堆使用(MB),堆提交(MB),非堆(MB),YGC次数,YGCT,FGC次数,FGCT,打开文件数" > $LOG_FILE
while true; do
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
# 1. 系统级CPU和内存
TOP_INFO=$(top -b -n1 -p $PID | grep $PID)
CPU=$(echo $TOP_INFO | awk '{print $9}')
MEM=$(echo $TOP_INFO | awk '{print $10}')
RSS=$(echo $TOP_INFO | awk '{print $6}' | sed 's/g/*1024/')
RSS_MB=$(echo "$RSS" | bc)
# 2. JVM内存信息
JSTAT_INFO=$(jstat -gc $PID 2>/dev/null | tail -1)
if [ -n "$JSTAT_INFO" ]; then
S0U=$(echo $JSTAT_INFO | awk '{print $3/1024}')
S1U=$(echo $JSTAT_INFO | awk '{print $4/1024}')
EU=$(echo $JSTAT_INFO | awk '{print $6/1024}')
OU=$(echo $JSTAT_INFO | awk '{print $8/1024}')
MU=$(echo $JSTAT_INFO | awk '{print $10/1024}')
YGC=$(echo $JSTAT_INFO | awk '{print $11}')
YGCT=$(echo $JSTAT_INFO | awk '{print $12}')
FGC=$(echo $JSTAT_INFO | awk '{print $13}')
FGCT=$(echo $JSTAT_INFO | awk '{print $14}')
HEAP_USED=$(echo "$S0U + $S1U + $EU + $OU" | bc)
HEAP_COMMITTED=$(echo "$(echo $JSTAT_INFO | awk '{print $1/1024}') + \
$(echo $JSTAT_INFO | awk '{print $2/1024}') + \
$(echo $JSTAT_INFO | awk '{print $5/1024}') + \
$(echo $JSTAT_INFO | awk '{print $7/1024}')" | bc)
else
HEAP_USED=0
HEAP_COMMITTED=0
MU=0
YGC=0
YGCT=0
FGC=0
FGCT=0
fi
# 3. 线程数
THREAD_COUNT=$(ps -Lf $PID | wc -l)
THREAD_COUNT=$((THREAD_COUNT - 1)) # 减去标题行
# 4. 打开文件数
FD_COUNT=$(ls -l /proc/$PID/fd 2>/dev/null | wc -l)
FD_COUNT=$((FD_COUNT - 1)) # 减去标题行
# 5. 网络连接数
CONN_COUNT=$(netstat -anp 2>/dev/null | grep $PID | wc -l)
# 输出到日志
echo "$TIMESTAMP,$CPU,$RSS_MB,$THREAD_COUNT,$HEAP_USED,$HEAP_COMMITTED,$MU,$YGC,$YGCT,$FGC,$FGCT,$FD_COUNT,$CONN_COUNT" >> $LOG_FILE
# 控制台输出
echo "=== $TIMESTAMP ==="
echo "CPU: ${CPU}%, 内存: ${RSS_MB}MB, 线程: ${THREAD_COUNT}, 堆: ${HEAP_USED}MB"
echo "YGC: ${YGC} (${YGCT}s), FGC: ${FGC} (${FGCT}s), 文件: ${FD_COUNT}, 连接: ${CONN_COUNT}"
# 检查异常情况
if [ $(echo "$CPU > 90" | bc) -eq 1 ]; then
echo "警告: CPU使用率过高!"
# 保存高CPU时的线程Dump
jstack $PID > thread_dump_cpu_high_$(date +%H%M%S).dump 2>/dev/null
fi
if [ $(echo "$HEAP_USED > 4096" | bc) -eq 1 ]; then
echo "警告: 堆内存使用超过4GB!"
fi
if [ $THREAD_COUNT -gt 500 ]; then
echo "警告: 线程数超过500!"
jstack $PID > thread_dump_thread_high_$(date +%H%M%S).dump 2>/dev/null
fi
sleep $INTERVAL
done
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
# 15. 调试排错 - Java 问题排查之工具单
# 15.1 问题排查工具箱
Java问题排查就像汽车维修,需要合适的工具:
Java问题排查工具矩阵:
┌─────────────────────────────────────────────────────────────────────┐
│ Java问题排查工具集 │
├─────────────┬──────────────┬─────────────────┬─────────────────────┤
│ 问题类型 │ 命令行工具 │ 可视化工具 │ 专项工具 │
├─────────────┼──────────────┼─────────────────┼─────────────────────┤
│ CPU使用率高 │ top, pidstat │ visualvm │ async-profiler │
│ │ jstack │ jmc │ perf, SystemTap │
│ │ │ arthas │ │
├─────────────┼──────────────┼─────────────────┼─────────────────────┤
│ 内存泄漏 │ jmap, jstat │ mat │ heaphero, gceasy │
│ │ pmap │ visualvm │ NMT │
│ │ │ jmc │ │
├─────────────┼──────────────┼─────────────────┼─────────────────────┤
│ 线程问题 │ jstack │ fastthread.io │ tda │
│ │ pstree │ visualvm │ IBM JCA │
│ │ │ arthas │ │
├─────────────┼──────────────┼─────────────────┼─────────────────────┤
│ GC问题 │ jstat │ gceasy │ GCViewer │
│ │ gc日志分析 │ visualvm │ HPjmeter │
│ │ │ jmc │ │
├─────────────┼──────────────┼─────────────────┼─────────────────────┤
│ 性能剖析 │ jstack采样 │ async-profiler │ JProfiler │
│ │ │ visualvm │ YourKit │
│ │ │ arthas │ │
├─────────────┼──────────────┼─────────────────┼─────────────────────┤
│ 网络问题 │ netstat │ wireshark │ tcpdump │
│ │ ss │ │ ngrep │
│ │ tcpdump │ │ │
├─────────────┼──────────────┼─────────────────┼─────────────────────┤
│ 磁盘IO │ iostat │ iotop │ blktrace │
│ │ iotop │ │ filebeat │
│ │ lsof │ │ │
└─────────────┴──────────────┴─────────────────┴─────────────────────┘
问题排查决策树:
开始 → 问题现象是什么?
├─ 应用响应慢 → CPU高吗?
│ ├─ 是 → 使用async-profiler分析热点方法
│ ├─ 否 → 检查GC停顿
│ └─ → 检查线程阻塞
│
├─ 内存使用高 → OOM错误吗?
│ ├─ 是 → jmap获取堆Dump,MAT分析
│ ├─ 否 → jstat监控GC,检查内存泄漏
│ └─ → 检查堆外内存
│
├─ 线程数异常 → 线程数持续增长?
│ ├─ 是 → jstack分析,检查线程池配置
│ ├─ 否 → 检查死锁
│ └─ → 检查线程等待
│
└─ 其他问题 → 根据具体现象选择工具
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
# 15.2 实战问题排查流程
// 模拟复杂问题的应用程序
public class ComplexProblemApp {
private static ExecutorService executor = Executors.newFixedThreadPool(100);
private static Map<String, byte[]> cache = new ConcurrentHashMap<>();
private static AtomicInteger counter = new AtomicInteger();
public static void main(String[] args) throws Exception {
System.out.println("复杂问题应用启动...");
System.out.println("PID: " + ManagementFactory.getRuntimeMXBean().getName().split("@")[0]);
// 启动各种问题模拟器
startCpuProblem();
startMemoryProblem();
startThreadProblem();
startDeadlockProblem();
// 监控线程
Thread monitor = new Thread(() -> {
while (true) {
try {
printStats();
Thread.sleep(5000);
} catch (InterruptedException e) {
break;
}
}
});
monitor.setDaemon(true);
monitor.start();
// 保持运行
Thread.sleep(Long.MAX_VALUE);
}
// 模拟CPU问题
static void startCpuProblem() {
executor.submit(() -> {
while (true) {
// 计算密集型任务
for (int i = 0; i < 1000000; i++) {
Math.pow(i, 2);
}
Thread.yield();
}
});
}
// 模拟内存问题
static void startMemoryProblem() {
executor.submit(() -> {
while (true) {
// 内存泄漏
String key = "data_" + counter.incrementAndGet();
cache.put(key, new byte[1024 * 1024]); // 1MB
if (cache.size() > 100) {
// 只移除部分,模拟部分泄漏
Iterator<String> it = cache.keySet().iterator();
for (int i = 0; i < 50 && it.hasNext(); i++) {
it.next();
it.remove();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
});
}
// 模拟线程问题
static void startThreadProblem() {
for (int i = 0; i < 1000; i++) {
final int id = i;
executor.submit(() -> {
synchronized (ComplexProblemApp.class) {
try {
// 线程等待
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("任务 " + id + " 完成");
});
}
}
// 模拟死锁
static void startDeadlockProblem() {
Object lock1 = new Object();
Object lock2 = new Object();
executor.submit(() -> {
synchronized (lock1) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("线程1获取两个锁");
}
}
});
executor.submit(() -> {
synchronized (lock2) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("线程2获取两个锁");
}
}
});
}
static void printStats() {
Runtime rt = Runtime.getRuntime();
long total = rt.totalMemory() / 1024 / 1024;
long free = rt.freeMemory() / 1024 / 1024;
long used = total - free;
System.out.println("\n=== 应用状态 ===");
System.out.println("内存: " + used + "MB/" + total + "MB");
System.out.println("缓存大小: " + cache.size() + " 项");
System.out.println("线程池活跃线程: " + ((ThreadPoolExecutor)executor).getActiveCount());
System.out.println("线程池队列大小: " + ((ThreadPoolExecutor)executor).getQueue().size());
}
}
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
128
#!/bin/bash
# comprehensive_troubleshooting.sh - 综合问题排查脚本
PID=$1
OUTPUT_DIR="troubleshoot_$(date +%Y%m%d_%H%M%S)"
echo "创建输出目录: $OUTPUT_DIR"
mkdir -p $OUTPUT_DIR
echo "=== 开始综合问题排查 ==="
# 1. 系统状态
echo "1. 收集系统状态..."
top -b -n1 > $OUTPUT_DIR/top.txt
free -h > $OUTPUT_DIR/free.txt
vmstat 1 5 > $OUTPUT_DIR/vmstat.txt
iostat -x 1 5 > $OUTPUT_DIR/iostat.txt
# 2. 进程状态
echo "2. 收集进程状态..."
ps aux | grep -E "(PID|$PID)" > $OUTPUT_DIR/ps_aux.txt
pstree -p $PID > $OUTPUT_DIR/pstree.txt 2>/dev/null
lsof -p $PID > $OUTPUT_DIR/lsof.txt 2>/dev/null
# 3. JVM状态
echo "3. 收集JVM状态..."
jps -l > $OUTPUT_DIR/jps.txt
jinfo $PID > $OUTPUT_DIR/jinfo.txt 2>/dev/null
jstat -gc $PID 1000 5 > $OUTPUT_DIR/jstat_gc.txt
jstat -gccapacity $PID > $OUTPUT_DIR/jstat_gccapacity.txt
jmap -heap $PID > $OUTPUT_DIR/jmap_heap.txt 2>/dev/null
# 4. 线程分析
echo "4. 收集线程信息..."
jstack $PID > $OUTPUT_DIR/jstack.txt 2>/dev/null
# 多次采样,用于分析线程变化
for i in {1..3}; do
jstack $PID > $OUTPUT_DIR/jstack_sample_$i.txt 2>/dev/null
sleep 2
done
# 5. 堆内存分析(如果内存使用高)
echo "5. 检查内存使用情况..."
HEAP_USAGE=$(jstat -gc $PID 2>/dev/null | tail -1 | awk '{print $3+$4+$6+$8}')
HEAP_USAGE_MB=$(echo "$HEAP_USAGE / 1024" | bc)
if [ $HEAP_USAGE_MB -gt 1000 ]; then
echo "堆内存使用 ${HEAP_USAGE_MB}MB > 1000MB,生成堆Dump..."
jmap -dump:live,format=b,file=$OUTPUT_DIR/heap_dump.hprof $PID
else
echo "堆内存使用 ${HEAP_USAGE_MB}MB,不生成堆Dump"
fi
# 6. GC日志分析(如果有GC日志)
echo "6. 检查GC日志..."
if [ -f gc.log ]; then
cp gc.log $OUTPUT_DIR/
# 使用gceasy.io的格式输出关键指标
echo "=== GC日志分析 ===" > $OUTPUT_DIR/gc_analysis.txt
grep -E "(Full GC|GC)" gc.log | tail -20 >> $OUTPUT_DIR/gc_analysis.txt
fi
# 7. 网络连接
echo "7. 收集网络连接信息..."
netstat -tunlp | grep $PID > $OUTPUT_DIR/netstat.txt 2>/dev/null || true
ss -tunlp | grep $PID > $OUTPUT_DIR/ss.txt 2>/dev/null || true
# 8. 生成分析报告
echo "8. 生成分析报告..."
cat > $OUTPUT_DIR/report.md << EOF
# JVM问题排查报告
生成时间: $(date)
## 1. 系统概览
\`\`\`
$(cat $OUTPUT_DIR/top.txt | head -20)
\`\`\`
## 2. 内存状态
\`\`\`
$(cat $OUTPUT_DIR/free.txt)
\`\`\`
## 3. JVM内存使用
堆内存: ${HEAP_USAGE_MB}MB
## 4. GC状态
\`\`\`
$(tail -10 $OUTPUT_DIR/jstat_gc.txt 2>/dev/null || echo "无GC数据")
\`\`\`
## 5. 线程状态
线程Dump数量: 4个
线程状态分布:
\`\`\`
$(grep "java.lang.Thread.State" $OUTPUT_DIR/jstack.txt 2>/dev/null | sort | uniq -c | sort -rn || echo "无线程数据")
\`\`\`
## 6. 建议的下一步操作
1. 检查高CPU线程: 查看 $OUTPUT_DIR/jstack.txt 中的RUNNABLE线程
2. 检查死锁: grep -i deadlock $OUTPUT_DIR/jstack.txt
3. 分析内存泄漏: 使用MAT分析 $OUTPUT_DIR/heap_dump.hprof
4. 检查GC问题: 分析 $OUTPUT_DIR/gc_analysis.txt
## 7. 可能的问题
$(if [ $HEAP_USAGE_MB -gt 2000 ]; then echo "- 内存使用过高,可能存在内存泄漏"; fi)
$(if grep -q "deadlock" $OUTPUT_DIR/jstack.txt 2>/dev/null; then echo "- 检测到死锁"; fi)
$(if [ $(grep "RUNNABLE" $OUTPUT_DIR/jstack.txt 2>/dev/null | wc -l) -gt 10 ]; then echo "- 大量RUNNABLE线程,可能CPU过高"; fi)
EOF
echo "=== 排查完成 ==="
echo "输出目录: $OUTPUT_DIR"
echo "报告文件: $OUTPUT_DIR/report.md"
echo ""
echo "常用命令:"
echo " 查看报告: cat $OUTPUT_DIR/report.md"
echo " 分析线程: grep -A 10 'RUNNABLE' $OUTPUT_DIR/jstack.txt"
echo " 检查死锁: grep -B 5 -A 5 'deadlock' $OUTPUT_DIR/jstack.txt"
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
# 16. 调试排错 - Java 问题排查之JVM可视化工具
# 16.1 VisualVM 实战指南
VisualVM 是功能全面的免费JVM监控工具:
VisualVM 功能架构:
┌─────────────────────────────────────────────────────────┐
│ VisualVM 主要功能 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 概览 (Overview) │
│ ┌─────────────────────────────────────────────┐ │
│ │ PID: 1234 │ │
│ │ 主类: com.example.Main │ │
│ │ JVM: OpenJDK 64-Bit 11.0.12 │ │
│ │ 参数: -Xms2g -Xmx2g │ │
│ │ 系统: Linux 4.15.0 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 2. 监控 (Monitor) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ CPU │ │ 堆 │ │ 类 │ │
│ │ 使用率曲线 │ │ 内存曲线 │ │ 加载曲线 │ │
│ │ │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 线程 │ │ GC活动 │ │
│ │ 数量曲线 │ │ 曲线 │ │
│ │ │ │ │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ 3. 线程 (Threads) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 状态图: □运行 ■休眠 □等待 □驻留 □监视 │ │
│ │ 线程列表: │ │
│ │ 名称 状态 CPU时间 堆栈 │ │
│ │ http-nio-... 运行 12:34.5 java.net │ │
│ │ pool-1-thread-1 等待 00:01.2 sun.misc │ │
│ │ ... │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 4. 采样器 (Sampler) │
│ CPU采样:热点方法调用树 │
│ 内存采样:对象分配统计 │
│ │
│ 5. 分析器 (Profiler) │
│ CPU性能分析:方法级耗时统计 │
│ 内存分析:对象分配跟踪 │
│ │
│ 6. BTrace 脚本 (动态跟踪) │
│ 运行时注入跟踪代码,无需重启 │
└─────────────────────────────────────────────────────────┘
VisualVM 内存分析示例:
问题:应用内存持续增长,怀疑内存泄漏
步骤1:监控堆内存使用
时间 | 堆使用 | 已提交 | 最大
10:00 | 512MB | 1GB | 2GB
10:05 | 768MB | 1GB | 2GB
10:10 | 1.2GB | 1.5GB | 2GB
10:15 | 1.6GB | 2GB | 2GB
趋势:持续增长,无下降 → 可能泄漏
步骤2:执行GC后观察
手动触发GC → 堆使用从1.6GB降到1.4GB
但很快又增长 → 确认泄漏
步骤3:堆Dump分析
1. 点击"堆Dump"按钮生成hprof文件
2. 点击"类"标签,按实例数排序
3. 发现: char[] 1000000+实例,占800MB
4. 右键 → 在实例视图中显示
5. 发现: 所有char[]被StringCache持有
6. 右键StringCache → 显示GC根路径
7. 发现: 静态Map不断添加,从不移除
步骤4:定位代码
查看引用链:
StringCache (static Map)
↓
LinkedHashMap.Entry
↓
char[] (存储字符串内容)
↓
实际字符串值
步骤5:修复验证
修复StringCache添加清除逻辑
重新监控 → 内存稳定
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
# 16.2 JMC (Java Mission Control) 深度使用
JMC 是Oracle官方的高级监控工具:
// 使用JMC飞行记录器(JFR)进行性能分析
public class JFRAnalysisExample {
// 启用JFR需要添加JVM参数
// -XX:+UnlockCommercialFeatures (JDK11之前需要)
// -XX:+FlightRecorder
// -XX:StartFlightRecording=duration=60s,filename=recording.jfr
public static void main(String[] args) throws Exception {
System.out.println("JFR分析示例开始...");
// 模拟不同性能问题
startMemoryAllocationTask();
startContendedLockTask();
startIOBlockingTask();
startCPUTask();
// 运行一段时间供JFR记录
Thread.sleep(60000);
System.out.println("示例结束,请使用JMC打开recording.jfr文件分析");
}
// 内存分配任务
static void startMemoryAllocationTask() {
Thread thread = new Thread(() -> {
List<byte[]> list = new ArrayList<>();
while (true) {
// 每秒分配10MB
list.add(new byte[10 * 1024 * 1024]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
// 保留一部分,模拟缓慢泄漏
if (list.size() > 5) {
list.remove(0);
}
}
}, "memory-allocator");
thread.setDaemon(true);
thread.start();
}
// 竞争锁任务
static void startContendedLockTask() {
Object lock = new Object();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
while (true) {
synchronized (lock) {
try {
// 持有锁一段时间,模拟竞争
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
break;
}
}
}, "lock-contender-" + i);
thread.setDaemon(true);
thread.start();
}
}
// IO阻塞任务
static void startIOBlockingTask() {
Thread thread = new Thread(() -> {
while (true) {
try {
// 模拟IO阻塞
Thread.sleep(500);
} catch (InterruptedException e) {
break;
}
}
}, "io-blocker");
thread.setDaemon(true);
thread.start();
}
// CPU密集型任务
static void startCPUTask() {
Thread thread = new Thread(() -> {
while (true) {
// 计算密集型
for (int i = 0; i < 1000000; i++) {
Math.sqrt(i);
}
Thread.yield();
}
}, "cpu-intensive");
thread.setDaemon(true);
thread.start();
}
}
// JFR事件监控示例
public class JFREventMonitor {
// JFR自定义事件
@jdk.jfr.Event
@Label("Business Transaction")
@Description("记录业务事务执行")
@Category("Business")
static class BusinessTransactionEvent extends Event {
@Label("Transaction ID")
String transactionId;
@Label("Duration")
@Timespan(Timespan.MILLISECONDS)
long duration;
@Label("Success")
boolean success;
@Label("User")
String user;
}
// 业务方法
public void processOrder(String orderId, String user) {
long start = System.currentTimeMillis();
boolean success = false;
// 开始事务
BusinessTransactionEvent event = new BusinessTransactionEvent();
event.begin();
event.transactionId = orderId;
event.user = user;
try {
// 模拟业务处理
Thread.sleep(100 + new Random().nextInt(100));
// 模拟偶尔失败
success = new Random().nextDouble() > 0.1;
if (!success) {
throw new RuntimeException("处理失败");
}
event.success = true;
} catch (Exception e) {
event.success = false;
} finally {
event.duration = System.currentTimeMillis() - start;
event.commit();
}
}
public static void main(String[] args) throws Exception {
JFREventMonitor monitor = new JFREventMonitor();
// 模拟业务请求
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
final int id = i;
executor.submit(() -> {
monitor.processOrder("ORDER_" + id, "USER_" + (id % 100));
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("业务处理完成,请使用JMC查看Business Transaction事件");
}
}
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# 17. 调试排错 - Java 问题排查之应用在线调试Arthas
# 17.1 Arthas 核心功能
Arthas 是Alibaba开源的Java诊断工具,像Java应用的"听诊器":
Arthas 核心功能架构:
┌─────────────────────────────────────────────────────────┐
│ Arthas 功能体系 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 基础命令 │
│ dashboard # 系统仪表板,实时监控 │
│ thread # 查看线程,支持死锁检测 │
│ jvm # JVM信息,参数、内存、类加载等 │
│ sysprop # 查看/修改系统属性 │
│ sysenv # 查看系统环境变量 │
│ │
│ 2. 类/类加载器命令 │
│ sc # 查看类信息 (Search Class) │
│ sm # 查看方法信息 (Search Method) │
│ jad # 反编译字节码为Java代码 │
│ mc # 编译.java文件为.class │
│ redefine # 热更新类 (热修复) │
│ │
│ 3. 方法执行监控 │
│ watch # 方法执行数据观测 │
│ trace # 方法内部调用路径跟踪 │
│ stack # 输出当前方法被调用的调用路径 │
│ tt # 方法执行数据的时空隧道 │
│ monitor # 方法执行监控 │
│ │
│ 4. 性能诊断 │
│ profiler # 生成火焰图,性能分析 │
│ heapdump # 生成堆Dump文件 │
│ logger # 查看和修改logger配置 │
│ │
│ 5. 高级功能 │
│ ognl # 执行ognl表达式 │
│ mbean # 查看Mbean信息 │
│ vmtool # 查询/操作JVM内存对象 │
│ options # 查看/设置Arthas全局选项 │
└─────────────────────────────────────────────────────────┘
Arthas 诊断流程示例:
问题:用户服务响应慢,怀疑某个方法执行过慢
步骤1:启动Arthas
$ ./as.sh # 选择目标Java进程PID
步骤2:查看总体状态
$ dashboard # 查看CPU、内存、线程概况
┌─ID─┬─名称──────────┬─组───────┬─优先级─┬─状态───┬─CPU%─┐
│ 23 │ http-nio-8080 │ main │ 5 │ RUNNAB │ 85.3 │
│ 24 │ pool-1-thread│ main │ 5 │ WAITING│ 0.0 │
└────┴───────────────┴─────────┴────────┴────────┴──────┘
步骤3:查找热点方法
$ profiler start # 开始性能采样
$ profiler stop # 停止采样,生成火焰图
$ profiler getSamples # 获取采样数据
步骤4:跟踪具体方法
$ trace com.example.UserService getUserInfo # 跟踪方法调用
`---ts=2023-10-01 10:00:00;thread_name=http-nio-8080-exec-1;id=1;is_daemon=true;priority=5;TCCL=AppClassLoader
`---[12.345678ms] com.example.UserService:getUserInfo()
+---[0.123456ms] com.example.UserService:validate() # 验证
+---[10.234567ms] com.example.UserService:queryDB() # 数据库查询慢!
`---[0.123456ms] com.example.UserService:format() # 格式化
步骤5:观察方法参数和返回值
$ watch com.example.UserService queryDB "{params,returnObj}" -x 3
ts=2023-10-01 10:00:01; [cost=10.234567ms] result=@ArrayList[
@Object[][
@String[userId_123]
],
@User[
id=123,
name=张三,
age=30
]
]
步骤6:分析数据库查询
$ trace com.example.UserDAO findById # 跟踪DAO方法
`---[8.765432ms] com.example.UserDAO:findById()
+---[0.123456ms] getConnection() # 获取连接
+---[8.123456ms] executeQuery() # 执行查询慢
`---[0.123456ms] close() # 关闭连接
步骤7:检查SQL执行
# 发现SQL缺少索引,添加索引后再次测试
$ trace com.example.UserDAO findById
`---[1.234567ms] com.example.UserDAO:findById() # 明显改善!
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
# 17.2 Arthas 实战示例
#!/bin/bash
# arthas_diagnosis.sh - 使用Arthas进行问题诊断
echo "=== Arthas Java诊断实战 ==="
# 1. 启动Arthas(假设已下载as.sh)
# ./as.sh
# 模拟问题场景:内存泄漏诊断
echo "场景:内存泄漏诊断"
# 步骤1: 查看JVM内存
echo "步骤1: 查看JVM内存信息"
cat << EOF
在Arthas中执行:
jvm
查看内存各部分使用情况
重点关注堆内存使用趋势
EOF
# 步骤2: 监控堆内存增长
echo -e "\n步骤2: 监控堆内存增长"
cat << EOF
执行dashboard命令观察内存变化:
dashboard -i 1000 # 每秒刷新一次
观察堆内存(heap)是否持续增长而不下降
EOF
# 步骤3: 查看类实例数量
echo -e "\n步骤3: 查看类实例数量"
cat << EOF
如果怀疑某个类实例过多,执行:
jvm --class
或使用更详细的命令:
sc -d *MemoryLeak* # 查找相关类
EOF
# 步骤4: 堆Dump分析
echo -e "\n步骤4: 生成堆Dump分析"
cat << EOF
生成堆Dump:
heapdump /tmp/heapdump.hprof
然后使用MAT或JVisualVM分析
EOF
# 模拟问题场景:CPU过高诊断
echo -e "\n\n场景:CPU过高诊断"
# 步骤1: 查看线程CPU使用
echo "步骤1: 查看线程CPU使用"
cat << EOF
执行:
thread -n 5 # 查看CPU使用率最高的5个线程
关注RUNNABLE状态的线程
EOF
# 步骤2: 查看线程栈
echo -e "\n步骤2: 查看热点线程栈"
cat << EOF
对CPU高的线程,查看其栈:
thread <线程ID>
例如:
thread 23
EOF
# 步骤3: 方法级性能分析
echo -e "\n步骤3: 方法级性能分析"
cat << EOF
如果发现某个方法可疑,进行跟踪:
trace com.example.Service expensiveMethod
观察方法内部各调用的耗时
EOF
# 步骤4: 生成火焰图
echo -e "\n步骤4: 生成火焰图进行深度分析"
cat << EOF
profiler start # 开始采样
等待30秒收集数据
profiler stop --format html # 停止并生成HTML格式火焰图
profiler getSamples # 获取原始采样数据
EOF
# 模拟问题场景:死锁诊断
echo -e "\n\n场景:死锁诊断"
# 步骤1: 检测死锁
echo "步骤1: 检测死锁"
cat << EOF
执行:
thread -b # 检测死锁
或
thread --state BLOCKED # 查看所有阻塞线程
EOF
# 步骤2: 查看死锁线程详情
echo -e "\n步骤2: 查看死锁线程详情"
cat << EOF
对死锁涉及的线程,查看详细栈:
thread <线程ID1>
thread <线程ID2>
注意waiting to lock和locked的信息
EOF
# 模拟问题场景:动态修改代码
echo -e "\n\n场景:动态修改代码(热修复)"
# 步骤1: 反编译类
echo "步骤1: 反编译类查看源码"
cat << EOF
jad com.example.BuggyClass
查看有问题的代码
EOF
# 步骤2: 修改源代码
echo -e "\n步骤2: 修改源代码"
cat << EOF
将反编译的代码保存到文件:
jad --source-only com.example.BuggyClass > BuggyClass.java
修改BuggyClass.java中的问题代码
EOF
# 步骤3: 编译修改后的类
echo -e "\n步骤3: 编译修改后的类"
cat << EOF
在Arthas中编译:
mc BuggyClass.java -d /tmp
生成新的.class文件
EOF
# 步骤4: 热替换类
echo -e "\n步骤4: 热替换类"
cat << EOF
使用redefine命令加载新类:
redefine /tmp/com/example/BuggyClass.class
注意:修改的方法签名不能改变,只能修改方法体
EOF
# Arthas常用命令速查
echo -e "\n=== Arthas常用命令速查 ==="
cat << EOF
1. 启动/连接: ./as.sh 或 java -jar arthas-boot.jar
2. 基础信息:
dashboard # 仪表板
sysprop # 系统属性
sysenv # 环境变量
3. 类/方法:
sc -d *ClassName* # 搜索类
jad ClassName # 反编译
sm ClassName # 查看方法
4. 监控诊断:
watch ClassName method "{params,returnObj}" -x 3
trace ClassName method
stack ClassName method
monitor -c 5 ClassName method # 5秒统计一次
5. 性能分析:
profiler start/stop # CPU性能分析
heapdump # 堆Dump
thread -n 3 # 最忙的3个线程
6. 高级功能:
ognl '@com.example.Bean@staticField'
options unsafe true # 开启不安全操作
vmtool --action getInstances --className ClassName
7. 退出:
stop # 停止Arthas
shutdown # 关闭Arthas服务器
EOF
echo -e "\n=== 实战示例结束 ==="
echo "更多信息参考: https://arthas.aliyun.com/doc/"
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# 18. 调试排错 - Java 问题排查之使用IDEA本地调试和远程调试
# 18.1 IDEA 本地调试技巧
IDEA调试器就像程序的时间机器,可以控制执行流程:
IDEA 调试功能全景:
┌─────────────────────────────────────────────────────────┐
│ IDEA 调试器功能 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 断点类型 │
│ ┌─────────────┬─────────────────────────────────┐ │
│ │ 行断点 │ 最常用,在代码行左侧点击 │ │
│ ├─────────────┼─────────────────────────────────┤ │
│ │ 方法断点 │ 在方法签名行,入口/出口 │ │
│ ├─────────────┼─────────────────────────────────┤ │
│ │ 字段断点 │ 在字段声明,监控读/写访问 │ │
│ ├─────────────┼─────────────────────────────────┤ │
│ │ 异常断点 │ Debug窗口 → View Breakpoints → │ │
│ │ │ Exception Breakpoints │ │
│ ├─────────────┼─────────────────────────────────┤ │
│ │ 条件断点 │ 右键断点 → 设置条件表达式 │ │
│ └─────────────┴─────────────────────────────────┘ │
│ │
│ 2. 执行控制 │
│ ┌─────┬──────┬──────────────────────────────────┐ │
│ │ F8 │ Step Over │ 单步执行,不进入方法内部 │ │
│ ├─────┼──────┼──────────────────────────────────┤ │
│ │ F7 │ Step Into │ 进入方法内部 │ │
│ ├─────┼──────┼──────────────────────────────────┤ │
│ │ ⇧F8 │ Step Out │ 从当前方法跳出 │ │
│ ├─────┼──────┼──────────────────────────────────┤ │
│ │ F9 │ Resume │ 继续执行到下一个断点 │ │
│ ├─────┼──────┼──────────────────────────────────┤ │
│ │ ⌥F9 │ Run to Cursor │ 执行到光标位置 │ │
│ ├─────┼──────┼──────────────────────────────────┤ │
│ │ ⇧F9 │ Smart Step Into │ 智能进入(选择方法) │ │
│ └─────┴──────┴──────────────────────────────────┘ │
│ │
│ 3. 数据查看 │
│ • Variables窗口:查看局部变量和this │
│ • Watches窗口:添加自定义监控表达式 │
│ • Evaluate Expression:实时计算表达式 │
│ • Memory View:查看对象内存布局 │
│ │
│ 4. 高级功能 │
│ • Drop Frame:回退到上一个栈帧 │
│ • Force Return:强制从当前方法返回指定值 │
│ • Set Value:修改变量值 │
│ • Trace Current Stream Chain:跟踪Stream流水线 │
└─────────────────────────────────────────────────────────┘
IDEA调试多线程程序:
问题:多线程竞争导致数据不一致
调试步骤:
1. 在关键代码设置断点
2. 右键断点 → Suspend → Thread (默认是All)
3. 运行Debug,断点命中时只有当前线程暂停
4. 观察不同线程的执行顺序
示例:两个线程操作共享List
线程1: list.add("A")
线程2: list.add("B")
调试观察:
断点位置:synchronized (lock) { list.add(value); }
线程1暂停时:
Variables窗口:
this: ArrayList@753 (size=0)
value: "A"
lock: Object@754
切换到线程2(Frames窗口选择线程2):
Variables窗口:
this: ArrayList@753 (size=0) ← 同一个List!
value: "B"
lock: Object@754 ← 等待获取锁
让线程1继续执行(F9) → list大小变为1
线程2获取锁继续执行 → list大小变为2
发现问题:如果没有synchronized,可能出现size不一致
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
# 18.2 远程调试配置与实践
远程调试就像给生产环境的应用安装"监控摄像头":
// 需要远程调试的应用
public class RemoteDebugApp {
private static Map<String, User> userCache = new ConcurrentHashMap<>();
private static AtomicInteger counter = new AtomicInteger();
public static void main(String[] args) throws Exception {
System.out.println("远程调试示例应用启动...");
// 启动参数需要包含远程调试选项
// -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
// 启动HTTP服务器
startHttpServer(8080);
// 模拟业务处理
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
scheduler.scheduleAtFixedRate(() -> {
processUserRequest("user_" + counter.incrementAndGet());
}, 0, 1, TimeUnit.SECONDS);
// 保持运行
Thread.currentThread().join();
}
static void startHttpServer(int port) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/user", exchange -> {
String userId = exchange.getRequestURI().getQuery().split("=")[1];
User user = getUser(userId);
String response = user != null ?
"User: " + user.name + ", Age: " + user.age :
"User not found";
exchange.sendResponseHeaders(200, response.getBytes().length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
});
server.createContext("/cache", exchange -> {
String response = "Cache size: " + userCache.size();
exchange.sendResponseHeaders(200, response.getBytes().length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
});
server.createContext("/clear", exchange -> {
userCache.clear();
String response = "Cache cleared";
exchange.sendResponseHeaders(200, response.getBytes().length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
});
server.setExecutor(null);
server.start();
System.out.println("HTTP服务器启动在端口 " + port);
}
static User getUser(String userId) {
// 缓存中查找
User user = userCache.get(userId);
if (user != null) {
System.out.println("缓存命中: " + userId);
return user;
}
// 模拟数据库查询
System.out.println("查询数据库: " + userId);
try {
Thread.sleep(100); // 模拟数据库延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
user = new User(userId, "User_" + userId, 20 + new Random().nextInt(30));
// 放入缓存
userCache.put(userId, user);
return user;
}
static void processUserRequest(String userId) {
long start = System.currentTimeMillis();
try {
User user = getUser(userId);
// 模拟业务处理
Thread.sleep(50);
// 偶尔处理失败
if (new Random().nextDouble() < 0.01) {
throw new RuntimeException("处理失败: " + userId);
}
long cost = System.currentTimeMillis() - start;
if (cost > 200) {
System.err.println("慢请求: " + userId + ", 耗时: " + cost + "ms");
}
} catch (Exception e) {
System.err.println("请求失败: " + userId + ", 错误: " + e.getMessage());
}
}
static class User {
String id;
String name;
int age;
User(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
}
}
// 远程调试启动脚本
// start_with_remote_debug.sh
#!/bin/bash
# JVM远程调试参数
# 常用配置:
# 1. Socket连接,服务端模式,不挂起,端口5005
REMOTE_DEBUG="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"
# 2. 如果调试Docker容器中的Java应用
# REMOTE_DEBUG="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
# 3. 如果被调试程序在启动时需要先暂停等待调试器连接
# REMOTE_DEBUG="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005"
# 4. 共享内存传输(仅Windows)
# REMOTE_DEBUG="-agentlib:jdwp=transport=dt_shmem,server=y,suspend=n,address=javadebug"
# 应用参数
JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+PrintGCDetails"
MAIN_CLASS="com.example.RemoteDebugApp"
echo "启动远程调试应用..."
echo "调试端口: 5005"
echo "应用端口: 8080"
echo ""
echo "在IDEA中配置远程调试:"
echo "1. Run → Edit Configurations → + → Remote JVM Debug"
echo "2. Name: Remote Debug"
echo "3. Host: localhost (或远程服务器IP)"
echo "4. Port: 5005"
echo "5. 选择JDK版本"
echo ""
# 启动应用
java $JAVA_OPTS $REMOTE_DEBUG -cp . $MAIN_CLASS
# Docker容器中启动示例
# docker run -p 8080:8080 -p 5005:5005 \
# -e JAVA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" \
# my-java-app
# Kubernetes中配置示例
# 在Deployment的spec.template.spec.containers中添加:
# env:
# - name: JAVA_TOOL_OPTIONS
# value: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
# ports:
# - containerPort: 5005
# name: debug
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<!-- IDEA 远程调试配置示例 -->
<!-- 存储在 .idea/runConfigurations/Remote_Debug.xml -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Remote Debug" type="Remote">
<option name="USE_SOCKET_TRANSPORT" value="true" />
<option name="SERVER_MODE" value="true" />
<option name="SHMEM_ADDRESS" value="javadebug" />
<option name="HOST" value="localhost" />
<option name="PORT" value="5005" />
<option name="AUTO_RESTART" value="false" />
<RunnerSettings RunnerId="Debug">
<option name="DEBUG_PORT" value="5005" />
<option name="LOCAL" value="false" />
</RunnerSettings>
<method v="2">
<option name="Make" enabled="true" />
<!-- 调试前构建项目 -->
<option name="BuildArtifacts" enabled="true">
<artifact name="myapp:jar" />
</option>
</method>
</configuration>
</component>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Docker Compose 远程调试配置
# docker-compose-debug.yml
version: '3.8'
services:
java-app:
image: openjdk:11-jre-slim
ports:
- "8080:8080" # 应用端口
- "5005:5005" # 调试端口
environment:
- JAVA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Xms512m -Xmx1024m
volumes:
- ./app.jar:/app.jar
- ./logs:/logs
command: java $JAVA_OPTS -jar /app.jar
networks:
- app-network
# 可选:数据库等其他服务
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=appdb
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
networks:
- app-network
networks:
app-network:
volumes:
mysql-data:
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
# 19. 调试排错 - Java动态调试技术原理
# 19.1 JPDA 架构与原理
JPDA (Java Platform Debugger Architecture) 是Java调试的底层架构:
JPDA 三层架构:
┌─────────────────────────────────────────────────────────┐
│ JPDA 三层架构 │
├─────────────────────────────────────────────────────────┤
│ │
│ 第三层:调试器接口层 (JDI - Java Debug Interface) │
│ 面向调试工具开发者的API │
│ ┌─────────────────────────────┐ │
│ │ VirtualMachine │ │
│ │ │ connect() │ │
│ │ │ disconnect() │ │
│ │ │ │
│ │ EventRequestManager │ │
│ │ │ createXxxRequest() │ │
│ │ │ │
│ │ EventQueue │ │
│ │ │ remove() │ │
│ └─────────────────────────────┘ │
│ ↑ │
│ │ 基于事件的通知机制 │
│ ↓ │
│ 第二层:调试器传输层 (JDWP - Java Debug Wire Protocol) │
│ 调试器与被调试JVM之间的通信协议 │
│ ┌─────────────────────────────┐ │
│ │ 包格式: │ │
│ │ ┌───┬───┬───┬─────────────┐│ │
│ │ │长度│ID │标志│ 数据 ││ │
│ │ └───┴───┴───┴─────────────┘│ │
│ │ │ │
│ │ 命令集: │ │
│ │ • VirtualMachine命令集 │ │
│ │ • ReferenceType命令集 │ │
│ │ • ClassType命令集 │ │
│ │ • Method命令集 │ │
│ │ • ObjectReference命令集 │ │
│ │ • EventRequest命令集 │ │
│ └─────────────────────────────┘ │
│ ↑ │
│ │ 传输无关,支持Socket/共享内存 │
│ ↓ │
│ 第一层:Java虚拟机工具接口 (JVMTI - JVM Tool Interface) │
│ JVM提供的本地编程接口 │
│ ┌─────────────────────────────┐ │
│ │ 事件通知: │ │
│ │ • 类加载/卸载 │ │
│ │ • 线程启动/结束 │ │
│ │ • 方法进入/退出 │ │
│ │ • 字段访问/修改 │ │
│ │ • 异常抛出/捕获 │ │
│ │ │ │
│ │ 功能函数: │ │
│ │ • 获取线程信息 │ │
│ │ • 获取类/方法/字段信息 │ │
│ │ • 本地变量操作 │ │
│ │ • 断点管理 │ │
│ │ • 堆遍历 │ │
│ └─────────────────────────────┘ │
│ ↑ │
│ │ JVM内部实现 │
│ ↓ │
│ Java虚拟机 (JVM) │
│ - HotSpot │
│ - OpenJ9 │
│ - GraalVM │
└─────────────────────────────────────────────────────────┘
JDWP 通信流程示例:设置断点
调试器 → 被调试JVM:
1. 连接建立
调试器连接到jdwp端口
Handshake: "JDWP-Handshake"
2. 发送断点设置命令
命令: SetBreakpoint (ID=1)
参数:
- 类签名: Lcom/example/MyClass;
- 方法名: myMethod
- 方法签名: ()V
- 位置: 行号23
3. JVMTI处理
JVMTI收到请求,在指定方法的指定位置插入断点指令
原指令: 正常字节码
替换为: breakpoint指令 (不同JVM实现不同)
4. 方法执行到断点
线程执行到断点位置 → 触发JVMTI事件
JVMTI生成Breakpoint事件
5. 事件传输
JDWP打包事件发送给调试器
包: Event Composite (包含Breakpoint事件)
6. 调试器响应
调试器收到事件,暂停界面显示
用户查看变量、调用栈等信息
7. 继续执行
用户点击继续 → 调试器发送Resume命令
JVMTI恢复线程执行
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
# 19.2 动态调试技术实现
// JVMTI 断点实现的简化示例(C语言)
// 实际HotSpot源码复杂得多,这是概念性示例
#include <jvmti.h>
#include <stdio.h>
// JVMTI环境
static jvmtiEnv *jvmti = NULL;
// 设置断点的回调
void JNICALL BreakpointCallback(jvmtiEnv *jvmti_env,
JNIEnv* jni_env,
jthread thread,
jmethodID method,
jlocation location) {
printf("断点命中!\n");
// 1. 获取方法信息
char *method_name = NULL;
char *method_signature = NULL;
jvmti_env->GetMethodName(method, &method_name, &method_signature, NULL);
printf("方法: %s%s\n", method_name, method_signature);
// 2. 获取局部变量
jint entry_count;
jvmtiLocalVariableEntry *table = NULL;
jvmti_env->GetLocalVariableTable(method, &entry_count, &table);
for (int i = 0; i < entry_count; i++) {
// 读取局部变量值(简化)
printf("局部变量 %d: %s\n", i, table[i].name);
}
// 3. 获取调用栈
jint frame_count;
jvmtiFrameInfo *frames = NULL;
jvmti_env->GetStackTrace(thread, 0, 50, frames, &frame_count);
printf("调用栈深度: %d\n", frame_count);
// 清理
jvmti_env->Deallocate((unsigned char*)method_name);
jvmti_env->Deallocate((unsigned char*)method_signature);
if (table != NULL) {
jvmti_env->Deallocate((unsigned char*)table);
}
if (frames != NULL) {
jvmti_env->Deallocate((unsigned char*)frames);
}
}
// 设置断点
jvmtiError SetBreakpoint(jvmtiEnv *env, jclass klass,
const char *method_name,
const char *method_signature,
jlocation location) {
// 1. 获取方法ID
jmethodID method;
jint method_count;
jmethodID *methods;
env->GetClassMethods(klass, &method_count, &methods);
for (int i = 0; i < method_count; i++) {
char *name, *sig;
env->GetMethodName(methods[i], &name, &sig, NULL);
if (strcmp(name, method_name) == 0 &&
strcmp(sig, method_signature) == 0) {
method = methods[i];
env->Deallocate((unsigned char*)name);
env->Deallocate((unsigned char*)sig);
break;
}
env->Deallocate((unsigned char*)name);
env->Deallocate((unsigned char*)sig);
}
env->Deallocate((unsigned char*)methods);
// 2. 设置断点
return env->SetBreakpoint(method, location);
}
// JVMTI初始化
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options,
void *reserved) {
printf("JVMTI Agent加载\n");
// 1. 获取JVMTI环境
jint result = vm->GetEnv((void **)&jvmti, JVMTI_VERSION_1_0);
if (result != JNI_OK || jvmti == NULL) {
printf("无法获取JVMTI环境\n");
return JNI_ERR;
}
// 2. 设置能力(声明需要哪些功能)
jvmtiCapabilities capabilities;
memset(&capabilities, 0, sizeof(capabilities));
capabilities.can_generate_breakpoint_events = 1;
capabilities.can_get_bytecodes = 1;
capabilities.can_get_synthetic_attribute = 1;
capabilities.can_get_owned_monitor_info = 1;
capabilities.can_generate_method_entry_events = 1;
capabilities.can_generate_method_exit_events = 1;
capabilities.can_access_local_variables = 1;
capabilities.can_get_monitor_info = 1;
capabilities.can_generate_monitor_events = 1;
capabilities.can_generate_vm_object_alloc_events = 1;
jvmti->AddCapabilities(&capabilities);
// 3. 设置事件回调
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.Breakpoint = &BreakpointCallback;
callbacks.MethodEntry = NULL; // 可以设置方法进入回调
callbacks.MethodExit = NULL; // 可以设置方法退出回调
jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
// 4. 启用事件通知
jvmti->SetEventNotificationMode(JVMTI_ENABLE,
JVMTI_EVENT_BREAKPOINT,
NULL);
printf("JVMTI Agent初始化完成\n");
return JNI_OK;
}
// 清理
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
printf("JVMTI Agent卸载\n");
}
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// 基于Java Agent的动态调试示例
public class DynamicDebugAgent {
// 预定义的Transformer,用于字节码插桩
static class DebugTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
// 只处理特定包
if (className == null || !className.startsWith("com/example/")) {
return null; // 返回null表示不修改
}
try {
// 使用ASM操作字节码
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new DebugClassVisitor(cw, className);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
// 使用ASM插入调试代码
static class DebugClassVisitor extends ClassVisitor {
private String className;
public DebugClassVisitor(ClassVisitor cv, String className) {
super(Opcodes.ASM9, cv);
this.className = className;
}
@Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc,
signature, exceptions);
if (!"<init>".equals(name) && !"<clinit>".equals(name)) {
// 为方法添加调试逻辑
return new DebugMethodVisitor(mv, className, name, desc);
}
return mv;
}
}
static class DebugMethodVisitor extends MethodVisitor {
private String className;
private String methodName;
private String methodDesc;
public DebugMethodVisitor(MethodVisitor mv,
String className,
String methodName,
String methodDesc) {
super(Opcodes.ASM9, mv);
this.className = className;
this.methodName = methodName;
this.methodDesc = methodDesc;
}
@Override
public void visitCode() {
// 方法开始处插入调试代码
super.visitCode();
// 调用调试记录方法
mv.visitLdcInsn(className.replace('/', '.'));
mv.visitLdcInsn(methodName);
mv.visitLdcInsn(methodDesc);
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"com/example/DebugRecorder",
"methodEntry",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
false);
}
@Override
public void visitInsn(int opcode) {
// 在返回指令前插入调试代码
if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
// 调用方法退出记录
mv.visitLdcInsn(className.replace('/', '.'));
mv.visitLdcInsn(methodName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"com/example/DebugRecorder",
"methodExit",
"(Ljava/lang/String;Ljava/lang/String;)V",
false);
}
super.visitInsn(opcode);
}
}
// 调试记录器
public static class DebugRecorder {
private static ThreadLocal<Long> startTime = new ThreadLocal<>();
private static ThreadLocal<Integer> callDepth = ThreadLocal.withInitial(() -> 0);
public static void methodEntry(String className,
String methodName,
String methodDesc) {
int depth = callDepth.get();
callDepth.set(depth + 1);
startTime.set(System.currentTimeMillis());
// 打印调用信息
StringBuilder indent = new StringBuilder();
for (int i = 0; i < depth; i++) {
indent.append(" ");
}
System.out.printf("%s--> %s.%s%s%n",
indent, className, methodName, methodDesc);
}
public static void methodExit(String className, String methodName) {
Long start = startTime.get();
if (start != null) {
long cost = System.currentTimeMillis() - start;
int depth = callDepth.get();
StringBuilder indent = new StringBuilder();
for (int i = 0; i < depth; i++) {
indent.append(" ");
}
System.out.printf("%s<-- %s.%s [%dms]%n",
indent, className, methodName, cost);
callDepth.set(depth - 1);
}
}
}
// Java Agent入口
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("动态调试Agent启动");
// 添加类转换器
inst.addTransformer(new DebugTransformer(), true);
// 如果是重新定义类(热替换)
if (inst.isRedefineClassesSupported()) {
System.out.println("支持类重定义");
}
// 如果是重新转换类
if (inst.isRetransformClassesSupported()) {
System.out.println("支持类重新转换");
}
// 解析参数
if (agentArgs != null) {
String[] args = agentArgs.split(",");
for (String arg : args) {
System.out.println("Agent参数: " + arg);
}
}
}
// 动态attach入口
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("动态附加Agent启动");
premain(agentArgs, inst);
// 尝试重新转换已加载的类
try {
Class[] loadedClasses = inst.getAllLoadedClasses();
List<Class> classesToRetransform = new ArrayList<>();
for (Class clazz : loadedClasses) {
if (clazz.getName().startsWith("com.example.")) {
classesToRetransform.add(clazz);
}
}
if (!classesToRetransform.isEmpty()) {
inst.retransformClasses(
classesToRetransform.toArray(new Class[0]));
System.out.println("重新转换了 " + classesToRetransform.size() + " 个类");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 使用示例
/*
1. 静态加载(启动时):
java -javaagent:debugagent.jar=packages=com.example -jar app.jar
2. 动态附加(运行时):
// 需要tools.jar
import com.sun.tools.attach.VirtualMachine;
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent("debugagent.jar", "packages=com.example");
vm.detach();
3. Agent参数格式:
packages=com.example;methods=user*,order*;level=debug
4. 生成的调用跟踪:
--> com.example.UserService.getUser
--> com.example.UserDAO.findById
--> java.sql.Connection.prepareStatement
<-- java.sql.Connection.prepareStatement [15ms]
--> java.sql.PreparedStatement.executeQuery
<-- java.sql.PreparedStatement.executeQuery [120ms]
<-- com.example.UserDAO.findById [140ms]
<-- com.example.UserService.getUser [145ms]
*/
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
本教程全面覆盖了JVM的核心知识体系和实战调试技巧,从字节码基础到高级调试技术,从内存管理到性能优化。建议按照以下路径学习:
- 基础理解:先掌握JVM内存结构、类加载机制、字节码原理
- GC深入:理解不同垃圾回收器的工作原理和适用场景
- 实战调试:通过实际案例练习各种调试工具的使用
- 性能优化:结合监控数据,进行系统性性能调优
- 原理探究:深入理解动态调试、字节码增强等底层技术
在实际工作中,遇到问题时可根据症状快速定位到相关章节,利用提供的工具和方法进行排查。记住,理解原理是基础,熟练使用工具是关键,二者结合才能高效解决JVM相关问题。