news 2026/11/4 2:37:08

深入理解 JVM 内存划分:程序计数器、元数据区、栈区、堆区全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解 JVM 内存划分:程序计数器、元数据区、栈区、堆区全解析

深入理解 JVM 内存划分:程序计数器、元数据区、栈区、堆区全解析

写 Java 代码时,我们只管new对象、调方法,从不关心这些数据到底"住"在哪里。其实 JVM 在背后把内存划分成了几个风格迥异的区域,各自存储不同的数据、各司其职。理解内存划分,是理解对象创建过程、垃圾回收、StackOverflowError / OutOfMemoryError 的前提。本文将逐一讲解 JVM 的四大内存区域:程序计数器、元数据区、栈区、堆区。


一、JVM 内存划分总览

JVM 启动时会向操作系统申请一大块内存,并把它划分成若干区域(从线程的角度可以分成两类):

JVM 内存 ├── 线程私有区域(每个线程各有一份) │ ├── 程序计数器 │ └── 栈区(Java 虚拟机栈,核心是栈帧) └── 线程共享区域(所有线程共用一份) ├── 堆区(对象实例的家,GC 主战场) └── 元数据区(类信息、运行时常量池等)
区域线程私有/共享存什么会不会 OOM
程序计数器私有下一条要执行的字节码地址不会
栈区私有栈帧(局部变量、操作数栈等)StackOverflowError / OOM
堆区共享对象实例、数组OOM
元数据区共享类元信息、运行时常量池OOM(元空间溢出)

记忆技巧:线程私有的"跟着线程走"(线程创建就有、线程销毁就没),线程共享的"跟着对象和类走"(对象放堆,类信息放元数据区)。


二、程序计数器(Program Counter Register)

2.1 作用

程序计数器是一块很小的内存,记录的是当前线程下一条要执行的字节码指令的地址。

2.2 为什么需要它?—— 因为"抢占式调度"

CPU 采用抢占式调度,线程随时可能被中断、让出 CPU,过一会儿再被调度回来。恢复执行时必须知道"我上次执行到哪一条指令了",这个断点就记在程序计数器里。

  • 线程执行 Java 方法时:程序计数器记录下一条字节码指令的地址;
  • 线程执行本地方法(native 方法)时:程序计数器的值为undefined。

2.3 关键特点

  • 每个线程独有一份(线程私有):如果只有一个计数器,线程 A 被切换回来时就会发现计数器已经被线程 B 改乱了,所以必须每线程一份、互不干扰;
  • 唯一一块不会发生 OOM 的内存区域:它只存一个地址,大小固定,也没有 GC;
  • 是程序运行的"控制流指示器":分支、循环、跳转、异常处理、线程恢复都依赖它。

三、栈区(Java 虚拟机栈)

3.1 作用

栈区描述的是Java 方法执行的内存模型:每调用一个方法,JVM 就会在当前线程的栈里压入一个栈帧(Stack Frame);方法执行完毕,栈帧出栈。

方法调用链就是栈的入栈/出栈过程:

publicstaticvoidmain(String[]args){intresult=add(1,2);// main 的栈帧压栈 → add 的栈帧压栈}publicstaticintadd(inta,intb){returna+b;// add 执行完,add 栈帧出栈,回到 main}
执行 add 时: │ add 栈帧 │ ← 栈顶(正在执行的方法) │ main 栈帧 │ └───────────┘ add 结束后: │ main 栈帧 │ ← 回到 main 继续执行 └───────────┘

3.2 栈帧里存什么

每个栈帧主要包含以下内容:

  • 方法的参数:调用方法时传进来的实参,就存在当前方法对应的栈帧里;
  • 局部变量:方法内部定义的局部变量(基本类型直接存值,对象类型存的是引用/地址,对象本体在堆里);
  • 方法的返回值:方法执行结束之后的返回值,保存在栈帧中,供调用方(上一个方法)取走使用;
  • 返回地址:方法执行结束之后要跳转回去的地址,即回到上一个方法的调用位置继续执行。

3.3 关键特点

  • 线程私有:每个线程创建时都会分配自己的栈,方法调用天然隔离,局部变量不涉及线程安全问题;
  • 两个经典异常:
    • StackOverflowError:调用链太深、栈帧太多(典型:递归没写终止条件);
    • OutOfMemoryError:线程开太多,栈内存总和超出限制;
  • 可用-Xss设置每个线程的栈大小(如-Xss1m)。

常见误区:对象在栈里吗?不在。局部变量表里只存对象的引用(地址),对象本体永远在堆里。所以常说"局部变量在栈上、对象在堆上"。


四、堆区(Heap)

4.1 作用

堆是 JVM 中最大的一块内存,是所有对象实例和数组的家——每一次new出来的东西都分配在堆上。

Personp=newPerson();// p(引用)在栈上,new Person()(对象本体)在堆上int[]arr=newint[10];// 数组也在堆上

4.2 堆的结构:分代设计

堆内部按照对象的"年龄"划分为不同区域,这是为了配合垃圾回收:

堆区 ├── 新生代(Young):新 new 出来的对象 │ ├── Eden(伊甸园区):对象出生地 │ └── Survivor × 2(幸存者区 S0 / S1):熬过一次 Minor GC 的对象 └── 老年代(Old):长期存活的对象、大对象
  • 对象优先在Eden区分配;
  • Eden 满了触发Minor GC,存活对象被复制到 Survivor 区,反复"打扫"几次还活着的对象晋升到老年代;
  • 老年代满了触发Full GC(代价更大)。

为什么要分代?基于"弱分代假说"——绝大多数对象都是"朝生夕死"的,把新对象集中在一个小区域频繁回收,比整堆扫描高效得多。

4.3 关键特点

  • 线程共享:所有线程都能访问堆中的对象——堆正是线程安全问题的"事发地"(多个线程的栈上都持有指向同一个堆对象的引用);
  • GC 的主战场:垃圾回收主要发生在堆上,Eden/Survivor/老年代的划分都是为了更高效地 GC;
  • 会发生 OOM:对象太多、内存泄漏(该回收的对象还被引用着)都会导致java.lang.OutOfMemoryError: Java heap space;
  • 可用-Xms(初始大小)、-Xmx(最大大小)设置堆大小。

补充(了解):JDK 8 之后,如果开启了逃逸分析,个别不会"逃出"方法的对象可能通过栈上分配/标量替换直接在栈上创建,但这是 JVM 的优化手段,语义上对象仍然"属于"堆。


五、元数据区(Metaspace,即方法区)

5.1 作用

元数据区(JDK 8 之前叫方法区/永久代)存储的是类的元信息——即"类本身的数据",具体包括:

  • 类对象信息:类的全限定名、父类、实现的接口、访问修饰符等类的描述信息;
  • 方法信息:每个方法的字节码指令、方法签名;
  • 运行时常量池:字符串常量、final 常量、符号引用等(如"hello"这类字面量);
  • 静态变量:static修饰的类变量,属于类而不属于对象,所有实例共享一份。

打个比方:堆里存的是"人"(对象实例),元数据区存的是"户口模板/图纸"(类模板)。一个类只有一份"图纸",但可以按图纸 new 出无数个对象。

classPerson{// Person 类的元信息 → 元数据区staticintcount=0;// 静态变量(JDK 8+ 实际在堆中的 Class 对象里)Stringname;// 每个对象各自的 name → 堆中的对象实例里}Personp1=newPerson();// p1 指向的实例 → 堆Personp2=newPerson();// p2 指向的实例 → 堆

5.2 关键特点

  • 线程共享:类信息所有线程共用;
  • 会发生 OOM:大量动态生成类(如 CGLIB 动态代理、大量 JSP)可能导致Metaspace溢出;
  • GC 行为:元数据区也会回收——当类加载器被回收,且该类的实例全部销毁后,对应的类信息才可能被卸载,条件非常苛刻。

六、四大区域对比与记忆总结

6.1 对比表

维度程序计数器栈区堆区元数据区
线程私有/共享私有私有共享共享
存储内容下一条字节码地址栈帧(局部变量表、操作数栈等)对象实例、数组类元信息、运行时常量池
生命周期随线程随方法调用(栈帧入栈出栈)随对象(GC 决定)随类(类卸载才回收)
是否 GC不涉及不涉及(栈帧自动弹出)是(主战场)条件苛刻的类卸载
异常无StackOverflowError / OOMOOMOOM

6.2 用一段代码串起整个内存

publicclassMemoryDemo{staticintsharedCount=0;// 静态变量 → 堆中的 Class 对象publicstaticvoidmain(String[]args){intlocal=10;// 局部变量 → main 栈帧的局部变量表(栈区)Personp=newPerson("张三");// p 引用 → 栈;Person 实例和 "张三" → 堆MemoryDemo.class;// 类元信息 → 元数据区process(local);// 调 process:process 栈帧压栈}publicstaticvoidprocess(intx){// 方法字节码 → 元数据区x=x+1;// 执行过程由程序计数器逐条指引}// 方法结束:栈帧出栈}

6.3 一句话记忆

  • 程序计数器:每个线程的"进度条",记录下一条执行到哪(唯一不 OOM 的区域);
  • 栈区:每个线程的"方法调用记录本",一个方法一个栈帧,局部变量住这里,栈上存引用;
  • 堆区:所有线程共用的"对象仓库",new出来的都住这里,分代设计为 GC 服务;
  • 元数据区:所有线程共用的"类图纸仓库",存类信息、常量池,JDK 8 起搬到堆外本地内存。

理解了内存划分,很多问题会豁然开朗:为什么局部变量不用考虑线程安全(栈私有)?为什么共享对象才有并发问题(堆共享)?为什么static变量所有线程看到的是同一个(跟着类走)?为什么递归深了会 StackOverflowError(栈帧堆满了)?——答案都在这四个区域里。希望这篇文章对你有帮助,欢迎点赞收藏!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 2:35:19

4.DockerCompose是什么为什么算法服务需要多个容器

Docker Compose 是什么?为什么算法服务需要多个容器? 码海寻道 AI 算法工程化与系统落地实战第 4 篇 一个算法服务很少只需要一个进程。实际项目通常还需要 PostgreSQL 保存任务和结果,Redis 做缓存和限流,MinIO 保存文件&#x…

作者头像 李华
网站建设 2026/11/4 2:33:32

网络安全实战第18期:盲注极限之 1 bit 也能挖出数据

当页面什么都不显示时,数据还能被读走吗:盲注的 1 bit 通道 网络安全实战 90 天 Week 3 注入风险 | 难度 ★★★ 进阶 | 关键词:盲注 二分法 响应时间 信息泄露 请求频率告警前面讲的招式都建立在「页面会把结果打出来」这个前提上。…

作者头像 李华
网站建设 2026/11/4 2:31:57

高精度计算入门与immense数据模型精讲(四)

跳转链接 高精度计算入门与immense数据模型精讲(一)高精度计算入门与immense数据模型精讲(二)高精度计算入门与immense数据模型精讲(三)高精度计算入门与immense数据模型精讲(五)高…

作者头像 李华
网站建设 2026/11/4 2:31:57

LabVIEW 入门快是优势,也是最大的坑

很多人学 LabVIEW 的第一周,就能拖出带按钮、带图表的界面,看着波形在屏幕上动起来,特别有成就感。这确实是 LabVIEW 最迷人的地方——别的语言还在配环境、调编译,你已经把数据采集跑通了。但今天想泼一盆不太凉的冷水&#xff1…

作者头像 李华
网站建设 2026/11/4 2:31:56

【赵渝强老师】AI for PostgreSQL数据库

《AI for PostgreSQL》课程详细描述 📚 课程讲师:赵渝强|🛠 技术方向:大模型Agent Skill PostgreSQL 15.3实战 课程简介 《AI for PostgreSQL》是面向PostgreSQL DBA、后端开发工程师、数据库运维人员打造的实战课程…

作者头像 李华