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(本地线程分配缓冲)
- 定义: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区内存溢出。
3. 特殊晋升规则
- 超大对象:直接进入老年代,不经过新生代
- Minor GC后存活对象大于Survivor剩余空间,直接晋升老年代
九、永久代 & 元空间区别
1. 永久代(JDK1.7及之前)
- 属于JVM堆内存一部分,受JVM堆参数限制
- 容易发生永久代OOM
- 存放类元信息、常量、静态变量
2. 元空间(JDK1.8+)
- 使用操作系统本地物理内存,不在堆内
- 默认无固定大小限制,极大减少OOM概率
- 只存类元数据,静态变量、字符串常量池移入堆内存
3. 核心区别
永久代占用堆内存、容易OOM;元空间使用本地内存,内存充足、稳定性更高。
十、元空间 OOM 场景
元空间虽然使用本地内存,但以下场景依然会出现 Metaspace OOM:
- 动态频繁生成大量类:热部署、动态代理、CGLIB循环生成代理类
- 大量动态脚本执行:Groovy、规则引擎频繁编译脚本生成新类
- 循环加载大量外部类:代码中无限循环加载class文件,类无法卸载
- 人为限制元空间最大值:配置 MaxMetaspaceSize 过小,内存耗尽
- 类加载器不回收:自定义类加载器常驻内存,导致类无法卸载,元数据持续堆积
七、题纲
- JVM 五大内存区域:组成、线程属性、核心作用、是否会 OOM
- 类加载 7 个阶段及每个阶段核心职责
- 双亲委派流程、优势、破坏场景
- 5 类 GC Roots
- 三种 GC 算法的原理、优缺点、适用内存区域
- 分代回收思想 & 新生代/老年代特性
- 对象晋升规则 + 动态年龄判断
- 永久代 & 元空间区别
- 元空间 OOM 场景