JVM内存模型深度剖析与优化
约 1921 字大约 6 分钟
2025-09-24
JVM内存模型

其中堆和方法区是所有线程共享的。
栈、本地方法栈、程序计数器是每个线程都有的。
栈
只要开始一个线程,就会在 JVM虚拟机的栈内存空间 里开辟一个小空间,专门用于存这个线程的局部变量等。

线程栈空间里,还会给每个执行的方法分配一个栈帧内存空间,在这个内存空间里,会存储这个方法的局部变量。
栈的数据结构是先进后出,后进先出。契合代码的调用执行顺序,后调用的代码是先执行的。执行完之后,释放内存空间,就对应着栈数据结构的出栈操作,就契合了后进先出。
局部变量表
局部变量表类似于数组结构。
用于存放方法的局部变量。(顺便解决了变量的作用范围)
程序计数器
每个线程都有一个程序计数器,记录当前线程执行到的代码位置。
每次执行都是需要CPU资源的,而在本线程执行的中途,CPU资源被其他线程占用了,那么等下次执行本线程代码时,从哪里执行呢?不能重头重新执行,需要从上次中止的代码地址继续往下执行,程序计数器就是用来记录执行到的代码行数,每执行一行代码, 字节码执行引擎就会去更新程序计数器记录的数据。
操作数栈
在程序运行中,要进行操作计算的临时运行空间。
public static void main(String[] args) {
int a = 10086;
int b = 10000;
int c = a + b;
System.out.println("c:" + c);
}
如上图,在进行 a + b 运算时,会将a和b变量的值放入 操作数栈 ,然后计算的结果20086,也会先临时存放在操作数栈里,有因为计算结果是赋值给变量c,则会在 局部变量表 里给变量c分配一个内存空间,然后将计算结果20086从操作数栈进行出栈,赋值给变量c(如下图)。

动态链接
将符号引用转为直接引用。
将代码的符号调用转为内存地址调用。
方法名对应的内存地址就是存放在 动态链接 里。
方法出口
记录调用方法后,在方法执行完,回到本方法后,继续从哪一行代码执行。
如下示例代码,在执行demo.cal()方法的时候,就将cal方法执行完后要继续执行的代码位置,存放在了 方法出口 。
public static void main(String[] args) {
int a = 10086;
int b = 10000;
Demo demo = new Demo();
int c = demo.cal(a, b);
System.out.println("c:" + c);
}
publuc class Demo {
public int cal(int a, int b) {
int c = a + b;
return c;
}
}堆
这段代码里的new的Demo对象,demo变量名是存放在 main方法栈帧的局部变量表 里的,实际的Demo对象是存放在 堆内存 里,然后demo变量记录的是Demo对象在堆内存里的地址。
public static void main(String[] args) {
Demo demo = new Demo();
int c = demo.cal();
System.out.println("c:" + c);
}
publuc class Demo {
public int cal(int a, int b) {
int a = 10086;
int b = 10000;
int c = a + b;
return c;
}
}
堆又分为年轻代和老年代
一般是年轻代占1/3,老年代占2/3
年轻代:Eden区(8/10)和Survivor区(两个1/10)
如果确认系统绝大部分都是“朝生夕死”的对象(方法执行完就回收的对象),则可以年轻代大一些,老年代小一些,减少系统full gc次数。
new一个对象,对象会放到Eden区里,如果Eden区放满了,那么字节码执行引擎会建一个minor gc垃圾回收的线程,去判断将对象里没有引用其他的无用对象给清理掉,对象里有引用其他对象的会直接复制到Survivor区;
(栈里面找局部变量,方法区里找静态变量,这些就是gc root,从这些变量开始找这些变量所指向的堆里的对象有没有成员变量引用其他对象,如果有则复制到Survivor区,没有则删除)
当Eden区又满了之后,这次会清理Eden区和两Survivor区中有存对象的区,gc会将Eden区和有存对象的一个Survivor区里无用的对象清理掉,有用的对象则会移到另一个此时还没有存对象的Survivor区;
分代年龄:每经历一次gc回收,没有被回收的对象的分代年龄就会加1,一般情况下分代年龄达到15就会转移到老年代;
当老年代区域也放满之后,会先触发full gc,full gc与minor gc类似,但是full gc是回收整个堆空间和方法区;如果此时老年代区域还是满的,则会触发OOM,报内存溢出异常;
STW(stop the world): minor gc和full gc都可能会触发STW机制,STW会停止所有的用户线程,gc等是后台线程;(用户会感觉到网站卡了一下);
JVM调优的目的是减少full gc和一次full gc的执行时间(因为full gc执行一次的时间相对来说比较长,所以STW的时间也会比较长);minor gc的执行时间几乎不会有影响,所以执行STW也不会有影响;
JVM为什么要设计STW机制?
防止在进行GC的过程中,已经找到了一部分的有用对象,但是这个对象所属的线程执行完了,栈内存空间被释放了,意味着原先找出来的有用对象在GC还没有结束的时候变成了垃圾对象,意味着之前的GC分辨对象有用无用的操作白做了,所以干脆直接暂停了所有线程;
方法区
主要用于常量(在编译期就确定的各种字面量和符号引用)、静态变量、类信息。
常量
- 字面量:文本字符串(
String)、被声明为final的常量值。 - 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
静态变量
被 static 修饰的变量, 静态变量的引用 ,例如 static MyClass obj = new MyClass();
那么对象实例本身是存储在堆中的,而对这个对象的静态引用 obj 是存储在方法区的。
类信息
- 类型信息:类的全限定名、它的直接父类的全限定名、类的类型(是普通类还是接口)、类的访问修饰符(
public,abstract,final等)。 - 方法的元数据:方法名、方法的返回类型、参数数量和类型、方法的访问修饰符、方法的字节码、异常表等。
- 字段的元数据:字段名、字段类型、字段的访问修饰符。
本地方法栈
是C语言实现的方法专属使用的内存空间
调优
主要目的是减少full gc的次数和每次STW(stop the world)的时间。
STW期间,系统不可用。
