Java|JavaMemoryModel
//Java 内存模型在设计并发程序的时候至关重要,它规定了不同线程何时以及如何看到其他线程写入共享变量的值,以及在必要时如何同步对共享变量的访问。
需要知道的一点是,Java内存区域模型是一个抽象概念的模型。本身还是基于物理机器物理内存的一部分。
https://javaguide.cn/java/jvm/memory-area.html
内存区域模型总览
以 JDK8 为例,内存区域模型如下:
| 内存区域 | 线程共享or私有 | 存储内容 | 生命周期 |
|---|---|---|---|
| PC | 私有 | 当前线程执行指令行号 | 线程创建时创建,线程结束时销毁 |
| 虚拟机栈 | 私有 | 栈帧包含局部变量(未逃逸对象)、方法返回地址等 | 线程创建时创建,线程结束时销毁 |
| 本地方法栈 | 私有 | 与虚拟机栈类似,区别是本地方法栈服务于native方法 | 线程创建时创建,线程结束时销毁 |
| 堆 | 共享 | 绝大部分对象(逃逸的),字符串常量池,静态变量 | JVM启动时创建,JVM关闭时销毁 |
| 元空间(方法区) | 共享 | 已被虚拟机加载的 类信息、常量、即时编译器编译后的代码,运行时常量池 | JVM启动时创建,JVM关闭时销毁 |
关键内存区域模型详解
堆
堆是JMM中最核心的区域,也是JVM垃圾回收器的重点工作区域,也被称作GC堆
绝大部分程序执行过程中创建的对象都位于堆中
需要注意的是JDK1.7开始引入的逃逸分析优化机制:方法执行过程中如果一个对象未发生逃逸,那么这个对象将在虚拟机栈的栈帧中存储,并且可能会采用标量替换的方式存储。
这类的对象由于未逃逸出方法作用域,因此生命周期和方法完全相同。
方法执行结束后,栈帧弹出,对象自动销毁。
这类的对象不在堆中,自然也不需要进行垃圾回收。
对于堆,重点关注垃圾回收机制(不同垃圾回收算法,对堆进行分代)
为什么需要分代
分代是为了更好地管理堆中的对象,更好地回收内存,更快地分配内存
常见的分代区域
年轻代:Eden,S0, S1
老年代:Tenured
Metaspace
| 内存区域 | 作用 | 对象生命周期 | GC 行为 |
|---|---|---|---|
| 伊甸区(Eden) | 新对象 “出生地” | 刚创建的对象(比如new User())先放这 |
频繁回收(Young GC),速度快 |
| 幸存区(Survivor,分 S0 和 S1) | 伊甸区活下来的对象 “暂住地” | 伊甸区 GC 后没被回收的对象,会移到这里 | 每次 Young GC 后,存活对象在 S0 和 S1 之间移动,次数够多就进老年代 |
| 老年代(Tenured) | 长期存活对象 “养老院” | 幸存区里活了很多次 GC 的对象(比如静态变量引用的对象) | 很少回收(Full GC),速度慢 |
- 程序代码执行
User u1 = new User():u1对象先进入伊甸区; - 伊甸区满了,触发Young GC:没用的对象被回收,
u1如果还被引用(比如u1是类的成员变量),就移到幸存区 S0; - 下次伊甸区满了,再触发 Young GC:S0 里没用的对象被回收,
u1如果还活着,就移到 S1(同时 S0 清空); - 这样来回移动几次到达阈值后(默认 15 次),
u1如果还活着,就会被 “晋升” 到老年代; - 老年代满了,触发Full GC:回收老年代里没用的对象(这个过程很慢,会暂停程序)。
Young GC & Full GC
| 对比维度 | Young GC(年轻代 GC) | Full GC(老年代 GC) |
|---|---|---|
| 触发条件 | Eden区满 | 老年代满 |
| 回收区域 | Eden+幸存区 | 老年代 + 年轻代 + 元空间(少数情况) |
| 执行速度 | 快 | 慢 |
| 对程序影响 | 不大 | 很大 |
| 正常频率 | 高 | 低 |
虚拟机栈
虚拟机栈为线程私有,存储的都是某一个线程执行方法所压入的栈帧。一个方法的调用和结束,对应的就是在虚拟机栈中压入和弹出的过程。
虚拟机栈中的一个栈帧主要包含:
- 局部变量表 (Local Variable Table):存储方法参数和方法内部定义的局部变量(也就是未逃逸的变量)
- 操作数栈 (Operand Stack): 字节码执行引擎工作区。
- 动态连接 (Dynamic Linking): 指向运行时常量池中的符号引用。
- 方法返回地址 (Return Address): 正常返回(遇到
return指令)或异常返回。
方法区
本质上是JVM定义的一个规范,是线程共享的逻辑区域。
具体的实现有两种:永久代(JDK7及以前)和 元空间(JDK8开始)。
这俩的区别就在于,永久代位于JVM堆中,受堆的空间大小限制;而元空间位于堆外的本地内存,只受机器物理内存的限制。
方法区的规范定义了方法区中存储的是:类加载器信息、类结构信息(字段、方法数据)、静态变量、常量、JIT 编译器编译后的代码。
为什么要改成元空间
本质上其实是将原先的”永久代”迁移出了JVM内存,改用本地内存,主要有两点:
- 由于方法区中存储的是类加载器信息以及类结构信息、静态数据等,改用本地内存实现的元空间不再像原先永久代那样固定收到JVM空间的限制,可以存储更多的类相关信息
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低(因为基本上运行过程这些数据都要保证存在,很少会回收)
运行时常量池
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table) 。
由于我们上面说到方法区存储的是类相关的结构信息以及一些常量,所以运行时常量池是属于方法区的一部分的
字符串常量池
设计
字符串常量池的设计是JDK为了优化性能以及减少重复创建字符串导致的内存开销所设计的常量池
1 | |
为什么要移动
在 JDK 1.7及以后,字符串常量池和静态变量 放到了堆中
这是因为原先放在方法区时,由于方法区1.7以前的实现都是永久代,这部分区域的回收频率很低,而且真回收的时候也很慢;这与字符串需要频繁回收的需求相悖。把字符串常量池放入堆中,可以更高效地及时回收字符串
整理
HotSpot区域发展变动
JDK7
字符串常量池以及静态变量 方法区(永久代) –> 堆
JDK8
移除JVM中的永久代
本地内存新增元空间,存储原先永久代剩余的数据(方法区定义的类相关结构数据)