Skip to content

运行时数据区:Java 进程的内存地图

你有没有想过,当你执行 java -jar app.jar 的时候,JVM 是怎么规划和使用内存的?

就像一座城市有不同的区域——商业区、住宅区、工业区——JVM 的运行时数据区也被划分为不同的内存区域,每个区域有自己的职责和特点。

理解这些区域,是掌握 JVM 的第一步。


一、线程视角:私有还是共享?

JVM 的内存区域,从线程角度来看,分为两大类:

1.1 线程私有区域

这些区域每个线程独享,互不干扰:

  • 程序计数器(Program Counter Register):当前线程执行的字节码行号
  • 虚拟机栈(VM Stack):方法调用的栈帧
  • 本地方法栈(Native Method Stack):Native 方法的调用栈

为什么需要线程私有?

因为每个线程都在同时执行不同的方法,如果这些区域是共享的,线程切换时保存和恢复上下文会变得极其复杂。想象一下,你正在写代码,突然有人把你写到一半的代码和别人的代码混在一起——那将是灾难。

1.2 线程共享区域

这些区域所有线程共享,生命周期与 JVM 进程一致:

  • 堆(Heap):存储对象实例和数组
  • 方法区(Method Area):存储类信息、常量、静态变量

1.3 直接内存(不属于 JVM 规范)

这是 HotSpot 特有的实现细节,使用本地内存:

  • 直接内存(Direct Memory):通过 ByteBuffer.allocateDirect() 分配
  • 不属于堆内存,受 -XX:MaxDirectMemorySize 限制
  • 主要用于高性能 I/O 场景

二、整体架构图

┌─────────────────────────────────────────────────────────────────┐
│                         JVM 进程内存                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────┐     ┌─────────────────┐                   │
│  │    线程 1        │     │    线程 2        │    ... 线程 N     │
│  │  ┌───────────┐  │     │  ┌───────────┐  │                   │
│  │  │    PC     │  │     │  │    PC     │  │                   │
│  │  └───────────┘  │     │  └───────────┘  │                   │
│  │  ┌───────────┐  │     │  ┌───────────┐  │                   │
│  │  │  VM Stack │  │     │  │  VM Stack │  │                   │
│  │  └───────────┘  │     │  └───────────┘  │                   │
│  │  ┌───────────┐  │     │  ┌───────────┐  │                   │
│  │  │ Native    │  │     │  │ Native    │  │                   │
│  │  │ Stack     │  │     │  │ Stack     │  │                   │
│  │  └───────────┘  │     │  └───────────┘  │                   │
│  └─────────────────┘     └─────────────────┘                   │
│                                                                 │
│  ┌─────────────────────────────────────────────┐               │
│  │              线程共享区域                      │               │
│  │  ┌───────────────┐  ┌───────────────────┐   │               │
│  │  │      堆       │  │      方法区        │   │               │
│  │  │   (Heap)      │  │  (Method Area)    │   │               │
│  │  │               │  │  - 类信息         │   │               │
│  │  │  对象实例     │  │  - 常量池         │   │               │
│  │  │  数组        │  │  - 静态变量       │   │               │
│  │  │               │  │  - JIT编译代码    │   │               │
│  │  └───────────────┘  └───────────────────┘   │               │
│  └─────────────────────────────────────────────┘               │
│                                                                 │
│  ┌─────────────────────────────────────────────┐               │
│  │            直接内存 (Direct Memory)          │               │
│  │  - ByteBuffer.allocateDirect()              │               │
│  │  - NIO 零拷贝优化                           │               │
│  └─────────────────────────────────────────────┘               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

三、各区域的 OOM 风险

每个区域都有限制,超出限制就会报错:

区域异常类型触发场景
OutOfMemoryError: Java heap space对象创建过多,内存泄漏
虚拟机栈StackOverflowError递归调用过深
虚拟机栈OutOfMemoryError: unable to create new native thread线程创建过多
方法区OutOfMemoryError: Metaspace类加载过多(JDK 8+)
方法区OutOfMemoryError: PermGen space类加载过多(JDK 7-)
直接内存OutOfMemoryError: Direct buffer memoryNIO 使用不当

面试追问方向

  • 线程私有区域的 GC 行为是什么?(不 GC,随线程消亡而释放)
  • 为什么堆内存溢出和栈内存溢出的表现不同?(堆是 OOM,栈是 StackOverflowError)
  • 直接内存属于 JVM 内存吗?它和堆内存有什么区别?

四、一道经典面试题

:JVM 内存区域中,哪些是线程私有的,哪些是线程共享的?

  • 线程私有:程序计数器、虚拟机栈、本地方法栈
  • 线程共享:堆、方法区(JDK 8+ 是元空间)
  • 直接内存:不算在 JVM 规范内,属于本地内存

追问:如果让你设计一个 Java 运行时,你会怎么划分内存区域?为什么这样划分?

这个问题考察的是你对内存管理的理解。线程私有区域需要保证线程安全隔离,线程共享区域需要考虑并发访问控制。


五、常见配置参数

bash
# 堆内存配置
-Xms256m          # 初始堆大小
-Xmx512m          # 最大堆大小
-Xmn128m          # 新生代大小
-XX:NewRatio=2    # 老年代:新生代 = 2:1

# 栈配置
-Xss1m            # 线程栈大小(JDK 11+ Linux x64 默认 1MB)

# 方法区配置(JDK 7)
-XX:PermSize=128m
-XX:MaxPermSize=256m

# 元空间配置(JDK 8+)
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m

# 直接内存配置
-XX:MaxDirectMemorySize=256m

留给你的问题

了解了 JVM 的内存区域划分后,不知道你有没有想过:

既然堆内存是所有线程共享的,那多线程并发分配对象时,如何保证不出现冲突?

TLAB(Thread Local Allocation Buffer)就是为此而生的。这个设计非常巧妙——每个线程在 Eden 区有自己专属的小块内存,分配对象时先在本地缓冲区操作,只有缓冲区满了才需要加锁同步。

下一篇文章,我们就来详细聊聊堆内存的结构。

基于 VitePress 构建