JVM内存结构、类加载、GC基础速览

一、JVM 五大内存区域

1. 程序计数器

  • 唯一没有OOM的内存区域

  • 核心作用:记录当前线程执行的字节码行号,用于线程切换后恢复执行位置

  • 线程私有,生命周期跟随线程

2. 虚拟机栈(Java栈)

  • 线程私有,每个方法被调用时,都会创建一个独立栈帧

  • 栈帧组成:局部变量表、操作数栈、动态链接、方法出口

  • 异常类型:栈深度超限 → StackOverflowError;内存资源耗尽 → OOM

3. 本地方法栈

  • 结构、特性与虚拟机栈一致,专门为 Native 本地方法 提供运行空间

  • 线程私有,可抛出栈溢出异常、OOM异常

4. 堆 Heap

  • 线程共享,JVM 最大的内存区域

  • 核心作用:存储所有 Java 对象实例、数组,是 GC 垃圾回收的核心区域

  • 内存分区:新生代、老年代,频繁发生 Heap OOM

5. 方法区(元空间)

  • 线程共享

  • 存储内容:类结构信息、运行时常量、静态变量、即时编译代码

  • 版本差异:JDK8 之前为永久代,JDK8 彻底替换为元空间(使用本地物理内存)

  • 存在内存溢出,会触发 OOM

二、对象创建、内存分配、TLAB

1. 对象创建完整流程

  1. 类校验:检查对应类是否已完成加载、解析、初始化

  2. 内存分配:根据堆内存规整度,选择指针碰撞/空闲列表分配内存

  3. 对象头初始化:填充 MarkWord、类型指针等对象头信息

  4. 属性默认初始化:为对象实例变量赋予系统默认初始值

  5. 构造方法初始化:执行构造函数,赋值自定义初始值

  6. 返回当前对象引用

2. 堆内存两种分配方式

  • 指针碰撞:堆内存规整有序,通过移动分界指针完成内存分配,效率高

  • 空闲列表:堆内存存在大量碎片,JVM 维护空闲内存列表,筛选可用空间分配

3. 对象内存分配规则

  • 普通小对象:优先分配在新生代 Eden 区
  • 大对象:直接进入老年代(避免新生代频繁复制)
  • 长期存活对象:达到晋升年龄,从新生代移入老年代

3. TLAB(本地线程分配缓冲)

TLAB 全称 Thread Local Allocation Buffer

  • 定义:Eden区为每个线程单独分配的一小块私有内存区域
  • 目的:解决多线程并发创建对象的锁竞争问题,提升分配效率
  • 原理:线程优先在自己的TLAB内创建对象,无需竞争锁;TLAB空间耗尽再共享Eden区
  • 特点:默认开启,是JVM层面优化并发对象分配的核心手段

三、类加载 7 个阶段

完整顺序:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载

  1. 加载:读取 class 文件二进制数据,载入内存,生成运行时类数据结构

  2. 验证:包含格式、元数据、字节码、符号引用校验,保证 class 文件安全合法

  3. 准备:为类静态变量分配内存,赋予系统默认初始值

  4. 解析:将代码中的符号引用替换为直接内存地址引用

  5. 初始化:执行静态代码块、为静态变量赋予自定义初始值,仅首次主动使用类时触发

  6. 使用:程序正常调用类的属性、方法,执行业务逻辑

  7. 卸载:类无任何有效引用,满足回收条件,被 JVM 卸载销毁

四、双亲委派模型

1. 执行流程

子类加载器接收类加载请求 → 优先向上委托父加载器加载 → 顶层启动类加载器无法完成加载 → 由当前子类加载器自行加载。

2. 核心优势

  • 沙箱安全:防止自定义类篡改JDK核心类库

  • 类唯一性:避免类重复加载,节省内存资源

3. 破坏双亲委派常见场景

  • JDBC SPI 服务提供者加载机制

  • Tomcat 自定义类加载器(实现多应用依赖隔离)

  • 热部署、热修复框架

核心原理:打破向上委派规则,子类加载器优先自主加载类文件

五、GC Roots 可回收判定根节点 🔴

JVM 通过可达性分析判断对象存活,起始节点即为GC Roots,包含五类:

  1. 虚拟机栈中引用的对象(方法局部变量)
  2. 本地方法栈 Native 引用的对象
  3. 方法区中静态属性、常量引用的对象
  4. 系统内所有活跃线程持有的对象
  5. synchronized 同步锁持有的对象

六、三大垃圾回收算法

1. 标记-清除

  • 原理:遍历内存标记所有存活对象,统一清除未标记垃圾对象
  • 优点:算法简单,无需移动对象
  • 缺点:产生大量内存碎片,可能导致大对象分配失败
  • 适用:老年代

2. 标记-复制

  • 原理:将内存划分为两块对等区域,标记存活对象后,复制到空白区域,清空原内存区域
  • 优点:无内存碎片,内存分配效率极高
  • 缺点:永久浪费 50% 内存空间,对象复制存在开销
  • 适用:新生代(对象存活率低)

3. 标记-整理

  • 原理:标记存活对象后,将所有存活对象向内存一端压缩整理,清空末端垃圾内存
  • 优点:无内存碎片,内存利用率高
  • 缺点:需要移动大量存活对象,GC 耗时高、开销大
  • 适用场景:老年代

七、分代回收思想 & 新生代/老年代特性

1. 分代回收核心思想

根据对象存活周期不同,将堆内存分为新生代、老年代,针对不同生命周期对象使用不同GC算法,最大化回收性能、降低STW耗时。

2. 新生代特性

  • 分为:1块Eden区 + 2块Survivor区(S0/S1),比例 8:1:1
  • 对象存活率低、创建销毁频繁
  • 使用标记复制算法
  • 触发 Minor GC,频率高、速度快

3. 老年代特性

  • 存放长期存活对象、大对象
  • 对象存活率极高、极少消亡
  • 使用标记清除/标记整理算法
  • 触发 Major GC/Full GC,频率低、耗时极高

八、对象晋升规则 + 动态年龄判断 + 空间分配担保

1. 标准晋升规则

对象在新生代熬过一次Minor GC,年龄+1;默认年龄达到 15岁,晋升到老年代。

2. 动态年龄判断 🟠

并非必须等到15岁:新生代相同年龄所有对象总大小,大于Survivor区域一半,当前年龄及以上对象,直接全部晋升老年代。

目的:减少 Survivor 区内存占用,避免 Survivor 空间溢出与无序对象晋升

3. 特殊晋升规则

  • 超大对象:直接进入老年代,不经过新生代
  • Minor GC后存活对象大于Survivor剩余空间,直接晋升老年代

默认没有固定 “超大对象” 阈值,必须手动设置。

HotSpot 里的两种 “大对象” 定义。

  • 传统分代(Serial/ParNew + CMS/Old)
    • 控制参数:-XX:PretenureSizeThreshold(单位:字节)
    • 默认值0 → 表示不启用,所有对象先进新生代
    • 含义:对象浅堆大小(对象头 + 实例数据)> 阈值 → 直接分配在老年代
    • 示例-XX:PretenureSizeThreshold=5242880 # 5MB,超过5MB直接进老年代
    • 限制仅 Serial、ParNew 收集器有效;Parallel Scavenge、G1、ZGC 下该参数无效
  • G1 GC 的 Humongous Object(巨型对象)
    • 判定规则对象 > 1/2 个 G1 Region 大小,直接视为巨型对象,分配在老年代的 Humongous 区
    • Region 大小:由 JVM 自动计算,或手动设 -XX:G1HeapRegionSize(1MB~32MB,必须是 2 的幂)
    • 示例:
      • 若 G1 Region = 2MB → 阈值 = 1MB → 大于 1MB 的对象直接进老年代
      • 若 G1 Region = 4MB → 阈值 = 2MB

为什么要 “超大对象直接进老年代”?

  • 避免新生代复制开销:大对象在 Eden→Survivor 来回复制,成本极高
  • 防止 Survivor 溢出:大对象容易占满 Survivor,导致 “Minor GC→放不下→直接晋升老年代” 的混乱局面
  • 减少 Minor GC 次数:分配大对象时不用先占 Eden,避免频繁触发 Young GC

4. 空间分配担保

Minor GC 之前,JVM 先预判:最坏情况新生代所有对象全部存活,能不能塞进老年代?

老年代剩余空间 能装下 → 放心执行 Minor GC;

老年代 装不下 → 不玩复制了,直接触发 Full GC,这就叫空间分配担保

5. 为什么要有「空间分配担保」?

Minor GC 用复制算法,正常流程:

Eden + 活跃 Survivor 存活对象 → 复制到 空闲 Survivor

但有个极端最坏场景:

本次 Minor GC,新生代对象 100% 全部存活

空闲 Survivor 肯定装不下,按规则就得全部直接晋升老年代

如果此时老年代剩余空间也不够

直接往老年代塞就会内存溢出 OOM。

所以 JVM 必须提前担保预判,防止塞不下炸掉。

6. 担保核心规则

Minor GC 触发前 JVM 做判断:

老年代剩余可用空间 > 新生代总容量
✅ 满足:担保成功,放心执行 Minor GC,真放不下再往老年代挪;
❌ 不满足:担保失败,放弃 Minor GC,直接先触发 Full GC 清理老年代,腾空间。

关键点:比的是「老年代剩余空间」和「新生代整个容量」,不是实际存活大小
JVM 保守按最坏全部存活来算。

7.触发担保失败的后果

  1. 不执行本次普通 Minor GC
  2. 直接 Full GC 整理老年代
  3. 回收老年代垃圾,腾出足够空间
  4. 再后续处理新生代对象晋升

九、永久代 & 元空间区别

1. 永久代(JDK1.7及之前)

  • 属于JVM堆内存一部分,受JVM堆参数限制
  • 容易发生永久代OOM
  • 存放类元信息、常量、静态变量

2. 元空间(JDK1.8+)

  • 使用操作系统本地物理内存,不在堆内
  • 默认无固定大小限制,极大减少OOM概率
  • 只存类元数据,静态变量、字符串常量池移入堆内存

3. 核心区别

永久代占用堆内存、容易OOM;元空间使用本地内存,内存充足、稳定性更高。

十、元空间 OOM 场景

元空间虽然使用本地内存,但以下场景依然会出现 Metaspace OOM

  1. 动态频繁生成大量类:热部署、动态代理、CGLIB循环生成代理类
  2. 大量动态脚本执行:Groovy、规则引擎频繁编译脚本生成新类
  3. 循环加载大量外部类:代码中无限循环加载class文件,类无法卸载
  4. 人为限制元空间最大值:配置 MaxMetaspaceSize 过小,内存耗尽
  5. 类加载器不回收:自定义类加载器常驻内存,导致类无法卸载,元数据持续堆积

十一、类卸载

类卸载只发生在:Full GC 时

Minor GC 不会卸载类,只有 Full GC(或 Old GC)才会尝试卸载类。

原因:

  • 类元数据存在 Metaspace(元空间)
  • 元空间回收、类卸载,只在 Full GC 里做

类能被卸载的「三个硬性必要条件」(必须同时满足)

  1. 该类所有实例都已被 GC
  2. 加载该类的 ClassLoader 已被回收 这是最关键一条:
    • Bootstrap、Ext、AppClassLoader 是全局单例 → 永远不会被回收 → 它们加载的类永远不会卸载
    • 只有自定义 ClassLoader 才可能被 GC,从而让类可卸载
  3. 该类的 Class 对象无任何强引用 没有反射、没有静态缓存、没有 ThreadLocal、没有 JNI 全局引用等强引用。

实例没了、加载器没了、Class 对象也没人要了 → Full GC 才会卸

常见会触发类卸载的场景

  1. Web 容器热部署 / 应用重启
    Tomcat 的 WebAppClassLoader 被丢弃 → 旧应用所有类满足条件 → Full GC 时卸载。
  2. OSGi / 插件化框架 卸载插件
    插件 ClassLoader 被释放 → 插件类被卸载。
  3. 动态加载 + 卸载(如脚本引擎、动态代理) 自定义 ClassLoader 加载后不再引用 → 类可被卸载。
  4. Metaspace 接近满,触发 Full GC 为了释放元空间,JVM 会努力卸载可卸载的类。