JVM内存模型的核心是运行时数据区 ,它是JVM在运行Java程序时划分的内存区域,用于存储程序执行过程中需要的各类数据。根据《Java虚拟机规范》,JDK 8及以后版本的JVM运行时数据区可分为 线程私有区域 和 线程共享区域 两大类,另外还有一块特殊的直接内存(堆外内存),虽不属于规范定义的运行时数据区,但在实际应用中频繁使用。
一、 线程私有区域
线程私有区域的生命周期与线程完全一致,线程创建时分配内存,线程终止时释放内存,不存在线程安全问题。主要包含3个区域:程序计数器、虚拟机栈、本地方法栈。
1. 程序计数器(Program Counter Register)
程序计数器是一块极小的内存空间 ,可以看作是当前线程执行的字节码指令的行号指示器。
- 核心功能
- 记录当前线程正在执行的Java方法的字节码指令地址;如果执行的是
native方法(由C/C++实现,非Java代码),计数器值为undefined。 - 支持线程切换:CPU通过时间片轮转调度线程,线程切换后,需要通过程序计数器恢复到切换前的执行位置,保证线程能继续执行。
- 记录当前线程正在执行的Java方法的字节码指令地址;如果执行的是
- 关键特性
- 是JVM运行时数据区中唯一不会抛出
OutOfMemoryError的区域,因为它占用的内存空间固定且极小。 - 线程私有:每个线程都有独立的程序计数器,互不干扰。
- 是JVM运行时数据区中唯一不会抛出
2. 虚拟机栈(Java Virtual Machine Stacks)
虚拟机栈是描述Java方法执行的内存模型 ,每个方法在执行时都会创建一个栈帧(Stack Frame),栈帧是方法执行的基本单位,存储方法的局部变量、操作数、动态链接等信息。
- 核心结构:栈帧 一个栈帧包含4个部分:
- 局部变量表 :存储方法的局部变量(如方法参数、方法内定义的变量),包括基本数据类型(
byte、int等)、对象引用(指向堆中对象的地址)、returnAddress类型(指向字节码指令的地址)。局部变量表的大小在编译期确定,运行时不会改变。 - 操作数栈 :作为方法执行的临时数据存储区,用于存放计算过程中的中间结果。例如执行
a + b时,会先将a和b压入操作数栈,再弹出计算,最后将结果压回栈中。 - 动态链接:指向运行时常量池的方法引用。Java方法在编译时会以符号引用的形式存储在常量池,运行时需要将符号引用转换为直接引用(即方法在内存中的实际地址),这个过程就是动态链接。
- 方法返回地址:记录方法执行完毕后,需要返回的字节码指令地址。方法正常执行完会返回这里,异常退出时则通过异常处理器处理,无需记录返回地址。
- 局部变量表 :存储方法的局部变量(如方法参数、方法内定义的变量),包括基本数据类型(
- 生命周期 方法调用时,栈帧入栈;方法执行完毕时,栈帧出栈并释放内存。栈帧的入栈和出栈遵循先进后出(FILO) 原则。
- 常见异常
StackOverflowError:当线程请求的栈深度超过虚拟机栈的最大容量时抛出,典型场景是无限递归调用。OutOfMemoryError:当虚拟机栈可以动态扩展(大部分JVM实现支持),但扩展时无法申请到足够内存时抛出。
3. 本地方法栈(Native Method Stack)
本地方法栈的作用与虚拟机栈类似,区别在于服务对象不同 :虚拟机栈为Java方法服务,本地方法栈为native方法服务(如System.currentTimeMillis()、Object.hashCode()等底层方法)。
- 实现细节
- 不同JVM对本地方法栈的实现差异较大,例如HotSpot虚拟机直接将本地方法栈与虚拟机栈合并实现,两者共享同一块内存区域。
- 常见异常 与虚拟机栈完全一致,会抛出
StackOverflowError和OutOfMemoryError。
二、 线程共享区域
线程共享区域由所有线程共同访问,生命周期与JVM一致,JVM启动时创建,关闭时销毁,是垃圾回收(GC)的主要战场。主要包含2个区域:堆、方法区。
1. 堆(Heap)
堆是JVM运行时数据区中最大的一块内存空间 ,也是对象实例和数组的唯一存储区域 (几乎所有通过new关键字创建的对象都存放在堆中)。
-
核心特性
- 线程共享 :所有线程都可以访问堆中的对象,因此多线程操作堆中对象时需要通过
volatile、synchronized等机制保证线程安全。 - 垃圾回收重点:堆中存储的对象由垃圾收集器自动回收,开发者无需手动释放内存。堆的内存大小可以通过JVM参数动态调整。
- 可扩展性 :堆的初始大小和最大大小可通过参数配置:
-Xms:堆的初始内存大小,默认值为物理内存的1/64;-Xmx:堆的最大内存大小,默认值为物理内存的1/4;- 建议将
-Xms和-Xmx设置为相同值,避免JVM在运行时频繁扩容堆内存,导致性能损耗。
- 线程共享 :所有线程都可以访问堆中的对象,因此多线程操作堆中对象时需要通过
-
堆的细分结构(分代模型) 为了提高垃圾回收效率,HotSpot虚拟机将堆划分为 新生代 和 老年代,采用"分代收集"的垃圾回收策略。
erlang堆 = 新生代(约占1/3) + 老年代(约占2/3) 新生代 = Eden区(约占80%) + Survivor 0区(S0,约占10%) + Survivor 1区(S1,约占10%)- 新生代 :存储新创建的对象,垃圾回收频率高,回收速度快(采用Minor GC )。
- Eden区:新对象优先分配到Eden区,当Eden区满时触发Minor GC,存活的对象会被转移到S0区。
- Survivor区(S0/S1) :两个Survivor区始终有一个为空,用于存放Minor GC后存活的对象。下次Minor GC时,将当前非空Survivor区的存活对象转移到另一个空的Survivor区,同时清空原Survivor区。对象在Survivor区中每存活一次,年龄就增加1。
- 老年代 :存储存活时间较长的对象,垃圾回收频率低,回收速度慢(采用Major GC/Full GC )。
- 当对象在Survivor区的年龄达到阈值(默认15,可通过
-XX:MaxTenuringThreshold调整),会被转移到老年代。 - 大对象(如超大数组)会直接进入老年代 ,避免在新生代中频繁复制(可通过
-XX:PretenureSizeThreshold设置大对象阈值)。 - 当老年代内存不足时,会触发Major GC;若Major GC后内存仍不足,则触发Full GC(回收整个堆+方法区),Full GC会导致程序卡顿,应尽量避免。
- 当对象在Survivor区的年龄达到阈值(默认15,可通过
- 新生代 :存储新创建的对象,垃圾回收频率高,回收速度快(采用Minor GC )。
2. 方法区(Method Area)
方法区用于存储类的元数据信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据,也被称为"永久代"(JDK 7及之前)或"元空间"(JDK 8及之后)。
-
核心存储内容
- 类的元数据:类的全限定名、字段信息(变量名、类型、修饰符)、方法信息(方法名、参数、返回值、修饰符)、父类信息、接口信息等。
- 运行时常量池 :是方法区的一部分,存储编译期生成的字面量 (如字符串常量
"hello"、基本数据类型常量100)和符号引用(如类名、方法名、字段名的引用)。JDK 7及以后,字符串常量池被移到堆中。 - 静态变量 :被
static修饰的变量,属于类而非对象,直接存储在方法区。 - JIT编译后的代码:JVM的即时编译器会将热点代码(频繁执行的代码)编译为机器码,优化执行效率,编译后的机器码也存储在方法区。
-
版本演进(核心变化) 方法区的实现是JVM内存模型的重要变更点,不同JDK版本差异显著:
JDK版本 实现名称 内存来源 常见溢出异常 JDK 7及之前 永久代(PermGen) JVM堆内存 java.lang.OutOfMemoryError: PermGen spaceJDK 8及之后 元空间(Metaspace) 本地内存(操作系统内存) java.lang.OutOfMemoryError: Metaspace变更原因:永久代的大小受JVM堆内存限制,容易因动态生成类(如Spring AOP、CGLIB代理)导致溢出;元空间使用本地内存,默认无上限(可通过参数限制),大幅降低了溢出风险。
-
元空间参数配置(JDK 8+)
-XX:MetaspaceSize:元空间的初始大小,也是触发Full GC的阈值,默认约21MB;-XX:MaxMetaspaceSize:元空间的最大限制,默认无上限,建议设置为256m~1g,避免占用过多系统内存;-XX:MinMetaspaceFreeRatio:元空间空闲比例低于该值时触发扩容,默认40%;-XX:MaxMetaspaceFreeRatio:元空间空闲比例高于该值时触发缩容,默认70%。
三、 特殊区域:直接内存(Direct Memory)
直接内存不属于JVM运行时数据区 ,而是JVM直接向操作系统申请的堆外内存,通过java.nio.DirectByteBuffer类访问。
-
核心优势
- 访问速度快:直接内存避免了JVM堆与操作系统内存之间的数据拷贝,适合大文件IO、网络通信等场景(如Netty框架大量使用直接内存)。
- 不受JVM堆大小限制:直接内存的大小仅受操作系统总内存限制。
-
关键注意事项
- 需要手动管理 :直接内存的回收不依赖JVM的垃圾收集器 ,需通过
Cleaner机制或调用System.gc()触发回收,若管理不当容易导致内存泄露。 - 可通过参数限制 :
-XX:MaxDirectMemorySize:设置直接内存的最大上限,默认与堆的最大内存(-Xmx)一致;若分配的直接内存超过该值,会抛出OutOfMemoryError。
- 需要手动管理 :直接内存的回收不依赖JVM的垃圾收集器 ,需通过