JVM

类加载-初始化

加载过程

Loading

  1. 双亲委派,主要出于安全来考虑

  2. LazyLoading 五种情况

    1. –new getstatic putstatic invokestatic指令,访问final变量除外

      –java.lang.reflect对类进行反射调用时

      –初始化子类的时候,父类首先初始化

      –虚拟机启动时,被执行的主类必须初始化

      –动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic REF_putstatic REF_invokestatic的方法句柄时,该类必须初始化

  3. ClassLoader的源码

    1. findInCache -> parent.loadClass -> findClass()
  4. 自定义类加载器
    1. extends ClassLoader
    2. overwrite findClass() -> defineClass(byte[] -> Class clazz)
    3. 加密
    4. parent是如何指定的,打破双亲委派
      1. 用super(parent)指定
      2. 双亲委派的打破
        1. 如何打破:重写loadClass()
        2. 何时打破过?
        3. JDK1.2之前,自定义ClassLoader都必须重写loadClass()
        4. ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定
        5. 热启动,热部署
          1. osgi tomcat 都有自己的模块指定classloader(可以加载同一类库的不同版本)
  5. 混合执行 编译执行 解释执行
    1. 检测热点代码:-XX:CompileThreshold = 10000

Linking – 链接

  1. Verification-验证
    1. 验证文件是否符合JVM规定
  2. Preparation-准备
    1. 静态成员变量赋默认值
  3. Resolution-解析
    1. 将类、方法、属性等符号引用解析为直接引用
      常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用

Initializing – 初始化

  1. 调用类初始化代码 ,给静态成员变量赋初始值

例子:

public class T001_ClassLoadingProcedure {
    public static void main(String[] args) {
        System.out.println(T.count);
    }
}
class T {
    public static T t = new T(); //Preparation-> null
    public static int count = 2; //Preparation-> 0
    private T() {
        count ++;
    }
}
/**结果
2
*/
public class T001_ClassLoadingProcedure {
    public static void main(String[] args) {
        System.out.println(T.count);
    }
}
class T {
    public static int count = 2; //Preparation-> 0
        public static T t = new T(); //Preparation-> null
    private T() {
        count ++;
    }
}
/**结果
3
*/

小总结:

  1. load – 默认值 – 初始值

  2. new – 申请内存 – 默认值 – 初始值. (半初始化状态,DCL单例模式)

    public static void main(String[] args) {
           Hello_03 h = new Hello_03();
       }
    //字节码
    0 new #2 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_03>
    3 dup
    4 invokespecial #3 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_03.<init>>
    7 astore_1
    8 aload_1
    9 invokevirtual #4 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_03.m1>
    12 istore_2
    13 return
    
    

JMM

硬件层数据一致性

协议很多

intel 用MESI

https://www.cnblogs.com/z00377750/p/9180644.html

现代CPU的数据一致性实现 = 缓存锁(MESI …) + 总线锁

读取缓存以cache line为基本单位,目前64bytes

位于同一缓存行的两个不同数据,被两个不同CPU锁定,产生互相影响的伪共享问题

伪共享问题:JUC/c_028_FalseSharing

使用缓存行的对齐能够提高效率

private static class Padding {
    public volatile long p1, p2, p3, p4, p5, p6, p7;
}

private static class T extends Padding {
    public volatile long x = 0L;
}

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 < 1000_0000L; i++) {
            arr[0].x = i;
        }
    });

    Thread t2 = new Thread(()->{
        for (long i = 0; i < 1000_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为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系

https://www.cnblogs.com/liushaodong/p/4777308.html

写操作也可以进行合并

https://www.cnblogs.com/liushaodong/p/4777308.html

JUC/029_WriteCombining

public final class WriteCombining {

    private static final int ITERATIONS = Integer.MAX_VALUE;
    private static final int ITEMS = 1 << 24;
    private static final int MASK = ITEMS - 1;

    private static final byte[] arrayA = new byte[ITEMS];
    private static final byte[] arrayB = new byte[ITEMS];
    private static final byte[] arrayC = new byte[ITEMS];
    private static final byte[] arrayD = new byte[ITEMS];
    private static final byte[] arrayE = new byte[ITEMS];
    private static final byte[] arrayF = new byte[ITEMS];

    public static void main(final String[] args) {

        for (int i = 1; i <= 3; i++) {
            System.out.println(i + " SingleLoop duration (ns) = " + runCaseOne());
            System.out.println(i + " SplitLoop  duration (ns) = " + runCaseTwo());
        }
    }

    public static long runCaseOne() {
        long start = System.nanoTime();
        int i = ITERATIONS;

        while (--i != 0) {
            int slot = i & MASK;
            byte b = (byte) i;
            arrayA[slot] = b;
            arrayB[slot] = b;
            arrayC[slot] = b;
            arrayD[slot] = b;
            arrayE[slot] = b;
            arrayF[slot] = b;
        }
        return System.nanoTime() - start;
    }

    public static long runCaseTwo() {
        long start = System.nanoTime();
        int i = ITERATIONS;
        while (--i != 0) {
            int slot = i & MASK;
            byte b = (byte) i;
            arrayA[slot] = b;
            arrayB[slot] = b;
            arrayC[slot] = b;
        }
        i = ITERATIONS;
        while (--i != 0) {
            int slot = i & MASK;
            byte b = (byte) i;
            arrayD[slot] = b;
            arrayE[slot] = b;
            arrayF[slot] = b;
        }
        return System.nanoTime() - start;
    }
}

乱序执行的证明:JVM/jmm/Disorder.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) {
                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

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. 字节码层面
    access_flag: 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. 字节码层面
    方法上加synchronized,是access_flag: ACC_SYNCHRONIZED
    非方法上加synchronized,是monitorenter monitorexit

    void n() {
       synchronized (this) {
    
       }
    }
    
    //字节码
    0 aload_0
    1 dup
    2 astore_1
    3 monitorenter
    4 aload_1
    5 monitorexit
    6 goto 14 (+8)
    9 astore_2
    10 aload_1
    11 monitorexit
    12 aload_2
    13 athrow
    14 return
    
    
  2. JVM层面
    C C++ 调用了操作系统提供的同步机制

  3. OS和硬件层面
    X86 : lock cmpxchg / xxx
    https://blog.csdn.net/21aspnet/article/details/88571740

对象的内存布局

待补充

运行时数据区和指令集

指令集分类

  1. 基于寄存器的指令集
  2. 基于栈的指令集
    Hotspot中的Local Variable Table = JVM中的寄存器

Runtime Data Area

PC 程序计数器

存放指令位置

虚拟机的运行,类似于这样的循环:

while( not end ) {

取PC中的位置,找到对应位置的指令;

执行该指令;

PC ++;

}

JVM Stack

  1. Frame – 每个方法对应一个栈帧
    1. Local Variable Table

      局部变量

    2. Operand Stack

      操作数栈

      对于long的处理(store and load),多数虚拟机的实现都是原子的

      单条指令都可能不是原子性的

      jls 17.7,没必要加volatile

      public class TestIPulsPlus {
        public static void main(String[] args) {
            int i = 8;
            //i = i++;
            i = ++i;
            System.out.println(i);
        }
      //main()字节码
      /**
      0 bipush 8         8压栈
      2 istore_1                   出栈存储到局部变量表1,也就是i
      3 iinc 1 by 1          局部变量表1增加1(局部变量1变成了9)
      6 iload_1                    从局部变量表1拿出来压栈,也就是i=9
      7 istore_1                   出栈存储到局部变量表1,也就是i=9
      8 getstatic #2 <java/lang/System.out>
      11 iload_1
      12 invokevirtual #3 <java/io/PrintStream.println>
      15 return
      */
      }
      
    3. Dynamic Linking

      指向运行时常量池的符号链接

      https://blog.csdn.net/qq_41813060/article/details/88379473
      jvms 2.6.3

    4. return address-返回值地址
      a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方

Heap

Method Area — 可能会导致OOM的

  1. Perm Space (<1.8)
    字符串常量位于PermSpace
    FGC不会清理
    大小启动的时候指定,不能变

  2. Meta Space (>=1.8)

    当使用lambda表达式时,会动态创建class,所以使用不当可能导致oom

    字符串常量位于堆
    会触发FGC清理
    不设定的话,最大就是物理内存

    思考:

    如何证明1.7字符串常量位于Perm,而1.8位于Heap?

    提示:结合GC, 一直创建字符串常量,观察堆,和Metaspace

Runtime Constant Pool

Native Method Stack

Direct Memory

JVM可以直接访问的内核空间的内存 (OS 管理的内存)

NIO , 提高效率,实现zero copy

常用指令

store

load

pop

mul

sub

invoke

  1. InvokeStatic
  2. InvokeVirtual
  3. InvokeInterface
  4. InovkeSpecial
    可以直接定位,不需要多态的方法
    private 方法 , 构造方法
  5. InvokeDynamic
    JVM最难的指令
    lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令
标签:

发表评论

邮箱地址不会被公开。 必填项已用*标注