第492集JVM原理
|字数总计:4.3k|阅读时长:17分钟|阅读量:
JVM原理
1. 概述
1.1 JVM的重要性
Java虚拟机(JVM)是Java程序运行的核心,理解JVM原理对于Java开发者至关重要,能够帮助我们编写高性能代码、进行性能调优、解决内存问题。
本文内容:
- JVM架构:JVM整体架构和组件
- 内存模型:堆、栈、方法区等内存区域
- 垃圾回收:GC算法和垃圾回收器
- 类加载机制:类加载过程和双亲委派模型
- 字节码执行:字节码指令和执行引擎
- 性能调优:JVM性能调优实战
1.2 本文内容结构
本文将从以下几个方面深入探讨JVM原理:
- JVM架构:JVM整体架构和运行时数据区
- 内存模型:堆、栈、方法区详解
- 垃圾回收:GC算法和垃圾回收器
- 类加载机制:类加载过程和双亲委派
- 字节码执行:字节码指令和执行引擎
- 性能调优:JVM性能调优实战
2. JVM架构
2.1 JVM整体架构
2.1.1 JVM组件
JVM主要组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ┌─────────────────────────────────────┐ │ Java应用程序 │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Java API (JDK) │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ JVM │ │ ┌──────────┐ ┌──────────┐ │ │ │ 类加载器 │ │执行引擎 │ │ │ └──────────┘ └──────────┘ │ │ ┌──────────┐ ┌──────────┐ │ │ │运行时数据区│ │本地方法库 │ │ │ └──────────┘ └──────────┘ │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ 操作系统 (OS) │ └─────────────────────────────────────┘
|
JVM核心组件:
- 类加载器(ClassLoader):加载类文件
- 运行时数据区(Runtime Data Areas):内存管理
- 执行引擎(Execution Engine):执行字节码
- 本地方法接口(JNI):调用本地方法
2.2 运行时数据区
2.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
| ┌─────────────────────────────────────┐ │ 运行时数据区 │ │ ┌──────────────────────────────┐ │ │ │ 方法区(Method Area) │ │ │ │ - 类信息 │ │ │ │ - 常量池 │ │ │ │ - 静态变量 │ │ │ └──────────────────────────────┘ │ │ ┌──────────────────────────────┐ │ │ │ 堆(Heap) │ │ │ │ - 新生代(Eden + Survivor) │ │ │ │ - 老年代(Old Generation) │ │ │ └──────────────────────────────┘ │ │ ┌──────────────────────────────┐ │ │ │ Java栈(Java Stack) │ │ │ │ - 局部变量表 │ │ │ │ - 操作数栈 │ │ │ │ - 方法返回地址 │ │ │ └──────────────────────────────┘ │ │ ┌──────────────────────────────┐ │ │ │ 程序计数器(PC Register) │ │ │ └──────────────────────────────┘ │ │ ┌──────────────────────────────┐ │ │ │ 本地方法栈(Native Stack) │ │ │ └──────────────────────────────┘ │ └─────────────────────────────────────┘
|
3. 内存模型
3.1 堆内存
3.1.1 堆内存结构
堆内存:存储对象实例,是GC主要工作区域。
堆内存划分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class HeapMemoryExample { public void heapAllocation() { Object obj = new Object(); int[] array = new int[1000]; String str = new String("Hello"); } }
|
堆内存结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ┌─────────────────────────────────────┐ │ 堆(Heap) │ │ ┌──────────────────────────────┐ │ │ │ 新生代(Young Generation) │ │ │ │ ┌──────────┐ ┌──────────┐ │ │ │ │ │ Eden │ │Survivor │ │ │ │ │ │ │ │ S0/S1 │ │ │ │ │ └──────────┘ └──────────┘ │ │ │ └──────────────────────────────┘ │ │ ┌──────────────────────────────┐ │ │ │ 老年代(Old Generation) │ │ │ │ │ │ │ └──────────────────────────────┘ │ └─────────────────────────────────────┘
|
堆内存参数:
1 2 3 4 5 6 7 8
| -Xms2g -Xmx4g -Xmn1g
-XX:NewRatio=2 -XX:SurvivorRatio=8
|
3.2 栈内存
3.2.1 栈帧结构
Java栈:每个线程有独立的栈,存储局部变量、方法参数、返回地址等。
栈帧结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class StackFrameExample { public int calculate(int a, int b) { int result = a + b; return result; } public void methodCall() { int x = 10; int y = 20; int sum = calculate(x, y); System.out.println(sum); } }
|
栈帧结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ┌─────────────────────────────────────┐ │ 栈帧(Stack Frame) │ │ ┌──────────────────────────────┐ │ │ │ 局部变量表(Local Variables) │ │ │ │ - 方法参数 │ │ │ │ - 局部变量 │ │ │ └──────────────────────────────┘ │ │ ┌──────────────────────────────┐ │ │ │ 操作数栈(Operand Stack) │ │ │ │ - 计算中间结果 │ │ │ └──────────────────────────────┘ │ │ ┌──────────────────────────────┐ │ │ │ 动态链接(Dynamic Linking) │ │ │ │ - 方法引用 │ │ │ └──────────────────────────────┘ │ │ ┌──────────────────────────────┐ │ │ │ 方法返回地址(Return Address)│ │ │ └──────────────────────────────┘ │ └─────────────────────────────────────┘
|
栈内存参数:
3.3 方法区
3.3.1 方法区内容
方法区:存储类信息、常量池、静态变量等。
方法区内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class MethodAreaExample { private static String staticField = "Static Field"; private static final int CONSTANT = 100; }
|
方法区(元空间)参数:
1 2 3 4 5 6 7
| -XX:PermSize=256m -XX:MaxPermSize=512m
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
|
3.4 程序计数器
3.4.1 PC Register
程序计数器:记录当前线程执行的字节码指令地址。
程序计数器特点:
- 线程私有
- 唯一不会OOM的区域
- 记录当前执行指令地址
3.5 本地方法栈
3.5.1 Native Stack
本地方法栈:为Native方法服务,类似Java栈。
4. 垃圾回收
4.1 对象存活判断
4.1.1 引用计数法
引用计数法:对象被引用时计数+1,引用失效时计数-1,计数为0时回收。
问题:无法解决循环引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class ReferenceCounting { public void circularReference() { Object obj1 = new Object(); Object obj2 = new Object(); obj1.ref = obj2; obj2.ref = obj1; obj1 = null; obj2 = null; } }
|
4.1.2 可达性分析
可达性分析:从GC Roots开始,标记所有可达对象,未标记的对象为垃圾。
GC Roots:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class ReachabilityAnalysis { private static Object staticObj = new Object(); private static final Object CONSTANT_OBJ = new Object(); public void method() { Object localObj = new Object(); } }
|
4.2 垃圾回收算法
4.2.1 标记-清除算法
标记-清除(Mark-Sweep):
- 标记所有需要回收的对象
- 清除被标记的对象
缺点:
1 2 3 4 5 6 7 8
| 标记前: [对象1][对象2][对象3][对象4] ✓ ✗ ✓ ✗
清除后: [对象1] [对象3] ↑ ↑ 内存碎片
|
4.2.2 标记-复制算法
标记-复制(Mark-Copy):
- 将内存分为两块
- 只使用其中一块
- 将存活对象复制到另一块
- 清空当前块
优点:无内存碎片
缺点:内存利用率低(50%)
1 2 3 4 5 6
| 复制前(Eden): [对象1][对象2][对象3][对象4] ✓ ✗ ✓ ✗
复制后(Survivor): [对象1][对象3]
|
4.2.3 标记-整理算法
标记-整理(Mark-Compact):
- 标记所有需要回收的对象
- 将存活对象向一端移动
- 清理边界外的内存
优点:无内存碎片,内存利用率高
缺点:效率较低
1 2 3 4 5 6
| 整理前: [对象1][对象2][对象3][对象4] ✓ ✗ ✓ ✗
整理后: [对象1][对象3] [空闲]
|
4.3 分代收集
4.3.1 分代策略
分代收集:根据对象存活周期,将堆分为新生代和老年代。
新生代:
老年代:
- 对象存活时间长
- 使用标记-清除或标记-整理算法
- 回收频率低
对象晋升:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class GenerationalGC { public void objectPromotion() { Object obj = new Object(); } }
|
4.4 垃圾回收器
4.4.1 Serial收集器
Serial收集器:单线程,适合小应用。
特点:
- 单线程收集
- 会Stop The World(STW)
- 适合客户端应用
4.4.2 Parallel收集器
Parallel收集器:多线程,适合吞吐量优先的应用。
特点:
- 多线程收集
- 会Stop The World
- 适合后台应用
1 2 3
| -XX:+UseParallelGC -XX:ParallelGCThreads=4
|
4.4.3 CMS收集器
CMS(Concurrent Mark Sweep)收集器:并发标记清除,适合响应时间优先的应用。
特点:
CMS收集过程:
- 初始标记(STW)
- 并发标记
- 重新标记(STW)
- 并发清除
1 2 3
| -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70
|
4.4.4 G1收集器
G1(Garbage First)收集器:面向服务端,低延迟。
特点:
- 将堆分为多个Region
- 可预测的停顿时间
- 适合大堆内存
G1收集过程:
- 初始标记(STW)
- 并发标记
- 最终标记(STW)
- 筛选回收(STW)
1 2 3 4
| -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=16m
|
G1 Region划分:
1 2 3 4 5 6 7 8
| ┌─────────────────────────────────────┐ │ G1堆(多个Region) │ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │ │ E │ │ S │ │ O │ │ H │ │空 │ │ │ └───┘ └───┘ └───┘ └───┘ └───┘ │ │ E: Eden S: Survivor │ │ O: Old H: Humongous │ └─────────────────────────────────────┘
|
4.4.5 ZGC收集器
ZGC收集器:低延迟,适合大堆内存。
特点:
- 停顿时间不超过10ms
- 支持TB级堆内存
- Java 11+支持
4.5 GC日志分析
4.5.1 GC日志配置
GC日志参数:
1 2 3 4 5 6 7 8 9 10 11 12 13
| -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
-Xlog:gc*:file=gc.log:time,tags,level
|
GC日志示例:
1 2 3 4
| [2024-01-01T10:00:00.123+0800] GC(0) Pause Young (Normal) (G1 Evacuation Pause) [2024-01-01T10:00:00.123+0800] GC(0) Using 8 workers of 8 for evacuation [2024-01-01T10:00:00.145+0800] GC(0) [Eden: 512M->0B(512M) Survivors: 0B->64M Heap: 512M->450M(2G)] [2024-01-01T10:00:00.145+0800] GC(0) [Times: user=0.15 sys=0.02, real=0.02 secs]
|
5. 类加载机制
5.1 类加载过程
5.1.1 加载阶段
类加载过程:
- 加载(Loading):读取类文件
- 验证(Verification):验证类文件格式
- 准备(Preparation):为静态变量分配内存
- 解析(Resolution):将符号引用转为直接引用
- 初始化(Initialization):执行类构造器
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class ClassLoadingExample { private static int staticVar; private static final int CONSTANT = 100; static { staticVar = 10; System.out.println("Static block executed"); } }
|
5.2 类加载器
5.2.1 类加载器层次
类加载器层次:
1 2 3 4 5 6 7
| Bootstrap ClassLoader (启动类加载器) ↓ Extension ClassLoader (扩展类加载器) ↓ Application ClassLoader (应用类加载器) ↓ Custom ClassLoader (自定义类加载器)
|
类加载器示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class ClassLoaderExample { public void showClassLoaders() { ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); System.out.println("Application ClassLoader: " + appClassLoader); ClassLoader extClassLoader = appClassLoader.getParent(); System.out.println("Extension ClassLoader: " + extClassLoader); ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println("Bootstrap ClassLoader: " + bootstrapClassLoader); ClassLoader currentClassLoader = this.getClass().getClassLoader(); System.out.println("Current ClassLoader: " + currentClassLoader); } }
|
5.3 双亲委派模型
5.3.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
| public class ParentDelegationModel { public Class<?> loadClass(String name) throws ClassNotFoundException { Class<?> c = findLoadedClass(name); if (c != null) { return c; } try { if (parent != null) { c = parent.loadClass(name); if (c != null) { return c; } } } catch (ClassNotFoundException e) { } return findClass(name); } }
|
双亲委派优势:
- 避免类重复加载
- 保证核心类安全(如java.lang.Object)
5.4 自定义类加载器
5.4.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
| public class CustomClassLoader extends ClassLoader { private String classPath; public CustomClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] classData = loadClassData(name); return defineClass(name, classData, 0, classData.length); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } private byte[] loadClassData(String name) throws IOException { String path = classPath + name.replace('.', '/') + ".class"; try (InputStream is = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { baos.write(buffer, 0, len); } return baos.toByteArray(); } } }
|
6. 字节码执行
6.1 字节码指令
6.1.1 常见字节码指令
字节码指令分类:
- 加载和存储:iload, istore, aload, astore
- 运算指令:iadd, isub, imul, idiv
- 类型转换:i2l, i2f, i2d
- 对象操作:new, getfield, putfield
- 方法调用:invokevirtual, invokespecial, invokestatic
字节码示例:
1 2 3 4 5 6 7
| public class BytecodeExample { public int calculate(int a, int b) { int result = a + b; return result; } }
|
对应字节码:
1 2 3 4 5 6 7 8
| public int calculate(int, int); Code: 0: iload_1 // 加载局部变量a(第1个参数) 1: iload_2 // 加载局部变量b(第2个参数) 2: iadd // 相加 3: istore_3 // 存储到result(局部变量3) 4: iload_3 // 加载result 5: ireturn // 返回
|
6.2 执行引擎
6.2.1 解释执行和编译执行
执行引擎:
- 解释执行:逐条解释字节码
- 编译执行:将字节码编译为机器码(JIT)
JIT编译:
1 2 3 4 5 6 7 8 9 10
| public class JITExample { public void hotMethod() { for (int i = 0; i < 1000000; i++) { int result = i * 2; } } }
|
JIT参数:
1 2 3 4 5
| -XX:+TieredCompilation
-XX:CompileThreshold=10000
|
7. 性能调优
7.1 内存调优
7.1.1 堆内存调优
堆内存调优:
1 2 3 4 5 6 7 8 9 10
| -Xms2g -Xmx4g -Xmn1g
-XX:NewRatio=2
-XX:SurvivorRatio=8
|
调优建议:
- -Xms和-Xmx设置相同:避免动态扩容
- 新生代大小:通常为堆的1/3到1/4
- Survivor比例:根据对象存活时间调整
7.2 GC调优
7.2.1 GC参数调优
G1 GC调优:
1 2 3 4 5
| -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=16m -XX:InitiatingHeapOccupancyPercent=45
|
Parallel GC调优:
1 2 3 4 5
| -XX:+UseParallelGC -XX:ParallelGCThreads=4 -XX:MaxGCPauseMillis=200 -XX:GCTimeRatio=19
|
7.3 监控工具
7.3.1 JVM监控工具
常用监控工具:
- jps:查看Java进程
- jstat:查看GC统计
- jmap:生成堆转储
- jstack:生成线程转储
- jvisualvm:可视化监控
jstat使用:
1 2 3 4 5 6
| jstat -gc <pid> 1000 10
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 0.0 1024.0 0.0 1024.0 8192.0 8192.0 20480.0 1024.0 4864.0 4096.0 512.0 256.0 10 0.100 2 0.200 0.300
|
jmap使用:
1 2 3 4 5
| jmap -dump:format=b,file=heap.hprof <pid>
jmap -heap <pid>
|
jstack使用:
1 2 3 4 5
| jstack <pid> > thread.dump
jstack -l <pid>
|
7.4 常见问题排查
7.4.1 OOM问题
OutOfMemoryError类型:
- Java heap space:堆内存不足
- Metaspace:元空间不足
- Direct buffer memory:直接内存不足
- Unable to create new native thread:线程数过多
OOM排查:
1 2 3 4 5
| jmap -dump:format=b,file=heap.hprof <pid>
|
7.4.2 CPU高问题
CPU高排查:
1 2 3 4 5 6 7
| top -H -p <pid>
jstack <pid> > thread.dump
|
7.4.3 GC频繁问题
GC频繁排查:
1 2 3 4 5
| jstat -gc <pid> 1000 10
|
8. 实战案例
8.1 内存泄漏排查
8.1.1 内存泄漏案例
内存泄漏示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class MemoryLeakExample { private static List<Object> cache = new ArrayList<>(); public void addToCache(Object obj) { cache.add(obj); } private static List<WeakReference<Object>> weakCache = new ArrayList<>(); public void addToWeakCache(Object obj) { weakCache.add(new WeakReference<>(obj)); } }
|
排查步骤:
- 使用jmap生成堆转储
- 使用MAT分析大对象
- 查看对象引用关系
- 找出泄漏原因
8.2 GC调优案例
8.2.1 GC调优实战
场景:应用响应慢,GC频繁。
调优步骤:
收集GC日志:
1
| -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
|
分析GC日志:
- GC频率:每分钟多少次
- GC耗时:每次GC耗时
- 堆使用率:GC前后堆使用率
调整参数:
1 2 3 4 5 6 7 8
| -Xms4g -Xmx4g
-Xmn2g
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
|
验证效果:对比调优前后的GC指标
9. 总结
9.1 核心要点
- 内存模型:理解堆、栈、方法区的作用
- 垃圾回收:掌握GC算法和垃圾回收器
- 类加载:理解类加载过程和双亲委派
- 性能调优:掌握JVM调优方法和工具
9.2 关键理解
- 堆是GC主要区域:大部分对象在堆中分配和回收
- 分代收集:根据对象存活周期采用不同策略
- 双亲委派:保证类加载安全和避免重复加载
- JIT编译:热点代码会被编译为机器码
9.3 最佳实践
- 合理设置堆内存:根据应用特点设置
- 选择合适的GC:根据应用特点选择GC
- 监控GC指标:定期监控GC频率和耗时
- 及时排查问题:OOM、CPU高等问题及时排查
相关文章: