JVM原理深度解析与调优实战

JVM(Java Virtual Machine)是Java程序的运行基石,深入理解JVM原理对于后端开发至关重要。本文将全面剖析JVM的核心机制,帮助读者掌握笔试面试重点,并提供实战调优经验。

1. JVM内存模型深度解析

1.1 内存区域划分

JVM将内存划分为以下几个运行时数据区域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
JVM内存结构
├── 线程共享区域
│ ├── 堆(Heap)
│ │ ├── 新生代(Young Generation)
│ │ │ ├── Eden区
│ │ │ ├── Survivor0区
│ │ │ └── Survivor1区
│ │ └── 老年代(Old Generation)
│ ├── 方法区(Method Area)
│ │ ├── 类信息
│ │ ├── 常量池
│ │ ├── 静态变量
│ │ └── JIT代码缓存
│ └── 运行时常量池(Runtime Constant Pool)
└── 线程私有区域
├── 程序计数器(Program Counter Register)
├── 虚拟机栈(JVM Stack)
└── 本地方法栈(Native Method Stack)

1.2 各区域详解与OOM分析

1.2.1 堆(Heap)

堆是JVM管理的最大一块内存区域,用于存放对象实例:

1
2
3
4
5
6
// 堆内存配置参数
-Xms512m # 初始堆大小
-Xmx1024m # 最大堆大小
-Xmn256m # 新生代大小
-XX:NewRatio=2 # 新生代与老年代比例
-XX:SurvivorRatio=8 # Eden与Survivor比例

堆OOM示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 堆内存溢出示例
public class HeapOOM {
static class OOMObject {}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}

// JVM参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
// 错误信息:java.lang.OutOfMemoryError: Java heap space

1.2.2 方法区(Method Area)

方法区用于存储已被虚拟机加载的类信息、常量、静态变量等:

1
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
// 方法区OOM示例(JDK8前)
public class MethodAreaOOM extends ClassLoader {
public static void main(String[] args) {
MethodAreaOOM loader = new MethodAreaOOM();
int i = 0;
try {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method,
Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
i++;
}
} catch (Exception e) {
System.out.println("创建类数量:" + i);
e.printStackTrace();
}
}

static class OOMObject {}
}

// 错误信息:java.lang.OutOfMemoryError: Metaspace(JDK8+)

1.2.3 虚拟机栈(JVM Stack)

每个方法执行时都会创建一个栈帧用于存储局部变量表、操作数栈等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 栈溢出示例
public class StackOOM {
private int stackLength = 1;

public void stackLeak() {
stackLength++;
stackLeak();
}

public static void main(String[] args) {
StackOOM oom = new StackOOM();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("栈深度:" + oom.stackLength);
e.printStackTrace();
}
}
}

// 错误信息:java.lang.StackOverflowError
// JVM参数:-Xss128k 设置栈大小

1.3 对象分配与内存布局

1.3.1 对象创建过程

1
2
// 对象创建步骤详解
Object obj = new Object();
  1. 类加载检查:检查类是否已加载、解析、初始化
  2. 分配内存
    • 指针碰撞(Bump the Pointer):内存规整时使用
    • 空闲列表(Free List):内存碎片较多时使用
  3. 初始化零值:将分配到的内存空间初始化为零值
  4. 设置对象头:包括哈希码、GC分代年龄、锁状态等
  5. 执行init方法:按照程序员的意愿初始化对象

1.3.2 对象内存布局

1
2
3
4
5
6
7
8
9
10
11
对象内存布局
├── 对象头(Header)
│ ├── Mark Word(64位系统占8字节)
│ │ ├── 哈希码(25位)
│ │ ├── GC分代年龄(4位)
│ │ ├── 锁状态标志(2位)
│ │ ├── 是否偏向锁(1位)
│ │ └── 偏向线程ID(23位)
│ └── 类型指针(Class Pointer)
├── 实例数据(Instance Data)
└── 对齐填充(Padding)

2. 垃圾回收机制深度剖析

2.1 垃圾回收基础理论

2.1.1 判断对象存活算法

引用计数法

  • 原理:给对象添加引用计数器,引用时+1,失效时-1
  • 缺点:无法解决循环引用问题

可达性分析算法

  • 原理:从GC Roots开始向下搜索,不可达的对象可被回收
  • GC Roots包括:
    • 虚拟机栈中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI引用的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 循环引用示例
public class ReferenceCountingGC {
public Object instance = null;

public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();

objA.instance = objB;
objB.instance = objA;

objA = null;
objB = null;

System.gc(); // 即使循环引用,也会被回收
}
}

2.1.2 垃圾回收算法

标记-清除算法(Mark-Sweep)

  • 标记所有需要回收的对象,然后统一回收
  • 缺点:效率低、产生内存碎片

复制算法(Copying)

  • 将内存分为两块,每次使用一块,回收时复制存活对象到另一块
  • 优点:实现简单、运行高效
  • 缺点:内存利用率低

标记-整理算法(Mark-Compact)

  • 标记存活对象,然后将所有存活对象向一端移动,清理边界外内存
  • 适用于老年代

分代收集算法

  • 新生代:复制算法
  • 老年代:标记-清除或标记-整理算法

2.2 HotSpot垃圾回收器详解

2.2.1 垃圾回收器概览

1
2
3
4
5
6
7
8
9
10
11
12
13
垃圾回收器分类
├── 新生代收集器
│ ├── Serial收集器(-XX:+UseSerialGC)
│ ├── ParNew收集器(-XX:+UseParNewGC)
│ └── Parallel Scavenge(-XX:+UseParallelGC)
├── 老年代收集器
│ ├── Serial Old收集器
│ ├── Parallel Old收集器(-XX:+UseParallelOldGC)
│ └── CMS收集器(-XX:+UseConcMarkSweepGC)
└── 整堆收集器
├── G1收集器(-XX:+UseG1GC)
├── ZGC(JDK11+)
└── Shenandoah(JDK12+)

2.2.2 CMS收集器详解

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器:

1
2
3
4
5
6
7
8
9
CMS收集器工作流程
├── 初始标记(Initial Mark)
│ └── 标记GC Roots能直接关联的对象,需要"Stop The World"
├── 并发标记(Concurrent Mark)
│ └── 进行GC Roots Tracing,与应用线程并发执行
├── 重新标记(Remark)
│ └── 修正并发标记期间变动的标记记录,需要"Stop The World"
└── 并发清除(Concurrent Sweep)
└── 清除无用对象,与应用线程并发执行

CMS优缺点

  • 优点:并发收集、低停顿
  • 缺点:
    • 对CPU资源敏感
    • 无法处理浮动垃圾
    • 基于标记-清除算法,会产生内存碎片

2.2.3 G1收集器详解

G1(Garbage First)收集器是一款面向服务端应用的垃圾收集器:

1
2
3
4
5
6
G1收集器特点
├── 并行与并发
├── 分代收集
├── 空间整合(整体基于标记-整理,局部基于复制)
├── 可预测的停顿时间模型
└── 将整个Java堆划分为多个大小相等的独立区域(Region)

G1收集器工作流程

  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

2.2.4 垃圾回收器对比

收集器 串行/并行/并发 算法 目标 适用场景
Serial 串行 复制算法 响应速度优先 单CPU环境,Client模式
ParNew 并行 复制算法 响应速度优先 多CPU环境,配合CMS
Parallel 并行 复制算法 吞吐量优先 后台运算,不需要太多交互
CMS 并发 标记-清除 响应速度优先 互联网站或B/S系统
G1 并发 标记-整理+复制 响应速度优先 面向服务端应用

2.3 Full GC触发条件

1
2
3
4
5
6
// Full GC触发条件总结
1. 老年代空间不足
2. 方法区空间不足(JDK8前)
3. 调用System.gc()
4. CMS GC时出现promotion failed和concurrent mode failure
5. 统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间

3. 类加载机制深度解析

3.1 类加载过程

1
2
3
4
5
6
7
8
类加载生命周期
├── 加载(Loading)
├── 验证(Verification)
├── 准备(Preparation)
├── 解析(Resolution)
├── 初始化(Initialization)
├── 使用(Using)
└── 卸载(Unloading)

3.1.1 加载阶段

1
2
3
4
5
6
7
8
9
10
11
12
// 类加载器获取二进制字节流
// 1. 从本地文件系统加载
ClassLoader.getSystemClassLoader().loadClass("com.example.MyClass");

// 2. 从网络加载
URL url = new URL("http://example.com/classes/");
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Class<?> clazz = loader.loadClass("com.example.NetworkClass");

// 3. 动态生成
byte[] classBytes = generateClassBytes();
Class<?> dynamicClass = defineClass("DynamicClass", classBytes, 0, classBytes.length);

3.1.2 验证阶段

  • 文件格式验证:验证字节流是否符合Class文件格式规范
  • 元数据验证:对字节码描述的信息进行语义分析
  • 字节码验证:通过数据流和控制流分析,确保程序语义是合法的
  • 符号引用验证:确保解析动作能正常执行

3.1.3 准备阶段

为类变量分配内存并设置类变量初始值:

1
2
3
4
5
6
7
public class PreparationTest {
// 准备阶段后,value值为0,不是123
public static int value = 123;

// 准备阶段后,value值为null
public static final String str = "hello";
}

3.1.4 解析阶段

将常量池内的符号引用替换为直接引用:

  • 类或接口的解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

3.1.5 初始化阶段

执行类构造器<clinit>()方法:

1
2
3
4
5
6
7
8
9
10
11
public class InitializationTest {
static {
System.out.println("InitializationTest类初始化");
}

public static int value = 123;

public static void main(String[] args) {
System.out.println(InitializationTest.value);
}
}

3.2 双亲委派模型

1
2
3
4
5
6
7
8
9
类加载器层次结构
├── 启动类加载器(Bootstrap ClassLoader)
│ └── 加载<JAVA_HOME>/lib目录下的类
├── 扩展类加载器(Extension ClassLoader)
│ └── 加载<JAVA_HOME>/lib/ext目录下的类
├── 应用程序类加载器(Application ClassLoader)
│ └── 加载用户类路径(ClassPath)上的类
└── 自定义类加载器(User Defined ClassLoader)
└── 用户自定义的类加载器

3.2.1 双亲委派模型实现

1
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
// 双亲委派模型的loadClass方法实现
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
// 委派给启动类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法完成加载请求
}

if (c == null) {
// 父类加载器无法加载,调用自身的findClass方法加载
long t1 = System.nanoTime();
c = findClass(name);

// 记录统计信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

3.2.2 破坏双亲委派模型

第一次破坏:JDK1.2引入双亲委派模型前,用户自定义类加载器已有loadClass方法

第二次破坏:JNDI、JDBC等服务提供者接口(SPI)需要调用启动类加载器无法加载的代码

第三次破坏:OSGi为了实现模块化热部署,每个模块都有自己的类加载器

3.3 类加载优化策略

3.3.1 预加载策略

1
2
3
4
5
6
7
8
9
10
11
12
// 预加载常用类
public class PreloadClasses {
static {
try {
Class.forName("java.util.concurrent.ConcurrentHashMap");
Class.forName("java.util.ArrayList");
Class.forName("java.util.HashMap");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

3.3.2 懒加载策略

1
2
3
4
5
6
7
8
9
10
11
12
// 单例模式的懒加载实现
public class LazySingleton {
private LazySingleton() {}

private static class Holder {
static final LazySingleton INSTANCE = new LazySingleton();
}

public static LazySingleton getInstance() {
return Holder.INSTANCE;
}
}

3.3.3 热部署实现

1
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
// 自定义类加载器实现热部署
public class HotDeployClassLoader extends URLClassLoader {
public HotDeployClassLoader(URL[] urls) {
super(urls, null);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classBytes = loadClassBytes(name);
return defineClass(name, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("Class " + name + " not found", e);
}
}

private byte[] loadClassBytes(String className) throws IOException {
String classFile = className.replace('.', '/') + ".class";
URL url = findResource(classFile);
if (url == null) {
throw new IOException("Class file not found: " + classFile);
}

try (InputStream in = url.openStream()) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int data = in.read();
while (data != -1) {
buffer.write(data);
data = in.read();
}
return buffer.toByteArray();
}
}
}

4. 对象创建与内存分配策略

4.1 对象创建详细过程

4.1.1 检查加载

首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。

4.1.2 分配内存

1
2
3
4
5
6
7
8
9
10
11
12
13
// 内存分配方式对比

// 1. 指针碰撞(Bump the Pointer)
// 适用场景:Serial、ParNew等带Compact过程的收集器
// 原理:假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器

// 2. 空闲列表(Free List)
// 适用场景:CMS这种基于Mark-Sweep算法的收集器
// 原理:虚拟机维护一个列表,记录哪些内存块是可用的,分配时从列表中找到一块足够大的空间划分给对象实例

// 内存分配并发问题解决
// 方案1:CAS + 失败重试
// 方案2:本地线程分配缓冲(TLAB)

4.1.3 内存分配实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// JVM参数设置示例
-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8

// 对象分配示例
public class ObjectAllocationTest {
private static final int _1MB = 1024 * 1024;

public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;

allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC
}
}

// GC日志分析
[GC [DefNew: 6487K->148K(9216K), 0.0038720 secs] 6487K->6292K(19456K), 0.0039180 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

4.2 内存分配策略

4.2.1 对象优先在Eden分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Eden区分配示例
public class EdenAllocation {
private static final int _1MB = 1024 * 1024;

public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // Minor GC
}

public static void main(String[] args) {
testAllocation();
}
}

4.2.2 大对象直接进入老年代

1
2
3
4
5
6
7
8
9
10
// 大对象直接分配到老年代
public class BigObjectToOld {
private static final int _1MB = 1024 * 1024;

public static void main(String[] args) {
// 大对象直接分配到老年代
byte[] bigObject = new byte[8 * _1MB];
}
}
// JVM参数:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:PretenureSizeThreshold=3145728

4.2.3 长期存活的对象进入老年代

1
2
3
4
5
6
7
8
9
10
11
12
// 长期存活对象晋升到老年代
public class LongLifeToOld {
private static final int _1MB = 1024 * 1024;

public static void main(String[] args) {
byte[] allocation1 = new byte[_1MB / 4];
byte[] allocation2 = new byte[4 * _1MB];
allocation2 = null;
allocation2 = new byte[4 * _1MB];
}
}
// JVM参数:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution

5. JVM并发编程支持机制

5.1 synchronized锁升级过程

5.1.1 锁状态演化

1
2
3
4
5
6
7
8
9
10
11
synchronized锁升级过程
├── 无锁状态
├── 偏向锁(Biased Locking)
│ ├── 偏向第一个获取锁的线程
│ └── 通过CAS记录线程ID
├── 轻量级锁(Lightweight Locking)
│ ├── 线程竞争不激烈时使用
│ └── 通过CAS操作获取锁
└── 重量级锁(Heavyweight Locking)
├── 线程竞争激烈时使用
└── 依赖操作系统Mutex Lock

5.1.2 锁升级实战

1
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
// 锁升级演示
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();

// 偏向锁
synchronized (lock) {
System.out.println("偏向锁状态");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}

// 轻量级锁
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("轻量级锁状态");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
});

Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("重量级锁状态");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
});

t1.start();
t2.start();
t1.join();
t2.join();
}
}

5.2 volatile关键字原理

5.2.1 内存语义

  • 可见性:写volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存
  • 有序性:读volatile变量时,JMM会把该线程对应的本地内存置为无效,从主内存中读取共享变量

5.2.2 volatile实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// volatile示例
public class VolatileExample {
private volatile boolean flag = false;

public void writer() {
flag = true; // 写volatile变量
}

public void reader() {
if (flag) { // 读volatile变量
System.out.println("flag is true");
}
}
}

// 汇编代码分析
// 写volatile变量:lock addl $0x0,(%rsp)
// 读volatile变量:mov 0x10(%rsi),%eax

5.3 Monitor原理

5.3.1 Monitor结构

1
2
3
4
5
Monitor结构
├── Owner
├── EntryList
├── WaitSet
└── Count

5.3.2 Monitor实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Monitor实现原理
public class MonitorDemo {
private final Object monitor = new Object();

public void synchronizedMethod() {
synchronized (monitor) {
// 临界区代码
try {
monitor.wait(); // 进入WaitSet
monitor.notify(); // 唤醒EntryList中的线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

6. JVM调优实战

6.1 调优工具

6.1.1 命令行工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# jps:查看Java进程
jps -lvm

# jstat:监控JVM统计信息
jstat -gc 12345 1000 5 # 每1000ms输出一次GC信息,共5次

# jinfo:查看和修改JVM参数
jinfo -flags 12345

# jmap:内存分析工具
jmap -histo 12345 # 查看堆内存直方图
jmap -dump:format=b,file=heap.hprof 12345 # 生成堆转储文件

# jstack:线程堆栈分析
jstack 12345 > thread_dump.txt

6.1.2 可视化工具

  • JConsole:JVM监控与管理控制台
  • VisualVM:功能更强大的可视化工具
  • MAT:内存分析工具
  • GCEasy:在线GC日志分析工具

6.2 调优案例分析

6.2.1 高并发场景调优

1
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
// 高并发Web应用调优案例
// 应用特点:高并发、短生命周期对象多

// JVM参数配置
-Xms4g -Xmx4g -Xmn2g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationStoppedTime
-Xloggc:/var/log/app/gc.log

// 代码优化
public class HighConcurrencyService {
// 使用对象池减少GC压力
private final ObjectPool<RequestContext> contextPool = new GenericObjectPool<>(
new BasePooledObjectFactory<RequestContext>() {
@Override
public RequestContext create() {
return new RequestContext();
}

@Override
public PooledObject<RequestContext> wrap(RequestContext obj) {
return new DefaultPooledObject<>(obj);
}
}
);

public void handleRequest() {
RequestContext context = null;
try {
context = contextPool.borrowObject();
// 处理请求
} catch (Exception e) {
// 异常处理
} finally {
if (context != null) {
context.reset();
contextPool.returnObject(context);
}
}
}
}

6.2.2 内存泄漏排查

1
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
// 内存泄漏排查案例
public class MemoryLeakDetector {

public static void main(String[] args) throws Exception {
// 1. 生成堆转储文件
String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];

// 2. 使用jmap生成堆转储
Process process = Runtime.getRuntime().exec(
"jmap -dump:format=b,file=heap.hprof " + pid
);
process.waitFor();

// 3. 使用MAT分析
System.out.println("堆转储文件已生成:heap.hprof");
System.out.println("使用MAT打开文件,查找内存泄漏");
}
}

// 常见内存泄漏场景
public class CommonMemoryLeaks {

// 1. 静态集合类导致的内存泄漏
private static final List<Object> staticList = new ArrayList<>();

public void addToStaticList(Object obj) {
staticList.add(obj); // 对象无法被GC
}

// 2. 未关闭的资源
public void resourceLeak() throws IOException {
FileInputStream fis = new FileInputStream("file.txt");
// 忘记关闭流,导致内存泄漏
}

// 3. 内部类持有外部类引用
public class InnerClass {
// 非静态内部类会隐式持有外部类引用
}

// 4. ThreadLocal使用不当
private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();

public void threadLocalLeak() {
threadLocal.set(new byte[1024 * 1024]); // 使用后未remove
}
}

6.3 性能调优参数总结

6.3.1 堆内存参数

1
2
3
4
5
6
7
8
9
10
11
12
# 基础参数
-Xms<size> # 初始堆大小
-Xmx<size> # 最大堆大小
-Xmn<size> # 新生代大小
-XX:NewRatio=<n> # 新生代与老年代比例
-XX:SurvivorRatio=<n> # Eden与Survivor比例

# 进阶参数
-XX:PretenureSizeThreshold=<size> # 大对象直接进入老年代阈值
-XX:MaxTenuringThreshold=<n> # 晋升老年代年龄阈值
-XX:+UseTLAB # 使用TLAB
-XX:TLABSize=<size> # TLAB大小

6.3.2 GC参数

1
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
# Serial收集器
-XX:+UseSerialGC

# ParNew收集器
-XX:+UseParNewGC
-XX:ParNewGCThreads=<n>

# Parallel收集器
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:ParallelGCThreads=<n>
-XX:MaxGCPauseMillis=<n>
-XX:GCTimeRatio=<n>

# CMS收集器
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=<n>
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=<n>

# G1收集器
-XX:+UseG1GC
-XX:MaxGCPauseMillis=<n>
-XX:G1HeapRegionSize=<size>
-XX:G1NewSizePercent=<n>
-XX:G1MaxNewSizePercent=<n>

6.3.3 监控参数

1
2
3
4
5
6
7
8
9
10
11
# GC日志
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationStoppedTime
-Xloggc:<filename>

# 堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=<path>
-XX:+UseGCOverheadLimit

7. 高频面试题总结

7.1 JVM内存模型相关

  1. JVM内存区域如何划分?各区域作用是什么?

    • 方法区:存储类信息、常量、静态变量
    • 堆:存放对象实例
    • 虚拟机栈:方法调用、局部变量
    • 本地方法栈:Native方法调用
    • 程序计数器:当前线程执行的字节码行号指示器
  2. 堆内存为什么要分代?

    • 基于弱代假说:绝大多数对象都是朝生夕灭的
    • 提高垃圾回收效率
    • 针对不同代采用不同的垃圾回收算法
  3. 方法区和元空间有什么区别?

    • JDK8前:方法区在永久代(PermGen)
    • JDK8后:方法区在元空间(Metaspace),使用本地内存
    • 元空间可以动态扩容,避免OOM

7.2 垃圾回收相关

  1. 如何判断对象是否存活?

    • 引用计数法:无法解决循环引用
    • 可达性分析:从GC Roots开始,不可达的对象可被回收
  2. 垃圾回收算法有哪些?

    • 标记-清除:简单但产生碎片
    • 复制算法:高效但内存利用率低
    • 标记-整理:避免碎片,适合老年代
    • 分代收集:新生代复制,老年代标记-整理
  3. CMS和G1收集器的区别?

    • CMS:标记-清除,老年代收集器,并发收集
    • G1:标记-整理+复制,整堆收集器,可预测停顿时间
  4. Full GC触发条件有哪些?

    • 老年代空间不足
    • 方法区空间不足
    • System.gc()调用
    • CMS GC时出现promotion failed和concurrent mode failure

7.3 类加载机制相关

  1. 类加载过程是怎样的?

    • 加载、验证、准备、解析、初始化、使用、卸载
  2. 什么是双亲委派模型?

    • 类加载请求先委派给父类加载器
    • 避免类的重复加载
    • 保证Java核心库的安全性
  3. 如何打破双亲委派模型?

    • SPI机制(JDBC、JNDI)
    • OSGi模块化
    • 自定义类加载器

7.4 并发编程相关

  1. synchronized锁升级过程?

    • 无锁 → 偏向锁 → 轻量级锁 → 重量级锁
  2. volatile关键字的作用?

    • 保证可见性
    • 禁止指令重排序
  3. Monitor的实现原理?

    • 基于ObjectMonitor实现
    • 包含Owner、EntryList、WaitSet

8. 总结与最佳实践

8.1 JVM调优黄金法则

  1. 先监控再调优:使用JConsole、VisualVM等工具收集数据
  2. 小步快跑:每次只调整一个参数,观察效果
  3. 关注GC日志:分析GC频率、停顿时间、回收量
  4. 避免过度优化:80%的性能问题由20%的代码引起

8.2 生产环境配置模板

1
2
3
4
5
6
7
8
# 通用配置模板
-Xms4g -Xmx4g # 堆内存固定,避免动态扩容
-XX:+UseG1GC # 使用G1收集器
-XX:MaxGCPauseMillis=200 # 最大停顿时间200ms
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps # GC日志
-Xloggc:/var/log/app/gc.log # GC日志文件
-XX:+HeapDumpOnOutOfMemoryError # OOM时生成堆转储
-XX:HeapDumpPath=/var/log/app/heap.hprof # 堆转储路径

8.3 学习建议

  1. 理论结合实践:通过实际调优案例加深理解
  2. 阅读源码:OpenJDK源码是最好的学习资料
  3. 关注社区:跟进JVM最新发展(ZGC、Shenandoah)
  4. 工具熟练:掌握各种JVM监控和调优工具

通过系统学习JVM原理,你将能够:

  • 快速定位和解决生产环境问题
  • 编写高性能的Java应用
  • 在技术面试中脱颖而出
  • 为架构设计提供坚实基础

参考资料

  1. 《深入理解Java虚拟机:JVM高级特性与最佳实践》
  2. 《Java性能优化权威指南》
  3. OpenJDK官方文档
  4. Oracle JVM调优指南
  5. 《Java并发编程实战》

本文档将持续更新,欢迎交流讨论!