Java 程序运行时由 JVM 划分 5 大内存区域:方法区、虚拟机栈 (线程栈)、堆、程序计数器、本地方法栈,各区域分工不同、存储数据有明确边界,底层依托数组 + 指针边界实现内存划分。结合上图多线程代码案例,拆解各区域职责与数据存储逻辑。
一、JVM 内存分区总览
JVM 在程序启动后向操作系统申请内存,划分为 5 个独立区域,其中虚拟机栈、程序计数器、本地方法栈为线程私有(一个线程一套内存),堆、方法区是线程共享,所有线程共用同一块内存。内存底层本质依托数组实现,通过标记变量维护内存边界,管控数据存取。
二、逐个解析五大内存区
1. 方法区(线程共享)
存储内容
加载类后存放类的字节码文件、静态成员、运行时常量池(类常量池)、static 修饰的方法与变量。
- 类常量池:类编译阶段生成,以 Map 结构存储字面量、符号引用,编译期常量存入此处,类加载后进入方法区;
- 静态存储区:所有被
static修饰的静态属性、静态方法统一存放,正因如此,静态方法无需实例化对象,可直接通过类名.方法名()调用(例:代码中Person.m2()静态方法,直接调用无需 new 对象)。
package JAVA算法锻炼; public class Test { // 本类静态方法:存入方法区 public static void m1() { System.out.println("Test静态m1方法执行"); } public static void main(String[] args) { // t1线程:演示静态方法调用 + new对象(堆) Thread t1 = new Thread() { @Override public void run() { Person.m2(); // 调用静态方法【方法区】 m1(); // 本类静态方法【方法区】 Person xx = new Person(); // new对象在堆,引用xx存在当前run栈帧 xx.m1(); // 实例方法 } }; // t2、t3简单线程 Thread t2 = new Thread() { @Override public void run() { System.out.println("我是线程2"); } }; Thread t3 = new Thread() { @Override public void run() { System.out.println("我是线程3"); } }; // 启动三个子线程 t1.start(); t2.start(); t3.start(); // 主线程调用静态方法 Person.m2(); System.out.println("我是主线程"); } }结合代码举例
Test类、Person类的字节码、Person.m2()静态方法、Test.m1()静态方法全部加载存入方法区,程序首次运行时,优先完成类加载,字节码进入方法区。
2. 虚拟机栈(线程栈|线程私有,一个线程对应一个独立栈)
存储内容
每个方法被调用时,会在栈中创建一块独立内存:栈帧,栈帧存放:局部变量、方法形参、方法返回地址。
核心特点
- 栈内存空间小、读写速度极快,遵循先进后出,底层基于数组实现;
- 方法执行完毕,对应栈帧立即出栈销毁,生命周期跟随线程;
- 易错点:A 方法调用 B 方法,两个方法的栈帧在同一线程栈中是并列结构,不是嵌套从属关系。
结合代码举例
主线程 (main 线程):执行
main()方法,main栈帧入主线程栈;后续调用Person.m2(),m2 栈帧继续入主线程栈,方法并列;t1/t2/t3 三个子线程:各自拥有专属独立线程栈:
- t1 线程运行
run(),run()栈帧入 t1 专属栈,内部依次调用Person.m2()、Test.m1()、new Person().m1(),三个方法各自生成独立栈帧并入 t1 栈; - t2、t3 线程各自的
run()方法,分别入自己的私有栈。
- t1 线程运行
关键点:
new Person xx是局部变量,存放在 t1 的 run 栈帧内,xx引用地址指向堆中的 Person 实例。
3. 堆(线程共享,JVM 最大内存区域)
存储内容
所有new创建的对象实例、数组对象,对象的成员属性全部存放在堆中;对象的引用地址(比如代码里Person xx)保存在栈局部变量里。
特点
堆空间是五大分区中容量最大的区域,GC 垃圾回收主要针对堆内存,闲置对象会被 JVM 自动回收。
结合代码举例
t1线程中执行 new Person(),Person 实体对象创建在堆内存,栈中局部变量xx保存堆中对象的内存地址,通过地址找到堆里的对象,进而调用成员方法xx.m1()。
4. 程序计数器(线程私有,最小内存区)
作用
记录当前线程正在执行的字节码行号,用来配合 CPU 切换线程:
当 CPU 时间片切换、线程暂停执行时,程序计数器保存断点位置;线程再次抢到 CPU 资源后,依靠计数器记录的地址,从上次暂停的代码继续执行。
唯一没有 OOM 内存溢出的 JVM 内存区域。
5. 本地方法栈(线程私有)
和虚拟机栈结构几乎一致,区别在于:虚拟机栈服务 Java 代码,本地方法栈服务 native 本地方法(C/C++ 实现的底层方法),Thread.start()底层调用 native 方法,对应栈帧存入本地方法栈。
三、结合示例代码完整执行流程
// 代码片段 Thread t1 = new Thread(){ public void run(){ Person.m2(); m1(); Person xx = new Person(); xx.m1(); } };类加载阶段:Test、Person 字节码、静态方法
m2()存入【方法区】;new Thread():Thread 对象、匿名内部类对象创建在【堆】;t1.start():- 主线程调用 start,start 底层 native 方法入【本地方法栈】;
- 操作系统创建 t1 子线程,分配专属【线程栈 + 程序计数器】;
t1 调度运行
run():run()栈帧入 t1【线程栈】;Person.m2()静态方法:从方法区读取方法,m2 栈帧入 t1 栈;m1()本类静态方法:同样生成栈帧入栈;new Person():Person 实例创建在【堆】,局部变量xx(存对象地址)保存在 run 栈帧中;xx.m1()成员方法:通过 xx 地址找到堆对象,m1 栈帧入 t1 线程栈;
t2、t3、main 线程同理,各自占用私有栈,堆和方法区全线程共享。
四、补充总结
- 找数据口诀:对象存堆,局部变量存栈,类和静态放方法区,行号记在计数器,native 方法走本地栈;
- 线程私有三区:栈、程序计数器、本地方法栈;共享两区:堆、方法区;
- 栈随方法销毁、堆靠 GC 回收、方法区随类卸载释放。