JVM
机器语言
一个指令由操作码和操作数组成
方法调用等于一个压栈的过程
- 栈有 BP寄存器 和 SP寄存器来占用空间
- BP -> Base Point 栈基址(栈底)
- SP -> Stack Point 栈顶
字节序用于规定数据在内存单元如何存放,二进制位的高位和低位映射到地址内存的高位和地位
- 高地址放在低地址的前面叫大端序
- 低地址放在高地址的前面叫小端序
//翻译成汇编语言
//b 8位 w 16位 l 32位 q 64位
//sub $4, %esp;
//movl $1, -4(%ebp);
int i = 1;
字节码文件
Magic Number:CAFE BABE
Minor Number:小版本号
Major Number:大版本号
Constant Pool Count:常量池长度
Constant Pool:常量池字节码 -> 常量池长度-1个常量,每个常量为1个字节的标志位+2个字节的实际值
Access Flag:修饰符
This_class 存储常量池的引用
super_class 存储常量池的引用
Interface Count 接口数量
Interfaces 接口,常量池的引用
Fields Count 属性数量
Fields 属性 常量池的引用
Methods Count 方法数量
Methods 方法 常量池引用
Attribute Count
Attribute :code Java的汇编代码
类加载
Loading
-
类加载主要是将.class文件通过二进制字节流的方式读入JVM中。加载阶段JVM需要完成三件事
- 通过classloader将.class文件读入内存
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个该类的java.lang.Class对象,作为方法区这个类各种数据的访问入口
-
类加载器
- Bootstrap(rt.jar、charset.jar等核心类 加载器由c++实现)
- Extension(lib/ext/*.jar)
- Application(classpath/ *.jar… *.class)
- 自定义类加载器,只需要重写findClass方法即可
-
双亲委派
-
优先使用上层加载器进行加载,捕获异常再尝试使用下层加载器
-
优点:
- 有效避免了某些恶意类的加载,安全问题是主要原因
- 每个类只被加载一次,避免了重复加载,提高效率
-
线程的默认加载器是applicationClassLoader
-
-
LazyLoading 五种情况
- –new getstatic putstatic invokestatic指令,访问final变量除外(final变量不需要加载就能读取到)
- –java.lang.reflect对类进行反射调用时
- –初始化子类的时候,父类首先初始化
- –虚拟机启动时,被执行的主类必须初始化
- –动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化
Linking
- Verification 验证
- 验证文件是否符合JVM规定
- Preparation 准备
- 将静态变量在方法区分配内存,并设置默认值
- Resolution 解析
- 将类、方法、属性等符号引用解析为直接引用(将符号引用转换为指向内存地址的指针)
- 常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
Initializing
- 调用类初始化代码
- 父类初始化
- static变量赋初始值/static块赋初始值
小总结:
- load - 默认值 - 初始值
- new - 申请内存 - 默认值 - 初始值
是ldc、iconst、lconst、fconst、dconst还是ldc指令,是根据赋值的大小来进行判定,在编译时进行处理
// 10在哪儿存?
// 在操作数栈中,字节码的指令中 bipush 10
static int a = 10;
// 10在哪儿存?
// 在静态常量池,因为java的指令中没有bfipush,只能复用 ldc #2 + putstatic
// float double 对象等初始化放到常量池中,运行时通过ldc指令,将他们的地址放入操作数栈来操作
static float a = 10.0;
Java执行引擎
混合模式
Java默认使用解释器+编译器组合来执行代码
起始阶段采用解释执行
热点代码会进行编译成本地文件执行
检测热点代码:
- 多次被调用的方法(方法计数器:检测方法执行频率)
- 多次被调用的循环(循环计数器:检测循环执行频率)
- 进行编译
JIT即时编译器
JIT内部包含:中间代码生成器、代码优化器、目标代码生成器、探测分析器。
JIT会将多个字节码文件生成的指令进行打包、优化、编译成本地代码然后放到方法区中,当执行代码时直接执行机器代码or汇编代码即可,不再需要一行行进行解释执行,增加代码执行效率。
可以通过配置参数来指定Java引擎执行模式
- -Xmixed 默认为混合模式 开始解释执行热点代码编译执行
- -Xint 使用解释执行,启动速度很快,执行稍慢
- -Xcomp 使用纯编译模式,执行很快,启动很慢
- 热点代码检测阈值 -XX:CompileThreshold = 10000
JMM
硬件层数据一致性
现代计算机存储器
MESI
协议很多
intel 用MESI(CPU给每个缓存行使用4种状态标记)
- Modified 当前缓存行在当前cpu被修改过
- Exclusive 当前缓存行只在当前cpu中被缓存,为当前cpu独享
- Shared 当前缓存行被多个cpu共享,且在多个cpu中当前缓存行数据一致
- Invalid 当前cpu持有的当前缓存行无效。(被其他cpu修改过)
参考文档:https://www.cnblogs.com/z00377750/p/9180644.html
现代CPU的数据一致性实现 = 缓存锁(MESI …) + 总线锁
缓存行
读取缓存以cache line为基本单位,目前64bytes
位于同一缓存行的两个不同数据,被两个不同CPU锁定,产生互相影响的伪共享问题
伪共享问题:JUC/c_028_FalseSharing
使用缓存行的对齐能够提高效率(填充到64bytes保证不会与其他线程发生缓存行伪共享)
乱序问题
CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系
写操作也可以进行合并(WCBuffer 合并写 是另一种形式的缓存,更新后数据直接发送到L2级别缓存,一般只有4个字节)
参考文档:https://www.cnblogs.com/liushaodong/p/4777308.html
参考代码:JUC/029_WriteCombining
Java中的乱序执行
原始参考:https://preshing.com/20120515/memory-reordering-caught-in-the-act/
public class T04_Disorder {
private static int x = 0, y = 0;
private static int a = 0, b =0;
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() {
//由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
//shortWait(100000);
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();other.start();
one.join();other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
// 理论上如果不发生指令重排是不可能出现x,y都为0的情况
System.err.println(result);
break;
} else {
//System.out.println(result);
}
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
}
如何保证特定情况下不乱序
硬件内存屏障 X86
在内存屏障指令前后的读or写不可乱序执行
sfence: store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence:load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序
JVM级别如何规范(JSR133)
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的实现细节
-
字节码层面
ACC_VOLATILE -
JVM层面
volatile内存区的读写 都加屏障StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
-
OS和硬件层面
参考文章:https://blog.csdn.net/qq_26222859/article/details/52235930
hsdis - HotSpot Dis Assembler
windows lock 指令实现 | MESI实现
synchronized实现细节
- 字节码层面
ACC_SYNCHRONIZED
monitorenter + monitorexit - JVM层面
C C++ 调用了操作系统提供的同步机制 - OS和硬件层面
X86 : lock cmpxchg / xxx
https😕/blog.csdn.net/21aspnet/article/details/88571740
Happens Before原则
Java语言的规范:如果两个操作之间具有happen-before关系,那么前一个操作的结果就会对后面的一个操作可 见。是Java内存模型中定义的两个操作之间的偏序关系。
常用于锁、volatile、传递性、线程启动、线程终止、线程中断
对象内存布局
对象创建过程
- load class
- loading
- linking verification preparation resolution
- intiallizing
- 申请内存空间
- 成员变量赋默认值
- 调用构造方法
- 成员变量赋初始值
- 执行构造语句(先执行super())
对象结构
普通对象
-
对象头 markword 8字节
- 默认开启压缩指针(-XX:+UseCompressedClassPointers) 64位下为4字节 不开启为8字节
-
类指针(指向对象对应的class对象)classpointer
-
实例数据 instance
- 默认开启压缩普通对象指针(-XX:+UseCompressedOops )64位下为4字节 不开启为8字节
- Oops Ordinary Object Pointers 普通对象指针与class对象是不同的压缩对象
-
对齐位数 padding(对齐后整个对象为8个字节的倍数)
数组对象
-
markword 同普通对象
-
classpointer 同普通对象
-
数组长度 4字节
-
数组数据
-
对齐位数 同普通对象
// markword8字节 classpointer4字节(关闭压缩指针则为8字节) padding后 为16字节
System.out.println(ObjectSizeAgent.sizeOf(new Object()));
// markword8字节 classpointer4字节(关闭压缩指针则为8字节)数组长度4字节 padding后为16字节or24字节
System.out.println(ObjectSizeAgent.sizeOf(new int[] {
}));
// markword8字节 classpointer4字节(关闭压缩指针则为8字节)
// 对象引用默认为4字节 padding后为32字节or40字节(压缩指针)
System.out.println(ObjectSizeAgent.sizeOf(new P()));
private static class P {
//8 _markword
//4 _class pointer
int id; //4
String name; //4
int age; //4
byte b1; //1
byte b2; //1
Object o; //4
byte b3; //1
}
markword具体内容
synchronized锁升级->markword变化
- 当一个对象刚开始new出来时,该对象是无锁状态。此时偏向锁位为0,锁标志位01
- 如果调用了对象的hashcode方法且该方法未被重写会System.identityHashCode获取当前hashcode并写入markword
- 如果有线程上锁
- 将markword中的线程Id改为当前线程Id cas操作
- 如果有线程竞争
- 撤销偏向锁,升级为轻量级锁
- 线程在自己的线程栈中生成lock_record,通过CAS操作让markword指向当前线程的lock_record
- 如果竞争加剧
- 有线程超过10次自旋 -XX:PreBlockSpin,或者自旋线程数量超过cpu核数的一半
- 升级为重量级锁,向操作系统升级资源,等待操作系统的线程调度然后映射到用户空间,此时markword指向对象监视器moniter
运行时数据区
方法区+jvm堆是线程共享的 即非线程安全
虚拟机栈、本地方法栈、程序计数器是线程独享的 即线程安全
直接内存 Direct Memory
jvm直接访问内核空间的内存