聊聊JVM的内存模型
JVM内存模型定义了Java程序运行时的内存组织结构和管理机制。它不仅是内存分配的基础,更是程序执行、垃圾回收、多线程并发的基石。这里需要分清一个点,JVM和JMM(Java内存模型)是两个完全不同的概念Java内存模型(JMM):Java语言层面的内存模型,关注多线程环境下的内存可见性、原子性、有序性JVM内存结构(运行时数据区):JVM进程在执行Java程序时的内存布局和管理机制1.分层设计:
目录
程序计数器(Program Counter Register)
虚拟机栈(Java Virtual Machine Stacks)
1.JVM的内存模型概述
JVM内存模型定义了Java程序运行时的内存组织结构和管理机制。它不仅是内存分配的基础,更是程序执行、垃圾回收、多线程并发的基石。
这里需要分清一个点,JVM和JMM(Java内存模型)是两个完全不同的概念
-
Java内存模型(JMM):Java语言层面的内存模型,关注多线程环境下的内存可见性、原子性、有序性
-
JVM内存结构(运行时数据区):JVM进程在执行Java程序时的内存布局和管理机制
2.JVM核心组成部分
JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。
其内部结构图大致如下:

接下来我们来一个个介绍下这五个部分
程序计数器(Program Counter Register)
核心概念:线程私有的内存区域,记录当前线程执行的字节码指令地址。
特性与作用:
-
线程私有:每个线程独立存储,互不干扰
-
执行控制:记录当前线程执行的字节码指令地址
-
Native方法:执行本地方法时,计数器值为空
-
无溢出:唯一不会发生内存溢出的区域
public static void main(String[] args) {
int a = 1; // 程序计数器记录指令地址
int b = 2; // 计数器递增
int c = a + b; // 继续记录下一条指令地址
System.out.println(c);
}
虚拟机栈(Java Virtual Machine Stacks)
核心概念:每个线程独立的空间,主要是存储栈帧(方法调用)

当一个线程调用一个方法时,就会在它的虚拟机栈中创建栈帧,当方法调用结束后,由于栈先进后出的特性,会从栈底弹出,释放空间。
栈帧的组成:
- 局部变量表:存放方法参数和局部变量
- 操作数栈:后进先出结构,存储计算中间结果
- 动态链接: 指向运行时常量池的方法引用
- 返回地址:方法正常或异常退出后的返回位置
本地方法栈(Native Method Stack)
核心概念:JVM调用本地(Native)方法服务,结构与虚拟机栈类似,本地方法执行时也会创建栈帧。
本地方法栈和虚拟机栈不同的区别在于虚拟机栈存放的是非本地方法,而本地方法栈顾名思义存放的是本地方法(如C/C++实现的方法)
堆(Heap)
核心概念:是 JVM 中最大的一块内存区域,被所有线程共享,在虚拟机启动时创建,用于存放对象实例,是垃圾回收的主要目标。
堆又分为新生代(Eden区和两个Survivor区)和老年代,其中新生代占堆的1/3左右,而老年代占堆的2/3左右。
-
新生代:存放临时对象,回收频繁但速度快
-
老年代:存放长期存活的重要对象,回收次数少但耗时长

对象分配流程:
public static void main(String[] args) {
// 1. 小对象优先在Eden区分配
byte[] smallObj = new byte[2 * 1024]; // 2KB
// 2. 大对象直接进入老年代
byte[] bigObj = new byte[10 * 1024 * 1024]; // 10MB
// 3. 长期存活对象进入老年代
List<Object> longLiveList = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
Object obj = new Object();
longLiveList.add(obj); // 多次GC后进入老年代
}
}
方法区(Method Area)
核心概念:存储类信息、常量、静态变量等数据,逻辑上属于堆的一部分。
存储内容分类:
-
类型信息:类名、访问修饰符、父类、接口等
-
常量池:字面量(字符串、数字)和符号引用
-
字段信息:字段名、类型、修饰符
-
方法信息:方法名、返回类型、参数、字节码
-
类变量:静态变量
-
类加载器引用
-
Class对象引用
3.JVM中栈和堆的主要区别
1.存储内容及可见性:栈存储方法参数、局部变量、返回地址等私有信息,堆存储对象实例、数组等所有线程共享信息。
2.生命周期:栈中的数据具有确定的生命周期,当一个方法调用结束时,其对应的栈帧就会被销毁,栈中存储的局部变量也会随之消失。堆中的对象生命周期不确定,对象会在垃圾回收机制检测到对象不再被引用时才被回收。
3.访问和存储速度:栈的存取速度通常比堆快,因为栈遵循先进后出的原则,操作简单快速。堆的存取速度相对较慢,因为对象在堆上的分配和回收需要更多的时间,而且垃圾回收机制的运行也会影响性能。而访问上,栈是基于cpu缓存的,所以查找极快,而堆是通过指针寻址,相对较慢。
4.存储空间:栈的空间相对较小,且固定,由操作系统管理。当栈溢出时,通常是因为递归过深或局部变量过大。堆的空间较大,动态扩展,由JVM管理。堆溢出通常是由于创建了太多的大对象或未能及时回收不再使用的对象。
4.JVM内存溢出与常见原因
内存溢出可以看作是一个有限的盒子中存放的物品已经到了极限,如果再进行放置那么盒子就会遭到损坏,由前面的jvm核心组成部分的介绍我们可以知道,内存溢出的出现场景一般会发生在堆、栈、方法区当中。
堆溢出(OutOfMemoryError)
经常发生在对象过多或内存泄漏导致。
// 场景1:对象过多
public class HeapOOM {
static class OOMObject {}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject()); // 不断创建对象
}
}
}
// 场景2:内存泄漏
public class MemoryLeak {
private static final List<byte[]> LEAK_LIST = new ArrayList<>();
public void leakMemory() {
for (int i = 0; i < 100; i++) {
LEAK_LIST.add(new byte[1024 * 1024]); // 每次1MB
}
}
}
栈溢出(StackOverflowError)
栈溢出主要来源于递归调用过深或栈帧过大(局部变量过多)。
// 场景1:无限递归
public class StackOverflow {
private int depth = 0;
public void recursiveCall() {
depth++;
recursiveCall(); // 无限递归
}
}
// 场景2:局部变量过多
public class LargeStackFrame {
public void methodWithManyLocals() {
int a1, a2, a3, a4, a5, a6, a7, a8, a9, a10;
int b1, b2, b3, b4, b5, b6, b7, b8, b9, b10;
// ... 更多局部变量
}
}
方法区溢出
方法区中存放的是类的元数据,导致方法区溢出的主要原因就是动态生成的类过多
// 场景:CGLIB大量动态生成类
public class MetaspaceOOM {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method,
Object[] args, MethodProxy proxy) {
return proxy.invokeSuper(obj, args);
}
});
while (true) {
enhancer.create(); // 不断创建代理类
}
}
static class OOMObject {}
}
5.JVM关键点总结
1.分层设计:线程私有与共享区域分离,平衡性能与安全性
2.分代管理:基于对象生命周期优化垃圾回收
3.自动管理:内存分配和回收自动化,减少程序员负担
4.平台无关:统一的抽象模型,屏蔽底层差异
最后,JVM内存模型是Java生态的基石,作为一名合格的java工程师,我们在编写高效的Java程序的同时,也需了解其内部的运作结构,不仅对java代码有更深层次的理解,也能为系统性能优化和问题排查提供关键支持。
制作不易,如果对你有帮助请点赞,评论,收藏,感谢大家的支持

更多推荐

所有评论(0)