JVM学习笔记 04、类加载与字节码技术

JVM学习笔记 04、类加载与字节码技术,第1张

文章目录
  • 前言
  • 一、类加载
    • 1.1、java文件、字节码文件
    • 1.2、类加载与字节码技术
    • 1.3、原理分析
      • 多态原理
      • 异常
      • sychronized
  • 二、字节码指令
    • 2.3、编译器处理(语法糖)
      • 2.3.1-2.3.9(精简)
      • 2.3.10、方法重写时的桥接方法
      • 2.3.11、匿名内部类(底层原理、引用常量值)
    • 2.4、类加载阶段
      • 2.4.1、加载阶段
      • 2.4.2、链接
        • 验证
        • 准备
        • 解析
      • 2.4.3、初始化
      • 初始化练习(包装类静态属性、懒惰初始化单例)
    • 2.5、类加载器
      • 2.5.1、启动类加载器
      • 2.5.2、扩展类加载器
      • 2.5.3、双亲委派模式
      • 2.5.4、线程上下文类加载器(JDBC引出该加载器)
        • 2.5.4.1、分析类加载器
        • 2.5.4.2、SPI类加载机制说明(引出线程上下文类加载器)
      • 2.5.5、自定义类加载器(含实 *** )
    • 2.6、运行期优化
      • 2.6.1、即时编译
        • 2.6.1.2、即时编译器
        • 2.6.1.3、逃逸分析(分析)
      • 2.6.2、方法内联
      • 2.6.3、字段优化
        • 2.6.3.1、JMH 基准测试
        • 2.6.3.2、分析
        • 2.6.3.3、数组成员变量添加volatile+方法内联
      • 2.6.4、反射优化
        • 2.6.4.1、案例介绍+源码分析
        • 2.6.4.2、arthas-boot来查看动态生成的方法访问器
        • 2.6.4.3、自定义膨胀阈值以及是否动态生成
  • 参考文章

前言

本篇博客是跟随黑马程序员JVM完整教程,全网超高评价,全程干货不拖沓的学习JVM的笔记,若文章中出现相关问题,请指出!

所有博客文件目录索引:博客目录索引(持续更新)

一、类加载 1.1、java文件、字节码文件

分别为Java文件、字节码文件、反编译文件

package com.changlu;

public class Main {
    public static void main(String[] args) {
        System.out.println("hello,world");
    }
}
cafe babe 0000 0034 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 124c 636f 6d2f 6368
616e 676c 752f 4d61 696e 3b01 0004 6d61
696e 0100 1628 5b4c 6a61 7661 2f6c 616e
672f 5374 7269 6e67 3b29 5601 0004 6172
6773 0100 135b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 0100 0a53 6f75 7263
6546 696c 6501 0009 4d61 696e 2e6a 6176
610c 0007 0008 0700 1c0c 001d 001e 0100
0b68 656c 6c6f 2c77 6f72 6c64 0700 1f0c
0020 0021 0100 1063 6f6d 2f63 6861 6e67
6c75 2f4d 6169 6e01 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 106a 6176
612f 6c61 6e67 2f53 7973 7465 6d01 0003
6f75 7401 0015 4c6a 6176 612f 696f 2f50
7269 6e74 5374 7265 616d 3b01 0013 6a61
7661 2f69 6f2f 5072 696e 7453 7472 6561
6d01 0007 7072 696e 746c 6e01 0015 284c
6a61 7661 2f6c 616e 672f 5374 7269 6e67
3b29 5600 2100 0500 0600 0000 0000 0200
0100 0700 0800 0100 0900 0000 2f00 0100
0100 0000 052a b700 01b1 0000 0002 000a
0000 0006 0001 0000 0009 000b 0000 000c
0001 0000 0005 000c 000d 0000 0009 000e
000f 0001 0009 0000 0037 0002 0001 0000
0009 b200 0212 03b6 0004 b100 0000 0200
0a00 0000 0a00 0200 0000 0b00 0800 0c00
0b00 0000 0c00 0100 0000 0900 1000 1100
0000 0100 1200 0000 0200 13
# java -v xxx.class
D:\workspace\workspace_idea\mavenexer\target\classes\com\changlu>javap -v Main.class
Classfile /D:/workspace/workspace_idea/mavenexer/target/classes/com/changlu/Main.class
  Last modified 2021年11月29日; size 539 bytes
  MD5 checksum 03f2133b550ae36e5c91e58698d6a7c7
  Compiled from "Main.java"
public class com.changlu.Main
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #5                          // com/changlu/Main
  super_class: #6                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello,world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // com/changlu/Main
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/changlu/Main;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Main.java
  #20 = NameAndType        #7:#8          // "":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello,world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/changlu/Main
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public com.changlu.Main();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/changlu/Main;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello,world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 11: 0
        line 12: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "Main.java"


1.2、类加载与字节码技术

1、类文件结构:整个类文件十六进制表示,分别来表示对应部分内容。

  • 是什么?类的文件十六进制形式。意义?通过类文件我们就可以看出类的原始内容信息,若是再懂一点对应的进制表示的含义,我们就可以直接来对字节码文件进行修改来改变类文件的内容!

2、字节码指令:研究字节码分别表示的含义?针对于特定字节码文件!

  • 通过编译器将.java文件编译成.class文件,最终通过jvm来进行转换对应平台的机器指令,最终执行!
  • 字节码文件作为中间码存在的二进制文件,默认我们文件中打开字节码是十六进制其对应标识常量、指令、引用的序列。机器码和字节码的概念与区别
  • 使用javap能够更好的帮助我们来查看字节码文件! javap -v 类.class,-v表示输出详细信息。

*** 作数栈默认是4个字节

java方法执行过程:①常量池载入运行时常量池。②方法字节码载入方法区。③main线程开始运行分配帧栈内存。④最终执行引擎开始执行字节码!

示例:

a++:是先iload,再iinc。++a:是先iinc,再iload

int a = 10;
int b = a++ + ++a + a--;
//a=>11,b=>34

帧栈存储a变量,另外一边是方法区来执行字节码命令:

  1. 对应a++,首先加载10进来,接着帧栈+1(11)。
  2. 对应++a,进行帧栈+1(12),再将12加载进来,此时相加得到22。
  3. 对应a–,加载12进来,接着帧栈-1(11),此时相加得到34。最终a=11

各种指令:

  • 条件判断指令(ifne 是否!=0)、循环控制指令(do、while字节码per会一致)。在jvm中使用goto字节码指令来进行跳转到指定代码位置的!

    • public class Main {
          public static void main(String[] args) {
              int i = 0;
              int x = 0;
              while (i < 10){
                  x = x++; // 禁止出现这类代码! x = x++在字节码层面每次进行赋值都会赋值为0
                  i++;
              }
              System.out.println(x);
          }
      }
      
  • ()V:整个类的构造方法。编译器从上至下收集所有static静态代码块及赋值的代码,合并成一个特殊的方法()v在类加载的初始化阶段被调用。

    • public class Main {
          static int i = 0;
      
          static {
              i = 20;
          }
      
          static {
              i = 30;
          }
      }
      
      static {};
          descriptor: ()V
          flags: (0x0008) ACC_STATIC
          Code:
            stack=1, locals=0, args_size=0
               //首先进行初始化,接着不断的取并进行赋值
               0: iconst_0 
               1: putstatic     #3                  // Field i:I
               4: bipush        20
               6: putstatic     #3                  // Field i:I
               9: bipush        30
              11: putstatic     #3                  // Field i:I
              14: return
            LineNumberTable:
              line 10: 0
              line 13: 4
              line 17: 9
              line 18: 14
      
  • ()V:实例化的构造方法,与()V类似也是从上到下依次来进行形成新的构造方法,原始构造方法内的代码总是在最后。

  • 方法调用:三种字节码指令invokespecial、invokevirtual、invokestatic

    • 构造方法、私有方法(private)、常量方法(final)  =》 由于这些一定是静态绑定不会出现其他重写情况,即为invokespecial
      public方法 => 可以会重写、多态情况,即为invokevirtual,晚绑定的含义
      static => 静态方法,即为invokestatic
      
    • // 指的是在堆中开辟一块空间B并且添加引用到 *** 作数栈
      0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9
      // 复制刚刚开辟的一块空间的引用A
      3: dup  
      //使用引用A来进行初始化 *** 作,结束后会将栈顶清除
      4: invokespecial #3 // Method "":()V
      //将B来进行存储到局部变量引用表中去
      7: astore_1
      
    • // 注意点:对于静态方法的调用尽量不要使用对象实例来进行调用,否则可能会产生出来两条不必要的指令
      // 下面20、21就是不必要的指令,会取出对象实例,由于是调用的静态方法所以会使用popd出
      20: aload_1
      21: pop
      22: invokestatic #7 // Method test4:()V
      25: invokestatic #7 // Method test4:()V
      28: return
      


1.3、原理分析 多态原理

多态的方法存储在一个虚方法表(vtable)中,也就是说真正调用的方法与其对应虚方法表中的有关!

通过对象找到class类,然后通过class类来找到虚方法表之后能够确定某个方法实际对应入口地址,有的是来自于父类有的是来自己的eat方法,此时就能够知道对应对象到底调用的是哪个方法!

重点说明:虚方法表是在类的加载过程中链接阶段就会生成虚方法表,在链接的时候确定每个方法对应的入口地址。

当执行 invokevirtual 指令时,
1. 先通过栈帧中的对象引用找到对象
2. 分析对象头,找到对象的实际 Class
3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
4. 查表得到方法的具体地址
5. 执行方法的字节码

从细微上来看,其效率是不如static的,因为其涉及到运行期间需要动态的进行查找,jvm也会对虚方法表做一定的优化,也有缓存处理,若是某个方法被调用了多次就会进行缓存



异常

针对于异常是如何检测的呢?通过一个Exception table,指定from…to,对应的异常类型。

类型:单个catch、多个catch、multcatch

//单个catch:匹配是否与Exception异常符合或者说是其子类
try {
    i = 10;
} catch (Exception e) {
    i = 20;
}

Exception table:
    from to target type
    2 5 8 Class java/lang/Exception
LocalVariableTable:
    Start Length Slot Name Signature
    9 3 2 e Ljava/lang/Exception;


//多个catch
try {
    i = 10;
} catch (ArithmeticException e) {
    i = 30;
} catch (NullPointerException e) {
    i = 40;
} catch (Exception e) {
    i = 50;
}   

Exception table:
from to target type  //依次根据异常来进行监测
    2 5 8 Class java/lang/ArithmeticException
    2 5 15 Class java/lang/NullPointerException
    2 5 22 Class java/lang/Exception
LineNumberTable: ...
LocalVariableTable: //本地变量表也会存储异常类
    Start Length Slot Name Signature
    9 3 2 e Ljava/lang/ArithmeticException;
    16 3 2 e Ljava/lang/NullPointerException;
    23 3 2 e Ljava/lang/Exception;


//jdk1.7之后,支持multi-catch:多个异常类型的入口都是一致的!
try {
    Method test = Demo3_11_3.class.getMethod("test");
    test.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException |  InvocationTargetException e) {
    e.printStackTrace();
}

Exception table:
from to target type  //注意这三个异常的入口都是一致的!
    0 22 25 Class java/lang/NoSuchMethodException
    0 22 25 Class java/lang/IllegalAccessException
    0 22 25 Class java/lang/reflect/InvocationTargetException
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
    12 10 1 test Ljava/lang/reflect/Method;
    26 4 1 e Ljava/lang/ReflectiveOperationException;
    0 31 0 args [Ljava/lang/String;

finally:可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程。其中没有若是有其他如error异常也会有对应的措施即执行finally中的内容后再次抛出异常!

int i = 0;
try {
    i = 10;
} catch (Exception e) {
    i = 20;
} finally {
    i = 30;
}
//三种情况:①第3段中出现Exception异常,执行finally方法完结束。③针对于第5端中出现Exception异常,执行完finally自动抛出异常。②第三段中出现Error或其他类型异常,执行完finally后自动抛出异常。

Code:
stack=1, locals=4, args_size=1
    0: iconst_0
    1: istore_1 // 0 -> i
    2: bipush 10 // try ①--------------------------------------
    4: istore_1 // 10 -> i |
    5: bipush 30 // ①finally |
    7: istore_1 // 30 -> i |
    8: goto 27 // return -----------------------------------
    11: astore_2 // ②catch Exceptin -> e ----------------------
    12: bipush 20 // |
    14: istore_1 // 20 -> i |
    15: bipush 30 // ②finally |
    17: istore_1 // 30 -> i |
    18: goto 27 // return -----------------------------------
    21: astore_3 // ③catch any -> slot 3 ----------------------
    22: bipush 30 // finally |
    24: istore_1 // 30 -> i |
    25: aload_3 // <- slot 3 |
    26: athrow // ③throw ------------------------------------
    27: return
Exception table:
from to target type
    2 5 11 Class java/lang/Exception
    2 5 21 any // 剩余的异常类型,比如 Error
    11 15 21 any // 剩余的异常类型,比如 Error
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
    12 3 2 e Ljava/lang/Exception;
    0 28 0 args [Ljava/lang/String;
    2 26 1 i I

finally面试题:本质就是入栈的过程,返回值最终返回的是栈顶的值!在finally中进行return会吞掉自动抛出异常!

面试题1:

public class Demo3_12_2 {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }
    public static int test() {
        try {
            return 10;
        } finally {
            return 20;
        }
    }
}
//结果:20

Code:
    stack=1, locals=2, args_size=0
         0: bipush        10   //对于return方法调用时会进行将常量10压入栈中
         2: istore_0
         3: bipush        20   //finally中也是return了,同样将常量20压入栈中
         5: ireturn   //直接将栈顶常量返回
         6: astore_1  //出现异常时执行finally中
         7: bipush        20
         9: ireturn
    Exception table:
    from    to  target type
             0     3     6   any

注意点:在finally中return会吞掉异常,也就是说只要你这个方法中的finally有返回值的 *** 作就也异常也不会抛出异常!

面试题2:只要记住在方法中返回值的本质是将指定要返回的进行入栈 *** 作即可,最终返回的值也是栈顶的值!

public class Main {

    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }
    public static int test() {
        int i = 10;
        try {
            return i;  //这里进行入栈 *** 作,栈顶为10
        } finally {
            i = 20; //将20赋值给i
        }
    }

}
//结果:10


sychronized
public class Main {
    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock){ //将该对象作为同步代码块的锁
            System.out.println("ok");
        }
    }
}

无论是同步代码块中的代码还是外面的代码最终都会走解锁的指定,也就是说都有对应解锁+抛出异常的动作。

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
    0: new #2 // new Object
    3: dup  // *** 作数栈复制一层来进行下面构造器实例化
    4: invokespecial #1 // invokespecial :()V
    7: astore_1 // lock引用 -> lock   将原始new的对象进行引用指向
    8: aload_1 // <- lock (synchronized开始)
    9: dup  //可以看到又复制了一份来进行上锁
    10: astore_2 // lock引用 -> slot 2
    11: monitorenter // monitorenter(lock引用)
    12: getstatic #3 // <- System.out
    15: ldc #4 // <- "ok"
    17: invokevirtual #5 // invokevirtual println:
    (Ljava/lang/String;)V
    20: aload_2 // <- slot 2(lock引用)
    21: monitorexit // monitorexit(lock引用)
    22: goto 30
    25: astore_3 // any -> slot 3
    26: aload_2 // <- slot 2(lock引用)
    27: monitorexit // monitorexit(lock引用)
    28: aload_3
    29: athrow
    30: return
    Exception table:
    from to target type
    12 22 25 any
    25 28 25 any
    LineNumberTable: ...
    LocalVariableTable:
    Start Length Slot Name Signature
    0 31 0 args [Ljava/lang/String;
    8 23 1 lock Ljava/lang/Object;
    StackMapTable: ...
    MethodParameters: ..

注意:方法级别的 synchronized 不会在字节码指令中有所体现。



二、字节码指令 2.3、编译器处理(语法糖) 2.3.1-2.3.9(精简)

1、默认构造器:无参构造器会自动调用super();

2、自动装拆箱:例如int与Integer不需要显示调用方法来进行转换。

3、泛型集合取值(类型擦除):编译成字节码以后,它执行的代码其实已经不区分你list.add()参数的类型了,统一当成Object来进行添加,之后取得时候默认会进行强转来进行取到。

  • 擦除的是字节码上的信息, LocalVariableTypeTable 仍然保留了方法参数泛型的信息。

4、可变参数(String … => String[]):底层依旧会将String… args转为String[]

5、foreach数组:底层是fori进行单个遍历循环;foreach集合,底层就是使用迭代器来进行循环遍历。

6、switch:jdk1.7开始可以传入字符串以及枚举类来进行匹配!

  • 举例若是对字符串进行switch,底层实际上会有两个switch,一个是针对于hashcode,另一个是针对于原始值。这也是为什么不能将null传入switch的原因。
  • 针对枚举类:底层通过一个数组来进行匹配,下标取得枚举类的ordinal()!

7、twr(try with resources):对于资源的关闭我们无需进行手动判断 *** 作,底层会为我们进行生成资源关闭的代码,并且比我们考虑的更周全,对于close()方法出现异常的捕获进行添加异常(该方式叫做压制异常!)我们也可以自己对其进行捕获。

//手动编写
public static void main(String[] args) {
    MyResource myResource = null;
    try {
        myResource = new MyResource();
        int i = 1 / 0;
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        if (myResource != null){
            try {
                myResource.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

//使用twr,我们专注于业务的异常步骤即可
try(MyResource myResource = new MyResource();){
    int i = 1 / 0;
}catch (Exception e){
    e.printStackTrace();
}

8、方法重写时的桥接方法:子类重写父类的方法返回值可以是父类方法返回值的子类。



2.3.10、方法重写时的桥接方法

方法重写时对返回值分两种情况:

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)

示例:

class A {
    public Number m() {
        return 1;
    }
}

class B extends A {
    @Override
// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
    public Integer m() {
        return 2;
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(new B().m());
    }
}

实际编译期间JVM会为我们进行桥接重写方法的实现:

class B extends A {
    
    public Integer m() {
        return 2;
    }
	// 此方法才是真正重写了父类 public Number m() 方法
    public synthetic bridge Number m() {
        // 间接调用 public Integer m()
        return m();
    }
}

验证:jvm给我们生成的合成方法,桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,能够让我们编写的方法重写真正符合规则,我们可以通过反射来进行验证查看

for (Method m : B.class.getDeclaredMethods()) {
    System.out.println(m);
}

//打印信息:
public java.lang.Integer com.changlu.JVM.B.m()
public java.lang.Number com.changlu.JVM.B.m()


2.3.11、匿名内部类(底层原理、引用常量值)

匿名内部类底层原理

源代码:

public class Candy11 {
    public static void main(String[] args) {
        //创建了一个匿名实例
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok");
            }
        };
    }
}

编译转换后代码:

// 额外生成的类:根据`外部类的名称+$+数字`则为匿名内部类的类名
final class Candy11$1 implements Runnable {
    Candy11$1() {
    }
    public void run() {
        System.out.println("ok");
    }
}

public class Candy11 {
    public static void main(String[] args) {
        Runnable runnable = new Candy11$1();//同样也是对某个类进行实例化
    }
}

引用局部变量匿名内部类

源代码:

public class Candy11 {
    public static void test(final int x) {  //常量
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                //这里应当传入静态或者常量,若是只是取值其他实例值也是可以的,但是一旦对该变量进行修改就会报提示错误
                System.out.println("ok:" + x);
            }
        };
    }
}

转换后代码:

// 额外生成的类
final class Candy11$1 implements Runnable {
    int val$x;
    Candy11$1(int x) {  //可以看到对于在匿名内部类中使用外部的变量实际上是通过构造器传入的
        this.val$x = x;
    }
    public void run() {
        System.out.println("ok:" + this.val$x);
    }
}
public class Candy11 {
    public static void test(final int x) {
        Runnable runnable = new Candy11$1(x);
    }
}

注意:这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建Candy11 对象时,将 x 的值赋值给了 Candy11 对象的 val$x 属性,所以 x 不应该再发生变化了,如果变化,那么 val$x 属性没有机会再跟着一起变化。



2.4、类加载阶段

类加载阶段:加载(字节码入方法区) -》链接(验证-安全检查、准备-为static分配空间、解析) -》初始化

2.4.1、加载阶段

一句话:将类的字节码加载到方法区中。

①内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

  • _java_mirror:java镜像,镜像起到桥梁作用,java的对象想要访问class的信息不能直接访问,需要通过这个镜像mirror来访问它。举个例子你若是想要用java对象来访问String.class得先访问镜像对象(java_mirror),接着镜像对象才能够间接去访问instanceKlass,此时才能够间接知道内部的一些属性内容。
  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法表

②如果这个类还有父类没有加载,先加载父类。

③加载和链接可能是交替运行的。

介绍对象、类、镜像类的关系:以一个Person类、Person对象来表示他们的关系。

  • ①类字节码加载到方法区,在JDK8中方法区叫做元空间,占据 *** 作系统中的内存空间。
  • ②class类对象与instanceKlass关系:加载的同时会在Java的堆内存空间中创建一个叫做_java_mirror的对象,这个类对象是在堆中存储的,并且它持有了instanceKlass的指针地址,反过来在元空间中isntanceKlass里的_java_mirror也指向了Person.class的内存地址。
  • ③创建的对象与class类对象关系:之后使用new关键字创建了一些实例对象,实际上每个实例对象都有自己的对象头(16个字节,8个字节对应着该对象的class地址),若是想通过对象获取class信息,其实就会访问这个对象的对象头,首先找到根据class地址找到_java_mirror对象(如图Person.class),接着通过该类对象间接的通过instanceKlass指向找到元空间中存储的instanceKlass。

总结:也就是无论我们是通过类.class还是对象实例的class来获取类对象的一些信息实际上都要从元空间中获取,通过对象实例获取class对象与从类名.class实际上获取的都是堆中的类对象,若是想要获取属性则都是要去访问元空间。

public static void main(String[] args) {
    //方式一:类.class获取
    Class<Main> mainClass = Main.class; 
    System.out.println(mainClass);
    //方式二:类实例.getClass()获取
    Main main = new Main();//class com.changlu.JVM.Main
    Class<? extends Main> aClass = main.getClass();
    System.out.println(aClass);//class com.changlu.JVM.Main
    System.out.println(mainClass.equals(aClass));//true 同一个对象地址
}

注意点:instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中可以通过前面介绍的 HSDB 工具查看。



2.4.2、链接 验证

一句话:验证类是否符合 JVM规范,安全性检查。

我们任意修改 HelloWorld.class 的魔数信息,接着使用java工具进行执行字节码文件:java xxx

  • 修改内容:cafe babe => cafe baba,更改魔数
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 3405691578 in class file Main
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
        at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
        at java.net.URLClassLoader.access0(URLClassLoader.java:74)
        at java.net.URLClassLoader.run(URLClassLoader.java:369)
        at java.net.URLClassLoader.run(URLClassLoader.java:363)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)


准备

为 static 变量分配空间,设置默认值:例如int类型的静态变量会存储四个字节在指定区域并设置默认值

  • 存储位置:static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾(存储在堆中)。
  • 动作:static 变量分配空间和赋值是两个步骤,分配空间在准备阶段(当前阶段)完成,赋值在初始化阶段(在构造方法中)完成。
    • static方法被调用时就会进行初始化 *** 作!
  • 额外情况:
    1. 如果 static 变量是 final 的基本类型以及字符串常量(String),那么编译阶段值就确定了,赋值在准备阶段就已经完成。
    2. 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段()完成。
//源代码
public class Main {
    static int a;//准备阶段:分配空间   初始化节点:赋值
    static int b = 10;//准备阶段:分配空间   初始化节点:赋值
    static final int c = 20;//准备阶段:分配空间、赋值
    static final String d = "hello";//准备阶段:分配空间、赋值
    static final Object o = new Object();//准备阶段:分配空间   初始化节点:赋值

    public static void main(String[] args) {

    }
}

//反编译 javap -v class类名
  static int a;
    descriptor: I
    flags: ACC_STATIC  

  static int b;
    descriptor: I
    flags: ACC_STATIC

  static final int c;
    descriptor: I
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: int 20   //static常量基本类型初始化准备阶段赋值

  static final java.lang.String d;
    descriptor: Ljava/lang/String;
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: String hello   //static字符串常量基本在准备阶段赋值 

  static final java.lang.Object o;
    descriptor: Ljava/lang/Object;
    flags: ACC_STATIC, ACC_FINAL
        
static {};  //类初始化方法过程
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: bipush        10   //赋值变量10
         2: putstatic     #2                  // Field b:I
         5: new           #3   //对象初始化动作    // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."":()V
        12: putstatic     #4                  // Field o:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 10: 0
        line 13: 5


解析

一句话:将常量池中的符号引用解析为直接引用。

  • 符号引用仅仅只是一个符号,其并不知道是类啊、方法也有可能是属性并不知道这些符号所指向的内存的位置,但是经过解析以后变成直接引用,此时就能够确切的知道这个类啊、方法、属性在内存中的位置了。
package cn.itcast.jvm.t3.load;
/**
* 解析的含义
*/
public class Load2 {
    public static void main(String[] args) throws ClassNotFoundException,
    IOException {
        ClassLoader classloader = Load2.class.getClassLoader();
        // loadClass 方法不会导致类的解析和初始化
        Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
        // new C();
        System.in.read();
    }
}
class C {
    D d = new D();
}
class D {
}


2.4.3、初始化

基础点认识

()V 方法:初始化即调用 ()V ,虚拟机会保证这个类的『构造方法』的线程安全。

发生的时机:触发与不触发。

概括得说,类初始化是【懒惰的】,会导致类发生初始化的动作如下:

  1. main 方法所在的类,总会被首先初始化(例如走static{},一些static变量)。
  2. 首次访问这个类的静态变量或静态方法时(只要调用方法就会初始化,无论这个方法返回的是不是常量)。
  3. 子类初始化,如果父类还没初始化,会引发父类先进行初始化。
  4. 子类访问父类的静态变量,只会触发父类的初始化。
  5. Class.forName。
  6. new 会导致初始化

不会导致类初始化的情况:

  1. 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化。
  2. 类对象.class 不会触发初始化。
  3. 创建该类的数组不会触发初始化。
  4. 类加载器的 loadClass 方法。
  5. Class.forName 的参数 2 为 false 时。
  6. ClassLoader.defineClass()

示例

package com.changlu.JVM;

class A {
    static int a = 0;
    static {
        System.out.println("a init");
    }
}
class B extends A {
    final static double b = 5.0;
    static boolean c = false;
    static {
        System.out.println("b init");
    }
}

public class Test {
    static {
        System.out.println("main init");  //main方法所在的类会进行初始化
    }
    public static void main(String[] args) throws ClassNotFoundException {
//        // 1. 静态常量(基本类型和字符串)不会触发初始化
//        System.out.println(B.b);
//        // 2. 类对象.class 不会触发初始化
//        System.out.println(B.class);
//        // 3. 创建该类的数组不会触发初始化
//        System.out.println(new B[0]);
//        // 4. 不会初始化类 B,但会加载 B、A
//        ClassLoader cl = Thread.currentThread().getContextClassLoader();
//        cl.loadClass("com.changlu.JVM.B");
//        // 5. 不会初始化类 B,但会加载 B、A
//        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
//        Class.forName("com.changlu.JVM.B", false, c2);// 若是为true,会执行初始化!
//        // 1. 首次访问这个类的静态变量或静态方法时
//        System.out.println(A.a);
//        // 2. 子类初始化,如果父类还没初始化,会引发父类初始化(先进行)
//        System.out.println(B.c);
//        // 3. 子类访问父类静态变量,只触发父类初始化
//        System.out.println(B.a);
//        // 4. 会初始化类 B,并先初始化类 A
        Class.forName("com.changlu.JVM.B");  //底层走的也是forName0(xx,true,xx)
    }

}


初始化练习(包装类静态属性、懒惰初始化单例)

包装类对象属性

class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;//底层会走Integer.valueOf()实际上是分配了一个对象,此时就会进行初始化

    static {
        System.out.println("E 初始化...");
    }
}

public class Test {

    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);
        System.out.println(E.c);//会进行初始化
    }

}

结论:对于包装类静态常量在获取时会对该类进行初始化,因为其底层走了Integer的指定方法。

典型应用 - 完成懒惰初始化单例模式:借助内部类

特点:①懒惰实例化。②初始化时的线程安全是有保障的。

final class Singleton{
    private Singleton(){}

    static {
        System.out.println("Singleton init ...");
    }

    public static void test(){
        System.out.println("test");
    }

    private static class LazyHolder{//内部类能够调用外部类的私有构造器
        static final Singleton INSTANCE = new Singleton();//静态对象实例存储在一个内部类中

        static {
            System.out.println("LazyHolder init ...");
        }
    }

    public static Singleton getInstance(){
        return LazyHolder.INSTANCE;
    }
}


public class Test {

    public static void main(String[] args) {
//        Singleton.test();//好处(用于测试):调用Singleton静态方法时只会触发Singleton的初始化
        //懒加载Singleton对象实例:只有当真正去获取单例实例时才会对LazyHolder内部类进行初始化
        Singleton.getInstance();
    }

}

用于证明调用Singleton静态方法只会触发Singleton的初始化方法而不会触发LazyHolder初始化:

使用内部类来取到单例对象好处:只有当取单例对象的时候才会对内部类进行初始化



2.5、类加载器

双亲委派类加载模式:每个类加载器各管一块,使用哪个类加载器加载类时首先会向上层进行询问!

名称加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap,显示为 null
Application ClassLoaderclasspath上级为 Extension
自定义类加载器自定义上级为 Application
  • Bootstrap ClassLoader:其中Bootstrap类加载器是由c++写的并由其进行调用,所有我们无法使用java代码来进行调用实现!

  • Application ClassLoader类加载器:classpath指的是加载类文件路径下的class类。

    • public class Test {
      
          public static void main(String[] args) {
              //获取当前类的类加载器
              //sun.misc.Launcher$AppClassLoader@18b4aac2
              System.out.println(Test.class.getClassLoader());
          }
      
      }
      
  • 自定义类加载器:自定义类加载器的路径可以进行自己定义。



2.5.1、启动类加载器

对于当前类路径下的类通过Application ClassLoader类加载器进行加载:

class F {
    static {
        System.out.println("bootstrap F init");
    }
}

public class Main {

    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("com.changlu.JVM.F");
        System.out.println(aClass.getClassLoader());
    }

}

默认加载的话会使用Application ClassLoader类加载器,我们也可以通过设置指令参数将该路径添加到bootclasspath下,之后测试得到null就表明使用的是BootStrap类加载器进行加载:

  • 参数:java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5

  • -Xbootclasspath 表示设置 bootclasspath
    其中 /a:. 表示将当前目录追加至 bootclasspath 之后
    
    可以用这个办法替换核心类
    java -Xbootclasspath:
    java -Xbootclasspath/a:<追加路径>:后追加
    java -Xbootclasspath/p:<追加路径>:前追加,一般使用前追加可以替换掉一些原有的核心类
    

理想效果如下:null则表示使用BootStrap类加载器进行加载

  • 通常研究开发jvm的工程师会经常使用这类命令来进行替换jre/lib下的一些核心类库!


2.5.2、扩展类加载器

扩展类加载器读取JAVA_HOME/jre/lib/ext目录下的jar包文件!

实验:若是某个jar包预先放置到/ext目录下,我们在程序中再次加载同包名的class类,此时会重新加载吗?以及此时它的类加载器是哪一个?

step1:编写一个名为C的类,其包名为com.changlu.JVM,添加初始化static的打印信息

Step2:将该类打成jar包放置到/ext目录下

我们在java目录下使用命令来进行打包:jar -cvf my.jar com\changlu\JVM\C.class

打包完成之后我们将其添加到/ext目录

Step3:添加好之后一旦我们程序运行就会进行加载链接指定目录中的jar包,但是不会进行初始化,我们要做的就是在程序里使用java来进行动态加载另一个同包名的类,看一下此时加载的是哪一个Class类文件以及其对应的类加载器

public class Main {

    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("com.changlu.JVM.C");//使用Class.forName来进行初始化该类
        System.out.println(aClass.getClassLoader());
    }

}

可以看到进行初始化的类文件是我们之前添加到/ext目录下的Class类,而不是我们后面使用程序加载的当前目录下的Class类。

结论:对于启动类加载器、扩展类加载器预先加载编译好的Class类,我们之后是不能够加载同包名的Class类的,使用Class.forName得到的Class对象也只是预先加载的类对象,那么自然其类加载器也就是初始时候进行加载的了,这就是双亲委派机制!



2.5.3、双亲委派模式

双亲委派:简单来说就是加载某个class类,首先会先去委派上级去加载,若是上级没有加载才会让本机的类加载器进行加载。

  • 国内翻译过来是双亲委派机制,而上级委派机制应该更恰当一些。

测试程序:

package com.changlu.JVM;

class CL{
    static {
        System.out.println("Cl init .....");
    }
}

public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = Test.class.getClassLoader();
        Class<?> aClass = classLoader.loadClass("com.changlu.JVM.CL");
        System.out.println(aClass.getClassLoader());
    }
}

源码分析:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查该类是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2. 有上级的话,委派上级 loadClass
                    c = parent.loadClass(name, false);
                } else {
                     3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            if (c == null) {
                long t1 = System.nanoTime();
                // 4. 每一层(扩展层、应用程序层)找不到,调用 findClass 方法(每个类加载器自己扩展)来加载。(若是找不到会抛出异常,在17行捕获)
                c = findClass(name);
                // 5. 记录耗时
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
  2. sun.misc.Launcher$AppClassLoader // 2 处,委派上级
    sun.misc.Launcher$ExtClassLoader.loadClass()
  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader
    查找
  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
  6. sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在
    JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher$AppClassLoader
    的 // 2 处
  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在
    classpath 下查找,找到了


2.5.4、线程上下文类加载器(JDBC引出该加载器) 2.5.4.1、分析类加载器

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?

Class.forName("com.mysql.jdbc.Driver")

源码分析:

package java.sql;

public class DriverManager {
    // 注册驱动的集合
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
        = new CopyOnWriteArrayList<>();
    // 初始化驱动
    static {
        loadInitialDrivers();//在初始化 *** 作中会进行初始化加载驱动
        println("JDBC DriverManager initialized");
    }

先不看别的,看看 DriverManager 的类加载器:其是JDK的核心类库默认由启动类加载器进行加载

System.out.println(DriverManager.class.getClassLoader());//null

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续看loadInitialDrivers()方法:也就是初始化方法,其中包含了两种方式来加载驱动第一种就是SPI,第二种就是

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>
                                                () {
                                                    public String run() {
                                                        return System.getProperty("jdbc.drivers");
                                                    }
                                                });
    } catch (Exception ex) {
        drivers = null;
    }
    // 1)使用 ServiceLoader 机制加载驱动,即 SPI
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            ServiceLoader<Driver> loadedDrivers =
                ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
                // Do nothing
            }
            return null;
        }
    });
    println("DriverManager.initialize: jdbc.drivers = " + drivers);
    // 2)使用 jdbc.drivers 定义的驱动名加载驱动
    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器。也就是说这里可以看到使用应用程序类加载器进行加载
            Class.forName(aDriver, true,
                          ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}
  • 对于方式2:这里其实打破了双亲委派模式,实际上使用应用程序加载器来去加载的sql驱动实现类,否则有些类是找不到的。

针对于SPI加载机制来说明:对于SPI加载也必定会使用应用程序加载器来进行加载。



2.5.4.2、SPI类加载机制说明(引出线程上下文类加载器)

Service Provider Interface (SPI):约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称。

在META-INF目录下有services这个包,包下写上对应的接口类名,该接口类名则作为该文件名的名称,这个文件内容就是这个接口的实现类,只要按照约定来设计这个jar包,之后即可根据接口来找到对应的实现类并加以进行实例化,通过这样的形式能够实现解耦

SPI类加载器使用:

public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        //使用SPI类加载器来进行加载定义好的接口名:java.sql.Driver。
        //      此时内部底层就会使用应用程序加载器来进行加载以指定接口名为文件名中的实现类
        ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
        Iterator<Driver> iterator = load.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());//这里即可打印得到对应的接口驱动实现类
        }
    }
}

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo(对 SPI 进行了扩展)

接着我们来看其ServiceLoader.load方法:

public static <S> ServiceLoader<S> load(Class<S> service) {
    //通过当前线程对象来获取到类加载器就称为线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器。它内部又是由Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类LazyIterator 中:在调用next()的过程中完成实现类的初始化 *** 作

//调用next()方法
public S next() {
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);//此时你可以看到这里底层依旧使用的是Class.forName()来进行类加载
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}


2.5.5、自定义类加载器(含实 *** )

知识点补充

  1. 想加载非 classpath 随意路径中的类文件。
  2. 都是通过接口来使用实现,希望解耦时,常用在框架设计。
  3. 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器。

自定义类加载器实现步骤:

1. 继承 ClassLoader 父类
2. 要遵从双亲委派机制,重写 findClass 方法(核心)
	注意不是重写 loadClass 方法,否则不会走双亲委派机制。这个loadClass是给使用者来进行使用的
3. 读取类文件的字节码
4. 调用父类的 defineClass 方法来加载类
5. 使用者调用该类加载器的 loadClass 方法

实 ***

Step1:首先准备好一个class文件编译成class文件后放置在指定目录用于之后自定义类加载器来进行读取:

public class Test {
    static {
        System.out.println("Test init ....");
    }
}

Step2:自定义类加载器,到指定类加载文件目录下进行读取指定的class文件

class MyClassLoader extends ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "C:\Users\93997\Desktop\新建文件夹\" + name + ".class";
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        try {
            //1、复制Class文件内容到字节数组输出流中
            Files.copy(Paths.get(path), os);
            //从字节数组输出流取得字节数组
            byte[] bytes = os.toByteArray();
            //2、生成Class对象返回:类名、读取的class字节数组、范围长度
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件路径找不到");
        }
    }
}

Step3:使用自定义类加载器进行 *** 作,我们同样来使用loadClass()进行加载指定的Class类文件,其内部会走双亲委派 *** 作,并最终执行findClass方法也就是自定义重写的加载方法

public class Main {

    public static void main(String[] args) throws Exception {
        //结论:同一个类加载器加载两次指定类取得的class类对象相同
        MyClassLoader loader = new MyClassLoader();
        Class<?> aClass = loader.loadClass("Test");
        System.out.println(aClass);//class Test
        System.out.println(aClass.getClassLoader());//com.changlu.JVM.MyClassLoader@65ab7765
        Class<?> aClass2 = loader.loadClass("Test");
        System.out.println(aClass == aClass2);//true

        //结论:使用不同类加载器加载相同的class类文件取得的实例不相同
        //原因:对于加载的类包名、类名、同一个加载器加载它们得到的class对象实例才是一致的,而这里由于类加载器不是同一个则导致最终实例不相同,不同类加载器进行加载时会进行相互隔离。
        //也就是说使用其他类加载器加载的相同类文件,会覆盖原先的类对象!
        //新的类加载器重新加载Test类
        MyClassLoader loader2 = new MyClassLoader();
        Class<?> aClass3 = loader2.loadClass("Test");
        System.out.println(aClass3 == aClass);//false

        //利用反射来尝试创建对象
        aClass3.newInstance();//创建实例则会触发初始化动作,执行初始化内容 => Test init ....
    }
}



2.6、运行期优化 2.6.1、即时编译 2.6.1.2、即时编译器

案例

首先我们来看下创建大量重复的对象锁耗费的时间

public class Main {
    //-XX:+PrintCompilation -XX:-DoEscapeAnalysis。打印编译信息、关闭逃逸分析
    //统计每创建1000个对象所耗费的纳秒数
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 200; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                new Object();
            }
            long end = System.nanoTime();
            System.out.printf("%s\t%s\n", i, (end - start));
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-347PZirv-1651711342384)(https://pictuhttp://www.kaotop.com/file/tupian/20220507/20211211195843.png)]

效果:可以看到当创建到135*1000个对象时,此时创建速度快了100倍,这就与即时编译器相关。


分析

JVM 将执行状态分成了 5 个层次:

  • 0 层,解释执行(Interpreter)
  • 1 层,使用 C1 即时编译器编译执行(不带 profiling)
  • 2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
  • 3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
  • 4 层,使用 C2 即时编译器编译执行
解释(以下是听课记录):在运行期间JVM会为我们的代码做优化

即时编译器与解释器区别:解释器是边解释边执行;即时编译器就是将返回执行的代码变成机器码,之后存储在一个CodeCache缓存中,此时增加了缓存之后要是再要运行就不会进行解释步骤了会直接将编译好的机器码直接进行使用。效率比逐行解释一定会高。	

C1与C2的区别:就是优化程度的不一样,C1的话就是仅仅做一些基本的优化;C2则是更完全彻底的优化。
	C1效率能提升5倍左右;C2能够提升10-100倍。

profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等,其是信息统计 *** 作,在代码运行期间会收集字节码运行状态的数据,进行统计信息。

即时编译器(JIT)与解释器的区别:

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释

  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译

  • 解释器是将字节码解释为针对所有平台都通用的机器码

  • JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来)。



2.6.1.3、逃逸分析(分析)

默认JVM是开启逃逸分析的,就拿2.6.1.2中举例来说,JVM会分析整个new Object()是否在循环外被使用到或者说被其他方法进行引用,若是没有就会采取优化手段:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nx7SgNeu-1651711342384)(https://pictuhttp://www.kaotop.com/file/tupian/20220507/20211211200829.png)]

没有则表示该对象不会逃逸,外界不会用到该对象,既然不会用到那就不必创建它,这也是为什么之后速度这么快的原因,这个逃逸分析是在C2编译器中做的优化,会把对应的创建对象字节码进行替换掉

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NYi0i2RG-1651711342384)(https://pictuhttp://www.kaotop.com/file/tupian/20220507/20211211201157.png)]

关闭逃逸分析vm参数(手动关闭):-XX:+PrintCompilation -XX:-DoEscapeAnalysis,关闭了之后就不会到达C2编译器的逃逸优化阶段了!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dh3B9I7n-1651711342385)(https://pictuhttp://www.kaotop.com/file/tupian/20220507/20211211202207.png)]



2.6.2、方法内联

优化策略(默认开启)

一句话:将函数中的代码拷贝到方法调用者部分去,针对于一些高频及较短函数方法。

private static int square(final int i) {
    return i * i;
}

System.out.println(square(9));

优化点1:如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置。

System.out.println(9 * 9);

优化点2:还能够进行常量折叠(constant folding)的优化,若是某个方法长期取得同一个结果就会执行常量折叠优化

System.out.println(81);

实 *** :

public class Main {
    
    // -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印
    inlining 信息
    // -XX:CompileCommand=dontinline,*JIT2.square 禁止类名中带有JIT2某个方法 inlining
    // -XX:+PrintCompilation 打印编译信息
    public static void main(String[] args) {
        int x = 0;
        for (int i = 0; i < 500; i++) {
            //对1000次方法调用时间进行统计
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                x = square(9);
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\t%d\n", i, x, (end - start));
        }
    }

    private static int square(final int i) {
        return i * i;
    }

}

效果:可以看到到之后791*1000次调用时,其速度已经被优化到0,也就是说进行方法内联两个优化后,压根没有进行方法调用 *** 作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UM3XS6MX-1651711342385)(https://pictuhttp://www.kaotop.com/file/tupian/20220507/20211211203651.png)]

虚拟机参数1:-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining,打印内联信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LnIKkK9R-1651711342385)(https://pictuhttp://www.kaotop.com/file/tupian/20220507/20211211204501.png)]

虚拟机参数2:-XX:CompileCommand=dontinline,*Main.square,禁用类名为含有Main的square方法进行内联(指定类中的指定方法)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GIiEsEiX-1651711342385)(https://pictuhttp://www.kaotop.com/file/tupian/20220507/20211211204708.png)]



2.6.3、字段优化

字段优化:针对于成员变量、静态成员变量的读写 *** 作的优化,对于经常要进行读写的进行缓存优化!

2.6.3.1、JMH 基准测试

笔记-JMH(Java Microbenchmark Harness):包含介绍、注解描述信息

<dependency>
    <groupId>org.openjdk.jmhgroupId>
    <artifactId>jmh-coreartifactId>
    <version>1.9.3version>
dependency>
<dependency>
    <groupId>org.openjdk.jmhgroupId>
    <artifactId>jmh-generator-annprocessartifactId>
    <version>1.9.3version>
    <scope>providedscope>
dependency>

进行基准测试:对三种遍历取数组的方式进行测试

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

@Warmup(iterations = 2, time = 1)   //让jvm进行热身,让jvm对代码进行一系列相关优化
@Measurement(iterations = 5, time = 1)  //进行五轮测试
@State(Scope.Benchmark)
public class Benchmark1 {

    int[] elements = randomInts(1_000);
    
    private static int[] randomInts(int size) {
        Random random = ThreadLocalRandom.current();
        int[] values = new int[size];
        for (int i = 0; i < size; i++) {
            values[i] = random.nextInt();
        }
        return values;
    }
    
    @Benchmark   //将要进行对比测试的
    public void test1() {
        for (int i = 0; i < elements.length; i++) {
            doSum(elements[i]);
        }
    }
    
    @Benchmark
    public void test2() {
        int[] local = this.elements;
        for (int i = 0; i < local.length; i++) {
            doSum(local[i]);
        }
    }
    
    @Benchmark
    public void test3() {
        for (int element : elements) {
            doSum(element);
        }
    }
    static int sum = 0;
    
    @CompilerControl(CompilerControl.Mode.INLINE)  //编译控制该方法进行内联,这里允许方法的内联
    static void doSum(int x) {
        sum += x;
    }
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
            .include(Benchmark1.class.getSimpleName())
            .forks(1)
            .build();
        new Runner(opt).run();
    }
}

我们可以看到使用了方法内联每秒的被调用此时可以达到百万级别,禁用方法内联时调用测试仅仅只有十几二十万次:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4dilJOgV-1651711342385)(https://pictuhttp://www.kaotop.com/file/tupian/20220507/20211211212756.png)]

针对于三种不同的方法取值其底层优化方式不同而导致ops的此时相差较大,在下面2.6.3.2中进行分析。



2.6.3.2、分析
static void doSum(int x) {  //要进行内联优化的代码
    sum += x;
}

@Benchmark   //将要进行对比测试的
public void test1() {   // =>运行期间优化
    for (int i = 0; i < elements.length; i++) {
        doSum(elements[i]);
    }
}

@Benchmark
public void test2() { // =>自己手动优化
    int[] local = this.elements;
    for (int i = 0; i < local.length; i++) {
        doSum(local[i]);
    }
}

@Benchmark
public void test3() {  // =>编译器进行优化 
    for (int element : elements) {
        doSum(element);
    }
}

在刚才的示例中,doSum 方法是否内联会影响 elements 成员变量读取的优化:
如果 doSum 方法内联了,刚才的 test1 方法会被优化成下面的样子(伪代码)

@Benchmark
public void test1() {
    // elements.length 首次读取会缓存起来 -> int[] local
    for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
        sum += elements[i]; // 1000 次取下标 i 的元素 <- local
    }
}

可以节省 1999 次 Field 读取 *** 作,但如果 doSum 方法没有内联,则不会进行上面的优化!

  • 局部变量存储在帧栈中,而静态、成员变量存储在堆中,通过该优化之后存取速度也会提升!

如果自己想要进行优化,尽可能使用局部变量不要使用成员变量以及静态成员变量,忘了也没事当其称为热点代码之后通过方法内联会自动帮你做优化。



2.6.3.3、数组成员变量添加volatile+方法内联
volatile int[] elements = randomInts(1_000);

效果:可以看到方法一的ops大大减少,说明尽管jvm会进行方法内联优化,但是对于volatile修改的成员变量并不会单独额外进行缓存!



2.6.4、反射优化 2.6.4.1、案例介绍+源码分析

下面的方法是对反射方法进行15次调用:

import java.lang.reflect.Method;

/**
 * @ClassName Reflect1
 * @Author ChangLu
 * @Date 2021/12/12 10:51
 * @Description TODO
 */
public class Reflect1 {

    public static void foo(){
        System.out.println("foo...");
    }

    public static void main(String[] args)throws Exception {
        Method foo = Reflect1.class.getMethod("foo");
        for (int i = 0; i <= 16; i++) {
            System.out.println(i);
            foo.invoke(null);//进行反射方法调用
        }
        System.in.read();
    }

}

foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现

@CallerSensitive
public Object invoke(Object obj, Object... args)
    throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, obj, modifiers);
        }
    }
    MethodAccessor ma = methodAccessor;   //方法访问器          // read volatile
    if (ma == null) {
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);//调用方法访问器的invoke方法来进行调用
}

默认的话初始进行反射调用都是走的本地方法访问器访问:

本地方法的性能调用起来比较低,前十五次调用的是本地方法,膨胀阈值为15

调换本地方法访问器为一个运行期间动态生成的新的方法访问器,根据当前调用的方法信息来生成访问器。其无源代码是在运行期间生成的。

public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
    //比较膨胀阈值 ,默认是15次,也就是说第16次的时候会进行反射优化,使用运行期间动态生成的新的方法访问器
    if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
        //该访问器中的invoke方法实际本质就是直接调用方法,能够提升运行效率
        // 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
        MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());//传入关于该类的相关信息
        this.parent.setDelegate(var3);
    }

    // 调用本地实现
    return invoke0(this.method, var1, var2);
}


2.6.4.2、arthas-boot来查看动态生成的方法访问器

对于在16次调用指定的方法反射时,就会在运行期间创建一个方法访问器,该访问器由于是运行期间产生的所以无法在源码中查看,在这里我们使用arthas-boot工具俩进行查看:arthas-boot 使用

  • 下载地址:https://alibaba.github.io/arthas/arthas-boot.jar

首先通过打上断点来查看动态生成的方法访问器的类名信息:

运行案例demo,并让其处在运行过程中:

# 运行arthas-boot的jar包
java -jar arthas-boot.jar

此时会与当前运行的项目进行连接,此时我们与当前运行的项目连接起来了,接下来我们就是要查看当前运行时创建的class类(使用jad):jad sun.reflect.GeneratedMethodAccessor1

下面就是动态生成的Class类,我们可一看到其中反射方法是直接进行类名.方法的调用执行的!

/*
 * Decompiled with CFR.
 *
 * Could not load the following classes:
 *  com.changlu.JVM.Reflect1
 */
package sun.reflect;

import com.changlu.JVM.Reflect1;
import java.lang.reflect.InvocationTargetException;
import sun.reflect.MethodAccessorImpl;

public class GeneratedMethodAccessor1
extends MethodAccessorImpl {
    /*
     * Loose catch block
     */
    public Object invoke(Object object, Object[] objectArray) throws InvocationTargetException {
        // 比较奇葩的做法,如果有参数,那么抛非法参数异常
        block4: {
            if (objectArray == null || objectArray.length == 0) break block4;
            throw new IllegalArgumentException();
        }
        try {
            // 可以看到,已经是直接调用了😱😱😱
            Reflect1.foo();
            // 因为没有返回值
            return null;
        }
        catch (Throwable throwable) {
            throw new InvocationTargetException(throwable);
        }
        catch (ClassCastException | NullPointerException runtimeException) {
            throw new IllegalArgumentException(super.toString());
        }
    }
}

那么本质就是:某个反射方法调用次数达到阈值就会将原本的方法访问器调用反射方法转为正常方法调用以达到优化的效果。



2.6.4.3、自定义膨胀阈值以及是否动态生成

在反射工厂中碰撞阈值是否反射优化的读取是通过环境变量来进行设置的:

设置方式:-Dsun.reflect.inflationThreshold=5 -Dsun.reflect.noInflation=true,前者是膨胀阈值的次数、后者设置为true则表示不会进行反射优化也就是运行期动态生成的方法访问器

  • sun.reflect.noInflation:可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
  • sun.reflect.inflationThreshold:可以修改膨胀阈值


参考文章

[1]. jvm类加载和解析过程(总结性质)

我是长路,感谢你的耐心阅读。如有问题请指出,我会积极采纳!
欢迎关注我的公众号【长路Java】,分享Java学习文章及相关资料
Q群:851968786 我们可以一起探讨学习
注明:转载可,需要附带上文章链接

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/871768.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-13
下一篇 2022-05-13

发表评论

登录后才能评论

评论列表(0条)

保存