运行环境:

下面说明一下我的运行环境。我是在mac上操作的. 先找到mac的java地址. 从~/.bash_profile中可以看到

java的home目录是: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home

1. JVM核心类加载器及类加载的全过程-LMLPHP

一. 类加载的过程

1.1 类加载器初始化的过程

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

package com.lxl.jvm;

public class Math {
    public static 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, 就能得到结果呢?

先来看看答题的类加载流程(宏观流程), 如下图:

1. JVM核心类加载器及类加载的全过程-LMLPHP

备注:

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.lxl.Math是怎么被加载到java虚拟机里面去的?  

1.2 类加载的过程

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

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

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

1. JVM核心类加载器及类加载的全过程-LMLPHP

第一步: 加载.

将class类加载到java虚拟机的内存里去, 在加载到内存之前, 会有一系列的操作。第一步是验证字节码。

1. JVM核心类加载器及类加载的全过程-LMLPHP

第二步:验证

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

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

第三步:准备

验证完了, 接下来是准备. 准备干什么呢? 比如我们的类Math, 他首先会给Math里的静态变量赋值一个初始值. 比如我们Math里有两个静态变量

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

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

第四步:解析

接下来说说解析的过程. 解析的过程略微复杂, 解析是将"符号引用"转变为直接引用.

什么是符号引用呢?

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

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

什么是动态链接呢?

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

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

我们来看看汇编代码

javap -v Math.class
Classfile /Users/luoxiaoli/Downloads/workspace/project-all/target/classes/com/lxl/jvm/Math.class
  Last modified 2020-6-27; size 777 bytes
  MD5 checksum a6834302dc2bf4e93011df4c0b774158
  Compiled from "Math.java"
public class com.lxl.jvm.Math
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#35         // java/lang/Object."<init>":()V
   #2 = Class              #36            // com/lxl/jvm/Math
   #3 = Methodref          #2.#35         // com/lxl/jvm/Math."<init>":()V
   #4 = Methodref          #2.#37         // com/lxl/jvm/Math.compute:()I
   #5 = Fieldref           #2.#38         // com/lxl/jvm/Math.initData:I
   #6 = Class              #39            // com/lxl/jvm/User
   #7 = Methodref          #6.#35         // com/lxl/jvm/User."<init>":()V
   #8 = Fieldref           #2.#40         // com/lxl/jvm/Math.user:Lcom/lxl/jvm/User;
   #9 = Class              #41            // java/lang/Object
  #10 = Utf8               initData
  #11 = Utf8               I
  #12 = Utf8               user
  #13 = Utf8               Lcom/lxl/jvm/User;
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               LocalVariableTable
  #19 = Utf8               this
  #20 = Utf8               Lcom/lxl/jvm/Math;
  #21 = Utf8               compute
  #22 = Utf8               ()I
  #23 = Utf8               a
  #24 = Utf8               b
  #25 = Utf8               c
  #26 = Utf8               main
  #27 = Utf8               ([Ljava/lang/String;)V
  #28 = Utf8               args
  #29 = Utf8               [Ljava/lang/String;
  #30 = Utf8               math
  #31 = Utf8               MethodParameters
  #32 = Utf8               <clinit>
  #33 = Utf8               SourceFile
  #34 = Utf8               Math.java
  #35 = NameAndType        #14:#15        // "<init>":()V
  #36 = Utf8               com/lxl/jvm/Math
  #37 = NameAndType        #21:#22        // compute:()I
  #38 = NameAndType        #10:#11        // initData:I
  #39 = Utf8               com/lxl/jvm/User
  #40 = NameAndType        #12:#13        // user:Lcom/lxl/jvm/User;
  #41 = Utf8               java/lang/Object
{
  public static int initData;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

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

  public com.lxl.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/lxl/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/lxl/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/lxl/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/lxl/jvm/Math;
    MethodParameters:
      Name                           Flags
      args

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

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

看看这里面有什么东西?

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

 Last modified 2020-6-27; size 777 bytes
  MD5 checksum a6834302dc2bf4e93011df4c0b774158
  Compiled from "Math.java"
public class com.lxl.jvm.Math
  minor version: 0
  major version: 52

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

Constant pool:
   #1 = Methodref          #9.#35         // java/lang/Object."<init>":()V
   #2 = Class              #36            // com/lxl/jvm/Math
   #3 = Methodref          #2.#35         // com/lxl/jvm/Math."<init>":()V
   #4 = Methodref          #2.#37         // com/lxl/jvm/Math.compute:()I
   #5 = Fieldref           #2.#38         // com/lxl/jvm/Math.initData:I
   #6 = Class              #39            // com/lxl/jvm/User
   #7 = Methodref          #6.#35         // com/lxl/jvm/User."<init>":()V
   #8 = Fieldref           #2.#40         // com/lxl/jvm/Math.user:Lcom/lxl/jvm/User;
   #9 = Class              #41            // java/lang/Object
  #10 = Utf8               initData
  #11 = Utf8               I
  #12 = Utf8               user
  #13 = Utf8               Lcom/lxl/jvm/User;
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               LocalVariableTable
  #19 = Utf8               this
  #20 = Utf8               Lcom/lxl/jvm/Math;
  #21 = Utf8               compute

这些标识符在后面都会被用到, 比如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/lxl/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/lxl/jvm/Math;
    MethodParameters:
      Name                           Flags
      args

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

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

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

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

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

这里要说的还是math.compute()这个方法, 不是在类加载的时候就被加载到内存中去了, 而是运行main方法的时候, 执行到这行代码才被加载进去, 这个过程叫做动态链接.

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

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

第五步: 初始化

对类的静态变量初始化为指定的值. 执行静态代码块. 比如代码

public static int initData = 666;

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

1.3 类的懒加载

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

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

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

package com.lxl.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没有被用到, 所以, 他不会被加载到内存中.

运行结果

********Dynamic load class**************
********load A**************
********initial A**************
*********load test*****************

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

总结几点如下:

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

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

二. 类加载器

2.1 类加载器的类型

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

1. 引导类加载器(Bootstrap ClassLoader)

在上面类加载流程中,说到在 [启动虚拟机的过程中, 会创建一个引导类加载器的实例] 这个引导类加载器的目的是什么呢?加载类

引导类加载器主要负责加载最最核心的java类型。 这些类库位于jre目录的lib目录下**. 比如:rt.jar, charset.jar等,

2. 扩展类加载器(Ext ClassLoader)

扩展类加载器主要是用来加载扩展的jar包。 加载jar的目录位于jre目录的lib/ext扩展目录中的jar包

3. 应用程序类加载器(App CloassLoader)

主要是用来加载用户自己写的类的。 负责加载classPath路径下的类包

4. 自定义类加载器

负责加载用户自定义路径下的类包

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

2.2 案例

案例一:测试jdk自带的类加载器

package com.lxl.jvm;
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().getClass().getName());
        System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
    }
}

我们来看这个简单的代码, 运行结果:

null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader

解析:
 第一个: String 是jdk自身自带的类, 所以, 他的类加载器是引导类加载器,引导类加载器是c++代码,所以这里返回null
 第二个: 加密类的classloader, 这是jdk扩展包的一个类, jdk扩展包里面使用的是extClassLoader类加载器加载的
 第三个: 是我们当前自己定义的类, 会被AppClassLoader应用程序加载器加载.

我们看到ExtClassLoader和AppClassLoader都是Launcher类的一部分. 那Launcher类是什么东西呢?

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

那么,第一个bootstrap引导类加载器, 那引导类加载器返回的为什么是null呢?

因为bootstrap引导类加载器, 他不是java的对象, 他是c++生成的对象, 所以这里是看不到的

案例二: BootstrapClassLoad和ExtClassLoader、AppClassLoader的关系

1. JVM核心类加载器及类加载的全过程-LMLPHP

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

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

我们看到Lanucher.java类是在核心的rt.jar包里的,Lanucher是非常核心的一个类。

1. JVM核心类加载器及类加载的全过程-LMLPHP

我们看到getLauncher()类直接返回了launcher. 而launcher是一个静态对象变量, 这是一个单例模式

C++调用了getLauncher()-->直接返回了lanucher对象, 而launcher对象是在构建类的时候就已经初始化好了. 那么,初始化的时候做了哪些操作呢?接下来看看他的构造方法.

1. JVM核心类加载器及类加载的全过程-LMLPHP

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

1. JVM核心类加载器及类加载的全过程-LMLPHP

这是一个典型的多线程同步的写法。

在这里, 判断当前对象是否初始化过, 如果没有, 那么就创建一个ExtClassLoader()对象, 看看createExtClassLoader()这个方法做了什么事呢?

1. JVM核心类加载器及类加载的全过程-LMLPHP

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

1. JVM核心类加载器及类加载的全过程-LMLPHP

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

1. JVM核心类加载器及类加载的全过程-LMLPHP

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

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

1. JVM核心类加载器及类加载的全过程-LMLPHP

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

1. JVM核心类加载器及类加载的全过程-LMLPHP

这就是传递过来的parent类加载器, 那么这里的parent类加载器为什么是null呢? 因为, ExtClassLoader的父类加载器是谁呢? 他是Bootstrap ClassLoader. 而BootStrap ClassLoader是C++的类加载器, 我们不能直接调用它, 所以, 设置为null.

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

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

1. JVM核心类加载器及类加载的全过程-LMLPHP

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

1. JVM核心类加载器及类加载的全过程-LMLPHP

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

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

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

1. JVM核心类加载器及类加载的全过程-LMLPHP

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

1. JVM核心类加载器及类加载的全过程-LMLPHP1. JVM核心类加载器及类加载的全过程-LMLPHP

最后, 我们看到, 将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);
}

通过这个demo, 我们也可以看出, appClassLoader的父类是extClassLoader, extClassLoader的父类是bootstrapClassLoader

输出结果:

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

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

09-30 21:39