Skip to content

内存结构整体概述

一张图说清楚 JVM 内存

JVM 运行时数据区是 Java 程序运行时的「内存地图」。理解它,就理解了 Java 程序在运行时的全貌。

┌──────────────────────────────────────────────────────────────────┐
│                        JVM 运行时数据区                           │
│                                                                  │
│  ┌──────────────────────────────┐  ┌──────────────────────────┐ │
│  │      线程私有区域              │  │     线程共享区域           │ │
│  │   (每个线程独立拥有)          │  │   (所有线程共同使用)      │ │
│  │                              │  │                          │ │
│  │  ┌──────────────────────┐   │  │  ┌────────────────────┐  │ │
│  │  │    PC 寄存器          │   │  │  │      堆(Heap)     │  │ │
│  │  │  当前执行指令地址       │   │  │  │  对象实例/数组      │  │ │
│  │  └──────────────────────┘   │  │  │  GC 的主战场        │  │ │
│  │                              │  │  └────────────────────┘  │ │
│  │  ┌──────────────────────┐   │  │  ┌────────────────────┐  │ │
│  │  │     虚拟机栈          │   │  │  │    方法区           │  │ │
│  │  │   方法调用 → 栈帧     │   │  │  │  (JDK 8 = Metaspace)│  │ │
│  │  │   局部变量/操作数栈   │   │  │  │  类信息/常量/静态变量│  │ │
│  │  └──────────────────────┘   │  │  └────────────────────┘  │ │
│  │                              │  │  ┌────────────────────┐  │ │
│  │  ┌──────────────────────┐   │  │  │    直接内存         │  │ │
│  │  │    本地方法栈         │   │  │  │  NIO 堆外内存       │  │ │
│  │  │  native 方法服务      │   │  │  └────────────────────┘  │ │
│  │  └──────────────────────┘   │  └──────────────────────────┘ │
│  └──────────────────────────────┘                               │
└──────────────────────────────────────────────────────────────────┘

线程私有 vs 线程共享

这是理解 JVM 内存最核心的分类:

类型区域生命周期GC
线程私有PC 寄存器、虚拟机栈、本地方法栈与线程共存亡无需回收,线程结束自动释放
线程共享堆、方法区、直接内存与 JVM 进程共存亡GC 管理的主要对象

线程私有区域

每个线程都有自己的 PC 寄存器和虚拟机栈/本地方法栈。线程之间完全隔离,互不干扰。

这带来的一个直接好处:多线程不需要加锁就能安全地操作自己的栈。因为每个线程只能访问自己的私有区域。

java
public class ThreadPrivateDemo {
    public static void main(String[] args) {
        // main 线程的 PC 寄存器指向当前指令
        // main 线程的虚拟机栈包含 main() 方法的栈帧

        new Thread(() -> {
            // 新线程有自己的 PC 寄存器和虚拟机栈
            // 这些区域对其他线程完全不可见
            doWork();
        }).start();
    }
}

线程共享区域

堆和方法区被所有线程共享,它们的内容需要考虑并发安全。

java
public class ThreadSharedDemo {
    // 这个静态变量存在于方法区,被所有线程共享
    private static final List<String> sharedList = new ArrayList<>();

    public static void main(String[] args) {
        // 多个线程同时访问 sharedList
        // 需要考虑线程安全(synchronized / ConcurrentHashMap)
        for (int i = 0; i < 10; i++) {
            new Thread(() -> sharedList.add("item")).start();
        }
    }
}

堆:Java 内存的核心

几乎所有对象实例都分配在堆上。这是 GC 的主战场,也是大多数性能问题的根源所在。

┌─────────────────────────────────────┐
│            堆(Heap)                │
│                                     │
│  ┌───────────────────────────────┐  │
│  │          老年代(Old Gen)     │  │
│  │   长期存活的对象 / 大对象      │  │
│  └───────────────────────────────┘  │
│  ┌───────────────────────────────┐  │
│  │        Survivor S0 / S1       │  │
│  │   MinorGC 后存活的对象        │  │
│  └───────────────────────────────┘  │
│  ┌───────────────────────────────┐  │
│  │        Eden 区                │  │
│  │   新对象优先分配在这里        │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘
  • Eden 区:新对象出生的地方
  • Survivor 区:MinorGC 后存活对象的暂存区
  • 老年代:长期存活或过大的对象

我们会在后续的 堆概述 和对象分配专题中详细讲解。

方法区:类信息的仓库

方法区存储的是类的元信息——类的结构、方法的字节码、常量池、静态变量。

JDK 8 之前,方法区用 PermGen(永久代) 实现;JDK 8 开始,改为 Metaspace(元空间),从堆内移到了本地内存。

┌─────────────────────────────────────┐
│         方法区 / 元空间               │
│                                     │
│  ├─ 类信息(类名/修饰符/字段/方法)   │
│  ├─ 运行时常量池                     │
│  ├─ 静态变量(JDK 7 及之前)         │
│  ├─ JIT 编译后的代码缓存             │
│  └─ 符号引用                         │
└─────────────────────────────────────┘

关于方法区的演进和细节,我们会在 方法区概述 深入讲解。

直接内存:NIO 的秘密武器

直接内存不是 JVM 运行时数据区的一部分,但经常与 JVM 内存打交道。它是操作系统分配的本地内存,通过 java.nio.DirectByteBuffer 与堆内对象交互。

主要用途是 Zero-Copy 场景:文件读写、网络传输时避免 JVM 堆和操作系统之间的数据复制,大幅提升 IO 性能。

典型使用场景:Netty 高性能网络框架、Kafka 的消息存储。

各区域与 Java 关键字的对应

理解了内存区域,再看 Java 关键字,会清晰很多:

关键字内存位置说明
new堆(Heap)创建对象,分配在堆上
static方法区类变量,所有对象共享
final堆(对象头)对象引用不可变,不影响内容
局部变量虚拟机栈方法内部的变量,栈帧中的局部变量表
ThreadLocal线程私有每个线程独立副本,不共享
intern()方法区(StringTable)字符串驻留

为什么需要分这么多区域

很多人会问:为什么不统一成一整块内存?

答案在于不同数据的生命周期不同。有些数据用完就丢(方法的局部变量),有些数据要存活很久(全局单例)。统一管理意味着要么浪费(一直保留短期数据),要么危险(过早回收长期数据)。

分代设计的思路是:根据对象的「年龄」分配到不同的区域,用不同的策略管理。这直接催生了现代 GC 的分代回收理论。

章节导航

接下来,我们按顺序深入每个区域:

  1. 类加载器与类加载过程 —— 类加载过程:Loading
  2. 线程私有区 —— PC 寄存器虚拟机栈本地方法栈
  3. 线程共享区 —— 堆空间方法区直接内存
  4. 对象实例化专题 —— 对象实例化方式

让我们从 类加载过程:Loading 开始。

基于 VitePress 构建