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. 对象创建完整流程
-
类校验:检查对应类是否已完成加载、解析、初始化
-
内存分配:根据堆内存规整度,选择指针碰撞/空闲列表分配内存
-
对象头初始化:填充 MarkWord、类型指针等对象头信息
-
属性默认初始化:为对象实例变量赋予系统默认初始值
-
构造方法初始化:执行构造函数,赋值自定义初始值
-
返回当前对象引用
2. 堆内存两种分配方式
-
指针碰撞:堆内存规整有序,通过移动分界指针完成内存分配,效率高
-
空闲列表:堆内存存在大量碎片,JVM 维护空闲内存列表,筛选可用空间分配
3. 对象内存分配规则
- 普通小对象:优先分配在新生代 Eden 区
- 大对象:直接进入老年代(避免新生代频繁复制)
- 长期存活对象:达到晋升年龄,从新生代移入老年代
3. TLAB(本地线程分配缓冲)
TLAB 全称 Thread Local Allocation Buffer
- 定义:Eden区为每个线程单独分配的一小块私有内存区域
- 目的:解决多线程并发创建对象的锁竞争问题,提升分配效率
- 原理:线程优先在自己的TLAB内创建对象,无需竞争锁;TLAB空间耗尽再共享Eden区
- 特点:默认开启,是JVM层面优化并发对象分配的核心手段
三、类加载 7 个阶段
完整顺序:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
-
加载:读取 class 文件二进制数据,载入内存,生成运行时类数据结构
-
验证:包含格式、元数据、字节码、符号引用校验,保证 class 文件安全合法
-
准备:为类静态变量分配内存,赋予系统默认初始值
-
解析:将代码中的符号引用替换为直接内存地址引用
-
初始化:执行静态代码块、为静态变量赋予自定义初始值,仅首次主动使用类时触发
-
使用:程序正常调用类的属性、方法,执行业务逻辑
-
卸载:类无任何有效引用,满足回收条件,被 JVM 卸载销毁
四、双亲委派模型
1. 执行流程
子类加载器接收类加载请求 → 优先向上委托父加载器加载 → 顶层启动类加载器无法完成加载 → 由当前子类加载器自行加载。
2. 核心优势
-
沙箱安全:防止自定义类篡改JDK核心类库
-
类唯一性:避免类重复加载,节省内存资源
3. 破坏双亲委派常见场景
-
JDBC SPI 服务提供者加载机制
-
Tomcat 自定义类加载器(实现多应用依赖隔离)
-
热部署、热修复框架
核心原理:打破向上委派规则,子类加载器优先自主加载类文件
五、GC Roots 可回收判定根节点 🔴
JVM 通过可达性分析判断对象存活,起始节点即为GC Roots,包含五类:
- 虚拟机栈中引用的对象(方法局部变量)
- 本地方法栈 Native 引用的对象
- 方法区中静态属性、常量引用的对象
- 系统内所有活跃线程持有的对象
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.触发担保失败的后果
- 不执行本次普通 Minor GC
- 直接 Full GC 整理老年代
- 回收老年代垃圾,腾出足够空间
- 再后续处理新生代对象晋升
九、永久代 & 元空间区别
1. 永久代(JDK1.7及之前)
- 属于JVM堆内存一部分,受JVM堆参数限制
- 容易发生永久代OOM
- 存放类元信息、常量、静态变量
2. 元空间(JDK1.8+)
- 使用操作系统本地物理内存,不在堆内
- 默认无固定大小限制,极大减少OOM概率
- 只存类元数据,静态变量、字符串常量池移入堆内存
3. 核心区别
永久代占用堆内存、容易OOM;元空间使用本地内存,内存充足、稳定性更高。
十、元空间 OOM 场景
元空间虽然使用本地内存,但以下场景依然会出现 Metaspace OOM:
- 动态频繁生成大量类:热部署、动态代理、CGLIB循环生成代理类
- 大量动态脚本执行:Groovy、规则引擎频繁编译脚本生成新类
- 循环加载大量外部类:代码中无限循环加载class文件,类无法卸载
- 人为限制元空间最大值:配置 MaxMetaspaceSize 过小,内存耗尽
- 类加载器不回收:自定义类加载器常驻内存,导致类无法卸载,元数据持续堆积
十一、类卸载
类卸载只发生在:Full GC 时
Minor GC 不会卸载类,只有 Full GC(或 Old GC)才会尝试卸载类。
原因:
- 类元数据存在 Metaspace(元空间)
- 元空间回收、类卸载,只在 Full GC 里做
类能被卸载的「三个硬性必要条件」(必须同时满足)
- 该类所有实例都已被 GC
- 加载该类的 ClassLoader 已被回收
这是最关键一条:
- Bootstrap、Ext、AppClassLoader 是全局单例 → 永远不会被回收 → 它们加载的类永远不会卸载
- 只有自定义 ClassLoader 才可能被 GC,从而让类可卸载
- 该类的 Class 对象无任何强引用 没有反射、没有静态缓存、没有 ThreadLocal、没有 JNI 全局引用等强引用。
实例没了、加载器没了、Class 对象也没人要了 → Full GC 才会卸
常见会触发类卸载的场景
- Web 容器热部署 / 应用重启
Tomcat 的 WebAppClassLoader 被丢弃 → 旧应用所有类满足条件 → Full GC 时卸载。 - OSGi / 插件化框架 卸载插件
插件 ClassLoader 被释放 → 插件类被卸载。 - 动态加载 + 卸载(如脚本引擎、动态代理) 自定义 ClassLoader 加载后不再引用 → 类可被卸载。
- Metaspace 接近满,触发 Full GC 为了释放元空间,JVM 会努力卸载可卸载的类。