Java核心 - 内存模型

JMM

  • Java内存模型(Java Memory Model,JMM)是Java虚拟机规范中定义的一种抽象模型,用于屏蔽不同硬件平台、操作系统的内存访问差异,实现一致的内存访问效果
  • JMM规定所有的对象变量都存在主内存中,每个线程都有自己的工作内存,保存该线程使用的主内存中变量的副本。线程只能操作工作内存中的变量,且不能访问其他线程的工作内存。线程间变量值的传递均需要通过主内存来完成
  • 多线程共享引入了复杂的数据依赖性,不管编译器、处理器怎么做重排序,都必须尊重数据依赖性的要求,否则就打破了正确性!这就是 JMM所要解决的问题
    JMM模型抽象

主内存

  • 物理内存,对应JVM堆,保存Java运行时绝大部分变量

工作内存

  • JMM提出的抽象概念,对应JVM程序计数器、虚拟机栈、本地方法栈等线程私有内存
  • 原始数据类型(boolean、byte、short、char、int、long、float、double)的局部变量直接保存在虚拟机栈内,不向主内存同步

内存变量操作

  1. Lock(锁定):作用于主内存,标识变量状态为某线程独占
  2. Read(读取):作用于主内存,读取主内存中的变量信息
  3. Load(加载):作用于工作内存,将Read的变量信息从主内存复制到工作内存中
  4. Use(使用):作用于工作内存,执行引擎从工作内存中提取变量的值
  5. Assign(赋值):作用于工作内存,执行引擎将计算后的值重新赋值给工作内存中的变量
  6. Store(存储):作用于工作内存,提取工作内存中的变量信息
  7. Write(写入):作用于主内存,将Store的变量信息从工作内存复制到主内存中
  8. Unlock(解锁):作用于主内存,释放被当前线程标识的变量

内存操作遵循规则

  1. Read & Load、Store & Write操作必须同时出现
  2. Assign操作必须执行(变量在工作内存被更改后必须同步回主内存)
  3. 未执行过Assign操作的变量,不允许同步回主内存
  4. 执行Use前必须执行Load,执行Store前必须执行Assign(Read & Load & Use、Assign & Store
    & Write 为绑定动作)
  5. 变量同时只允许被一个线程Lock,一个线程可以Lock同一变量多次(计数累加),且须执行相同次数的Unlock操作才可解锁(计数累减)
  6. 变量被Lock的同时会清空工作内存中此变量的信息,执行引擎在Use前必须重新Read和Load
  7. 不允许Unlock已被其他线程Lock的变量,Unlock操作必须在Lock操作之后
  8. 执行Unlock之前,必须执行Store和Write操作

运行时数据区

  • 运行时数据区是JVM对JMM的实现,包含堆、方法区、运行时常量池、程序计数器、虚拟机栈、本地方发栈

JVM内存模型

  • 存放对象、垃圾回收的主要回收区域,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/64JVM会保持堆使用率在40% ~ 70%之间
-Xmx / -XX:MaxHeapSize堆内存最大允许大小,默认:物理内存的1/4一般不要大于物理内存的80%
-Xns / -XX:NewSize年轻代内存初始大小
-Xmn / -XX:MaxNewSize年轻代内存最大允许大小
-XX:NewRatio年轻代和老年代的比例,默认:2年轻代(1) : 老年代(2)
-XX:SurvivorRatio年轻代中Survivor区和Eden区的比例,默认:8Survivor(1) : Eden(8)
-XX:PretenureSizeThreshold设置老年代分配阈值,对象大小超过该阈值时直接在老年代中分配,默认:00:不论多大优先分配在Eden中
-XX:MaxTenuringThreshold老年代晋升年龄,默认:15
-Xss每个线程内存大小,默认:1M
-XX:PermSize非堆内存初始大小,一般设置:200M,最大建议:1024MJDK1.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内存屏障

内存屏障指令样例说明
LoadLoadLoad1;Loadload;Load2
StoreStoreStore1;StoreStore;Store2
LoadStoreLoad1;LoadStore;Store2
StoreLoadStore1;StoreLoad;Load2确保Store的数据对Load可见

happen-before

  • Java指令执行顺序约定

现有happen-before

  1. hb(time1,time2)
  2. hb(unlock,lock)
  3. hb(volatile写,volatile读)
  4. 线程 hb(start,do)
  5. 线程 hb(do,isAlive)
  6. 线程 hb(interrupt,exception)
  7. 对象 hb(constract,finalize)
  8. hb(a,b),hb(b,c) > hb(a,c)

Volatile

  • 多线程可见,volatile写 > happen-before > volatile读
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值