JMM
- Java内存模型(Java Memory Model,JMM)是Java虚拟机规范中定义的一种抽象模型,用于屏蔽不同硬件平台、操作系统的内存访问差异,实现一致的内存访问效果
- JMM规定所有的对象变量都存在主内存中,每个线程都有自己的工作内存,保存该线程使用的主内存中变量的副本。线程只能操作工作内存中的变量,且不能访问其他线程的工作内存。线程间变量值的传递均需要通过主内存来完成
- 多线程共享引入了复杂的数据依赖性,不管编译器、处理器怎么做重排序,都必须尊重数据依赖性的要求,否则就打破了正确性!这就是 JMM所要解决的问题
主内存
- 物理内存,对应JVM堆,保存Java运行时绝大部分变量
工作内存
- JMM提出的抽象概念,对应JVM程序计数器、虚拟机栈、本地方法栈等线程私有内存
- 原始数据类型(boolean、byte、short、char、int、long、float、double)的局部变量直接保存在虚拟机栈内,不向主内存同步
内存变量操作
- Lock(锁定):作用于主内存,标识变量状态为某线程独占
- Read(读取):作用于主内存,读取主内存中的变量信息
- Load(加载):作用于工作内存,将Read的变量信息从主内存复制到工作内存中
- Use(使用):作用于工作内存,执行引擎从工作内存中提取变量的值
- Assign(赋值):作用于工作内存,执行引擎将计算后的值重新赋值给工作内存中的变量
- Store(存储):作用于工作内存,提取工作内存中的变量信息
- Write(写入):作用于主内存,将Store的变量信息从工作内存复制到主内存中
- Unlock(解锁):作用于主内存,释放被当前线程标识的变量
内存操作遵循规则
- Read & Load、Store & Write操作必须同时出现
- Assign操作必须执行(变量在工作内存被更改后必须同步回主内存)
- 未执行过Assign操作的变量,不允许同步回主内存
- 执行Use前必须执行Load,执行Store前必须执行Assign(Read & Load & Use、Assign & Store
& Write 为绑定动作) - 变量同时只允许被一个线程Lock,一个线程可以Lock同一变量多次(计数累加),且须执行相同次数的Unlock操作才可解锁(计数累减)
- 变量被Lock的同时会清空工作内存中此变量的信息,执行引擎在Use前必须重新Read和Load
- 不允许Unlock已被其他线程Lock的变量,Unlock操作必须在Lock操作之后
- 执行Unlock之前,必须执行Store和Write操作
运行时数据区
- 运行时数据区是JVM对JMM的实现,包含堆、方法区、运行时常量池、程序计数器、虚拟机栈、本地方发栈
堆
-
存放对象、垃圾回收的主要回收区域,JDK1.8前堆包含堆内内存(年轻代、老年代)、堆外内存(永久代)
-
JDK1.8后永久代被移除,原永久代数据记录在元数据区
年轻代、老年代的默认比例为1:2
年轻代
-
由Eden和2个Survivor组成,新创建的对象存在在Eden区
-
Survivor逻辑上分为from、to,主要用于垃圾回收时的复制(Eden + Survivor from => Survivor to)
Eden、Survivor的默认比例为8:1:1
老年代
- 年轻代多次GC都未被回收的对象,会晋升至老年代
常用参数
参数 | 描述 | 备注 |
---|---|---|
-Xms | 堆内存初始大小,默认:物理内存的1/64 | JVM会保持堆使用率在40% ~ 70%之间 |
-Xmx / -XX:MaxHeapSize | 堆内存最大允许大小,默认:物理内存的1/4 | 一般不要大于物理内存的80% |
-Xns / -XX:NewSize | 年轻代内存初始大小 | |
-Xmn / -XX:MaxNewSize | 年轻代内存最大允许大小 | |
-XX:NewRatio | 年轻代和老年代的比例,默认:2 | 年轻代(1) : 老年代(2) |
-XX:SurvivorRatio | 年轻代中Survivor区和Eden区的比例,默认:8 | Survivor(1) : Eden(8) |
-XX:PretenureSizeThreshold | 设置老年代分配阈值,对象大小超过该阈值时直接在老年代中分配,默认:0 | 0:不论多大优先分配在Eden中 |
-XX:MaxTenuringThreshold | 老年代晋升年龄,默认:15 | |
-Xss | 每个线程内存大小,默认:1M | |
-XX:PermSize | 非堆内存初始大小,一般设置:200M,最大建议:1024M | JDK1.8后被移除 |
-XX:MaxPermSize | 非堆内存最大允许大小 | JDK1.8后被移除 |
方法区(元数据区,Metaspace)
- 方法区与堆一样,是所有线程共享的内存区域。存放被虚拟机加载的类信息、常量、静态变量等
- 直接使用本地内存,通过配置设置初始空间(MetaspaceSize)、最大使用空间(MaxMetaspaceSize)
运行时常量池
存放编译期间生成的字面量、符号引用
常用参数
参数 | 描述 |
---|---|
-XX:MetaspaceSize | 元数据区初始化空间,12 ~ 20MB之间浮动,可使用-XX:+PrintFlagsInitial查看 |
-XX:MaxMetaspaceSize | 元数据区最大使用空间 |
-XX:MinMetaspaceFreeRatio | 元数据区最小空闲比,小于此值触发扩容,默认:40% |
-XX:MaxMetaspaceFreeRatio | 元数据区最大空闲比,大于此值触发缩容(释放空间),默认:70% |
-XX:MaxMetaspaceExpansion | 最大扩容空间,默认:5MB |
-XX:MinMetaspaceExpansion | 最小扩容空间,默认:330KB |
程序计数器
- 记录正在执行的当前方法的jvm信息
虚拟机栈
- 记录局部变量表,操作数,动态链接,方法正常或异常退出的定义
本地方法栈
- 记录本地方法执行的局部信息
重排序
- 代码顺序不一定是指令执行顺序,编译器和CPU在保证输出结果一致的情况下会对指令进行重排序
- 尽可能的利用多核CPU的并行能力,使性能得到优化
内存屏障
- 重排序后指令的执行顺序与代码的编写顺序并不一致,在核场景下会造成逻辑偏离问题。在指令中插入内存屏障,可通知编译器和CPU不对内存屏障前后的指令进行重排序,确保操作执行的顺序、数据的可见性
- Java内存屏障主要有Load和Store两类。Load Barrier在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。Store Barrier在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。
- Java内存模型屏蔽了底层硬件平台内存屏障的差异,由JVM来为不同的平台生成相应的内存屏障机器码
Java内存屏障
内存屏障 | 指令样例 | 说明 |
---|---|---|
LoadLoad | Load1;Loadload;Load2 | |
StoreStore | Store1;StoreStore;Store2 | |
LoadStore | Load1;LoadStore;Store2 | |
StoreLoad | Store1;StoreLoad;Load2 | 确保Store的数据对Load可见 |
happen-before
- Java指令执行顺序约定
现有happen-before
- hb(time1,time2)
- hb(unlock,lock)
- hb(volatile写,volatile读)
- 线程 hb(start,do)
- 线程 hb(do,isAlive)
- 线程 hb(interrupt,exception)
- 对象 hb(constract,finalize)
- hb(a,b),hb(b,c) > hb(a,c)
Volatile
- 多线程可见,volatile写 > happen-before > volatile读