JVM 内存结构、堆细分、对象生命周期、内存模型全解析

我理解 JVM 内存体系,可以从整体结构、线程私有/共享、对象分配路径、永久代/元空间区别 等几个方面去系统化地看。

JVM 的设计实际上兼顾了:线程隔离、垃圾回收效率、方法元数据存储、栈式执行模型等多个目标。

一、JVM 内存整体结构(JDK8 之后)

JDK8 之后的 JVM 内存区域可以分为两大类:
线程私有区域线程共享区域


1️⃣ 线程私有(每个线程独占)

这些区域随着线程创建/销毁:

① 程序计数器(PC寄存器)

  • 保存当前线程执行的字节码指令地址
  • 线程切换后能恢复执行位置
  • 没有 OOM 风险

② Java 虚拟机栈(Stack)

  • 存储局部变量、操作数栈、方法调用链等
  • 每个方法调用对应一个栈帧(Stack Frame)
  • 当栈深度过深会抛出:
    • StackOverflowError
    • OutOfMemoryError: unable to create new native thread

③ 本地方法栈(Native Stack)

  • 用于存储 native 方法调用
  • 与 JVM 栈类似,只是针对 JNI 和本地代码

2️⃣ 线程共享(所有线程共享)

这些区域属于整个 JVM 进程:

① 堆(Heap)

  • JVM 中最大的一块内存
  • 所有对象实例、数组都存放在堆中
  • 垃圾收集的核心区域
  • 会发生 OOM:java.lang.OutOfMemoryError: Java heap space

② 方法区(Method Area,JDK8 之后使用 Metaspace)

  • 主要存储:
    • 类元数据(Class 信息)
    • 方法信息
    • 常量池
    • 静态变量
  • JDK8 之后使用 元空间(Metaspace) 替代永久代

③ 运行时常量池(Runtime Constant Pool)

  • 存放字面量、符号引用、方法引用
  • 属于方法区的一部分

二、永久代(PermGen) vs 元空间(Metaspace)

永久代(JDK7 及之前)

  • 存储类元数据、静态变量、常量池等

  • 内存大小由 JVM 参数控制,如:

    diff 复制代码
    -XX:PermSize
    -XX:MaxPermSize
  • 过多动态类加载会导致:

diff 复制代码
java.lang.OutOfMemoryError: PermGen space

元空间(JDK8+)

  • 类元数据不再存于 JVM 内部,而是存在 本地内存(Native Memory)

  • 默认可以自动增长,减少 OOM 风险

  • 受参数限制:

    diff 复制代码
    -XX:MetaspaceSize
    -XX:MaxMetaspaceSize

设计意图:

  • 永久代经常 OOM,且垃圾回收对它管理不佳
  • 将类元数据移至本地内存后,更加灵活、安全

三、堆内存的细分结构(非常重要)

Java 堆分为两个区域:

sql 复制代码
Java Heap
├── 新生代(Young Generation)
│   ├── Eden(伊甸园)
│   └── Survivor(S0/S1)
└── 老年代(Old Generation)

新生代(Young)

  • 新生分配的对象都在 Eden
  • 当 Eden 填满时触发 Minor GC
  • 存活对象转移到 Survivor 区

Survivor 区采用:

scss 复制代码
S0 与 S1 轮换(复制算法)
一个 Always Empty,一个接收复制对象

老年代(Old)

  • 经多次 GC 仍存活的对象晋升到老年代
  • 老年代使用标记-整理(Mark-Compact) 或 CMS/ G1/ ZGC

四、JVM 中对象的分配过程(HotSpot 为例)

对象分配总体流程如下:

① new 对象 → 先检查栈上是否能分配(逃逸分析)

如果方法内对象没有逃逸到外部:

  • JVM 会直接在栈上分配(Stack Allocation)
  • 无 GC 压力
  • 还会触发锁消除、标量替换等优化

② 大部分对象在 Eden 区分配

  • Eden 内存足够:
    直接在 Eden 分配(使用指针碰撞 Bump-the-pointer)
  • Eden 不够 → 触发 Minor GC

③ Minor GC 之后,存活对象进入 Survivor S0

标记对象 → 拷贝到 S0 → 清空 Eden


④ 多次 Minor GC 后,对象晋升到老年代

晋升条件包括:

  • 对象达到年龄阈值(默认 15)
  • Survivor区放不下(担保机制)

⑤ 大对象可能直接进入老年代

例如大型数组

参数控制:

diff 复制代码
-XX:PretenureSizeThreshold

五、垃圾回收过程(对象生命周期本质上是 GC 过程)

对象在堆中的生命周期本质上是:

复制代码
创建 → 存活 → 晋升 → 回收

新生代 GC(Minor GC)

  • 频繁发生
  • 使用复制算法(清理快)

老年代 GC(Major/Full GC)

  • 较少发生
  • 用标记-整理或 CMS/G1/ZGC
  • 停顿较长

六、JVM 有哪些类加载机制(补充整合)

三种主要方式:

1. 隐式加载

  • new 对象
  • 调用静态方法
  • 调用静态变量
  • 反射

2. 显式加载

  • Class.forName()(会初始化)
  • ClassLoader.loadClass()(只加载不初始化)

3. 自定义类加载器

  • 用于模块隔离、热加载、服务发现

七、对象能否被回收的判断方式(可达性分析 GC Roots)

GC Roots 包括:

  • 栈帧中的局部变量表
  • 方法区中的静态变量
  • JNI 本地方法引用
  • 常量池引用

八、JVM 内存整体总结图(重点)

sql 复制代码
        线程私有
        --------------------------
        | 程序计数器 PC         |
        | Java栈 Stack          |
        | 本地方法栈 Native     |
        --------------------------

        线程共享
        --------------------------
        |      Heap(堆)        |
        |  ├─ Eden             |
        |  ├─ S0 / S1          |
        |  └─ Old              |
        --------------------------
        |  方法区(Metaspace)   |
        |  运行时常量池          |
        --------------------------

九、个人理解总结(面试很加分)

我认为 JVM 的内存结构整个核心目标是:
让对象分配高效(Eden、TLAB)、让 GC 有效分代(新生代+老年代)、让类元数据分离(Metaspace)、让线程隔离执行(栈+PC)

JVM 的设计体现了一个"以 GC 为中心的内存管理哲学",所有区域的划分都是为了提升 GC 和执行效率。


🔥 一句话总总结

JVM 内存由线程私有(PC/Stack/Native Stack)与线程共享(Heap/Metaspace)组成;

堆分为年轻代和老年代,采用分代收集;

对象分配优先 Eden,存活晋升老年代;

元空间替代永久代,不再 OOM 于 PermGen;

类加载遵循生命周期与双亲委派模型。

相关推荐
小二·2 小时前
Java虚拟机(JVM)面试题(51道含答案)
java·开发语言·jvm
无敌最俊朗@2 小时前
03-事务高频面试总结
java·开发语言·jvm
hygge9992 小时前
类加载机制、生命周期、类加载器层次、JVM的类加载方式
java·开发语言·jvm·经验分享·面试
修行者Java2 小时前
JVM 垃圾回收算法的详细介绍
jvm·算法
程序员爱钓鱼2 小时前
Python 编程实战 · 实用工具与库 — Flask 基础入门
后端·python·面试
一 乐2 小时前
海产品销售系统|海鲜商城购物|基于SprinBoot+vue的海鲜商城系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·后端
程序员爱钓鱼2 小时前
Python编程实战 - Python实用工具与库 - 文件批量处理脚本
后端·python·面试
mjhcsp3 小时前
C++ 三分查找:在单调与凸函数中高效定位极值的算法
开发语言·c++·算法
我命由我123453 小时前
Element Plus 组件库 - Select 选择器 value 为 index 时的一些问题
开发语言·前端·javascript·vue.js·html·ecmascript·js