一、JVM基础概念
1.1 JVM定义与作用
- 定义:Java虚拟机(Java Virtual Machine)本质上是一个运行在计算机上的程序,负责运行Java字节码文件
- 跨平台特性:支持"Write Once, Run Anywhere"理念,将Java源代码编译成字节码后,可在不同平台的JVM上运行
- 支持语言:不仅支持Java,还可运行Kotlin、Scala、Groovy等可编译成Java字节码的语言
1.2 JVM核心功能
- 解释执行:将字节码指令实时解释成机器码执行
- 内存管理 :
- 自动为对象、方法分配内存
- 垃圾回收机制自动回收不再使用的对象
- 即时编译(JIT):对热点代码进行优化,提升执行效率
1.3 JVM组成结构
| 组件 | 作用 | 说明 |
|---|---|---|
| 类加载子系统 | 读取、解析字节码文件并加载到内存 | 核心是类加载器 |
| 运行时数据区 | 管理JVM使用到的内存 | 包括堆、栈、方法区等 |
| 执行引擎 | 执行字节码指令 | 包含解释器、JIT编译器、垃圾回收器 |
| 本地接口 | 调用本地方法 | 保存已编译好的本地方法,通常用C/C++实现 |
1.4 常见JVM实现
- HotSpot:Oracle提供的默认JVM,应用最广泛
- GraalVM:高性能JVM,支持多语言互操作
- 龙井:阿里开源的JVM
- OpenJ9:IBM开发的高性能JVM
二、类加载机制
2.1 类的生命周期
类的生命周期包含五个主要阶段:
-
加载阶段:
- 通过类加载器获取字节码的二进制流
- 将字节码信息保存到方法区,生成InstanceKlass对象
- 在堆中创建对应的java.lang.Class对象
-
连接阶段:
- 验证 :检查字节码是否符合JVM规范
- 文件格式验证:如魔数0xCAFEBABE
- 元信息验证:如类必须有父类
- 字节码验证:指令语义是否正确
- 符号引用验证:如访问权限检查
- 准备 :为静态变量分配内存并设置初始值
- final修饰的基本数据类型静态变量直接赋代码中指定的值
- 解析:将常量池中的符号引用替换为直接引用
- 验证 :检查字节码是否符合JVM规范
-
初始化阶段:
- 执行静态代码块和静态变量赋值
- 执行字节码文件中的
clinit方法
-
使用阶段:
- 创建对象、调用方法等常规操作
-
卸载阶段:类被回收,需同时满足三个条件:
- 该类所有实例对象都已被回收
- 加载该类的类加载器已被回收
- 该类对应的Class对象没有在任何地方被引用
2.2 类加载器分类
JDK 8及之前
| 类加载器 | 实现语言 | 加载路径 | 说明 |
|---|---|---|---|
| 启动类加载器(Bootstrap) | C++ | jre/lib/ | 加载核心类,如rt.jar |
| 扩展类加载器(Extension) | Java | jre/lib/ext/ | 加载扩展类 |
| 应用程序类加载器(Application) | Java | classpath | 加载应用类 |
| 自定义类加载器 | Java | 自定义 | 继承ClassLoader |
JDK 9及之后
- 启动类加载器使用Java实现
- 扩展类加载器更名为平台类加载器(Platform)
- 模块化系统改变类加载机制
2.3 双亲委派机制
2.3.1 机制原理
- 当类加载器收到类加载请求时,先委托父加载器尝试加载
- 只有父加载器无法加载时,才尝试自己加载
- 加载顺序:自底向上检查,自顶向下加载
2.3.2 优点
- 安全性:防止恶意代码替换JDK核心类
- 避免重复加载:确保类的唯一性
2.3.3 打破双亲委派
- 方式 :自定义类加载器,重写
loadClass方法 - 应用场景 :
- Tomcat等Web容器,实现应用隔离
- OSGi框架,实现模块化部署
- 热部署功能
2.4 Tomcat类加载机制
Tomcat 9实现了自定义类加载器体系,打破双亲委派机制:
Bootstrap
↑
Platform
↑
Common
↗ | ↖
Catalina Shared WebApp1 WebApp2
↗ ↖
JasperLoader JasperLoader
- Common类加载器:加载Tomcat和应用共享的类
- Catalina类加载器:仅加载Tomcat自身使用的类
- Shared类加载器:仅加载应用使用的类
- WebAppClassLoader:为每个Web应用单独创建
- JasperLoader:负责加载JSP编译后的类,支持热部署
三、内存管理与垃圾回收
3.1 对象存活判定算法
3.1.1 引用计数法
- 原理:为对象维护引用计数器,被引用时+1,失去引用时-1
- 缺点 :
- 无法解决循环引用问题
- 计数器更新开销大
- 应用:Python等语言使用,Java未采用
3.1.2 可达性分析法
- 原理:以GC Roots为起点,通过引用链寻找存活对象
- GC Roots通常包括 :
- 虚拟机栈中的局部变量
- 方法区中的静态变量
- 本地方法栈中的JNI引用
- 同步锁持有的对象
3.2 JVM中的引用类型
| 引用类型 | 内存回收规则 | 典型应用场景 |
|---|---|---|
| 强引用(Strong) | 只要有GC Root可达就不回收 | 普通对象引用 |
| 软引用(Soft) | 内存不足时回收 | 缓存框架 |
| 弱引用(Weak) | 每次GC都回收 | ThreadLocal、WeakHashMap |
| 虚引用(Phantom) | 无法通过引用获取对象 | 跟踪对象回收状态,清理资源 |
| 终结器引用(Finalizer) | 对象回收前执行finalize方法 | 资源清理(不推荐使用) |
3.2.1 ThreadLocal中的弱引用
- 设计:Entry对象继承WeakReference,key为ThreadLocal的弱引用
- 原因:避免ThreadLocal对象无法被回收
- 注意事项:仍需手动调用remove()释放value,否则会导致内存泄漏
3.3 垃圾回收算法
3.3.1 标记-清除(Mark-Sweep)
- 原理 :
- 标记阶段:标记所有存活对象
- 清除阶段:回收未标记对象
- 优点:实现简单
- 缺点 :
- 产生内存碎片
- 分配速度慢
3.3.2 复制(Copying)
- 原理 :
- 准备两块空间From和To
- 复制From中存活对象到To空间
- 交换From和To角色
- 优点 :
- 无内存碎片
- 实现简单,效率较高
- 缺点:内存利用率低(只有50%)
3.3.3 标记-整理(Mark-Compact)
- 原理 :
- 标记阶段:同标记-清除
- 整理阶段:将存活对象向一端移动
- 优点 :
- 无内存碎片
- 内存利用率高
- 缺点:整理阶段效率较低
3.3.4 分代垃圾回收
- 原理:根据对象生命周期特点,将堆分为年轻代和老年代
- 年轻代 :
- 使用复制算法
- 对象朝生夕死,回收频率高
- 老年代 :
- 使用标记-清除或标记-整理
- 对象存活时间长,回收频率低
- 优点 :
- 提高内存利用率
- 减少Full GC次数
- 针对不同代使用最适合的算法
3.4 常见垃圾回收器
3.4.1 组合关系
年轻代 老年代
Serial Serial Old
ParNew CMS
Parallel Scavenge Parallel Old
G1 (独立)
ZGC (独立)
Shenandoah (独立)
3.4.2 各垃圾回收器特点
| 回收器 | 算法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| Serial+SerialOld | 复制+标记整理 | 单CPU、客户端应用 | 简单高效 | 多CPU下性能差 |
| ParNew+CMS | 复制+标记清除 | 响应时间敏感应用 | 停顿时间短 | CPU资源敏感、浮动垃圾 |
| PS+PO | 复制+标记整理 | 后台计算、吞吐量优先 | 吞吐量高,自动调整 | 停顿时间不可控 |
| G1 | 分区+混合回收 | 大内存、低延迟需求 | 可预测停顿、避免碎片 | CPU消耗大 |
| ZGC | 着色指针+读屏障 | 超大堆、极低延迟 | <1ms停顿,TB级堆 | JDK11+支持 |
| Shenandoah | Brooks指针+屏障 | 低延迟应用 | 停顿与堆大小无关 | JDK12+支持 |
四、运行时数据区
4.1 内存区域划分
JVM内存
┌──────────────┴──────────────┐
线程共享区 线程私有区
┌──────┴───────┐ ┌──────┼──────┐
堆 方法区 栈 本地方法栈 程序计数器
4.2 各区域详解
4.2.1 程序计数器
- 作用 :
- 记录当前线程执行的字节码指令地址
- 实现分支、跳转、异常处理
- 线程切换后恢复执行位置
- 特点:线程私有,不会发生内存溢出
4.2.2 虚拟机栈
- 结构:栈帧(Frame)组成,每个方法调用对应一个栈帧
- 栈帧包含 :
- 局部变量表:存储方法参数和局部变量
- 操作数栈:执行字节码指令的临时数据区
- 帧数据:动态链接、方法出口、异常表
- 异常:StackOverflowError(栈深度溢出)、OutOfMemoryError(栈空间不足)
4.2.3 本地方法栈
- 作用:为native方法服务
- 异常:同虚拟机栈
4.2.4 堆
- 作用:存放对象实例
- 结构 :
- JDK7及之前:新生代(Eden+S0+S1) + 老年代
- JDK8及之后:移除永久代,引入元空间
- 异常:OutOfMemoryError: Java heap space
4.2.5 方法区
- 作用:存储类元数据、常量、静态变量等
- JDK演进 :
- JDK7及之前:使用永久代(PermGen)实现
- JDK8及之后:使用元空间(Metaspace)实现,位于本地内存
- 异常 :
- JDK7:OutOfMemoryError: PermGen space
- JDK8+:OutOfMemoryError: Metaspace
4.2.6 直接内存
- 作用:NIO操作,避免数据在Java堆和Native堆间复制
- 特点:不受JVM堆大小限制,由操作系统管理
- 创建 :
ByteBuffer.allocateDirect(size) - 异常:OutOfMemoryError
4.3 内存溢出场景总结
| 区域 | 溢出原因 | 异常类型 | 解决方案 |
|---|---|---|---|
| 堆 | 对象创建过多,无法分配内存 | OutOfMemoryError: Java heap space | 扩大堆内存、优化代码、检查内存泄漏 |
| 栈 | 递归过深或线程过多 | StackOverflowError/OutOfMemoryError | 增加栈大小、优化递归算法 |
| 方法区 | 加载类过多,静态变量过多 | OutOfMemoryError: Metaspace/PermGen | 增加元空间、减少动态类生成 |
| 直接内存 | 申请直接内存超过限制 | OutOfMemoryError | 调整MaxDirectMemorySize参数 |
五、JVM性能调优与工具
5.1 内存泄漏诊断
5.1.1 诊断步骤
- 发现内存异常 :
- 监控工具发现堆内存持续增长
- 手动GC后内存无法释放
- 生成内存快照 :
- 自动:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path - 手动:
jmap -dump:live,format=b,file=heap.hprof <pid>
- 自动:
- 分析内存快照 :
- 使用MAT(Eclipse Memory Analyzer)分析泄漏点
- 检查支配树(Dominator Tree)、路径到GC Roots
- 修复问题 :
- 代码级:释放未使用的对象引用
- 设计级:优化数据结构,限制缓存大小
- 参数级:调整JVM参数
5.2 常用JVM工具
| 工具 | 用途 | 常用命令 |
|---|---|---|
| jps | 查看Java进程 | jps -v |
| jstat | 监控GC状态 | jstat -gcutil <pid> 1000 |
| jmap | 生成堆转储 | jmap -heap <pid> |
| jstack | 生成线程快照 | jstack <pid> |
| jinfo | 查看JVM配置 | jinfo <pid> |
| VisualVM | 图形化监控 | 连接进程,监控GC、线程、内存 |
| Arthas | 阿里开源诊断工具 | heapdump, thread, watch |
| MAT | 内存分析 | 分析hprof文件 |
5.3 常用JVM参数
5.3.1 内存参数
| 参数 | 说明 | 推荐值 |
|---|---|---|
-Xmx<size> |
最大堆内存 | 服务器总内存的1/2-2/3 |
-Xms<size> |
初始堆内存 | 与Xmx相同,避免扩容开销 |
-Xmn<size> |
年轻代大小 | 通常为堆的1/3,G1不建议设置 |
-XX:MaxMetaspaceSize |
最大元空间 | 256m-512m |
-Xss<size> |
栈大小 | 256k-1m |
-XX:MaxDirectMemorySize |
直接内存上限 | 根据NIO需求设置 |
5.3.2 GC日志与诊断
bash
# JDK8及之前
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log
# JDK9+
-Xlog:gc*:file=/path/to/gc.log
# 其他诊断参数
-XX:+DisableExplicitGC # 禁用System.gc()
-XX:+HeapDumpOnOutOfMemoryError # OOM时自动dump
-XX:HeapDumpPath=/path/to/dump.hprof
5.3.3 GC策略
bash
# 选择垃圾回收器
-XX:+UseSerialGC # 串行GC
-XX:+UseParallelGC # 并行GC(PS)
-XX:+UseConcMarkSweepGC # CMS回收器
-XX:+UseG1GC # G1回收器
-XX:+UseZGC # ZGC(超低延迟)
六、字节码与执行优化
6.1 字节码文件结构
字节码文件(.class)包含以下主要部分:
-
基础信息:
- 魔数(0xCAFEBABE)
- 版本号
- 访问标识
- 类、父类、接口信息
-
常量池:存储字符串常量、类名、方法名等
-
字段表:类/接口声明的字段信息
-
方法表:
- 方法名、描述符
- 访问标识
- 字节码指令
-
属性表:源文件名、内部类列表等
6.2 JIT即时编译
6.2.1 分层编译
现代JVM采用5层分层编译策略:
| 层级 | 组件 | 描述 | 优化内容 |
|---|---|---|---|
| 0 | 解释器 | 解释执行 | 记录方法/循环次数 |
| 1 | C1编译器 | 基础优化 | 生成优化机器码 |
| 2 | C1编译器 | 带收集信息优化 | 生成优化机器码+记录次数 |
| 3 | C1编译器 | 完整优化 | 生成优化机器码+类型信息 |
| 4 | C2编译器 | 深度优化 | 全局优化,生成高效机器码 |
6.2.2 JIT优化技术
-
方法内联:
- 将小方法体直接复制到调用处
- 消除方法调用开销
- 使更多优化成为可能
-
逃逸分析:
- 分析对象作用域是否逃逸出方法/线程
- 优化技术:
- 栈上分配:不逃逸对象分配在栈上
- 标量替换:拆分对象为基本类型
- 锁消除:消除不会竞争的锁
七、最佳实践建议
7.1 代码层面
-
避免内存泄漏:
- 及时释放集合引用
- ThreadLocal使用后调用remove()
- 监听器、回调及时注销
-
对象设计:
- 避免创建大对象
- 重用对象而非频繁创建
- 使用对象池处理高频创建对象
-
集合优化:
- 指定初始容量
- 避免使用大HashMap
- 优先考虑基本类型集合
7.2 配置层面
- JVM参数模板:
bash
-server
-Xms4g
-Xmx4g
-XX:MaxMetaspaceSize=512m
-XX:+UseG1GC
-XX:+DisableExplicitGC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump.hprof
-Xloggc:/data/logs/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
- 生产环境建议 :
- 64位JDK + G1回收器
- 堆内存不超过物理内存的70%
- 开启GC日志,定期分析
- 预留20%内存给操作系统和其他进程
7.3 监控层面
-
必须监控指标:
- GC频率和停顿时间
- 堆内存使用率
- 线程数
- CPU使用率
-
告警阈值设置:
- Full GC频率 > 1次/小时
- 堆内存持续 > 80%
- GC停顿时间 > 1秒
- 线程数 > 1000
八、总结
JVM作为Java程序运行的核心基础设施,其知识体系涵盖类加载机制、内存管理、垃圾回收、性能优化等多个方面。掌握JVM原理不仅有助于解决生产环境中的性能问题和内存泄漏,还能指导我们编写更高效的Java代码。
在实际工作中,应遵循以下原则:
- 以问题为导向:不要过度优化,先解决实际存在的性能瓶颈
- 监控先行:建立完善的监控体系,及时发现问题
- 循序渐进:调优应小步快跑,每次只调整少量参数
- 理解原理:不盲目套用调优参数,应理解背后的工作原理