引言:为什么要理解JVM内存结构?

当你遇到以下问题时,是否感到困惑:

  • StackOverflowErrorOutOfMemoryError 有什么区别?
  • 为什么有的对象存储在堆中,有的却在栈中?
  • 静态变量存储在哪里?方法代码又存储在哪里?
  • 为什么多线程之间可以共享对象,却不能共享局部变量?

这些问题的答案,都藏在 JVM内存结构 中。

理解JVM内存结构是深入学习JVM的基础:

  • 性能调优:知道内存如何分配,才能优化内存参数
  • 问题排查:90%的内存问题与内存区域的使用不当相关
  • 代码优化:理解对象存储位置,才能写出高效代码

本文将带你建立JVM内存结构的全景认知,为后续深入学习每个区域打下基础。


JVM内存结构全景图

运行时数据区概览

当Java程序运行时,JVM会将内存划分为不同的数据区域,这些区域统称为 运行时数据区(Runtime Data Area)

根据《Java虚拟机规范》,JVM运行时数据区包括以下5大区域:

┌─────────────────────────────────────────────────────────┐
│                     JVM 运行时数据区                       │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌──────────────────┐  ┌──────────────────┐           │
│  │  程序计数器       │  │  程序计数器       │  线程私有  │
│  │ (PC Register)    │  │ (PC Register)    │           │
│  └──────────────────┘  └──────────────────┘           │
│                                                         │
│  ┌──────────────────┐  ┌──────────────────┐           │
│  │   虚拟机栈        │  │   虚拟机栈        │  线程私有  │
│  │  (VM Stack)      │  │  (VM Stack)      │           │
│  └──────────────────┘  └──────────────────┘           │
│                                                         │
│  ┌──────────────────┐  ┌──────────────────┐           │
│  │  本地方法栈       │  │  本地方法栈       │  线程私有  │
│  │ (Native Stack)   │  │ (Native Stack)   │           │
│  └──────────────────┘  └──────────────────┘           │
│                                                         │
├─────────────────────────────────────────────────────────┤
│                                                         │
│              ┌───────────────────────┐                 │
│              │        堆内存          │   线程共享       │
│              │       (Heap)          │                 │
│              │                       │                 │
│              │  ┌─────────────────┐ │                 │
│              │  │   新生代 (Young) │ │                 │
│              │  │  Eden + S0 + S1 │ │                 │
│              │  └─────────────────┘ │                 │
│              │  ┌─────────────────┐ │                 │
│              │  │   老年代 (Old)   │ │                 │
│              │  └─────────────────┘ │                 │
│              └───────────────────────┘                 │
│                                                         │
│              ┌───────────────────────┐                 │
│              │       方法区           │   线程共享       │
│              │    (Method Area)      │                 │
│              │                       │                 │
│              │  · 类信息             │                 │
│              │  · 常量池             │                 │
│              │  · 静态变量           │                 │
│              │  · JIT编译后的代码    │                 │
│              └───────────────────────┘                 │
│                                                         │
└─────────────────────────────────────────────────────────┘

              ┌───────────────────────┐
              │      直接内存          │  (堆外内存)
              │   (Direct Memory)     │
              │                       │
              │  · NIO Buffer         │
              │  · Netty Zero-Copy    │
              └───────────────────────┘

关键特征

  • 线程私有:程序计数器、虚拟机栈、本地方法栈(每个线程独立一份)
  • 线程共享:堆、方法区(所有线程共享)
  • 堆外内存:直接内存(不属于JVM规范,但实际使用广泛)

5大内存区域详解

1️⃣ 程序计数器(Program Counter Register)

定义:记录当前线程正在执行的字节码指令地址。

核心特点

  • 线程私有(每个线程独立一个)
  • 占用内存极小(通常几十字节)
  • 唯一不会发生OutOfMemoryError的区域
  • 如果执行Native方法,计数器值为空(Undefined)

作用

  • 线程切换后能恢复到正确的执行位置
  • 多线程情况下,记录每个线程的执行进度

示例理解

public void method() {
    int a = 1;        // 字节码指令地址:0
    int b = 2;        // 字节码指令地址:2
    int c = a + b;    // 字节码指令地址:4
}

程序计数器会依次存储 0 → 2 → 4,指示当前执行到哪一行字节码。


2️⃣ 虚拟机栈(Java Virtual Machine Stack)

定义:每个方法执行时创建一个 栈帧(Stack Frame),用于存储局部变量、操作数、方法出口等信息。

核心特点

  • 线程私有
  • 生命周期与线程相同
  • 栈深度受限(默认1MB,可通过 -Xss 调整)
  • 后进先出(LIFO) 的数据结构

栈帧结构

┌─────────────────────┐
│      栈帧3          │  ← 当前方法(栈顶)
├─────────────────────┤
│      栈帧2          │
├─────────────────────┤
│      栈帧1          │  ← main方法
└─────────────────────┘

每个栈帧包含:

  • 局部变量表:存储方法参数和局部变量
  • 操作数栈:进行算术运算和方法调用
  • 动态链接:指向运行时常量池的方法引用
  • 方法返回地址:方法正常退出或异常退出的位置

可能的异常

  • StackOverflowError:栈深度超过限制(如无限递归)
  • OutOfMemoryError:无法分配新的栈内存(极少见)

3️⃣ 本地方法栈(Native Method Stack)

定义:为Native方法(使用JNI调用的C/C++方法)服务的栈。

核心特点

  • 线程私有
  • 与虚拟机栈作用类似,但服务于Native方法
  • HotSpot虚拟机中,虚拟机栈和本地方法栈合二为一

典型Native方法示例

// Object类的hashCode方法是Native方法
public native int hashCode();

// System类的currentTimeMillis方法也是Native方法
public static native long currentTimeMillis();

可能的异常

  • StackOverflowError
  • OutOfMemoryError

4️⃣ 堆(Heap)

定义:JVM管理的最大内存区域,用于存储几乎所有的对象实例

核心特点

  • 线程共享(所有线程访问同一个堆)
  • JVM启动时创建,进程结束时销毁
  • 垃圾收集器的主要工作区域
  • 可通过 -Xms(初始大小)和 -Xmx(最大大小)调整

分代设计(JDK 8及之前):

┌─────────────────────────────────────┐
│             堆内存 (Heap)            │
├─────────────────────────────────────┤
│  新生代 (Young Generation) - 1/3    │
│  ┌──────────┬──────┬──────┐        │
│  │  Eden    │ S0   │ S1   │        │
│  │   8      │  1   │  1   │        │
│  └──────────┴──────┴──────┘        │
├─────────────────────────────────────┤
│  老年代 (Old Generation) - 2/3      │
│                                     │
└─────────────────────────────────────┘

分代原因

  • 弱分代假说:大部分对象朝生夕死
  • 强分代假说:熬过多次GC的对象难以消亡
  • 通过分代,可以针对不同区域使用不同的GC策略

可能的异常

  • OutOfMemoryError: Java heap space(堆内存溢出)

5️⃣ 方法区(Method Area)

定义:存储已被虚拟机加载的 类信息、常量、静态变量、JIT编译后的代码 等数据。

核心特点

  • 线程共享
  • 也称为"非堆"(Non-Heap),但本质上仍属于堆外内存
  • JDK 7及之前称为"永久代"(PermGen)
  • JDK 8及之后改为"元空间"(Metaspace),使用本地内存

存储内容

数据类型说明示例
类信息类的版本、字段、方法、接口java.lang.String 类的元数据
运行时常量池字面量、符号引用字符串常量 "hello"
静态变量类变量public static int count = 0;
JIT编译代码即时编译器编译的机器码热点方法的本地代码

永久代 → 元空间的演变

  • JDK 7及之前:方法区使用永久代实现,容易发生 OutOfMemoryError: PermGen space
  • JDK 8及之后:移除永久代,改用元空间(使用本地内存),动态扩展,减少OOM风险

可能的异常

  • OutOfMemoryError: Metaspace(元空间溢出,JDK 8+)
  • OutOfMemoryError: PermGen space(永久代溢出,JDK 7及之前)

🔄 直接内存(Direct Memory)

定义:不属于JVM规范定义的内存区域,但在NIO、Netty等场景下广泛使用的 堆外内存

核心特点

  • 不受JVM堆大小限制
  • 不受GC管理(手动管理或通过Cleaner机制回收)
  • 通过 -XX:MaxDirectMemorySize 限制大小
  • 读写性能更高(避免Java堆与本地内存之间的复制)

典型使用场景

// NIO的DirectByteBuffer使用直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

// Netty的零拷贝技术也依赖直接内存
PooledByteBufAllocator.DEFAULT.directBuffer();

优势

  • 避免Java堆与本地内存之间的数据复制
  • 提升IO性能(零拷贝)

可能的异常

  • OutOfMemoryError: Direct buffer memory

线程私有 vs 线程共享

为什么要区分线程私有和共享?

线程私有区域(程序计数器、虚拟机栈、本地方法栈):

  • 目的:保证线程安全,避免数据竞争
  • 生命周期:与线程相同(线程启动时创建,线程结束时销毁)
  • 典型存储:局部变量、方法调用链、字节码指令地址

线程共享区域(堆、方法区):

  • 目的:实现线程间数据共享
  • 生命周期:与JVM进程相同
  • 典型存储:对象实例、类信息、静态变量
  • 并发风险:需要考虑线程安全问题

示例:线程私有与共享的区别

public class MemoryDemo {
    private static int sharedVar = 100;  // 存储在方法区(线程共享)

    public static void main(String[] args) {
        Object obj = new Object();  // obj引用在栈中,对象实例在堆中(共享)
        int localVar = 10;          // 存储在栈中(线程私有)

        new Thread(() -> {
            // 每个线程有独立的栈,localVar不可见
            // 但可以访问堆中的obj对象和方法区的sharedVar
            System.out.println(obj);
            System.out.println(sharedVar);
        }).start();
    }
}

内存分布

线程1栈                    线程2栈
┌──────────┐              ┌──────────┐
│ localVar │              │ (空)   │
│ = 10     │              │          │
└──────────┘              └──────────┘
        ↓                        ↓
┌────────────────────────────────────┐
│           堆内存 (共享)              │
│      new Object() 对象实例           │
└────────────────────────────────────┘
        ↓
┌────────────────────────────────────┐
│          方法区 (共享)               │
│      sharedVar = 100                │
└────────────────────────────────────┘

常见问题与误区

❌ 误区1:所有对象都存储在堆中

真相:绝大多数对象存储在堆中,但有例外:

  • 逃逸分析优化:如果对象不会逃逸出方法,JIT编译器可能将其分配在 栈上(栈上分配)
  • 标量替换:如果对象可以拆解为基本类型,可能直接在栈中存储
// 对象未逃逸,可能在栈上分配
public void method() {
    Object obj = new Object();  // obj仅在方法内使用,不会逃逸
    System.out.println(obj);
}

❌ 误区2:方法区不会发生GC

真相:方法区也会进行垃圾回收,但效率较低:

  • 回收目标:废弃的常量、无用的类
  • 条件苛刻:类的所有实例已回收、类加载器已回收、类对应的Class对象无引用

❌ 误区3:静态变量存储在方法区

真相

  • JDK 7及之前:静态变量存储在方法区(永久代)
  • JDK 7及之后:静态变量随Class对象存储在

总结

核心要点

  1. JVM运行时数据区包括5大区域

    • 程序计数器:记录字节码指令地址(线程私有)
    • 虚拟机栈:存储方法调用链和局部变量(线程私有)
    • 本地方法栈:为Native方法服务(线程私有)
    • 堆:存储对象实例(线程共享,GC主战场)
    • 方法区:存储类信息、常量、静态变量(线程共享)
  2. 线程私有区域保证线程安全,线程共享区域实现数据共享

  3. 直接内存不属于JVM规范,但在高性能场景下广泛使用

  4. 理解内存结构是性能调优和问题排查的基础

与下篇文章的衔接

下一篇文章,我们将深入学习 程序计数器(PC Register),这个JVM中最小但至关重要的内存区域,理解它在多线程环境中的作用。


参考资料


下一篇预告:《程序计数器:最小的内存区域》 深入理解PC寄存器在多线程环境中的作用,以及为什么它是唯一不会OOM的区域。