JVM 内存模型
内存模型
多个线程同时对同一个共享变量进行读写的时候会产生线程安全问题。那为什么CPU不直接操作内存,而要在CPU和内存间加上各种缓存和寄存器等缓冲区呢?因为CPU的运算速度要比内存的读写速度快得多,如果CPU直接操作内存的话势必会花费很长时间等待数据到来,所以缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾。
内存间交互协议
JMM规定了主内存和工作内存间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,这主要包含了下面8个步骤:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
这8个步骤必须符合下述规则:
- 不允许read和load,store和write操作之一单独出现。
- 不允许一个线程丢弃它最近的assign操作。即变量在工作内存中改变了账号必须把变化同步回主内存
- 一个新的变量只允许在主内存中诞生,不允许工作内存直接使用未初始化的变量。
- 一个变量同一时刻只允许一条线程进行lock操作,但同一线程可以lock多次,lock多次之后必须执行同样次数的unlock操作
- 如果对一个变量进行lock操作,那么将会清空工作内存中此变量的值。
- 不允许对未lock的变量进行unlock操作,也不允许unlock一个被其它线程lock的变量
- 如果一个变量执行unlock操作,必须先把此变量同步回主内存中。
指令重排
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性。 编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序。不管怎么重排序,单线程下的执行结果不能被改变,编译器、runtime和处理器都必须遵守as-if-serial语义。
内存屏障
通过插入内存屏障(Memory Barrier)可以阻止特定类型的指令重排。JMM将内存屏障划分为四种:
| :——- | ——- |——–: |
|屏障类型| 示例| 描述|
|LoadLoad Barriers | Load1-LoadLoad-Load2 |Load1数据装载过程要先于Load2及所有后续的数据装载过程|
|StoreStore Barriers | Store1-StoreStore-Store2| Store1刷新数据到内存的过程要先于Strore2及后续所有刷新数据到内存的过程|
|LoadStore Barriers |Load1-LoadStore-Store2| Load1数据装载要先于Strore2及后续所有刷新数据到内存的过程|
|StoreLoad Barriers |Store1-StoreLoad-Load2 |Store1刷新数据到内存的过程要先于Load2及所有后续的数据装载过程|
Java中volatile关键字的实现就是通过内存屏障来完成的。
内存回收
Eden 内存分配
为了方便垃圾回收 ,jvm 将对内存分为新生代,老生带
新生代分为 Eden ,from Survivor, to Survivor 区
其中Eden 和Survivor 区比例默认是 8:1:1 ,参数调整配置 -XX:SurvivorRatio=8
当在Eden区分配内存不足时,会发生minorGC 由于多数对象生命周期很短,minorGC发生频繁
当发生minorGC 时jvm 会根据复制算法 将存活的对象拷贝到另一个未使用的Survivor区如果Survovor 区内存不足
会使用分配担保策略将对象移动到老年代
谈到minorGC 相对的fullGC(majorGC) 是指发生在老年代的GC,不论是效率还是速度都比minorGC慢得多
回收时会发生stop the world 是程序发生停顿
jvm config
https://docs.oracle.com/cd/E40972_01/doc.70/e40973/cnf_jvmgc.htm#autoId2
1 | -server -Xms24G -Xmx24G -XX:PermSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=20 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70 |
javac
- 词法分析
- 语法分析
- 语义分析
字节码生成器
从源代码找出规范化的token流
判断是否复合Java语言规范 形成抽象语法树
将复杂语法转换为简单语法 语法糖处理
生成字节码
jvm 运行时数据区域
方法区
虚拟机栈
本地方法栈
程序计数器 – 没有oom
堆
运行时常量区是方法区的一部分存放编译时生成的字面量和符号引用
对象的创建
- 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一 个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没 有,那必须先执行相应的类加载过程
对象的内存布局
- 示例数据
- 对齐填充
- 对象头 虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找 到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对 象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对 象头会有不同的设置方式
对象的访问定位
引用
java 1.2如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块 内存代表着一个引用
回收方法区
- 判断是否是一个无用的类
- 所有类的实例都已经被回收 , java堆中不存在类的任何实例
- 加载该类的
ClassLoader已经被回收 - 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该 类的方法
CMS 收集器
- 初始标记(CMS initial mark) - Stop The World
- 并发标记(CMS concurrent mark) -
- 重新标记(CMS remark) - Stop The World
- 并发清除(CMS concurrent sweep) -
G1
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
内存分配与回收策略
对象主要分配在新生代的Eden区上 ,少数情况下也可能会直接分配在老年代中 其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟 机中与内存相关的参数的设置
- 对象优先在
Eden分配 当Eden区没有足够空间进行分配时,虚拟 机将发起一次Minor GC