Skip to content

JVM 内存模型和垃圾回收平时零零散散接触过——OOM、GC 停顿、堆栈这些词都听过,但"内存到底分几块、对象分配在哪、GC 怎么回收"一直没系统梳理。这篇把 JVM 最核心的两块讲清楚:运行时数据区 + 垃圾回收。

一、解决什么问题:对象放哪、垃圾怎么收

Java 的"自动内存管理"是它的招牌——不用像 C/C++ 那样手动 malloc/free。但自动不等于不用懂:内存溢出(OOM)怎么排查、GC 为什么卡顿、怎么调优,都得先搞清楚 JVM 内存怎么分的、垃圾怎么收的。

二、运行时数据区:内存分几块

JVM 把内存分成几块,各管各的:

区域存什么线程私有?GC 管吗
堆(Heap)所有对象实例共享(GC 主战场)
虚拟机栈(Stack)方法调用、局部变量私有不管(方法结束自动弹出)
方法区(元空间)类信息、常量、静态变量共享管(类的卸载)
程序计数器当前执行到哪一行私有不管

关键new 出来的对象都在上,堆是 GC 的主战场;方法调用和局部变量在上,方法结束自动清理,不用 GC。

java
public void method() {
    int local = 10;               // 局部变量在栈上(方法结束自动回收)
    Object obj = new Object();    // obj 引用在栈上,对象在堆上(靠 GC 回收)
}

三、堆的分代:新生代 + 老年代

堆又分两代,基于一个经验假设——大部分对象"朝生夕死"(创建后很快就没用了):


├── 新生代(Young)     ← 新对象放这,大部分很快死
│   ├── Eden 区         ← 新对象先到 Eden
│   ├── Survivor 0(S0)
│   └── Survivor 1(S1)
└── 老年代(Old)        ← 活得久的对象晋升到这
  • 新对象 → Eden
  • Eden 满了触发 Minor GC → 活下来的对象进 Survivor(S0/S1 来回复制)
  • 在 Survivor 来回几次还没死 → 晋升到老年代
  • 老年代满了触发 Full GC(最慢,要尽量避免频繁发生)

JVM 堆的分代:Eden → Survivor → 老年代,对象随 GC 逐步晋升

四、垃圾回收算法

GC 的核心算法有三种,分代收集就是它们的组合:

算法做法缺点用在哪
标记-清除标记垃圾 → 清除产生内存碎片老年代基础
复制把活对象复制到另一块,清空原块浪费一半空间新生代(对象死得快)
标记-整理标记垃圾 → 把活对象整理到一边老年代(解决碎片)

为什么新生代用复制、老年代用整理:新生代对象大多"朝生夕死",活对象少,复制成本低;老年代对象存活率高,复制浪费空间,改用"整理"解决碎片。

五、类加载与双亲委派(一句话了解)

类加载分五步:加载 → 验证 → 准备 → 解析 → 初始化。核心机制是双亲委派:加载类时,先让父加载器加载,父加载不了才自己加载。

java
// 双亲委派:Application → Extension → Bootstrap(自下而上委派)
// 好处:保证核心类(如 java.lang.String)不会被篡改——永远由最顶层的 Bootstrap 加载

小结

  • 内存分几块:堆(对象+GC 主战场)、栈(方法调用)、方法区(类信息)、程序计数器
  • 堆分两代:新生代(Eden+Survivor,对象朝生夕死)、老年代(长寿对象)
  • GC 算法:标记-清除(碎片)、复制(新生代)、标记-整理(老年代)
  • 类加载双亲委派:保证核心类不被篡改

想了解 JVM 之上并发是怎么保证线程安全的,看《Java 并发编程》;想了解对象怎么在堆上组织、HashMap 怎么存,看《Java 集合框架详解》。