JVM内存模型概述
JVM内存模型定义了Java程序运行时数据的存储和管理方式,包括线程共享区域和线程私有区域。核心结构可分为堆、方法区、虚拟机栈、本地方法栈和程序计数器。
堆(Heap)
堆是JVM中最大的一块内存区域,被所有线程共享,用于存储对象实例和数组。堆分为新生代(Young Generation)和老年代(Old Generation)。
- 新生代:包含Eden区、Survivor0和Survivor1区,新对象优先分配在Eden区,触发Minor GC后存活对象移至Survivor区。
- 老年代:存放长期存活的对象,当空间不足时触发Major GC(Full GC)。
堆的大小通过-Xms(初始堆大小)和-Xmx(最大堆大小)参数配置。
方法区(Method Area)
方法区存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等。在HotSpot JVM中,方法区的实现称为"元空间"(Metaspace),默认不限制大小,但可通过-XX:MaxMetaspaceSize调整。
虚拟机栈(Java Virtual Machine Stacks)
每个线程拥有独立的虚拟机栈,用于存储栈帧(Frame)。栈帧包含局部变量表、操作数栈、动态链接和方法返回地址。
- 局部变量表:存储基本数据类型和对象引用。
- 操作数栈 :用于方法执行时的中间计算。
栈深度不足时会抛出StackOverflowError,可通过-Xss调整栈大小。
本地方法栈(Native Method Stack)
与虚拟机栈类似,但服务于JVM调用的本地(Native)方法,如C/C++编写的库函数。
程序计数器(Program Counter Register)
线程私有,记录当前线程执行的字节码指令地址。若执行Native方法,计数器值为undefined。此区域是唯一不会发生OutOfMemoryError的内存区域。
直接内存(Direct Memory)
非JVM规范定义,但可通过ByteBuffer.allocateDirect分配堆外内存,受限于-XX:MaxDirectMemorySize。直接内存的回收依赖Cleaner机制或显式调用System.gc()。
内存溢出与调优
OutOfMemoryError:堆溢出(对象过多)、方法区溢出(类加载过多)、栈溢出(递归过深)。- 调优工具 :
jstat监控GC状态。jmap生成堆转储快照。VisualVM或MAT分析内存泄漏。
示例代码:模拟内存溢出
java
public class OOMDemo {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 持续分配1MB对象
}
}
}
通过-Xms10m -Xmx10m限制堆大小可快速触发OutOfMemoryError。
常见参数
-XX:+UseG1GC:启用G1垃圾收集器。-XX:NewRatio=2:设置新生代与老年代的比例。-XX:SurvivorRatio=8:设置Eden与Survivor区的比例。
一个类在JVM的一生
对象在JVM中的生命周期示例
以Person类为例,展示一个对象从创建到回收的完整过程:
1. 类加载阶段
当代码中首次出现new Person()时,JVM检查Person.class是否已加载。若未加载,类加载器将字节码加载到方法区,验证并初始化静态变量。
2. 内存分配
执行new Person()时,JVM在堆中分配内存空间。内存大小由字段类型决定(如String name和int age占用的空间)。
3. 对象初始化
- 分配的内存被清零,基本类型赋默认值(如
age=0,name=null) - 调用构造方法进行显式初始化(如
this.name="张三")
4. 引用建立
变量person存储在栈帧中,持有堆中对象的地址引用:
java
Person person = new Person("张三", 25);
5. 对象使用阶段
- 通过引用调用方法:
person.sayHello() - 方法调用时创建栈帧,局部变量存储在栈中
- 实例变量始终存在于堆内存
6. 垃圾回收准备
当发生以下情况时,对象成为垃圾回收候选:
- 引用置空:
person = null - 超出作用域:方法执行完毕导致栈帧弹出
7. 回收过程
- 垃圾收集器通过可达性分析标记不可达对象
- 调用
finalize()方法(仅执行一次) - 内存被回收(标记-清除/复制/整理算法)
关键内存区域交互
- 堆:存储对象实例和数组
- 栈:存储局部变量和部分结果
- 方法区:存储类信息、常量池等元数据
典型场景示例
新生代晋升
若对象在Minor GC后存活,年龄计数器增加。当超过阈值(默认15),对象从Eden区晋升到老年代。
内存泄漏场景
静态集合持续添加对象却不移除,导致对象始终可达,无法被回收。
大对象直接进入老年代
超过-XX:PretenureSizeThreshold的对象直接在老年代分配,避免新生代复制开销。
Minor GC 和 Major GC 的触发条件
Minor GC
当新生代(Young Generation)的Eden区空间不足时触发。对象优先在Eden区分配,若Eden区满且无法分配新对象,会触发Minor GC。存活的对象会被移动到Survivor区(From或To空间),年龄达到阈值(默认15)的对象会晋升到老年代。
Major GC/Full GC
通常指老年代(Old Generation)的垃圾回收,触发条件包括:
- 老年代空间不足,通常由晋升失败(Promotion Failed)或分配大对象直接进入老年代时触发。
- 显式调用
System.gc()(不保证立即执行)。 - 元空间(Metaspace)不足时也可能触发Full GC。
- 垃圾收集器自身的策略(如CMS的并发模式失败)。
垃圾收集器优缺点及适用场景
Serial 收集器
特点 :单线程,STW(Stop-The-World)时间长。
优点 :简单高效,无线程交互开销,适合客户端模式或资源受限环境。
缺点 :停顿时间长。
适用场景:单核CPU或小型应用(如嵌入式系统)。
ParNew 收集器
特点 :Serial的多线程版本,仅用于新生代。
优点 :多线程并行GC,缩短停顿时间。
缺点 :需配合CMS使用,老年代仍需其他收集器。
适用场景:需低停顿的Web应用,与CMS搭配使用。
Parallel Scavenge 收集器
特点 :新生代收集器,关注吞吐量(Throughput)。
优点 :可调节吞吐量目标(-XX:GCTimeRatio)和最大停顿时间(-XX:MaxGCPauseMillis)。
缺点 :停顿时间可能不稳定。
适用场景:后台计算任务,如批处理系统。
Serial Old 收集器
特点 :Serial的老年代版本,单线程标记-整理算法。
适用场景:与Parallel Scavenge搭配或作为CMS的备用方案。
Parallel Old 收集器
特点 :Parallel Scavenge的老年代版本,多线程标记-整理。
优点 :高吞吐量,适合与Parallel Scavenge组合。
适用场景:吞吐量优先的服务器应用。
CMS(Concurrent Mark-Sweep)收集器
特点 :并发标记清除,低停顿。
优点 :减少STW时间,适合交互式应用。
缺点:
- 内存碎片问题(需配置
-XX:+UseCMSCompactAtFullCollection)。 - 并发模式失败时退化为Serial Old。
- 对CPU资源敏感。
适用场景:Web服务、B/S系统,要求快速响应。
G1(Garbage-First)收集器
特点 :分Region收集,混合代模型,可预测停顿时间。
优点:
- 兼顾吞吐量和低停顿。
- 适合大堆(>4GB),通过
-XX:MaxGCPauseMillis设定目标停顿时间。
缺点 :内存占用较高(Remembered Set)。
适用场景:JDK 9+默认收集器,适用于大内存多核环境。
ZGC 和 Shenandoah
ZGC:
- 特点:并发整理,停顿时间不超过10ms。
- 适用场景:超大堆(TB级),JDK 15+生产可用。
Shenandoah:
- 特点:低停顿,与ZGC类似但实现不同。
- 适用场景:RedHat主导,OpenJDK支持。
选择建议
- 响应优先:CMS/G1(低延迟)。
- 吞吐优先:Parallel Scavenge + Parallel Old。
- 大堆平衡:G1或ZGC。
- JDK版本:JDK 8默认Parallel,JDK 11+默认G1。
可通过JVM参数(如-XX:+UseG1GC)显式指定收集器。
by the way
并发标记清除法(Concurrent Mark-Sweep, CMS)的底层原理
并发标记清除法是一种以最小化停顿时间为目标的垃圾收集算法,主要用于老年代(Old Generation)的垃圾回收。其核心思想是通过并发执行标记和清除阶段,减少应用线程的停顿时间。
初始标记(Initial Mark)
初始标记阶段需要暂停所有应用线程(Stop-The-World)。该阶段仅标记从GC Roots直接可达的对象,速度极快。GC Roots包括虚拟机栈中的引用、方法区中的静态变量和常量等。
并发标记(Concurrent Mark)
在并发标记阶段,应用线程与垃圾收集线程并发执行。垃圾收集线程从初始标记阶段标记的对象出发,递归遍历整个对象图,标记所有可达对象。由于应用线程可能同时修改对象引用关系,因此存在漏标或错标问题。
重新标记(Remark)
重新标记阶段需要暂停所有应用线程,用于修正并发标记阶段因应用线程运行导致的标记错误。通常采用增量更新(Incremental Update)或原始快照(Snapshot-At-The-Beginning, SATB)算法处理并发标记期间的对象变化。
并发清除(Concurrent Sweep)
并发清除阶段与应用线程并发执行,垃圾收集线程清理未标记的对象(即不可达对象),回收内存空间。由于无需移动存活对象,CMS不会产生内存碎片问题,但长期运行后可能因碎片导致Full GC。
关键技术与挑战
写屏障(Write Barrier)
CMS通过写屏障技术记录并发标记期间对象引用的变化。例如,在增量更新算法中,写屏障会记录被修改的引用关系,供重新标记阶段处理。
内存碎片问题
CMS不压缩内存,可能导致内存碎片。当无法找到足够大的连续空间分配对象时,会触发Full GC(通常使用Serial Old收集器),导致较长停顿。
并发模式失败(Concurrent Mode Failure)
如果在垃圾收集完成前老年代空间不足,CMS会退化为Serial Old收集器,引发长时间停顿。通常通过设置合理的堆大小和触发阈值(-XX:CMSInitiatingOccupancyFraction)来避免。
适用场景与参数配置
CMS适合对停顿时间敏感的应用,但需要充足的CPU资源支持并发操作。常用JVM参数包括:
-XX:+UseConcMarkSweepGC:启用CMS收集器。-XX:CMSInitiatingOccupancyFraction=70:老年代使用率达到70%时触发CMS。-XX:+UseCMSCompactAtFullCollection:Full GC时启用内存压缩(默认禁用)。
与其他收集器的对比
- G1收集器:同样注重低延迟,但采用分区域(Region)和标记-整理算法,避免内存碎片问题。
- Parallel Old:注重吞吐量,但停顿时间较长。
CMS已在新版JDK中被标记为废弃(JEP 291),推荐使用G1或ZGC等现代收集器。