类加载过程
- 加载:把一个class文件加载到内存中
- 链接:
- 验证:校验加载进来的class文件是不是符合class文件的标准
- 准备:把class文件静态变量赋默认值,不是赋初始值,(static int i = 8; 此时准备阶段把 i 赋值为 0 )
- 解析:把class文件中常量池里面用到的符号引用转换为直接内存地址,直接可以访问到的内容
- 初始化:静态变量这时候赋值才成为初始值
类加载器
JVM是按需动态加载,采用双亲委派机制
双亲委派:class加入到你自定义的ClassLoader,这时候首先尝试去自定义里面找,他内部维护着缓存,如果加载过就不需要再加载一遍,没加载就会先去他的父加载器,检查是否已经加载,没有就一直往上直到启动类加载器,如果都没有加载过,则从启动类加载器往回(往下)委托加载
为什么用双亲委派?
- 为了安全
Java内存模型
MESI是硬件层面多核cpu缓存一致性的实现(仅限英特尔cpu)
每个缓存行标记四种状态(额外两位):
- M:更改过
- E:独享
- S:共享
- I:无效
现代CPU的数据一致性实现 = 缓存锁(MESI等等各种协议)+总线锁
相关知识
- synchronized
- volatile+cas自旋(无锁优化)
volatile:
- 线程间可见(可见性)
- 禁止指令重排序(有序性)
1.可见性(缓存一致性 —— 硬件上的实现 MESI)
加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个 内存屏障 ,它有三个功能:
- 确保指令重排序时不会把其后面的指令重排到内存屏障之前的位置,也不会把前面的指令排到内存屏障后面,即在执行到内存屏障这句指令时,前面的操作已经全部完成;(A|B : 确保AB顺序不会颠倒)
- 将当前处理器缓存行的数据立即写回系统内存(由volatile先行发生原则 保证(happens-before原则));
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。写回操作时要经过总线传播数据,而每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器要对这个值进行修改的时候,会强制重新从系统内存里把数据读到处理器缓存(也是由volatile先行发生原则保证);
既然CPU有了MESI协议可以保证cache的一致性,那么为什么还需要volatile这个关键词来保证可见性(内存屏障)?或者是只有加了volatile的变量在多核cpu执行的时候才会触发缓存一致性协议?
两个解释结论:
- 多核情况下,所有的cpu操作都会涉及缓存一致性的校验,只不过该协议是弱一致性,不能保证一个线程修改变量后,其他线程立马可见,也就是说虽然其他CPU状态已经置为无效,但是当前CPU可能将数据修改之后又去做其他事情,没有来得及将修改后的变量刷新回主存,而如果此时其他CPU需要使用该变量,则又会从主存中读取到旧的值。而volatile则可以保证可见性,即立即刷新回主存,修改操作和写回操作必须是一个原子操作;
- 正常情况下,系统操作并不会进行缓存一致性的校验,只有变量被volatile修饰了,该变量所在的缓存行才被赋予缓存一致性的校验功能。
2.(有序性)禁止指令重排序
cpu为了提高指令执行效率,会在一条指令执行过程中,去同时执行令一条指令,前提是两条指令没有依赖关系。
禁止指令重排序底层实现:intel lock汇编指令(#lock前缀指令),指令是一个内存屏障,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU
cas:操作的原子性
对象创建过程
- 1.class 加载
- 2.class 链接(验证 -> 准备 -> 解析)
- 3.class 初始化
- 4.申请对象内存
- 5.成员变量赋默认值
- 6.调用构造方法
- 1)成员变量顺序赋值赋初始值
- 2)执行构造方法语句
对象在内存中的存储布局
对象定位方式
- 句柄池(GC的时候效率高)
- 直接指针(找对象效率高)
对象分配过程
运行时数据区
(持续更新中)