一、类加载机制深度解析

一、类加载运行的全过程

1.1 类加载器初始化的过程

假如现在有一个java类 com.jvm.Math类,里面有一个main方法

 public class Math {
 public static final int initData = 666;
 public static User user = new User();

 public int compute() { //一个方法对应一块栈帧内存区域
 int a = 1;
 int b = 2;
 int c = (a + b) * 10;
 return c;
}

 public static void main(String[] args) {
 Math math = new Math();
 math.compute();
 }

}

        这个方法很简单,通常我们直接执行main方法就OK,可以运行程序了,那么点击运行main方法,整个过程是如何被加载运行的呢?为什么点击执行main方法就能得到结果呢?

首先可以看看Java命令执行代码的大体流程(宏观流程)如下:

其中loadClass的类加载器过程有如下几步:

加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

        加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

        验证:校验字节码文件的正确性

        准备:给类的静态变量分配内存,并赋予默认值

        解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用(后续会讲动态链接)

        初始化:对类的静态变量初始化为指定的值,执行静态代码块

        使用 :程序代码执行时使用,new出对象程序中使用。

        卸载 :程序代码退出、异常、结束等,执行垃圾回收。

备注:

1.windows系统上的java启动程序是java.exe,mac系统是java。

2.c语言部分只了解,java部分才是需要掌握部分。

第一步:java调用底层的jvm.dll文件创建java虚拟机(这一步由C++实现),这里java.exe是c++写的代码,调用jvm.dll也是c++底层的一个函数,通过调用jvm.dll文件(dll文件相当于java的jar包),会创建java虚拟机,java虚拟机的启动都是c++程序实现的。

第二步:在启动虚拟机的过程中,会创建一个引导类加载器的实例,这个引导类的加载器是C语言实现的,然后jvm虚拟机就启动起来了。

第三步:接下类,c++会调用java的启动程序,刚刚只是创建了java虚拟机,java虚拟机里面还有很多启动程序,其中有一个叫做Launcher类,该类的全称是sun.misc.Launcher,通过启动这个java类,会由这个类引导加载器加载并创建很多其他的类加载器,而这些加载器才是真正启动并加载磁盘上的字节码文件。

第四步:真正去加载本地磁盘的字节码文件,然后启动执行main方法。(这一步后面会详细说,到底怎么加载本地磁盘的字节码文件)

第五步:main方法执行完毕后,引导类加载器会发起一个c++调用,销毁JVM

以上是启动一个main方法加载的全过程

下面, 我们重点来看一下, 我们的类com.jvm.Math是怎么被加载到java虚拟机里面去的?

1.2 类的加载时机

        1、创建类的实例,也就是new一个对象

        2、访问某个类或者接口的静态变量,或者对该静态变量赋值

        3、调用类的静态方法

        4、反射(Class.forName("com.jvm.Math"))

        5、初始化一个类的子类(会首先初始化子类的父类)

        5、JVM启动实际标明的启动类,及文件名和类名相同的那个类

1.3 类加载的过程

继续看上面的com.jvm.Math类最终会生成class字节码文件,字节码文件是怎么被加载器加载到JVM虚拟机的呢?

类加载器有五步,加载, 验证, 准备, 解析, 初始化. 那么这五步都是干什么的呢?我们来看一下

我们的类在哪里呢?在磁盘里(比如:target文件夹下的class文件),我们先要将class类加载到内存中,加载到内存区域以后,不是简简单单的转换成二进制字节码文件,他会经过一系列的过程,比如:验证、准备、解析、初始化等。把这一系列的信息转变成内元信息,放到内存里面去,我们来看看具体的过程

 第一步:加载

加载阶段主要查找并加载类的二进制数据文件。在该阶段,虚拟机需要完成以下3件事情:

        (1)通过一个类的全限定名来获取定义此类的二进制字节流。

        (2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

        (3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

第一步是验证字节码。

第二步:验证

验证字节码加载是否正确,比如:打开一个字节码文件。打眼一看感觉像是乱码,实际上不是的,其实这里面每一个字符都是有对应的含义,那么文件里面的内容我们能不能替换呢?当然是不能的,一旦替换,这个class文件就不能执行成功了。

验证的是什么呢?

验证字节码加载是否正确、格式是否正确、内容是否符合java虚拟机的规范。

 第三步:准备

验证完了,接下来是准备操作,比如我们的类Math,他首先会给Math里面的静态变量赋值一个初始值。

准备阶段主要为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中分配

该阶段有两点需要注意:

        (1)首先,这时候进行内存分配的仅包括类的静态变量(static),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。

        (2)其次,这里所设置的初始值通常是数据类型默认的初始值,而不是被在Java代码中被赋予的值。这里还需要注意如下几点:

  •         对基本数据类型来说,对于类的静态变量(static)和全局变量,会为其赋予默认值,而对于局部变量来说,在使用前必须显示代码中的赋值,否则编译时不通过。
  •         对于同时被static和final修饰的常量,必须在声明的时候就为其显示代码中的赋值,否则编译时不通过;而只被final修饰的常量则即可以在声明时显示其赋值,也可以在类类初始化时显示赋值,总之在使用前必须赋值,系统不会为其赋予默认值。
  •         对于引用数据类型来说,如数组引用,对象引用等,如果没有对其进行显示赋值而直接使用,系统都会为其赋予默认值,即为null
  •        如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认值。

如果类字段的字段属性表中存在ConstantValue属性(同时被final和static修饰),即在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,假设变量value被定义为:

    public static final int value= 123;
    public static int initData = 666;
    public static User user = new User();

 编译时javac就会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue设置将value赋值为123.而initData就会赋值为0

    public static final int value= 123;
    public static int initData = 0;
    public static User user = null;

在准备过程中,就会给这个两个变量赋初始值,这个初始值并不是代码中真实的值,比如initData属性是int类型的,那么他的初始值是0,如果是boolean类型的初始值为false,对象就会赋值为null,也就是说,准备阶段的值是jvm固定的,不是我们定义的值。如果一个final的常量, 比如public static final String name="zhangsan", 是直接赋初始值"zhangsan"的. 这里只是给静态变量赋初始值

需要注意的有以下几点:

  • 实例变量是在创建对象的时候完成赋值的,没有赋初值一说
  • final和static修饰的常量在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步

第四步:解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量;直接引用就是直接指向目标的指针、相对偏移量或一个间接定位的目标句柄。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行。

注意:

        1)实例变量不在该阶段分配内存

        2)因为类方法和私有方法符合“编译器可知、运行期不可变”的要求,即不会被继承或重写,所以适合在类加载过程中解析

        3)若类变量为常量(被final修饰),则直接赋值开发者定义的值

接下类说说解析的过程:将常量池的符号引用(间接引用)转换为直接引用,对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。

什么是符号引用呢?

比如我们的程序中的main方法,写法是固定的,我们就可以将main当成一个符号。比如上面的initData,int,static,我们都可以将其称之为符号,java虚拟机内部有个专业名词,把他叫做符号,这些符号被加载到JVM内存里都会对应一个地址,将”符号引用“转变为直接引用,指的就是,将”main、initData、int“等这些符号转变为对应的内存地址。这个地址就是代码的直接引用。根据直接引用的值,我们就可以知道代码在什么位置,然后根据地址拿到到代码去真正的运行。

将符号引用转变为“内存地址”,这种有一个专业名词,叫静态链接。上面的解析过程就是相当于静态链接的过程,在类加载期间完成符号到内存地址的转换,有静态链接,那么与之对应的就是动态链接?

什么是动态链接呢?

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }

 比如:上面这段代码, 只有当我运行到math.compute()这句话的时候, 才会去加载compute()这个方法. 也就是说, 在加载的时候不一定会把compute()这个方法解析成内存地址. 只有当运行到这行代码的时候, 才会解析.

我们来看看汇编代码(打开该Math.class类的Termimal,然后输入下列命令)

javap -v Math.class
Classfile /E:/TuLin_case/all/target/classes/com/jvm/Math.class
  Last modified 2022-6-21; size 904 bytes
  MD5 checksum f8065d7c5d711691a8a358a8a5100d41
  Compiled from "Math.java"
public class com.jvm.Math
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #11.#38        // java/lang/Object."<init>":()V
   #2 = Class              #39            // com/jvm/Math
   #3 = Methodref          #2.#38         // com/jvm/Math."<init>":()V
   #4 = Methodref          #2.#40         // com/jvm/Math.compute:()I
   #5 = Fieldref           #41.#42        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = String             #43            // ---------------
   #7 = Methodref          #44.#45        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #8 = Class              #46            // com/jvm/User
   #9 = Methodref          #8.#38         // com/jvm/User."<init>":()V
  #10 = Fieldref           #2.#47         // com/jvm/Math.user:Lcom/jvm/User;
  #11 = Class              #48            // java/lang/Object
  #12 = Utf8               initData
  #13 = Utf8               I
  #14 = Utf8               ConstantValue
  #15 = Integer            666
  #16 = Utf8               user
  #17 = Utf8               Lcom/jvm/User;
  #18 = Utf8               <init>
  #19 = Utf8               ()V
  #20 = Utf8               Code
  #21 = Utf8               LineNumberTable
  #22 = Utf8               LocalVariableTable
  #23 = Utf8               this
  #24 = Utf8               Lcom/jvm/Math;
  #25 = Utf8               compute
  #26 = Utf8               ()I
  #27 = Utf8               a
  #28 = Utf8               b
  #29 = Utf8               c
  #30 = Utf8               main
  #31 = Utf8               ([Ljava/lang/String;)V
  #32 = Utf8               args
  #33 = Utf8               [Ljava/lang/String;
  #34 = Utf8               math
  #35 = Utf8               <clinit>
  #36 = Utf8               SourceFile
  #37 = Utf8               Math.java
  #38 = NameAndType        #18:#19        // "<init>":()V
  #39 = Utf8               com/jvm/Math
  #40 = NameAndType        #25:#26        // compute:()I
  #41 = Class              #49            // java/lang/System
  #42 = NameAndType        #50:#51        // out:Ljava/io/PrintStream;
  #43 = Utf8               ---------------
  #44 = Class              #52            // java/io/PrintStream
  #45 = NameAndType        #53:#54        // println:(Ljava/lang/String;)V
  #46 = Utf8               com/jvm/User
  #47 = NameAndType        #16:#17        // user:Lcom/jvm/User;
  #48 = Utf8               java/lang/Object
  #49 = Utf8               java/lang/System
  #50 = Utf8               out
  #51 = Utf8               Ljava/io/PrintStream;
  #52 = Utf8               java/io/PrintStream
  #53 = Utf8               println
  #54 = Utf8               (Ljava/lang/String;)V
{
  public static final int initData;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 666

  public static com.jvm.User user;
    descriptor: Lcom/jvm/User;
    flags: ACC_PUBLIC, ACC_STATIC

  public com.jvm.Math();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/jvm/Math;

  public int compute();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn
      LineNumberTable:
        line 8: 0
        line 9: 2
        line 10: 4
        line 11: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Lcom/jvm/Math;
            2      11     1     a   I
            4       9     2     b   I
           11       2     3     c   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/jvm/Math
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method compute:()I
        12: pop
        13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: ldc           #6                  // String ---------------
        18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        21: return
      LineNumberTable:
        line 15: 0
        line 16: 8
        line 17: 13
        line 18: 21
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      22     0  args   [Ljava/lang/String;
            8      14     1  math   Lcom/jvm/Math;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #8                  // class com/jvm/User
         3: dup
         4: invokespecial #9                  // Method com/jvm/User."<init>":()V
         7: putstatic     #10                 // Field user:Lcom/jvm/User;
        10: return
      LineNumberTable:
        line 5: 0
}
SourceFile: "Math.java"

使用这个指令, 就可以查看Math的二进制文件. 其实这个文件,就是上面那个二进制代码文件.

看看这里面有什么东西?

类的名称, 大小,修改时间, 大版本,小版本, 访问修饰符等等

  Last modified 2022-6-21; size 904 bytes
  MD5 checksum f8065d7c5d711691a8a358a8a5100d41
  Compiled from "Math.java"
  public class com.jvm.Math
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER

 还有一个Constant pool 常量池. 这个常量池里面有很多东西. 我们重点看中间那一行. 第一列表示一个常量的标志符, 这个标识符可能在其他地方会用到. 第二列就表示常量内容.

Constant pool:
   #1 = Methodref          #11.#38        // java/lang/Object."<init>":()V
   #2 = Class              #39            // com/jvm/Math
   #3 = Methodref          #2.#38         // com/jvm/Math."<init>":()V
   #4 = Methodref          #2.#40         // com/jvm/Math.compute:()I
   #5 = Fieldref           #41.#42        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = String             #43            // ---------------
   #7 = Methodref          #44.#45        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #8 = Class              #46            // com/jvm/User
   #9 = Methodref          #8.#38         // com/jvm/User."<init>":()V
  #10 = Fieldref           #2.#47         // com/jvm/Math.user:Lcom/jvm/User;
  #11 = Class              #48            // java/lang/Object
  #12 = Utf8               initData
  #13 = Utf8               I
  #14 = Utf8               ConstantValue
  #15 = Integer            666
  #16 = Utf8               user
  #17 = Utf8               Lcom/jvm/User;
  #18 = Utf8               <init>
  #19 = Utf8               ()V
  #20 = Utf8               Code
  #21 = Utf8               LineNumberTable
  #22 = Utf8               LocalVariableTable
  #23 = Utf8               this
  #24 = Utf8               Lcom/jvm/Math;
  #25 = Utf8               compute
  #26 = Utf8               ()I
  #27 = Utf8               a
  #28 = Utf8               b
  #29 = Utf8               c
  #30 = Utf8               main
  #31 = Utf8               ([Ljava/lang/String;)V
  #32 = Utf8               args
  #33 = Utf8               [Ljava/lang/String;
  #34 = Utf8               math
  #35 = Utf8               <clinit>
  #36 = Utf8               SourceFile
  #37 = Utf8               Math.java
  #38 = NameAndType        #18:#19        // "<init>":()V
  #39 = Utf8               com/jvm/Math
  #40 = NameAndType        #25:#26        // compute:()I
  #41 = Class              #49            // java/lang/System
  #42 = NameAndType        #50:#51        // out:Ljava/io/PrintStream;
  #43 = Utf8               ---------------
  #44 = Class              #52            // java/io/PrintStream
  #45 = NameAndType        #53:#54        // println:(Ljava/lang/String;)V
  #46 = Utf8               com/jvm/User
  #47 = NameAndType        #16:#17        // user:Lcom/jvm/User;
  #48 = Utf8               java/lang/Object
  #49 = Utf8               java/lang/System
  #50 = Utf8               out
  #51 = Utf8               Ljava/io/PrintStream;
  #52 = Utf8               java/io/PrintStream
  #53 = Utf8               println
  #54 = Utf8               (Ljava/lang/String;)V

 这些标识符在后面都会被用到, 比如main方法

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/jvm/Math
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method compute:()I
        12: pop
        13: return
      LineNumberTable:
        line 15: 0
        line 16: 8
        line 17: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  args   [Ljava/lang/String;
            8       6     1  math   Lcom/jvm/Math;

 这里面就用到了#2 #3 #4 ,这都是标识符的引用.

第一句: new了一个Math(). 我们看看汇编怎么写的? 

 0: new           #2                  // class com/jvm/Math

 new + #2. #2是什么呢? 去常量池里看, #2代表的就是Math类

#2 = Class              #36            // com/jvm/Math

这里要说的还是math.compute()这个方法, 不是在类编译加载的时候就被加载到内存中去了,因此compute()只是一个在常量池中的静态符号, 而是运行main方法执行到这行代码时, 根据符号去内存中找这个符号对应的内存位置(懒加载), 这个过程叫做动态链接.

类加载的时候, 我们可以把"解析"理解为静态加载的过程. 一般像静态方法(例如main方法),或者其他不变的静态方法会被直接加载到内存中, 因为考虑到性能, 静态方法加载完以后几乎是不会再变了, 就直接将其转变为在内存中对应的代码位置.

而像math.compute()方法, 在加载过程中可能会变的方法(比如compute是个多态,有不同实现或者是接口), 那么在编译加载的时候, 我们不知道他会调用谁, 只有到运行时才能知道代码的实现, 所以在运行的时候根据符号动态的去查询他在内存中的位置, 这个过程就是动态加载。

第五步:初始化

初始化是指为类的静态变量赋予正确的初始值,JVM负责对类机械能初始化,主要对类变量进行初始化。

在Java中对类变量进行初始值设定有两种方法:

        (1)声明类变量时指定初始值;

        (2)使用静态代码块为类变量指定初始值。

JVM初始化步骤:

        (1)假如这个类还没有被加载和连接,则程序先加载并连接该类;

        (2)假如该类的直接父类还没有被初始化,则先初始化其直接父类;

        (3)假如类中有初始化语句,则系统依次执行这些初始化语句。

类初始化时机:只有当对类主动使用的时候才会导致类的初始化,类的主动使用包括以下6种

        (1)创建类的实例,也就是new的方式;

        (2)访问某个类或接口的静态变量,或者对该静态变量赋值;

        (3)调用类的静态方法;

        (4)反射(如Class.forName(“”));

        (5)初始化某个类的子类,则其父类也会被初始化;

        (6)Java虚拟机启动时被标明为启动类,直接使用java.exe命令来运行某个类。

初始化静态变量和静态代码块,先初始化父类,再初始化当前类,

 对类的静态变量初始化为指定的值.

public static int initData = 666;

 在准备阶段将其赋值为0, 而在初始化阶段, 会将其赋值为设定的666 

 1.4 类的懒加载

类被加载到方法区中以后,主要包含:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用,对应class实例的引用等信息。

什么意思呢?就是说,当一个类被加载到内存,这个类的常量,有常量名、类型、域信息等;方法有方法名、返回类型、参数类型、方法作用域等符号信息都会被加载放入到不同的区域。

注意:如果主类在运行中用到其他类,会逐步加载这些类,也就是说懒加载,用到的时候才加载。

package com.jvm;

public class TestDynamicLoad {
    static {
        System.out.println("********Dynamic load class**************");
    }

    public static void main(String[] args) {
        new A();
        System.out.println("*********load test*****************");
        B b = null; // 这里的b不会被加载, 除非这里执行 new B();
    }
}

class A {
    static {
        System.out.println("********load A**************");
    }

    public A(){
        System.out.println("********initial A**************");
    }
}

class B {
    static {
        System.out.println("********load B**************");
    }

    public B(){
        System.out.println("********initial B**************");
    }
}

 这里定义了两个类A和B,当使用到哪一个类的时候,那个类才会被加载,比如:main方法中,B没有被用到,所以他不会被加载到内存中。

运行结果

********load TestDynamicLoad**************
********load A**************
********initial A**************
*********load test*****************

 我们看到A类被加载了,而B类没有被加载,原因是B类只是声明了,没有用到。

总结几点如下:

        1.静态代码块在构造方法之前执行

        2.没有被真正使用的类不会被加载

二、类加载器

2.1 类加载器的类型

类主要通过类加载器来加载,java里面有如下几种类加载器

1)引导类加载器(Bootstrap ClassLoade):主要负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等。

2)扩展类加载器(Ext ClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包。

3)应用程序类加载器(App CloassLoader):负责加载ClassPath(src)路径下的类包,主要就是加载自己写的那些类

4)自定义加载器:负责加载用户自定义路径下的类包

引导类加载器是由C++帮我们实现的,然后C++语言会通过一个Luancher类将扩展加载器和应用程序类加载器构造出来,并且把他们之间的关系构造好。

2.2 案例 一

import sun.misc.Launcher;
import java.net.URL;
public class TestJDKClassLoader {
    public static void main(String[] args) {
        /**
         * 第一个: String 是jdk自身自带的类,位于jre/lib核心目录下, 所以, 他的类加载器是引导类加载器
         * 第二个: 加密类的classloader, 这是jdk扩展包的一个类
         * 第三个: 是我们当前自己定义的类, 会被应用类加载器加载
         */
        System.out.println(String.class.getClassLoader()); 						                 
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
        System.out.println(TestJDKClassLoader.class.getClassLoader());
    }
}

 运行的结果:

null
sun.misc.Launcher$ExtClassLoader@2d209079
sun.misc.Launcher$AppClassLoader@18b4aac2

解析:
第一个:String是jdk自带的类,他的类加载器是引导类加载器,引导类加载器是C++写的,
不是Java对象,是C++生成的对象,所以在java代码中是看不到的,因此这里的结果是null

第二个:加密类的classLoader,这是JDK扩展包的一个类,JDK扩展包里面使用的是
EXTClassLoader类加载器加载的

第三个:是我们当前自己定义的类,会被AppClassLoader应用程序加载器加载

 我们可以通过结果可以看到ExtClassLoader和AppClassLoader都是Launcher类的一部分,那Launcher类是什么东西呢?

上面有提到,Luancher类是jvm启动的时候由C++调用启动的一个类,这个类是在引导加载器加载并创建其他的类加载器。

2.3 BootstrapClassLoad和ExtClassLoader、AppClassLoader的关系

 

 如上图,左边是C语言程序代码实现,右边是Java代码实现。这里是跨语言调用,JNI实现了由C++向Java跨语言调用。C语言调用的第一个java类是Launcher类。

从这个图中我们可以看出C++调用Java创建JVM启动器,其中一个启动器是Launcher,他实际是调用了sun.misc.Launcher类的getLauncher()方法,那我们就从这个方法入手看看到底是如何运行的?

Launcher.java类是在核心的rt.jar包里的,Launcher是一个非常核心的类。

 我们看到getLauncher()方法直接返回了Launcher对象,而Launcher是一个静态对象变量,是一个单例模式,C++调用getLauncher() --> 直接返回了Launcher对象,而Launcher对象是在构建类的时候就已经初始化好了,那么初始化的时候做了哪些操作呢?

类加载器初始化过程: 参见类运行加载全过程图可知其中会创建JVM启动器实例sun.misc.Launcher。 sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个 sun.misc.Launcher实例。 在Launcher构造方法内部,其创建了两个类加载器,分别是 sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用程序类加载器)。 JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们 的应用程序。

接下类看看他的构造方法。

 在构造方法里,首先定义了一个ExtClassLoader,这是一个扩展类加载器,扩展类加载器调用的是getExtClassLoader(),接下来看getExtClassLoader这个方法做了什么?

 doPrivileged是一个权限校验的操作, 我们可以先不用管, 直接看最后一句, return new Launcher.ExtClassLoader(var0). 直接new了一个ExtClassLoader, 其中参数是var0, 代表的是ext扩展目录下的文件.

 在ExtClassLoader(File[] var1)这个方法中, 这里第一步就是调用了父类的super构造方法. 而ExtClassLoader继承了谁呢? 我们可以看到他继承了URLClassLoader.

 而URLClassLoader是干什么用的呢? 其实联想一下大概能够猜数来, 这里有一些文件路径, 通过文件路径加载class类.

我们继续看调用的super(parent), 我们继续往下走, 就会看到调用了ClassLoader接口的构造方法:

 这里设置了ExtClassLoader的parent是谁? 注意看,我们发现, ExtClassLoader的parent类是null.

 这就是传递过来的parent类加载器,那么这里的parent类加载器为什么是null呢?

因为,ExtClassLoader的父类加载器是谁呢?他是Bootstrap ClassLoader,而BootStrap ClassLoader是C++的类加载器, 我们不能直接调用他, 所以, 设置为null.

其实, ExtClassLoader在初始化阶段就是调用了getExtClassLoader方法, 初始化了ExtClassLoader类

接下来,我们回到Launcher的构造方法,看看Launcher接下来又做了什么?

 可以看到, 接下来调了AppClassLoader的getAppClassLoader(var1), 这个方法. 需要注意一下的是var1这个参数. var1是谁呢? 向上看, 可以看到var1是ExtClassLoader.

 这是AppClassLoader, 应用程序类加载器, 这个类是加载我们自己定义的类的类加载器. 他也是继承自URLClassLoader.

 我们来看看getAppClassLoader(final ClassLoader var0)方法. 这个方法的参数就是上面传递过来的ExtClassLoader

第一句System.getProperty("java.class.path")就是获取当前项目的(target)class文件路径,然后将其转换为URL,并调用了Launcher.AppClassLoader(var1x, var0),其中var1x就是class类所在的路径集合,var0是扩展的类加载器ExtClassLoader,接下来,我们进入到这个方法里面看一看。

AppClassLoader直接调用了其父类的构造方法, 参数是class类路径集合, 和ExtClassLoader

 最后, 我们看到, 将ExtClassLoader传递给了parent变量. 这是定义在ClassLoader中的属性, 而ClassLoader类是所有类加载器的父类. 因此, 我们也可以看到AppClassLoader的父类加载器是ExtClassLoader

 同时, 我们也看到了, C++在启动JVM的时候, 调用了Launcher启动类, 这个启动类同时加载了ExtClassLoader和AppClassLoader.

public static void main(String[] args) {
        
  ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
  ClassLoader extClassLoader = appClassLoader.getParent();
  ClassLoader bootstrapClassLoad = extClassLoader.getParent();


  System.out.println("bootstrap class loader: " + bootstrapClassLoad);
  System.out.println("ext class loader " + extClassLoader);
  System.out.println("app class loader "+ appClassLoader);
}

输出结果:
bootstrap class loader: null
ext class loader sun.misc.Launcher$ExtClassLoader@2a84aee7
app class loader sun.misc.Launcher$AppClassLoader@18b4aac2 

 通过上面的demo得出appClassLoader的父加载器是extClassLoader,extClassLoader的父加载器是BootstrapClassLoader

  通过上面的源码分析,我们发现引导类加载器创建并加载了扩展类加载器和应用程序类加载器。而扩展类加载器的父加载器是引导类加载器。应用程序类加载器的父加载器是扩展类加载器。这个结构,决定了后面类的加载方式,也就是双亲委派机制。

三、双亲委派机制

为什么要研究类加载器的过程?为什么要研究双亲委派机制?

研究类加载的过程就是要知道类加载的时候使用了双亲委派机制。但仅仅知道双亲委派机制不是目的,目的是要了解什么要使用双亲委派机制,他的原理是什么?知道双亲委派机制的逻辑思想,然后这个思想是否可以被我们借鉴,为已所用。

比如:双亲委派机制可以避免类的重复加载,避免核心类库被修改。那么我们在做框架设计的时候,框架底层的东西不能被篡改,或者不可以被黑客攻击,那么我们就可以借鉴双亲委派机制了。

 3.1 什么是双亲委派机制

我们先来看一个案例:打印引导类加载器、扩展类加载器、应用程序类加载器的加载路径目录

public class TestJDKClassLoader {

    public static void main(String[] args) {

        System.out.println("bootstrapLoader加载以下文件:");
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
            System.out.println(urL);
        }

        System.out.println("extClassloader加载以下文件:");
        System.out.println(System.getProperty("java.ext.dirs"));

        System.out.println();
        System.out.println("appClassLoader加载以下文件:");
        System.out.println(System.getProperty("java.class.path"));

    }
}


引导类加载器加载的文件是:Launcher.getBootstrapClassPath().getURLs()下的文件

扩展类加载器加载的文件是: java.ext.dirs , java扩展类目录

应用程序类加载器, 加载的是: java.class.path(target)


执行结果:

bootstrapLoader加载以下文件:
file:/E:/Program%20Files/Java/jdk1.8.0_111/jre/lib/resources.jar
file:/E:/Program%20Files/Java/jdk1.8.0_111/jre/lib/rt.jar
file:/E:/Program%20Files/Java/jdk1.8.0_111/jre/lib/sunrsasign.jar
file:/E:/Program%20Files/Java/jdk1.8.0_111/jre/lib/jsse.jar
file:/E:/Program%20Files/Java/jdk1.8.0_111/jre/lib/jce.jar
file:/E:/Program%20Files/Java/jdk1.8.0_111/jre/lib/charsets.jar
file:/E:/Program%20Files/Java/jdk1.8.0_111/jre/lib/jfr.jar
file:/E:/Program%20Files/Java/jdk1.8.0_111/jre/classes

extClassloader加载以下文件:
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext;
E:\WINDOWS\Sun\Java\lib\ext

appClassLoader加载以下文件:
E:\Program Files\Java\jdk1.8.0_111\jre\lib\charsets.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\deploy.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\access-bridge-64.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\cldrdata.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\dnsns.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\jaccess.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\jfxrt.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\localedata.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\nashorn.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\sunec.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\sunjce_provider.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\sunmscapi.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\sunpkcs11.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\zipfs.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\javaws.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\jfr.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\jfxswt.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\jsse.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\management-agent.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\plugin.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\resources.jar;
E:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar;

E:\case\all\target\classes;

F:\Tools\IDEA\IntelliJ IDEA 2019.1\lib\idea_rt.jar;
E:\Users\ASUS\.IntelliJIdea2019.1\system\captureAgent\debugger-agent.jar

通过观察执行结果,我们可以发行

引导类加载器,确实只加载了java home下的/jre/lib目录下的类 

扩展类加载器加载了java扩展目录里面的类

但是应用程序类加载器,加载的类包含了java home下/jre/lib目录,java home 扩展目录下的类,还有idea的类,还有就是我们的类路径下的target的class类。

问题来了,为什么AppClassLoader加载器加载了引导类加载器和扩展类加载器要加载的类呢?这样加载不是重复了吗?

其实,不会重复加载,appClassLoader主要加载的类是target目录下的类,其他目录下的类事实上是不会加载。为什么呢?这是因为双亲委派机制。

JVM类加载器是有亲子层级架构的,如下图

这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找目标,找不到再委托上层父加载器加载,如果所有父加载器在各自的加载路径下都找不到目标类,则在自己的类加载器路径中查找并加载入目标类。

就以自定义的java.jvm.Math类为例,我们来看看这个类是如何被类加载器加载的。

第一步: 首先是由应用程序类加载器去查找java.jvm.Math类, 他要去看他已经加载的类中是否有这个类, 如果有, 就直接返回, 如果没有, 就去加载这个类,但是不是由应用程序类加载器直接加载。而是委托他的父加载器也就是扩展类加载器去加载。

第二步扩展类加载器也是先搜索,查看已经加载的类是否有java.jvm.Math, 如果有就返回,如果没有就加载这个类。在加载的时候,也不是由自己来加载,而是委托他的父加载器,引导类加载器去加载。

第三步引导类加载器先查找已经加载的类中是否有这个类,有则返回,没有就去加载这个类。这时候, 我们都知道, Math类是我自己定义的, 引导类加载器中不可能有, 加载失败,所以, 他就会去加载这个类。会去扫描/lib/jar包中有没有这个类,发现没有,于是让扩展类加载器去加载, 扩展类加载器会去扫描扩展包lib/jar/ext包,里面有没有呢? 当然也没有, 于是委托应用程序类加载器, 应用程序类加载器是有的, 于是就可以加载了, 然后返回这个类。

双亲委派机制说简单点就是,先找父加载器加载,不行在由儿子自己加载

【通过分析,我们可以得出,双亲委派机制的实现使用的是责任链设计模式。】 

那么, 这里有一个问题, 那就是, 由应用程序类加载器首先加载, 然后最后又回到了应用程序类加载器. 绕了一圈又回来了, 这样是不是有些多此一举呢, 循环了两次? 为什么一定要从应用程序类加载器加载呢? 直接从引导类加载器加载不好么?只循环一次啊....

其实, 对于我们的项目来说, 95%的类都是我们自己写的, 因此, 而我们自己写的类是有应用程序类加载器加载. 其实,应用程序类加载器只有在第一次的时候, 才会加载两次. 以后, 当再次使用到这个类的时候, 就直接去访问应用程序类加载器, 有这个类么? 已经有了, 就直接返回了。

3.2 源码分析双亲委派机制 

 还是从这张图说起,C++语言调用了sun.misc.Launcher.getLauncher()获取了Launcher对象,Launcher类初始化的时候其构造器创建了ExtClassLoader和AppClassLoader。然后接下来调用launcher对象的getClassLoader()方法。

public ClassLoader getClassLoader() {
    return this.loader;
}

 getClassLoader()返回了this.loader对象。 而loader对象是在Launcher初始化的时候进行了赋值, loadClass是AppClassLoader。

public Launcher() {
  Launcher.ExtClassLoader var1;
  try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
  } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
  }

  try {
       // loader的值是AppClassLoader
       this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
  } catch (IOException var9) {
       throw new InternalError("Could not create application class loader", var9);
  }

  ......
}

 类加载器是如何加载类的呢?

调用了loader.loderClass(“com.jvm.Math”)方法。我们来看一下类加载器,类加载器主要调用的是classLoader.loadClass("com.jvm.Math")这个方法来实现双亲委派机制的,根据上面的分析,我们知道,在Launcher类初始化的时候,loadClass是AppClassLoader,那么也就是说,双亲委派机制的起点就是AppClassLoader。

3.2.1 第一次向上查找

1.从AppClassLoader加载目标类

首先, 我们在Launcher的AppClassLoader的loadClass(String var1, boolean var2) 这个方法添加一个断点, 并将其赋值为我们的com.jvm.Math类(右击debug红点)

然后运行Math的main方法,我们来看一下这个类到底是如何被加载的 

 启动debug调试模式,首先进入了Launch.AppClassLoader.loadClass(...)方法

 上面都是在做权限校验, 我们看重点代码.

public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
            int var3 = var1.lastIndexOf(46);
            if (var3 != -1) {
                SecurityManager var4 = System.getSecurityManager();
                if (var4 != null) {
                    var4.checkPackageAccess(var1.substring(0, var3));
                }
            }
            // 缓存中是否有目标路径,如果有,说明之前已经加载过了,
            // 直接调动findLoadedClass(var1)从已经加载的类中查找,找到后直接返回。

            if (this.ucp.knownToNotExist(var1)) {
                Class var5 = this.findLoadedClass(var1);
                if (var5 != null) {
                    if (var2) {
                        this.resolveClass(var5);
                    }

                    return var5;
                } else {
                    throw new ClassNotFoundException(var1);
                }
            } else {
            //缓存中没有,则调用loadClass(var1, var2)加载类
                return super.loadClass(var1, var2);
            }
        }

看注释部分,我们知道这是双亲委派机制里的第一步,现在AppClassLoader中查找,先从已经加载过的类中查找,如果找到了就直接返回,如果没有找到,则加载这个类。我们分两步来看:一部分是findLoaderClass()的源码, 另一部分是super.loadClass(...)的源码

第一步:在已加载的类中查找是否存在

if (this.ucp.knownToNotExist(var1)) {
    Class var5 = this.findLoadedClass(var1);
    if (var5 != null) {
      if (var2) {
        this.resolveClass(var5);
      }

      return var5;
    } else {
      throw new ClassNotFoundException(var1);
    }
  }

调用findLoadClass(var1)之前先判断this.ucp.knownToNotExist(var1)在缓存中是的存在,如果存在则调用 用this.findLoadedClass(var1);查找。而findLoadedClass最终调用的是本地方法查找(通过C语言实现)

private native final Class<?> findLoadedClass0(String name);

 第二步:之前没有加载过此类,首次加载

else {
    // 缓存中没有,则调用loadClass加载类。
    return super.loadClass(var1, var2);
  }

首次加载调用了super.loadClass(var1,var2), 而这个super是谁呢? 我们来看看AppClassLoader的集成关系

 我们看到AppClassLoader继承自URLClassLoader, 而URLClassLoader又继承了上面四个类,最终有继承一个叫做ClassLoader的类, 所有的类加载器, 最终都要继承这个ClassLoader类.

而这里调用的是super.loadClass(),我们来看看URLClassLoader中是否有loadClass()类, 看过之后发现,他没有, 最终这个super.loadClass()是继承了ClassLoader类的loadClass(....)方法

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

正是这个类实现了双亲委派机制, 下面我们就来看看, 他到底是怎么实现的?

当前的类加载器是AppClassLoader类加载器, 首先第一步是查找AppClassLoader中已经加载的类中,有没有这个类, 我们看到这里又检查了一遍。

 通过调用findLoadedClass(name)方法来查询已经加载的类中, 有没有com.jvm.Math类. 那么findLoadedClass(name)里面做了什么呢? 我们进去看看

 我们看到, findLoaderClass(name)方法调用了自己的一个方法findLoadedClass0, 这个方法是native的, 也就是是本地方法, 使用c++实现的, 我们不能看到底部的具体实现细节了. 但是大致的逻辑就是在已经加载的类中查找有没有com.jvm.Math这个类, 如果有就返回Class类信息。

没有就继续往下执行走到if(c == null)里面了, 这里做了什么事呢?

他判断了,当前这个类加载器的parent(父加载器)是否是null. 我们知道当前这个类加载是 AppClassLoader, 他的parent是ExtClassLoader, 自然不是null, 所以, 就会执行里面的parent.loadClass(name, false);

2.从ExtClassLoader中加载目标类

 也就是执行扩展类加载器的loadClass(...)方法. 我们来看看扩展类 ExtClassLoader

 我们发现ExtClassLoader类里面没有loadClass(...)方法, 那他没有, 肯定就是在父类里定义的了, 通过查找, 最后我们发现这个方法还是ClassLoader里的loadClass(...)方法. 于是,我们继续debug.肯定会再次走到loadClass(...)这个方法里来. 而此时, loadClass是ExtClassloader的loadClass(...)方法 ​

果然, 又走到这个方法里面来了

继续往下执行, 首先查找ExtClassLoader中已经加载的类中,是否有java.jvm.Math类, 过程和上面是一样的. 最后调用的是本地方法.

我们知道, 这肯定是没有的了. 然后继续判断, ExtClassLoader的parent是否为空. 很显然, 他就是空啊, 因为ExtClassLoader的父加载器是引导类加载器BootStrapClassLoader, 而引导类加载器是c++写的,所以,这里的parent为空. parent为空执行的是else中的代码

3.从BootStrapClassLoader中查找

 这个方法就是去引导类加载器BootstrapClassLoad中查找, 是否有这个类, 我们来看看引导类加载器里面的具体实现

我们发现, 最后具体的逻辑也是一个本地方法实现的. 我们还是猜测一下, 这就是去查找引导类加载器已经加载的类中有没有com.jvm.Math, 如果有就返回这个类, 如果没有就返回null.

很显然, 是没有的. c == null. 我们继续来看下面的代码

到此为止, 我们第一次向上查找的过程就完完事了. 用图表示就是这样

首先有应用程序类加载器加载类, 判断应用程序已加载的类中, 是否有这个类, 结果是没有, 没有则调用其父类加载器ExtClassLoader的loadClass()方法, 去扩展类加载器中查找是否有这个类, 也没有. 那么判断其父类是否为空, 确实为空, 则进入到引导类加载器中取查找是否有这个类, 最后引导类加载器中也没有, 返回null。

 3.2.2 类加载器向下委派加载

下面来看看类加载器是如何向下委派的?

1.启动类加载器加载目标类

引导类加载器中也没有这个类, 返回null, 这里的返回null包含了两个步骤,一个是查找,没找到,二是没找到后去/lib/jar目录下加载这个类,也没有加载到。最后返回null。然后回到ExtClassLoader.loadClass(...).

2.扩展类加载器加载目标类

接下来调用findClass(name);查找ExtClassLoader中是否有com.jvm.Math, 我们来看看具体的实现. 首先这是谁的方法呢?是ExtClassLoader的.

进入到findClass(name)方法中, 首先看看ExtClassLoader类中是否有这个方法, 没有, 这里调用的是父类UrlClassLoader中的findClass()方法

在findClass()里面, 我们看到将路径中的.替换为/,并在后面增加了.class. 这是在干什么呢? 是将com.jvm.Math替换为com/jvm/Math.class,这就是类路径

然后去resource库中查找是否有这个路径. 没有就返回null, 有就进入到defineClass()方法.

我们想一想, 在ExtClassLoader类路径里面能找到这个类么?显然是找不到的, 因为这个类使我们自己定义的.

他们他一定执行return null.

 正如我们分析, debug到了return null; 这时执行的ExtClassLoader的findClass(). 返回null, 回到AppClassLoader加载类里面

3.应用程序类加载器加载目标类

 c就是null, 然后继续执行findClass(name), 这时还是进入到了URLClassPath类的findClass(name)

如上图, 此时调用的是AppClassLoader的findClass(name), 此时的resource还是空么?当然不是了, 在target目录中就有Math.class类, 找到了, 接下来执行defineClass(name,res)

defindClass这个方法是干什么的呢? 这个方法就是加载类. 类已经找到了, 接下来要做的就是将其加载进来了.

类加载的四个步骤

 defindClass()这个类执行的就是类加载的过程。 也就是下图中的四个步骤:验证->准备->解析->初始化。如下图红线圈出的部分。

 再看看这四个步骤:

private Class<?> defineClass(String name, Resource res) throws IOException {
        long t0 = System.nanoTime();
        int i = name.lastIndexOf('.');
        URL url = res.getCodeSourceURL();
        // 获取classes目录的绝对路径,
        // 如:file:/Users/用户名/workspace/demo/target/classes/
        if (i != -1) {
        // 获取包名
            String pkgname = name.substring(0, i);
            // Check if package already loaded.
            Manifest man = res.getManifest();
            definePackageInternal(pkgname, man, url);
        }
        // Now read the class bytes and define the class
        java.nio.ByteBuffer bb = res.getByteBuffer();
        if (bb != null) {
            // Use (direct) ByteBuffer:
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, bb, cs);
        } else {
            byte[] b = res.getBytes();
            // must read certificates AFTER reading bytes.
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, b, 0, b.length, cs);
        }
    }

这里面的核心逻辑代码都是本地方法。我们能看到的通常是一些基础的校验,比如准备阶段,解析阶段,初始化阶段都是本地方法 。

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
  			// 预定义类信息
        protectionDomain = preDefineClass(name, protectionDomain);
  			// 定义类源码
        String source = defineClassSourceLocation(protectionDomain);
  			// 初始化类
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
        // 类定义后置处理
        postDefineClass(c, protectionDomain);
        return c;
    }

这块代码了解即可。不用深入研究。

以上就是双亲委派机制的源码.

那么当下一次在遇到com.jvm.Math类的时候, 我们在AppClassLoader中就已经有了, 直接就返回了.

在来看一遍双亲委派机制的流程图

 

 3.3 为什么要有双亲委派机制?

两个原因:

1. 沙箱安全机制, 自己写的java.lang.String.class类不会被加载, 这样便可以防止核心API库被随意修改

2. 避免类重复加载. 比如之前说的, 在AppClassLoader里面有java/jre/lib包下的类, 他会加载么? 不会, 他会让上面的类加载器加载, 当上面的类加载器加载以后, 就直接返回了, 避免了重复加载.(保证被加载类的唯一性)

我们来看下面的案例

加入, 我在本地定义了一个String类, 包名是java.lang.String. 也就是是rt.jar包下的String类的包名是一样。这是我们运行main方法, 会怎么样? 没错, 会报错

下面分析一下, 为什么会报错呢?

还是看双亲委派机制的流程, 首先由AppClassLoader类加载器加载, 看看已经加载的类中有没有java.lang.String这个类, 我们发现, 没有, 找ExtClassLoader加载, 也没有, 然后交给引导类BootStrapClassLoader加载, 结果能不能找到呢? 当然可以了. 但是这个java.lang.String是rt.jar中的类, 不是我们自定义的类, 加载了rt.jar中的java.lang.String类以后, 去找main 方法, 没找到.....结果就抛出了找不到main方法异常.

所以说, 如果我们自己定义的时候, 想要重新定义一个系统加载的类, 比如String.class, 可能么? 不可能, 因为自己定义的类根本不会被加载

这就是双亲委派机制的第一个作用: 沙箱安全机制, 自己写的java.lang.String.class类不会被加载, 这样便可以防止核心API库被随意修改

双亲委派机制还有一个好处: 避免类重复加载. 比如之前说的, 在AppClassLoader里面有java/jre/lib包下的类, 他会加载么? 不会, 他会让上面的类加载器加载, 当上面的类加载器加载以后, 就直接返回了, 避免了重复加载.

第三个作用:全盘委托机制。比如Math类,里面有定义了private User user;那么user也会由AppClassLoader来加载。除非手动指定使用其他类加载器加载。也就是说,类里面调用的其他的类都会委托当前的类加载器加载。

四、自定义类加载器示例

自定义类的加载器只需要继承java.lang.ClassLoader类,该类有两个核心方法,一个是loadClass(String,boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法

4.1 自定义类加载器

第一步:自定义类加载器继承自ClassLoader抽象类,然后定义一个构造方法, 用来接收要加载器加载的类路径

 第二步:重写核心方法findClass(String name)

 这里有两步操作:

第一个是:从类路径中读取要加载类的文件内容(自定义)

第二个是:调用构造方法的方法,调用系统的defindClass

接下来看看自定义的loadByte是如何实现的

 这里的实现就是找到类, 并且将类的内容读取出来, 转换成二进制的字节码, 返回

 最后一步:调用自定义加载器.

 在E盘中创建test/com/jvm目录,并把User1.class文件放入目录中

  用类加载器加载类, 然后实例化, 使用反射机制调用User1 的方法sout() 

 这里面System.out.println(clazz.getClassLoader().getClass().getName()); 获取当前类的类加载器, 猜一猜这里打印的会是谁?

看到了么? 是AppClassLoader, 为什么呢?

原因是我的项目里已经有一个类User1了

因为初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader

因此在启动自定义类加载器的时候,会先委托父加载器(AppClassLoader)去加载路径下加载类,由于项目中存在User1类,所以打印出来的是AppClassLoader加载器,这里就是使用了双亲委派机制的特点

那么如果我们将项目中的User1类删除掉,这时类的加载器是谁呢?

答案是:我们自定义的类加载器

那么问题来了,自定义类加载器的父加载器为什么是AppClassLoader呢?

4.2 分析自定义类加载的父类为什么是appClassLoader?

我们来看一下源码

我们自定义的类加载器, 继承自ClassLoader类加载器, 那么在调用自定义类加载器的构造方法之前, 应该先加载父类ClassLoader的无参构造函数.

首先会执行ClassLoader的无参的构造方法.

 

 而无参的构造方法会调用自身的构造方法

 

 里面有一个parent, 我们就是要看看这个parent到底是谁呢. 来看看getSystemClassLoader()方法

 之前我们已经研究过getClassLoader()这个方法了, 这里面定义的loadClass是谁呢?就是AppClassLoader.

 

 这就是为什么自定义class类加载器的父类是AppClassLoader的原因了。

 4.3 打破双亲委派机制

从上面讲的可知,类加载器的双亲委派机制是在classLoader.loadClass()方法中实现的,因此我们要重写loadClass()方法。

第一步:重写loadClass()方法,然后尝试把if(c==null)里面实现的双亲委派机制的方法注释掉。

package com.jvm;

import java.io.FileInputStream;
import java.lang.reflect.Method;

/**
 * 自定义类加载器
 */
public class MyClassLoaderTest{
    static class MyClassLoader extends ClassLoader {
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        public Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

        private byte[] loadByte(String name) throws Exception {
            //需要读取类的路径
            name = name.replaceAll("\\.", "/");
            //根据路径查找这个类
            FileInputStream file = new FileInputStream(classPath + "/" + name + ".class");
            int len = file.available();
            byte[] bytes = new byte[len];
            file.read(bytes);
            file.close();
            return bytes;
        }

        /**
         * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
         * @param name
         * @param resolve
         * @return
         * @throws ClassNotFoundException
         */
        protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
//                if (c == null) {
//                    long t0 = System.nanoTime();
//                    try {
//                        if (parent != null) {
//                            c = parent.loadClass(name, false);
//                        } else {
//                            c = findBootstrapClassOrNull(name);
//                        }
//                    } catch (ClassNotFoundException e) {
//                        // ClassNotFoundException thrown if class not found
//                        // from the non-null parent class loader
//                    }
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        c = findClass(name);

                        // this is the defining class loader; record the stats
//                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
//                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    }

    public static void main(String[] args) throws Exception {
        //初始化自定义类加载器会先初始化父类ClassLoader,
        // 其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("E:/test");
        //E盘创建 test/com/jvm 几级目录,将User类的复制类User1.class丢入该目录
        Class<?> clazz = classLoader.loadClass("com.jvm.User1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj,null);
        System.out.println(clazz.getClassLoader().getClass().getName());

    }
}

第二步:在项目中重新创建User1,表示在应用程序类加载器(AppClassLoader)也存在User1类,因为应用程序类是自定义类加载器的父加载器。所以在运行结果的时候可以查看打印出来的类加载器是属于哪个。

第三步:执行输出结果

 如图可看输出的结果现实找不到Object.class这个类,那我们来思考一下为什么会报这个问题?

因为我们的User1有个父类是Object类,在初始化自定义类加载器的时候是要先加载父类ClassLoader,但是当前的代码是已经打破双亲委派机制了,而这个类又只会从一个路径加载此类(引导类加载器的加载路径JRE的lib目录下的核心类库,rt.jar)所以提示找不到这个类,那么我们看看怎么解决这个问题?

我们可以试试在E:\test目录创建java\lang目录,然后把Object.class文件拷贝到lang文件夹中,看看行不行得通

 现在重新执行代码查看输出结果

想想为什么又会报错呢?而且还是报SecurityException:Prohibited(禁止加载)异常。

Object.class类是在JRE的lib目录下的核心类,核心类会让我们在外面对他加载吗,肯定是不行的,这里就体现出沙箱安全机制,如果我们可以加载成功的话,相当于重写了Object类,肯定是不行的,对于JVM来说,凡是JDK内部核心的包都是不允许自己去加载,为的就是防止安全问题的发生。

那现在怎么办来解决这个问题呢?

由于Object类是引导类加载的,自己是不能去加载核心类,那我们实现一个逻辑,就是自定义的类加载器只加载我们写的类,Object类由引导类加载。就是我们可以这样判断,如果不是com.jvm开头的就不打破双亲委派机制继续由父加载器加载。这样问题就解决了

 执行代码查看最终的结果

现在输出的结果就是自己写的类由自定义的类加载器执行,而不是显示AppCLassLoader加载,因为刚刚在项目中也创建了一个User1类,所以沙箱安全问题就搞定啦。 

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值