香蕉大香蕉(06敬畏市场)头像
高频八股 · JVM(3):运行时数据区,五块内存分别装什么封面图

高频八股 · JVM(3):运行时数据区,五块内存分别装什么

高频八股 · JVM(3):运行时数据区全景,五块内存分别装什么

先给你一张总表,后面逐块拆:

区域线程存什么会 OOM 吗
程序计数器私有当前字节码的行号不会
虚拟机栈私有栈帧(局部变量、操作数栈…)会(+ 栈溢出)
本地方法栈私有native 方法的栈帧会
堆共享对象实例、数组会
方法区共享类元信息、常量、静态变量会

记法:私有三块(PC、虚拟机栈、本地方法栈)+ 共享两块(堆、方法区)。

一、程序计数器(PC Register)

它是什么:一块很小的内存,记录当前线程正在执行的字节码行号。

你可以把它理解成"代码执行到哪了"的书签。

为什么它必须是线程私有的?

因为 CPU 是轮流给线程分配时间片的。线程 A 执行到第 10 行被切走,切回来的时候,得知道"我刚才执行到哪了"——这个信息就存在 PC 里。

所以每个线程必须有自己的一份 PC,不能共享。

为什么它不会 OOM?

这是面试最爱问的一句:程序计数器是 JVM 规范里唯一没有规定任何 OutOfMemoryError 情况的区域。

因为它存的东西是定长的(一个行号 / 一个地址),大小固定,不会随程序运行增长。

补充一个细节:执行 native 方法时,PC 的值是**未定义(Undefined)**的——因为 native 方法不走字节码,由本地代码执行。

二、虚拟机栈(VM Stack)

生命周期与线程相同:线程创建时创建,线程结束时销毁。

它存的是栈帧(Stack Frame)——每调用一个方法,就压入一个栈帧;方法返回,栈帧弹出。

一个栈帧里有四部分(下一期细讲):

组成作用
局部变量表存方法参数和局部变量
操作数栈字节码运算的工作区
动态链接指向运行时常量池中该方法的引用
方法返回地址方法结束后回到哪里

两种异常,别搞混:

异常触发条件
StackOverflowError栈深度超过 -Xss 限制(典型:无限递归)
OutOfMemoryError栈可以动态扩展,但扩展时申请不到内存
public void recursion() {
    recursion();   // 无限递归 → StackOverflowError
}

注意区分:栈溢出是 StackOverflowError,不是 OOM。这两个是 JVM 里最容易混的一对。

三、本地方法栈(Native Method Stack)

作用跟虚拟机栈一样,只不过它服务的是 native 方法(用 C/C++ 写的方法)。

一个实现细节:在 HotSpot 里,虚拟机栈和本地方法栈是合并实现的——所以你用 jstack 看不到单独的"本地方法栈"。

四、堆(Heap)

JVM 里最大的一块内存,也是 GC 的主战场。

  • 线程共享;
  • 存对象实例和数组(几乎所有 new 出来的东西都在这);
  • 是 OOM 最常见的发生地。

分代结构(下一期细讲,先有个印象):

堆
├── 新生代(Young)
│   ├── Eden
│   └── Survivor 0 / Survivor 1
└── 老年代(Old / Tenured)

常用参数:

  • -Xms:堆初始大小;
  • -Xmx:堆最大大小。

生产建议:-Xms 和 -Xmx 设成一样,避免堆反复扩容带来的抖动。

五、方法区(Method Area)

存什么:

  • 类的元信息(类结构、字段、方法、接口);
  • 运行时常量池;
  • 静态变量;
  • JIT 编译后的代码缓存。

注意:方法区 是 JVM 规范里的逻辑概念,不同 JDK 版本的实现不一样。

版本实现位置
JDK 7 及之前永久代(PermGen)堆内(受 -XX:MaxPermSize 限制)
JDK 8 及之后元空间(Metaspace)本地内存(受 -XX:MaxMetaspaceSize 限制)

5.1 为什么用元空间替换永久代?

四个原因:

  1. 永久代大小难调——设小了容易 PermGen space OOM,设大了浪费堆;
  2. 永久代在堆里,和对象抢空间——类加载多了会把堆挤爆;
  3. 永久代的 GC 效率低——回收条件苛刻,容易堆积;
  4. 和 JRockit 合并——JRockit 没有永久代,为了统一实现。

换成元空间后:类元信息搬到本地内存,理论上只受物理内存限制,PermGen space 这个经典 OOM 也就消失了。

但注意:元空间不是无限大——如果不设上限,类加载过多(比如大量动态代理)同样会 Metaspace OOM。

六、直接内存(Direct Memory)

它不属于运行时数据区,但经常被一起讨论。

它是什么:通过 Unsafe.allocateMemory() 直接向操作系统申请的堆外内存。最常见的使用者是 NIO 的 DirectByteBuffer。

ByteBuffer buf = ByteBuffer.allocateDirect(1024 * 1024);  // 1MB 直接内存

为什么要用它?

因为少一次拷贝。

普通堆内 buffer 做 IO 时,数据要从**堆内 → 堆外(内核缓冲区)**拷一次;直接内存本来就在堆外,省掉了这一步,所以 NIO 高吞吐场景更愿意用它。

代价也很明显:

优点缺点
少一次内存拷贝,IO 更快分配和回收成本高(不受 GC 直接管理)
不受 -Xmx 限制受物理内存限制,容易把机器吃满
适合长生命周期的缓冲区出问题难排查

参数:-XX:MaxDirectMemorySize(不设时默认约等于 -Xmx)。

它也会 OOM,报错是:

java.lang.OutOfMemoryError: Direct buffer memory

七、全景对比:每个区域怎么"爆"

区域关键参数典型 OOM 场景
程序计数器—不会 OOM
虚拟机栈-Xss递归太深(StackOverflowError)/ 线程太多
本地方法栈-Xss同上
堆-Xms / -Xmx对象太多、内存泄漏
方法区(元空间)-XX:MaxMetaspaceSize类加载过多(动态代理、热部署)
直接内存-XX:MaxDirectMemorySizeNIO 大缓冲区、未释放

一个实用的排查思路:看到 OOM,先看报错的后缀——Java heap space 是堆,Metaspace 是元空间,Direct buffer memory 是直接内存,unable to create native thread 是线程数到顶。

八、高频面试题速答

Q1:JVM 运行时数据区有哪几块?
程序计数器、虚拟机栈、本地方法栈(私有),堆、方法区(共享)。

Q2:为什么程序计数器不会 OOM?
它存的是定长的字节码行号,大小固定,不会随运行增长;JVM 规范也没规定它会抛 OOM。

Q3:StackOverflowError 和 OutOfMemoryError 有什么区别?
前者是栈深度超限(如无限递归),后者是申请不到内存(栈扩展失败、堆满、元空间满等)。

Q4:JDK 8 为什么用元空间替换永久代?
永久代大小难调、和堆抢空间、GC 效率低,且与 JRockit 合并需要统一实现。元空间放在本地内存,更灵活。

Q5:直接内存是 JVM 管理的吗?
不是。它由操作系统分配,通过 DirectByteBuffer 使用,不受 -Xmx 限制,但受 -XX:MaxDirectMemorySize 和物理内存限制。

Q6:堆和栈的区别?
堆线程共享、存对象、GC 管理、空间大;栈线程私有、存栈帧、随方法调用自动出入、空间小但快。

九、一句话总结

私有三块(PC、虚拟机栈、本地方法栈)管"执行",共享两块(堆、方法区)管"存放";PC 是唯一不 OOM 的,堆是最容易爆的,直接内存是"不归 JVM 管但会拖垮机器"的。

(本系列持续更新中,共 10 期,敬请关注~)

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2401_88601046/article/details/169267062

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--