1 硬件层面保证数据一致性
存储器的层次结构:l0、l1等都是存储器,他们有些在cpu内部,有些在cpu外部,cpu使用其内存存储器中的数据,传输速度快,使用外部的传输速度慢 cpu执行读取读指令时,会先读取将该指令需要的数据,数据所在的位置不同,传输给cpu所需要的的时间也不同,它会最先向CPU中的一级缓存(L1 Cache)去寻找相关的数据,一级缓存是与CPU同频运行的,但是由于容量较小,所以不可能每次都命中,也就是没找到数据。这时CPU会继续向下一级的二级缓存(L2 Cache)寻找,同样的道理,当所需要的数据在二级缓存中也没有的话,会继续转向L3 Cache、内存(主存)和硬盘 cpu乱序执行:如果需要的数据恰好不在cpu的L0、L1、L2甚至L3中,那么cpu必须要通过内存总线到主存中读取,这个速度要远低于cpu处理指令的速度,此时cpu不会等待数据传输回来,而是会继续执行其他的与之前未处理完的指令无关的那些指令,这就是导致cpu乱序执行的根源 合并写(不重要)
简单解释:由于ALU速度太快,为了提高写的效率,CPU在写入L1同时,写入一个4字节的WC Buffers,满了之后直接更新到L2 详细解释:cpu执存储指令时,它会首先试图将数据写到离cpu最近的L1,如果此时cpu出现L1未命中,则会访问下一级缓存L2甚至L3,但L2的速度就已经比cpu慢20倍,又会造成cpu等待缓存的情况,因此现代cpu采取合并写技术,当L1未命中时,cpu会先写入一个64bit的缓冲区,注意不是缓存区,这个缓冲区允许cpu在从中读取,或写入数据的同时,执行其他操作,缓解了cpu写数据时cache miss时的性能影响。当后续的写操作需要修改相同的缓存行时,这些缓冲区在将后续的写操作提交到L2缓存之前,可以进行缓冲区写合并,缓冲区的数据会在某个延时的时刻更新到外部的缓存(L2_cache),如果我们可以在缓冲区被传输到L2之前能够填补这些缓冲区(buffers ),那么我们将大大提高传输总线的效率。对于intel的cpu,可以用于合并写的只有4个字节 MESI缓存一致性协议
当从主存读取数据到cpu时,由于一个电脑有多个cpu,所以主存会为每个cpu都传一份数据,那么如果两个cpu都对这个数据进行了修改,会产生数据不一致 对于老cpu,cpu会将数据总线锁住,保证同时只有一个cpu可以访问这个数据,这种方式叫做总线锁,效率低 对于新cpu,每种cpu解决问题采用的协议不同,Intel采用MESI缓存一致性协议,是缓存锁的实现之一 所谓缓存一致性协议,就是给每个缓存行,做一个标记,表示这个缓存行状态,通过这些状态,让各cpu内缓存,保持一致
M:Modified,被修改过 E:Exclusive,独享的,即该缓存行只传给了一个cpu S:Shared,共享的,该缓存行传给了多个cpu I:Invalid,无效的,一个cpu中缓存行被改为M后,其他cpu内缓存行标记为I 有些无法被缓存的数据,或跨越多个缓存行的数据,依然必须使用总线锁 现代cpu的数据一致性实现 = 缓存锁(MESI)+总线锁 缓存行:可以简单的理解为CPU缓存中的最小缓存单位,大小为64字节,内存和高速缓存之间或高速缓存之间的数据移动不是以单个字节完成的,而是以缓存行为最小数据单元。例如当读取一个int型数据时,不会只将这4个字节读入到cpu,而是将其后面整个64字节,称为一个缓存行,一起读入 伪共享:位于同一缓存行的两个不同数据,被两个不同cpu锁定,产生相互影响的问题,称为伪共享,例如cpu1上有线程1,该线程1只需要使用变量x,而cpu2上的线程2,只需要变量y,但由于xy在同一缓存行,当cpu1读入x时,会把y也读入,cpu2读y时,x也被读入,那么当cpu1中x修改,虽然cpu2根本不用x,但由于缓存一致性协议,发现整个缓存行变为了Invalid,因此也需要从内存中重新读取。伪共享问题,可以通过缓存行对齐来解决,从而提升效率。
package com. mashibing. jvm. c2_classloader;
public class T02_CacheLinePadding {
private static class T {
public volatile long p1, p2, p3, p4, p5, p6, p7;
public volatile long x = 0 L;
public volatile long p8, p9, p10, p11, p12, p13, p14;
}
public static T[ ] arr = new T [ 2 ] ;
static {
arr[ 0 ] = new T ( ) ;
arr[ 1 ] = new T ( ) ;
}
public static void main ( String[ ] args) throws Exception {
Thread t1 = new Thread ( ( ) - > {
for ( long i = 0 ; i < 10000 _0000L; i++ ) {
arr[ 0 ] . x = i;
}
} ) ;
Thread t2 = new Thread ( ( ) - > {
for ( long i = 0 ; i < 10000 _0000L; i++ ) {
arr[ 1 ] . x = i;
}
} ) ;
final long start = System. nanoTime ( ) ;
t1. start ( ) ;
t2. start ( ) ;
t1. join ( ) ;
t2. join ( ) ;
System. out. println ( ( System. nanoTime ( ) - start) / 100 _0000) ;
}
}
cpu乱序执行的证明:证明方法有问题
public class T04_Disorder {
private static int x = 0 , y = 0 ;
private static int a = 0 , b = 0 ;
static Object obj1 = new Object ( ) ;
static Object obj2 = new Object ( ) ;
public static void main ( String[ ] args) throws InterruptedException {
int i = 0 ;
for ( ; ; ) {
i++ ;
x = 0 ;
y = 0 ;
a = 0 ;
b = 0 ;
Thread one = new Thread ( new Runnable ( ) {
public void run ( ) {
a = 1 ;
synchronized ( obj1) {
x = b;
}
}
} ) ;
Thread other = new Thread ( new Runnable ( ) {
public void run ( ) {
b = 1 ;
synchronized ( obj2) {
y = a;
}
}
} ) ;
one. start ( ) ;
other. start ( ) ;
one. join ( ) ;
other. join ( ) ;
String result = "第" + i + "次 (" + x + "," + y + ")" ;
if ( x == 0 && y == 0 ) {
System. err. println ( result) ;
break ;
} else {
}
}
}
public static void shortWait ( long interval) {
long start = System. nanoTime ( ) ;
long end;
do {
end = System. nanoTime ( ) ;
} while ( start + interval >= end) ;
}
}
cpu层面保证有序性:为两条不能重排序的指令中间加内存屏障来实现有序性,而内存屏障在intel生产的cpu中,是通过原语(sfence、lfence、mfence)或总线锁(lock)实现 sfence:fence为屏障,s为save,表示存屏障,表示屏障两侧的写指令不允许重排 lfence:l为load,读屏障,lfence两侧的读指令,不允许重排 mfence:屏障两侧的读写操作,都不允许重排 lock[指令1]的语义
会加一个总线锁,其他CPU对内存的读写都会被阻塞 指令1执行完毕后,将缓存中的数据写回内存 禁止该指令与之前和之后的读和写指令重排序 对volatile修饰的变量进行写操作时,其实就是lock[写操作] JVM层面保证有序性:JVM制定了一些规范来保证有序性,但LoadLoad等屏障的底层实际上是通过cpu的lock指令实现的。Load为读,Store为写
LoadLoad:Load1;LoadLoad;Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕 StoreStore:Store1;StoreStore;Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见 LoadStore:Load1;LoadStore;Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕 StoreLoad:Store1;StoreLoad;Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见 volatile的实现方式
字节码层面(Jclasslib查看):ACC_VOLATILE JVM层面:由于volatile写完才能volatile读,因此volatile读之前没必要再加StoreLoad,同理volatile读之后才能volatile写,因此volatile写之前不用再加LoadStore
StoreStore–对volatile变量执行写操作–StoreLoad
如果对一个volatile变量进行写操作,那么该写操作,不能与其前面的任何写操作互换,也不能与后面任何读操作互换 volatile变量的写操作,可以与前面的读操作换位置,因为之前无论怎么读,都不会改变我写入volatile的结果,但如果与其前面的写操作更换位置,就有可能影响到写入的值,例如int x=3;int volatilevar = x+5;一旦x=3这个写操作和volatile修饰的volatilevar变量的写操作互换位置,就会导致,volatilevar得到的不是最新的值 LoadLoad–对volatile变量执行读操作–LoadStore OS(操作系统)和硬件层面:使用hsdis工具(hotspot的反汇编),就是观察虚拟机编译好的字节码在cpu级别用什么汇编指令完成的
lock汇编指令实现,具体指令为为lock addl,addl表示向esp这个寄存器上,加一个0,而加0对寄存器没有任何作用,所以该lock指令主要作用就是lock 这是因为只有intel的cpu有sfence、lfence、mfence这三个原语,而java虚拟机可能跑在不同cpu上,因此只好使用了一个对大部分cpu都通用的lock指令来实现 synchronized实现细节
字节码层面:
同步方法:ACC_SYNCHRONIZED 同步代码块:monitorenter(进入同步块),monitorexit(退出同步块) JVM层面:
C、C++调用了操作系统级别的同步机制 OS和硬件层面
X86:lock指令 happens-before原则:JVM规定的重排序必须遵守的规则 as if serial:不管如何重排序,单线程执行结果一定不会变,只有在多线程中,重排序才会造成结果混乱
2 对象的内存布局
2.1 对象的创建过程
加载.class文件
loading linking initializing 创建对象
为对象分配内存 成员变量默认值 调用构造器
成员变量顺序赋初始值 执行构造器中语句
2.2 对象在内存中的存储布局
与虚拟机的实现、虚拟机的设置有关 观察虚拟机配置
C: \Users\含低调> java - XX: + PrintCommandLineFlags - version
- XX: InitialHeapSize= 261755840 - XX: MaxHeapSize= 4188093440 - XX: + PrintCommandLineFlags - XX: + UseCompressedClassPointers - XX: + UseCompressedOops - XX: - UseLargePagesIndividualAllocation - XX: + UseParallelGC
java version "1.8.0_221"
Java ( TM) SE Runtime Environment ( build 1.8 .0 _221- b11)
Java HotSpot ( TM) 64 - Bit Server VM ( build 25.221 - b11, mixed mode)
普通对象
对象头:Hotspot中叫markword,8位,用于标记对象状态 ClassPointer指针:指向该对象属于的类的Class对象,-XX:+UseCompressedClassPointers开启为4字节,不开启为8字节 实例数据
基本类型:直接存值 引用类型:存放其引用的对象的地址,-XX:+UseCompressedOops开启为4位,不开启为8位
Oops:ordinary object pointers Padding对齐:将整个对象的大小,对齐为8的倍数,因为对64位操作系统,是按块读而不是按接读,直接读取8的倍数个字节,速度反而更快 数组对象
对象头:markword,8位 ClassPointer指针 数组长度:4字节 数组数据 对齐成8的倍数
2.3 获取对象的大小
javaagent:主要用于在加载.class文件之前做拦截,对字节码进行修改 创建包含指定方法签名的premain方法的类
import java. lang. instrument. Instrumentation;
public class ObjectSizeAgent {
private static Instrumentation inst;
public static void premain ( String agentArgs, Instrumentation _inst) {
inst = _inst;
}
public static long sizeOf ( Object o) {
return inst. getObjectSize ( o) ;
}
}
src下建立文件夹META-INF,其内建立文件MANIFEST.MF,注意Premain-Class必须独自放一行,注意写完intellij不能报错
Premain- Class: ObjectSizeAgent
将整个项目打成一个jar包 新建项目,导入该jar包,并创建测试类
public class T03_SizeOfAnObject {
public static void main ( String[ ] args) {
System. out. println ( ObjectSizeAgent. sizeOf ( new Object ( ) ) ) ;
System. out. println ( ObjectSizeAgent. sizeOf ( new int [ ] { } ) ) ;
System. out. println ( ObjectSizeAgent. sizeOf ( new P ( ) ) ) ;
}
private static class P {
int id;
String name;
int age;
byte b1;
byte b2;
Object o;
byte b3;
}
}
配值jvm启动参数,增加代理
- javaagent: E: \IdeaProjects\untitled1\out\artifacts\untitled1_jar\untitled1. jar
执行
2.4 Hotspot开启内存压缩的规则(64位机 )
4G以下:无需压缩 4G - 32G:默认开启内存压缩 ClassPointers Oops 32G以上:压缩无效,使用64位
2.5 对象头具体包括的内容
对象状态不同,虚拟机位数不同,其内容格式不同,图中为64位Hotspot对象头中的内容 unsed:未利用 hashcode:对象哈希值,只有调用对象的hashCode方法,才放入
如果重写了hashCode,存放重写的方法的返回值的 如果没重写,存放根据对象的不同状态,算出的一个hashCode值,也称为identityHashCode,可以用System.identityHashCode(Object x)得到该值 当一个对象计算过identityHashCode,就无法进入偏向锁状态是对的,因为偏向锁前几位要记录线程id,此时没法记录了 age:分代年龄,也就是被回收了多少次,4字节最大值为15,所以年轻代到老年代的gc年龄为15 thread:线程ID ptr_to_lock_record:指向栈中锁记录的指针 ptr_to_heavyweight_monitor:指向重量级锁的指针
2.6 对象如何定位
句柄池:相当于t先指向一个内存,这个内存分别存放两个地址,一个地址指向对象,另一个地址指向这个对象属于的Class对象,访问效率低,垃圾回收时效率高 直接指针:直接指向对象,Hotspot用的就是这种