1. JVM 架构与运行机制
1.1 核心职责
JVM 是运行在计算机上的程序,负责运行 Java 字节码文件。其核心功能包括:
- 解释与运行:将字节码指令实时解释为机器码。Java 性能不如 C++ 的主要原因在于运行时的实时解释 。
- 内存管理:自动为对象分配内存并回收不再使用的对象(垃圾回收机制),解决了 C/C++ 中手动释放内存易导致内存泄漏或悬空指针的问题 。
- 即时编译 (JIT):为了优化性能,JVM 会识别"热点代码"(高频调用的方法或循环),将其编译为机器码并保存在内存中(CodeCache),下次直接调用,从而获得接近 C++ 的性能 。
1.2 跨平台原理
Java 的"一次编写,到处运行"依赖于 JVM。不同操作系统(Windows, Linux)有不同版本的 JVM 实现。源代码编译成统一的字节码文件(.class),由特定平台的 JVM 将其翻译成该平台对应的机器码 。
2. 字节码文件深度解析
2.1 文件结构详解
字节码文件是以二进制流存储的,主要包含:
- 魔数 (Magic Number) :前4个字节固定为
0xcafebabe,用于校验文件类型 。 - 版本号:主版本号 52 代表 JDK 8。计算公式:JDK版本 = 主版本号 - 44 。
- 常量池:存储字面量(如字符串 "abc")和符号引用(类名、字段名)。字节码指令通过编号引用常量池内容,避免重复定义,节省空间 。
- 方法表:包含方法的字节码指令(Code属性)。
2.2 字节码指令案例分析
通过 i++ 和 ++i 的字节码差异理解执行逻辑:
i++原理 :先将局部变量i的值压入操作数栈(iload),然后直接在局部变量表中对i加 1(iinc),最后将栈中原来的值赋回给目标变量。因此i=i++结果仍为原值 。++i原理 :先在局部变量表中对i加 1(iinc),然后再将加 1 后的值压入操作数栈(iload) 。
2.3 常用工具
- jclasslib:可视化查看字节码文件结构(常量池、接口、属性等) 。
- javap :JDK 自带反编译工具,
javap -v可查看详细信息 。 - Arthas :阿里开源的线上诊断工具。
dump:将内存中的字节码保存到本地 。jad:反编译运行中的类代码,检查源码是否一致 。retransform:实现类的热替换(热部署),用于线上不停机修复 Bug 。
3. 类的生命周期与加载机制
类的生命周期包括:加载 -> 连接 (验证、准备、解析) -> 初始化 -> 使用 -> 卸载 。
3.1 加载过程 (Loading -> Linking -> Initializing)
-
加载 (Loading) :通过类加载器获取字节码流,在方法区生成
InstanceKlass,在堆中生成java.lang.Class对象 。 -
连接 (Linking):
-
验证:检查文件格式(魔数、版本号)和元信息 。
-
准备 :为静态变量分配内存并赋默认初值 (如
int为 0)。注意:final修饰的基本类型静态变量会在准备阶段直接赋代码中的值 。 -
解析:将符号引用替换为直接引用(内存地址) 。
-
-
初始化 (Initialization) :执行
<clinit>方法。该方法由静态代码块和静态变量赋值语句组成。- 执行顺序:按代码编写顺序执行。父类
<clinit>优先于子类执行 。 - 触发条件:
new对象、访问静态变量(非 final 常量)、Class.forName等 。
- 执行顺序:按代码编写顺序执行。父类
3.2 类加载器与双亲委派
- 加载器层级 :
- 启动类加载器 (Bootstrap) :加载
JAVA_HOME/jre/lib下的核心类(如 String),由 C++ 实现,Java 中获取为null。 - 扩展类加载器 (Extension) :加载
jre/lib/ext下的类 。 - 应用程序类加载器 (Application):加载 classpath 下的类(项目代码)。
- 启动类加载器 (Bootstrap) :加载
- 双亲委派机制 :
- 流程:向上查找缓存是否加载 -> 向上委托加载 -> 顶层无法加载则向下尝试加载 。
- 作用 :保证核心类库安全(防止自定义
java.lang.String覆盖核心类),避免类重复加载。
- 打破双亲委派 :
- 自定义类加载器 :重写
loadClass方法。Tomcat 使用此机制隔离不同 Web 应用的同名类 。 - 线程上下文类加载器 :JDBC 利用 SPI 机制,通过
Thread.currentThread().getContextClassLoader()加载驱动实现类,打破了"启动类加载器不能依赖应用类加载器"的限制 。
- 自定义类加载器 :重写
4. 运行时数据区 (内存模型)
4.1 线程不共享区域
- 程序计数器:记录下一条字节码指令的地址。是唯一不会发生内存溢出的区域 。
- Java 虚拟机栈 :
- 栈帧 :每个方法调用对应一个栈帧,包含局部变量表 、操作数栈 、帧数据(动态链接、返回地址) 。
- 局部变量表 :存放
this、参数和局部变量。槽位(Slot)可复用 。 - 栈溢出 :递归调用过深会导致
StackOverflowError。可通过-Xss调整栈大小(如-Xss256k) 。
- 本地方法栈 :存储
native方法的栈帧 。
4.2 线程共享区域
- 堆 (Heap) :
- 存储对象实例。
- 参数设置 :
-Xmx(最大值) 和-Xms(初始值) 建议设置为相同,避免运行时申请内存的开销和堆收缩 33。 - 查看命令 :
arthas的dashboard或memory命令查看used/total/max。
- 方法区 (Method Area) :
- 存储类元信息、运行时常量池。
- 实现变迁 :JDK 7 使用永久代 (PermGen) (在堆中);JDK 8 使用元空间 (Metaspace)(直接内存)。
- 参数 :JDK 8 使用
-XX:MaxMetaspaceSize限制元空间大小,防止耗尽系统内存 。 - 字符串常量池 :JDK 7 以后从方法区移到了堆中。
String.intern()在 JDK 7+ 中,如果池中没有,会直接存储堆中对象的引用,而不是复制对象 。
- 直接内存 :NIO 使用,不属于 JVM 运行时数据区。通过
ByteBuffer.allocateDirect分配,读写性能高(零拷贝),但分配回收成本高。需防范内存泄漏 。
5. 垃圾回收 (GC)
5.1 对象存活判断
- 可达性分析法 :从 GC Root 出发,不可达的对象视为垃圾。
- GC Root 包括:线程栈中引用的对象、静态变量引用的对象、JNI 引用的对象、监视器锁对象 。
- 引用类型 :
- 强引用:普通引用,宁可 OOM 也不回收。
- 软引用 (Soft):内存不足时回收,适用于缓存 。
- 弱引用 (Weak):无论内存是否充足,GC 时都会回收。
- 虚引用 (Phantom):用于接收对象回收通知(如管理直接内存) 。
5.2 垃圾回收算法
- 标记-清除:简单,但产生内存碎片,大对象无法分配 。
- 复制算法 :无碎片,吞吐量高,但内存利用率只有一半。适用于年轻代 。
- 标记-整理 :无碎片,利用率高,但整理阶段效率低。适用于老年代 。
5.3 垃圾回收器
| 回收器 | 适用区域 | 算法 | 特点 |
|---|---|---|---|
| Serial / Serial Old | 年轻代/老年代 | 复制 / 标记-整理 | 单线程,适合客户端简单场景 。 |
| ParNew | 年轻代 | 复制 | 多线程,常与 CMS 配合 。 |
| Parallel Scavenge | 年轻代 | 复制 | JDK 8 默认 。关注吞吐量,支持自适应调整堆大小 。 |
| CMS | 老年代 | 标记-清除 | 关注低延迟。并发收集,但有碎片和浮动垃圾,JDK 9 后废弃 。 |
| G1 | 整堆 | 复制 + 标记-整理 | JDK 9 默认 。将堆划分为 Region,支持可控停顿时间,无内存碎片 。 |
5.4 分代回收机制
- Young GC (Minor GC):Eden 区满时触发。存活对象由 Eden/From 区复制到 To 区,年龄 +1 。
- 晋升机制:对象年龄达到阈值(默认 15)或 Survivor 区空间不足时,晋升到老年代 。
- Full GC :老年代空间不足时触发,回收整个堆。若 Full GC 后仍不足,抛出
OutOfMemoryError。