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;

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

相关推荐
x***381629 分钟前
springboot和springframework版本依赖关系
java·spring boot·后端
故事不长丨35 分钟前
C#定时器与延时操作的使用
开发语言·c#·.net·线程·定时器·winform
hefaxiang37 分钟前
C语言常见概念(下)
c语言·开发语言
S***84881 小时前
SpringSecurity踢出指定用户
java
p***s911 小时前
Spring数据库原理 之 DataSource
java·数据库·spring
adobehu1 小时前
麒麟系统安装jdk17
java·jdk
欧阳天风1 小时前
js实现鼠标横向滚动
开发语言·前端·javascript
spencer_tseng1 小时前
java.util.IllegalFormatPrecisionException
java·printf
虹科网络安全1 小时前
艾体宝干货 | Redis Java 开发系列#1 从零开始的环境搭建与实践指南
java·数据库·redis
铅笔侠_小龙虾1 小时前
Arthas 命令
java·jvm