JVM
JVM(Java Virtual Machine)是运行Java字节码的虚拟计算机,负责内存管理、垃圾回收、类加载等核心功能。JVM 内存管理的核心是堆空间分代和垃圾收集器选型,理解 GC 算法和收集器差异是 JVM 调优的基础。
JVM 知识结构:
堆空间的基本结构
JVM 堆是垃圾收集器管理的主要区域,分为新生代(Eden 区 + 两个 Survivor 区)和老年代。对象在堆中的分配遵循三个规则:
- 对象优先在 Eden 区分配
- 大对象直接进入老年代
- 长期存活的对象将进入老年代
JVM 堆内存分代结构:
GC 分类
针对 HotSpot VM 的实现,GC 分为两大类:
部分收集(Partial GC):
- Minor GC / Young GC:只对新生代进行垃圾收集
- Major GC / Old GC:只对老年代进行垃圾收集(部分语境中也指整堆收集)
- Mixed GC:对整个新生代和部分老年代进行垃圾收集
整堆收集(Full GC):收集整个 Java 堆和方法区
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
可达性分析算法
通过一系列称为 "GC Roots" 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连,则证明此对象是不可用的。
可作为 GC Roots 的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
垃圾收集算法
垃圾收集算法是虚拟机垃圾收集的理论基础,用于确定哪些对象需要回收以及如何回收。
四种 GC 算法的关系与适用场景:
标记-清除算法
分为"标记"和"清除"阶段:首先标记出所有不需要回收的对象,标记完成后统一回收掉所有没有被标记的对象。这是最基础的收集算法,后续算法都是对其不足进行改进。两个明显问题:效率问题和空间碎片问题。
标记-复制算法
将内存分为大小相同的两块,每次使用其中一块。当这一块用完后,将存活对象复制到另一块,然后把使用空间一次清理掉。代价是每次只使用一半内存。
标记-整理算法
针对老年代特点:标记过程与标记-清除一样,但后续让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
根据对象存活周期将内存分为新生代和老年代,分别选择合适的 GC 算法。新生代每次有大量对象死去,适合标记-复制算法;老年代对象存活率高,适合标记-清除或标记-整理算法。
垃圾收集器
垃圾收集器是垃圾收集算法的具体实现,不同收集器适用于不同场景。
收集器演进路线——从单线程到低停顿:
Serial 收集器
最基本、历史最悠久的收集器,单线程执行,进行垃圾收集时必须暂停所有工作线程(Stop The World)。新生代采用标记-复制算法,老年代采用标记-整理算法。
ParNew 收集器
Serial 的多线程版本,除了使用多线程进行垃圾收集外,其余行为与 Serial 完全一样。是 Server 模式下首选的新生代收集器,因为只有它能与 CMS 配合工作。
Parallel Scavenge 收集器
关注吞吐量(CPU 用于运行用户代码时间与总时间的比值),适合后台计算密集型任务。可通过 -XX:MaxGCPauseMillis 控制停顿时间,或 -XX:GCTimeRatio 设置吞吐量目标。
CMS 收集器
以最短回收停顿时间为目标,适合与用户交互的应用。基于标记-清除算法,分为四个阶段:
CMS 收集器工作流程:
缺点:CPU 敏感、无法处理浮动垃圾、产生空间碎片。
G1 收集器
面向服务端应用,目标是可控停顿时间。将堆划分为多个等大的 Region,每个 Region 可以是 Eden、Survivor、Old 或 Humongous。通过 -XX:MaxGCPauseMillis 设定停顿目标。
ZGC 收集器
JDK 11 引入的实验性收集器,目标是亚毫秒级停顿,支持 TB 级堆。使用染色指针和读屏障技术,几乎全程并发执行。
收集器对比:
| 收集器 | 算法 | 代 | 特点 | 适用场景 |
|---|---|---|---|---|
| Serial | 标记-复制 | 新生代 | 单线程,STW | 客户端、小堆 |
| ParNew | 标记-复制 | 新生代 | 多线程,STW | 配合 CMS |
| Parallel Scavenge | 标记-复制 | 新生代 | 吞吐量优先 | 后台计算 |
| CMS | 标记-清除 | 老年代 | 低停顿 | 交互式应用 |
| G1 | 混合 | 整堆 | 可控停顿 | 服务端(默认) |
| Z1 | 染色指针 | 整堆 | 亚毫秒停顿 | 超大堆 |
内存分配策略
对象内存分配流程:
Java 内存模型(JMM)
Java 内存模型定义了多线程环境下变量的访问规则。JMM 规定所有变量存储在主内存,每个线程有自己的工作内存,线程对变量的操作必须在工作内存中进行。
JMM 线程间通信模型:
并发编程三个特性
| 特性 | 说明 | 保证方式 |
|---|---|---|
| 原子性 | 操作不可中断 | synchronized、Lock、CAS |
| 可见性 | 修改后其他线程可见 | volatile、synchronized、final |
| 有序性 | 按代码顺序执行 | volatile(禁止重排序)、Happens-Before |
volatile 关键字
volatile 通过内存屏障保证可见性和有序性,但不保证原子性。典型使用场景:状态标志位、DCL 单例模式中的 instance 变量。
Happens-Before 原则
Happens-Before 是 JMM 的核心规则,用于判断多线程环境下的操作可见性:
| 规则 | 说明 | 示例 |
|---|---|---|
| 程序顺序 | 同一线程内,前面的操作先行于后面 | a=1; b=2; → a 先于 b |
| 监视器锁 | unlock 先行于后续的 lock | synchronized 块结束先于进入 |
| volatile 变量 | 写先于后续读 | volatile x=1 先于 read x |
| 线程启动 | start() 先于线程内操作 | t.start() 先于 t 内代码 |
| 线程终止 | 线程内操作先于 isAlive() 为 false | 检测线程结束 |
| 中断规则 | interrupt() 先于检测中断 | t.interrupt() 先于 t.isInterrupted() |
| 终结器规则 | 构造函数先于 finalize() | 对象创建先于回收 |
| 传递性 | A 先于 B,B 先于 C → A 先于 C | 规则可传递组合 |
类加载机制
类的生命周期包括加载、验证、准备、解析、初始化、使用、卸载七个阶段。其中验证、准备、解析统称为连接。
类加载过程:
双亲委派模型
类加载器收到加载请求时,先委托父加载器处理,只有父加载器无法完成时才自己加载。这保证了核心类(如 java.lang.Object)不会被篡改。
| 加载器 | 职责 | 加载路径 |
|---|---|---|
| BootstrapClassLoader | 加载核心类库 | JAVA_HOME/lib |
| ExtensionClassLoader | 加载扩展类 | JAVA_HOME/lib/ext |
| AppClassLoader | 加载应用类 | classpath |
| 自定义加载器 | 特殊需求 | 自定义路径 |
线程安全与锁优化
线程安全级别
| 级别 | 说明 | 示例 |
|---|---|---|
| 不可变 | 状态不可修改 | final 对象、String、无状态对象 |
| 绝对线程安全 | 任何操作都安全 | ConcurrentHashMap |
| 相对线程安全 | 单操作安全,组合可能不安全 | Vector |
| 线程兼容 | 需要外部同步 | ArrayList |
| 线程对立 | 无论如何都不安全 | Thread.stop() |
锁升级过程
锁状态从低到高升级,不可降级(重量级锁只能回到无锁):
| 锁类型 | 适用场景 | 加锁开销 |
|---|---|---|
| 偏向锁 | 同一线程反复进入 | 极低(无需 CAS) |
| 轻量级锁 | 不同线程交替进入 | 较低(一次 CAS) |
| 重量级锁 | 竞争激烈 | 较高(线程阻塞) |
自旋锁与自适应自旋
互斥锁在多核 CPU 下,如果锁持有时间很短,线程切换的开销反而更大。自旋锁不让线程阻塞,而是忙循环等待。自适应自旋根据历史成功率动态调整自旋次数。
锁消除
JIT 在逃逸分析后发现某个锁对象只能被一个线程访问,会自动消除这个锁。例如 StringBuffer 的 append 在方法内创建时,锁会被消除。
锁粗化
如果一系列操作对同一个对象反复加锁解锁,JIT 会将多次锁操作合并为一次。
类文件结构
每个 .class 文件包含一个类或接口的定义,由魔数、版本号、常量池、访问标志、类索引、字段表、方法表、属性表组成。
// 伪代码结构
ClassFile {
u4 magic; // 0xCAFEBABE
u2 minor_version;
u2 major_version;
cp_info constant_pool[]; // 常量池
u2 access_flags; // 访问标志
u2 this_class; // 类索引
u2 super_class; // 父类索引
u2 interfaces[]; // 接口表
field_info fields[]; // 字段表
method_info methods[]; // 方法表
attribute_info attrs[]; // 属性表
}字节码指令由操作码(1字节)+ 操作数组成。javap 是 JDK 内置反编译工具,配合 jclasslib 可可视化分析。