Bootstrap

JVM-从熟悉到精通

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块赋初始值

小总结:

  1. load - 默认值 - 初始值
  2. 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的实现细节
  1. 字节码层面
    ACC_VOLATILE

  2. JVM层面
    volatile内存区的读写 都加屏障

    StoreStoreBarrier

    volatile 写操作

    StoreLoadBarrier

    LoadLoadBarrier

    volatile 读操作

    LoadStoreBarrier

  3. OS和硬件层面
    参考文章:https://blog.csdn.net/qq_26222859/article/details/52235930
    hsdis - HotSpot Dis Assembler
    windows lock 指令实现 | MESI实现

synchronized实现细节
  1. 字节码层面
    ACC_SYNCHRONIZED
    monitorenter + monitorexit
  2. JVM层面
    C C++ 调用了操作系统提供的同步机制
  3. 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变化
  1. 当一个对象刚开始new出来时,该对象是无锁状态。此时偏向锁位为0,锁标志位01
    • 如果调用了对象的hashcode方法且该方法未被重写会System.identityHashCode获取当前hashcode并写入markword
  2. 如果有线程上锁
    • 将markword中的线程Id改为当前线程Id cas操作
  3. 如果有线程竞争
    • 撤销偏向锁,升级为轻量级锁
    • 线程在自己的线程栈中生成lock_record,通过CAS操作让markword指向当前线程的lock_record
  4. 如果竞争加剧
    • 有线程超过10次自旋 -XX:PreBlockSpin,或者自旋线程数量超过cpu核数的一半
    • 升级为重量级锁,向操作系统升级资源,等待操作系统的线程调度然后映射到用户空间,此时markword指向对象监视器moniter

请添加图片描述

请添加图片描述

运行时数据区

方法区+jvm堆是线程共享的 即非线程安全

虚拟机栈、本地方法栈、程序计数器是线程独享的 即线程安全

直接内存 Direct Memory

jvm直接访问内核空间的内存

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;