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
相关推荐
小羊没烦恼!6 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
俊昭喜喜里6 天前
java中的继承和多态的区别
java
小羊没烦恼!6 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
譕痕6 天前
JSONObject与JSONArray封装数据格式区别
java·json
胡写代码6 天前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖6 天前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时6 天前
01-06-A-JVM排查实战详解
java·jvm
伞伞悦读6 天前
【第38期】Python 模块与包详解:import、from、模块搜索路径、包结构和 __init__
开发语言·python
vipxieliang6 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot
C语言小火车6 天前
C/C++ 为什么需要编译器?
开发语言·c++