Java随笔-JVM内存模型

JVM 内存模型(运行时数据区)分为线程共享和线程私有两大部分,上图是 JDK 8+ 版本的完整结构。

一、线程共享区域

1. 堆(Heap)

  • 作用:存放所有对象实例数组
  • 参数控制:-Xms(初始大小)、-Xmx(最大大小)
  • 分代设计
    • 年轻代(Young) :新对象优先分配在 Eden 区,Minor GC 后存活对象进入 Survivor 区(S0/S1 通过复制算法交替使用)
    • 老年代(Old):对象年龄达到阈值(默认 15)或 Survivor 空间不足时晋升,使用标记-清除/整理算法
    • 大对象:超过 -XX:PretenureSizeThreshold 直接进入老年代

2. 元空间(Metaspace)

  • JDK 8+ 替代永久代(PermGen) ,使用本地内存(Native Memory)
  • 存储:类元数据、常量池、字段/方法数据、静态变量
  • 参数:-XX:MaxMetaspaceSize 控制上限
  • OOM 场景:动态生成大量类(如 CGLIB 代理、反射)
  1. 直接内存(Direct Memory)
  • NIO 使用(ByteBuffer.allocateDirect())
  • 通过 Unsafe.allocateMemory() 分配,不受 JVM 堆大小限制
  • 参数:-XX:MaxDirectMemorySize

二、线程私有区域

区域 作用 特点
虚拟机栈(VM Stack) 存储栈帧(局部变量表、操作数栈、动态链接、方法返回地址) 每个方法调用对应一个栈帧;-Xss 控制栈大小
本地方法栈(Native Stack) 为 Native 方法(C/C++)服务 HotSpot 中与虚拟机栈合并实现
程序计数器(PC Register) 记录当前线程执行的字节码行号 唯一不会 OOM 的区域;线程切换时恢复执行位置

三、对象从创建到销毁的完整生命周期

1. 对象生命周期总览

2. 每个阶段的详细流程

① 类加载检查
java 复制代码
Object obj = new Object();  // 触发类加载
步骤 内容
检查类是否已加载 在方法区(元空间)查找该类的类型信息
未加载则触发类加载 加载 → 验证 → 准备 → 解析 → 初始化
检查访问权限 private/protected/public 是否允许当前上下文访问
② 内存分配

这是最关键的一步,JVM 有三种分配策略:

策略 A:堆上分配(最常用)
java 复制代码
// 绝大多数对象在堆上分配
Person p = new Person();  // 在 Eden 区分配

分配流程:

TLAB 详解:

特性 说明
全称 Thread Local Allocation Buffer
作用 每个线程在 Eden 区预先分配一小块私有内存,避免多线程竞争
大小 默认 Eden 的 1%(-XX:TLABWasteTargetPercent
优势 无锁分配,性能极高
溢出 TLAB 放不下时,回退到 Eden 区 CAS 分配
策略 B:栈上分配(逃逸分析优化)
java 复制代码
public void method() {
    Person p = new Person();  // p 不会逃逸出方法
    p.sayHello();
}  // p 随栈帧销毁,无需 GC

逃逸分析(Escape Analysis):

JVM 判断对象是否"逃逸"出方法/线程:

├─ 不逃逸 → 栈上分配(随栈帧弹出自动销毁)

├─ 线程逃逸 → 堆上分配(但可能标量替换)

└─ 全局逃逸 → 必须在堆上分配

标量替换(Scalar Replacement):

java 复制代码
// 如果 Person 对象不逃逸,JVM 可能不创建对象
// 而是直接将其字段拆分为局部变量
Person p = new Person();
p.name = "Tom";   // → 直接变成 String name = "Tom";
p.age = 20;       // → 直接变成 int age = 20;

开启参数:

  • -XX:+DoEscapeAnalysis(JDK 8+ 默认开启)
  • -XX:+EliminateAllocations(标量替换,默认开启)
策略 C:大对象直接进入老年代
java 复制代码
// 超过 -XX:PretenureSizeThreshold 的大数组/对象
byte[] bigArray = new byte[1024 * 1024 * 10];  // 10MB,直接进入老年代
③ 对象初始化

内存分配完成后,JVM 执行三步初始化:

┌─────────────────┐

│ 1. 零值初始化 │ 所有字段设为默认值(int=0, boolean=false, ref=null)

│ 2. 设置对象头 │ 设置 Mark Word、Klass Pointer、数组长度

│ 3. 执行 │ 执行构造方法(字段赋值、代码块、构造函数)

└─────────────────┘

对象头(Object Header)结构:
④ 引用建立
java 复制代码
Person p = new Person();  // p 是栈帧中的局部变量,指向堆中对象

引用关系:

⑤ 对象使用

对象被引用期间,可以被:

  • 读取/修改字段
  • 调用方法
  • 作为参数传递
  • 被其他对象引用
⑥ 引用失效

引用失效的几种情况:

场景 说明
作用域结束 方法执行完毕,栈帧弹出,局部变量引用消失
显式置 null p = null;
重新赋值 p = new Person(); 旧对象引用断开
容器清理 list.remove(obj)map.clear()
弱引用回收 WeakReference 被 GC 时回收
软引用回收 SoftReference 内存不足时回收
虚引用 PhantomReference 仅用于跟踪回收
⑦ 可达性分析(判断垃圾)

JVM 使用 可达性分析算法(Reachability Analysis) ,而非引用计数:

GC Roots(根对象集合):

├─ 虚拟机栈中局部变量引用的对象

├─ 方法区中静态变量引用的对象

├─ 方法区中常量引用的对象

├─ 本地方法栈中 JNI 引用的对象

├─ JVM 内部的引用(基本数据类型对应的 Class 对象)

├─ 所有被同步锁(synchronized)持有的对象

└─ 反映 JVM 内部情况的 JMXBean、JVMTI 中注册的回调等

⑧ GC 回收

根据对象所在区域,触发不同的 GC:

年轻代回收(Minor GC / Young GC)

触发条件:Eden 区空间不足

回收流程:

  1. STW(Stop The World)暂停所有应用线程
  2. 标记 Eden + From Survivor 中的存活对象
  3. 复制存活对象到 To Survivor
  4. 清空 Eden + From Survivor
  5. From 和 To 交换角色
  6. 对象年龄 +1
  7. 年龄达到阈值(默认15)→ 晋升老年代
  8. 恢复应用线程

老年代回收(Major GC / Old GC)

触发条件:

├─ 老年代空间不足

├─ 空间分配担保失败

└─ CMS 的并发标记周期

回收算法:

├─ CMS:标记-清除(并发低停顿,但碎片多)

├─ G1:标记-整理 + 复制(Region 化,可预测停顿)

└─ ZGC/Shenandoah:并发整理(几乎零停顿)

Full GC

触发条件:

├─ 老年代空间不足且无法分配

├─ 元空间不足

├─ System.gc() 调用(建议性)

└─ CMS 的并发模式失败(Concurrent Mode Failure)

特点:回收整个堆(年轻代 + 老年代 + 元空间),停顿时间长

⑨ 内存释放

对象被 GC 回收后:

├─ 内存被标记为空闲

├─ 空闲内存加入空闲列表(Free List)

├─ 下次分配时复用

└─ 长期运行后可能触发内存整理(Compact)消除碎片

内存整理(Compact):

整理前(标记-清除后):

┌────┬────┬────┬────┬────┬────┬────┬────┐

│存活│空闲│存活│空闲│存活│空闲│空闲│存活│ ← 大量碎片

└────┴────┴────┴────┴────┴────┴────┴────┘

整理后(标记-整理):

┌────┬────┬────┬────┬────────────────────┐

│存活│存活│存活│存活│ 连续空闲空间 │ ← 无碎片

└────┴────┴────┴────┴────────────────────┘

3. 完整流程图(文字版)

阶段 核心内容
类加载检查 双亲委派模型、加载→验证→准备→解析→初始化
逃逸分析 栈上分配、标量替换、不逃逸/线程逃逸/全局逃逸
内存分配 TLAB 快速分配、Eden CAS 分配、大对象直接进入老年代
零值初始化 所有字段自动设默认值
设置对象头 Mark Word、Klass Pointer、数组长度
执行构造方法 <init>() 的完整执行顺序
引用建立 栈帧局部变量 → 堆对象,四种引用类型
对象使用 读写字段、调用方法、同步锁
引用失效 作用域结束、置 null、容器清理、弱/软引用回收
可达性分析 GC Roots、两次标记、finalize() 复活机制
GC 回收 Minor GC(复制算法)、Major GC(CMS/G1/ZGC)、Full GC
内存释放 空闲列表、内存整理(Compact)

4. 关键参数汇总

复制代码
| 参数                             | 作用                           |

| ------------------------------ | ---------------------------- |

| -Xms / -Xmx | 堆初始/最大大小 |

| -Xmn / -XX:NewRatio | 年轻代大小 / 年轻代:老年代比例 |

| -XX:SurvivorRatio | Eden : Survivor 比例(默认 8:1:1) |

| -XX:MaxTenuringThreshold | 晋升老年代年龄阈值(默认 15) |

| -XX:PretenureSizeThreshold | 大对象直接进入老年代的阈值 |

| -XX:+UseTLAB | 开启 TLAB(默认开启) |

| -XX:TLABWasteTargetPercent | TLAB 占 Eden 的比例 |

| -XX:+DoEscapeAnalysis | 开启逃逸分析(默认开启) |

| -XX:+EliminateAllocations | 开启标量替换(默认开启) |

| -XX:+UseG1GC / -XX:+UseZGC | 使用 G1 / ZGC 收集器 |

四、关键对比

特性 JDK 7 及以前 JDK 8+
方法区实现 永久代(PermGen),在堆内 元空间(Metaspace),在本地内存
字符串常量池 永久代
静态变量 永久代 元空间
OOM 风险 PermGen space Metaspace

五、常见 OOM 场景

异常类型 发生区域 典型原因
Java heap space 内存泄漏、大对象、堆设置过小
GC overhead limit exceeded 98% 时间用于 GC,回收不足 2% 空间
Metaspace 元空间 动态生成大量类、类加载器未释放
Unable to create new native thread 虚拟机栈 线程数超过系统限制
Direct buffer memory 直接内存 NIO 使用不当、未释放 DirectByteBuffer
相关推荐
是小李呀1 小时前
Java集合扩容机制解析:ArrayList与HashMap底层原理及性能差异
java
SimonKing1 小时前
FreeLLMAPI:24 家厂商、128 个模型、1 个 Key智能切换所有可用的模型
java·后端·程序员
小白学大数据1 小时前
无库无捷径,PyTorch 手写完整 Transformer 大语言模型 LLM
开发语言·pytorch·语言模型·transformer
AI 大模型学习不踩坑1 小时前
Harness 架构原理与工程实践详解
java·人工智能·神经网络·机器学习·计算机视觉·架构
虚心的百褶裙2 小时前
JavaScript性能优化
开发语言·javascript·性能优化
凯瑟琳.奥古斯特2 小时前
二分查找解力扣1011最优运载能力
开发语言·c++·算法·leetcode·职场和发展
深盾科技_Virbox2 小时前
软件交付保护如何从加壳走向代码虚拟化
java·python·安全
学计算机的计算基2 小时前
二叉树算法下篇:递归核心技巧与高频面试题详解
java·笔记·算法
Tim_102 小时前
【C++】014、左值与右值的区别
开发语言·c++