前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM笔记-类加载机制

JVM笔记-类加载机制

作者头像
WriteOnRead
发布2020-03-30 18:14:21
4530
发布2020-03-30 18:14:21
举报
文章被收录于专栏:WriteOnReadWriteOnReadWriteOnRead

JVM 不和包括 Java 在内的任何语言绑定,它只与 "Class文件" 这种特定的二进制文件格式所关联。而 Class 文件也并非只能通过 Java 源文件编译生成,可以通过如下途径而来:

JVM 把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称为虚拟机的「类加载机制」。

即Class 文件中描述的关于类的信息最终要加载到 JVM 中才能被运行和使用。

1. 类加载的时机

1.1 类的生命周期

一个类型(类或接口)从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期会经历加载(Loading)、验证(Verification)、准备(Prepare)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析统称为连接(Linking)。如图所示:

1.2 初始化时机

JVM 规范对于“加载”阶段并未强制约束。但对于“初始化”阶段,则规定有且仅有以下六种情况必须立即对其“初始化”:

  1. 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时。场景如下:
    1. 使用 new 关键字实例化对象;
    2. 读/写静态字段(static 修饰,无 final);
    3. 调用静态方法。
  2. 使用 java.lang.reflect 的方法对类型进行反射调用时。
  3. 初始化类时,若父类尚未初始化,需要先初始化其父类。
  4. 虚拟机启动时,需要先初始化用户指定的主类(main 方法所在类)。
  5. 使用 JDK 7 新加入的动态语言支持时,若一个 java.lang.invoke.MethodHandle 实例最后解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,且该方法句柄对应的类未初始化,需要先初始化【平时似乎没用到过,暂不深究,以后有机会再分析】。
  6. 接口中定义了 JDK 8 加入的默认方法(default 修饰)时,在该接口的实现类初始化之前,需要先初始化这个接口。

注意:当一个“类”在初始化时,要求其父类全都已经初始化;但是,一个“接口”在初始化时,并不要求父接口全都初始化,只有真正使用到父接口时才会初始化(比如引用接口定义的常量)。

1.3 主动引用&被动引用

上述六种情况的行为称为对一个类型的“主动引用”,而除此之外的其他所有引用类型方式都不会触发初始化,称为“被动引用”。被动引用举例如下:

  • 示例代码
public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;
    public static final String HELLO_WORLD = "hello, world";
}

public class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
}

PS: 为了跟踪类加载信息,可配置虚拟机参数 -XX:+TraceClassLoading

  • eg1
/**
 * 通过子类引用父类的静态字段,不会导致子类初始化
 */
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

/* 类加载情况:SubClass 和 SuperClass 均被加载
 * 
 * 输出结果(父类初始化,子类未初始化):
 * SupClass init!
 * 123
 */
  • eg2
/**
 * 通过数组定义来引用类,不会触发此类的初始化
 */
public class NotInitialization {
    public static void main(String[] args) {
        SuperClass[] superClasses = new SuperClass[10];
    }
}

/* 类加载情况:SuperClass 被加载
 * 输出结果为空,SuperClass 未初始化 
 */
  • eg3
/**
 * 常量在【编译阶段】会存入调用类(NotInitialization)的常量池中,
 * 本质上并没有直接引用到定义常量的类,因此不会触发其初始化
 */
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SuperClass.HELLO_WORLD);
    }
}

/* 类加载情况:SubClass 和 SuperClass 均未被加载
 *
 * 输出结果:
 * hello, world
 */

编译阶段通过常量传播优化,已将该常量的值("hello, world")直接存储在 NotInitialization 类的常量池中,以后 NotInitialization 对常量 SuperClass.HELLO_WORLD 的引用实际都被转化为对自身常量池的引用了。

PS: 其实 NotInitialization 类的 Class 文件中并不存在 SuperClass 类的符号引用入口,这两个类在编译成 Class 文件之后就没联系了。

2. 类加载过程

2.1 加载

加载阶段,JVM 主要做了三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流;
  2. 将该字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在(堆)内存中生成一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

PS: 二进制字节流的来源有很多,例如:从 ZIP 压缩包读取、从网络获取、运行时计算生成(动态代理),从加密文件读取等。

需要注意的是,数组类的加载情况有所不同:数组类本身不通过类加载器创建,而是由 JVM 直接在内存动态构造(newarray 指令)。它的创建过程遵循以下原则:

  • 若数组的组件类型(数组去掉一个维度)为引用类型,则递归加载该组件类型;
  • 若数组的组件类型不是引用类型(例如 int[] 组件类型为 int),JVM 会把数组标记为与引导类加载器关联;
  • 数组类的可访问性与其组件类型的可访问性一致(若组件类型不是引用类型,可访问性默认为 public)。
2.2 验证

主要目的:确保 Class 文件信息符合 JVM 规范,防止恶意代码危害虚拟机自身安全。

有点类似我们平时开发接口时的参数校验,不能因为入参问题把程序搞崩溃了。

该阶段大致会完成下面四个阶段的验证:文件格式验证、元数据验证、字节码验证和符号引用验证。

2.2.1 文件格式验证

验证字节流是否符合 Class 文件格式的规范,且能被当前虚拟机处理。主要验证:

  • 是否以魔数 0xCAFEBABY 开头;
  • 主次版本号是否在当前虚拟机处理范围内;
  • ……

PS: 该阶段是基于二进制字节流进行的,验证通过之后才允许进入 JVM 的方法区。而后面的验证都是基于方法区的存储结构进行的,不再直接读取字节码。

2.2.2 元数据验证

对类的元数据信息进行语义校验,确保不违背 Java 语言规范。比如:

  • 一个类是否有父类;
  • 该父类是否继承了 final 修饰的类;
  • ……
2.2.3 字节码验证

该阶段最复杂,主要是数据流分析和控制流分析,确定语义合法、符合逻辑。验证点如下:

  • 操作数栈的数据类型与指令代码序列能配合工作;
  • 跳转指令不会跳到方法体以外的字节码指令上;
  • 类型转换有效;
  • ……
2.2.4 符号引用验证

发生在虚拟机将符号引用转为直接引用时(即后面的解析阶段),确保解析动作能正常执行。验证点如下:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类;
  • 符号引用中的类、字段、方法的可访问性验证;
  • ……

验证阶段虽然很重要,却并非必须执行。若程序代码已被反复使用和验证,可以考虑关闭大部分类验证,以缩短类加载的时间。JVM 参数:

-Xverify:none
2.3 准备

主要目的:为类变量(即 static 修饰的静态变量)分配内存并设置初始值。

初始值“通常”情况指的是零值,基本数据类型的零值如下:

// 经过「准备」阶段后,该初始值为 0
// 而把 value 赋值为 123 是在后面的「初始化」阶段
public static int value = 123;

注意,上面的“通常”不包含一种情况,即静态变量被 final 修饰的时候,例如:

public static final int value = 123;

编译阶段会为 value 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 将其设置为 123.

2.4 解析

主要动作:把常量池内的符号引用替换为直接引用。

该阶段主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行。

2.4.1 符号引用

符号引用(Symbolic References):以一组符号描述所引用的目标,可以是任何形式的字面量(比如全限定类名)。引用的目标不一定加载到 JVM 内存中。

  • 代码示例

比如有两个 java 文件,分别为 A.java 和 B.java,如下:

public class A {
}

public class B {
  private A a;
}

其中 B 持有对 A 的引用,但此时两个类并未加载到内存中,仅仅是一个标记而已。

2.4.2 直接引用

直接引用(Direct References)可以是:

  1. 直接指向目标的指针;
  2. 相对偏移量(例如实例变量、实例方法);
  3. 能间接定位到目标的句柄。

直接引用就是能够直接在内存中找到相应对象(的内存地址)。若有直接引用,则目标必定已在虚拟机中。

2.5 初始化

初始化阶段就是执行类构造器 <clinit>() 方法的过程,<clinit>() 方法有如下特点:

  • 由编译器根据源文件中的顺序、自动收集类中的所有静态变量的赋值动作和静态代码块合并产生。
  • 此方法与类的构造方法(虚拟机视角中的实例构造器 <init>() 方法,也就是我们在代码中定义的构造器)不同,不需要显式地调用父类构造器,JVM 会保证子类 <clinit>() 方法执行前,父类 <clinit>() 方法已执行完。

PS: 与类不同的是,接口的方法不需要先执行父接口的 <clinit>() 方法。 接口的实现类在初始化时也不会执行接口的 <clinit>() 方法。

  • 该方法并不是必需的,若类中无静态语句块和对变量的赋值操作,编译器可以不生成这个方法。

接口中虽然不能使用静态代码块,却可以为变量始化赋值,因此也会生成 <clinit>() 方法。

  • JVM 必须保证一个类的 <clinit>() 方法在多线程环境被正确地加锁同步。如果多个线程同时去初始化一个类,只能有一个线程去执行 <clinit>() 方法,其他线程都要阻塞等待。

说到这里,设计模式的「单例模式」就有一种写法是利用该机制来保证线程安全性的,示例代码如下:

public class BeanFactory {
  private BeanFactory() {
  }

  public BeanFactory getBeanFactory() {
    return BeanFactoryHolder.beanFactory;
  }

  /**
   * 使用内部嵌套类实现单例,利用 JVM 的类加载机制可保证线程安全
   */
  private static class BeanFactoryHolder {
    private static BeanFactory beanFactory = new BeanFactory();
  }
}

3. 类加载器

所谓类加载器(Class Loader),其实就是一段代码。

这段代码的主要功能就是:通过一个类的全限定名来获取描述类信息的二进制字节流。

3.1 类与类加载器

对于任意一个类,都必须由其「类加载器」和「该类本身」共同确定它在 JVM 中的唯一性。

即,若要比较两个类是否相等,前提是这两个类必须是由同一个类加载器加载(后面代码进行验证)。

PS: 这里的“相等”,包括 equals、isAssignableFrom、isInstance 等方法,还有 instanceof 关键字。

3.2 双亲委派模型

类加载器的分类及其主要特点如下:

  • 启动类加载器(Bootstrap Class Loader)
    • 虚拟机的一部分(C++ 实现);
    • 负责加载 JAVA_HOME\lib 目录,或者 -Xbootclasspath 参数指定路径下,且被 JVM 识别的类库。
  • 扩展类加载器(Extension Class Loader)
    • 由 sun.misc.Launcher$ExtClassLoader 类实现;
    • 负责加载 JAVA_HOME\lib\ext 目录,或者 java.ext.dirs 系统变量指定的路径中的类库。
  • 应用程序类加载器(Application Class Loader)
    • 由 sun.misc.Launcher$AppClassLoader 类实现;
    • 加载用户类路径(ClassPath)下所有的类库;
    • 默认的系统类加载器(若应用程序没有自定义过类加载器,一般使用该类进行加载)。

若有必要,还可以加入自定义的类加载器进行扩展。

JDK 9 之前的 Java 应用都是由这三种类加载器互相配合完成加载的。它们之间的协作关系如图所示:

这种层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。

双亲委派模型的工作流程大致如下:

若一个类加载器收到了加载类的请求,它首先不会自己尝试去加载这个类,而是将其委派给父类加载器,父加载器亦是如此,直至启动类加载器;仅当父加载器无法加载该类的时候,子加载器才会尝试自己进行加载。

注意:这里的它们之间并非「继承」关系,通常是采用「组合」的方式。

3.2.1 实现源码

双亲委派模型的实现代码在 java.lang.ClassLoader 类的 loadClass 方法中,如下:

private final ClassLoader parent;

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 先检查请求的类是否已加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 若未加载过,调用父类加载器进行加载(父类加载器也会继续该过程)
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 使用启动类加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父类加载器无法完成加载
            }
            
            // 父类加载器未完成加载时,使用自身的 findClass 方法尝试加载
            if (c == null) {
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

// JDK 1.2 提供的
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
3.2.2 优点

为什么要采用双亲委派模型?这样做有什么好处呢?

一个好处就是:Java 类随着类加载器有了层级关系,把最基础的类,例如 java.lang.Object,交给最顶端的类加载器加载,保证在各个加载器环境中都是同一个 Object 类。

说到这里,有些面试题会问:如果自定义一个 java.lang.Object 类会怎样?

  • 自定义 java.lang.Object 类

这里做下测试,自定义一个 java.lang.Object 类:

package java.lang;

public class Object {
  public String toString() {
    return "hello";
  }

  public static void main(String[] args) {
    System.out.println("hello");
  }
}

如果能正常加载,这里会打印字符串 "hello",结果呢?会报错:

Error: Main method not found in class java.lang.Object, please define the main method as:
   public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application

错误原因是 main 方法未找到,就是我们自定义的方法未找到。

查看类加载信息:

[Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar]
...

可以发现,JVM 只加载了 rt.jar 中的 java.lang.Object ,并没有加载我们定义的这个 Object 类,而 rt.jar 中的 Object 是没有 main 方法的。

  • 自定义 java.lang.HelloWorld 类

如果我们定义一个全类名为 java.lang.HelloWorld 的类呢?

package java.lang;

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("hello");
  }
}

可以正常加载和运行吗?并不会!

异常如下:

Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
  at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
  at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
  at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
  at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
  at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
  ...

可以看到,java.lang 这个包名是禁止使用的。

3.3 破坏双亲委派模型

PS: “破坏双亲委派模型“这个概念刚开始听起来可能有些费解,尤其是这个”破坏“,至少我是这样。 其实呢,双亲委派模型可以理解为一个规范,然鹅,某些地方由于某些原因并未遵循这个规范。对于那些没有遵循该规范的地方,就是破坏了双亲委派模型。

总的来说,破坏双亲委派模型的行为大致有三次:

  • 第一次

由于“双亲委派模型”是 JDK 1.2 引入的,但类加载和 java.lang.ClassLoader 类在此之前就已经存在了,为了兼容已有代码,双亲委派模型做了妥协。

由于 ClassLoader 类的 loadClass 方法可以直接被子类重写,这样的类加载机制就不符合双亲委派模型了。

如何实现兼容呢?在 ClassLoader 类添加了 findClass 方法(代码见 3.2.1),并引导用户重写该方法,而非 loadClass 方法。

这就是第一次破坏双亲委派模型,其实就是兼容历史遗留问题。

  • 第二次

双亲委派模型的类加载都是自底向上的(越基础的类由越上层的加载器来加载),但有些场景可能会出现基础类型要反回来调用用户代码,这个场景如何解决呢?

一个典型的例子就是 JNDI (启动类加载器加载)服务,其目的是调用其它厂商实现并部署在应用程序 ClassPath 下的服务提供者接口(Service Provider Interface,SPI)。启动类加载器是不认识这些 SPI 的,如何解决呢?

Java 团队引入了一个线程上下文类加载器(Thread Context ClassLoader),可以设置类加载器,在启动类加载器不认识的地方,调用其它类加载器去加载。这其实也打破了双亲委派模型。

比如 JDBC 的类加载机制,后文再详细分析。

  • 第三次

第三次破坏是对程序动态性的追求导致的,代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。典型的如 IBM 的 OSGi 模块化热部署。

4. 代码示例

4.1 自定义类加载器
public class MyClassLoader extends ClassLoader {

  // 重写 findClass 方法
  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    byte[] classData = loadClassData(name);
    if (classData == null) {
      throw new ClassNotFoundException();
    }
    return defineClass(name, classData, 0, classData.length);
  }

  // 读取 class 文件
  private byte[] loadClassData(String className) {
    String fileName = "~/Code/Java/test/target/classes" +
        File.separatorChar + className.replace('.', File.separatorChar) + ".class";
    try {
      FileInputStream inputStream = new FileInputStream(fileName);
      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
      byte[] buffer = new byte[1024];
      int length;
      while ((length = inputStream.read(buffer)) != -1) {
        outputStream.write(buffer, 0, length);
      }
      return outputStream.toByteArray();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return null;
  }
}
4.2 双亲委派模型类加载
  • 自定义一个 Person 类
package loader;

public class Person {
  static {
    // 当 Person 类初始化时,会打印该代码
    System.out.println("Person init!");
  }

  private String name;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

使用上面自定义的类加载加载 Person 类:

private static void test1() throws Exception {
  // 创建类加载器实例
  MyClassLoader myClassLoader1 = new MyClassLoader();
  // 加载 Person 类(注意这里是 loadClass 方法)
  Class<?> aClass1 = myClassLoader1.loadClass("loader.Person");
  aClass1.newInstance(); // Person init!

  MyClassLoader myClassLoader2 = new MyClassLoader();
  Class<?> aClass2 = myClassLoader2.loadClass("loader.Person");
  aClass2.newInstance();
    
  System.out.println("--->" + aClass1.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
  System.out.println("--->" + aClass2.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
  System.out.println("--->" + aClass1.equals(aClass2)); // true
}

可以看到,这里虽然使用了两个类加载器实例加载 Person 类,但实际上 aClass1 和 aClass2 的类加载器并不是自定义的 MyClassLoader,而是 Launcher$AppClassLoader,即应用类加载器。为什么会是这个结果呢?

其实这就是前面分析的双亲委派模型,示意图如下:

大体流程分析:

  1. 使用 MyClassLoader 加载 Person 类时,它会先委托给 AppClassLoader;
  2. AppClassLoader 委托给 ExtClassLoader;
  3. ExtClassLoader 委托给启动类加载器;
  4. 但是,启动类加载器并不认识 Person 类,无法加载,于是就再反回来交给 ExtClassLoader;
  5. ExtClassLoader 也无法加载,于是交给了 AppClassLoader;
  6. AppClassLoader 可以加载 Person 类,加载结束。
4.2 非双亲委派模型类加载

上面演示了双亲委派模型加载一个类,如何破坏双亲委派模型呢?把上面的 loadClass 方法换成 findClass 就行,示例代码:

  • 测试类加载 eg.1
private static void test2() throws Exception {
  MyClassLoader cl1 = new MyClassLoader();
  // 加载自定义的 Person 类
  Class<?> aClass1 = cl1.findClass("loader.Person");
  // 实例化 Person 对象
  aClass1.newInstance(); // Person init!

  MyClassLoader cl2 = new MyClassLoader();
  Class<?> aClass2 = cl2.findClass("loader.Person");
  aClass2.newInstance(); // Person init!

  System.out.println("--->" + aClass1); // class loader.Person
  System.out.println("--->" + aClass2); // class loader.Person

  System.out.println("--->" + aClass1.getClassLoader()); // loader.MyClassLoader@60e53b93
  System.out.println("--->" + aClass2.getClassLoader()); // loader.MyClassLoader@1d44bcfa  
  
  System.out.println("--->" + aClass1.equals(aClass2)); // false
}

这里创建了两个自定类加载器 MyClassLoader 的实例,分别用它们来加载 Person 类。

虽然两个打印结果都是 class loader.Person ,但类加载器不同,导致 equals 方法的结果是 false,原因就是二者使用了不同的类加载器。

根据 MyClassLoader 的代码,这里实际并未按照双亲委派模型的层级结构去加载 Person 类,而是直接使用了 MyClassLoader 来加载的。

  • 测试类加载 eg.2

上述代码中,如果使用同一个类加载器进行加载呢?修改代码如下:

private static void test3() throws Exception {
  MyClassLoader cl1 = new MyClassLoader();
  Class<?> aClass1 = cl1.findClass("loader.Person");
  aClass1.newInstance();

  // 这里改用上面的类加载进行加载呢?
  Class<?> aClass2 = cl1.findClass("loader.Person");
  aClass2.newInstance();

  System.out.println("--->" + aClass1);
  System.out.println("--->" + aClass2);
  System.out.println("--->" + aClass1.equals(aClass2)); // true ??
}

这样的比较结果会是 true 吗?似乎应该是的吧。。

然而,这样会报错的:

Exception in thread "main" java.lang.LinkageError: loader (instance of  loader/MyClassLoader): attempted  duplicate class definition for name: "loader/Person"
  at java.lang.ClassLoader.defineClass1(Native Method)
  at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
  at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
  at loader.MyClassLoader.findClass(MyClassLoader.java:21)
  at loader.TestClassLoader.test1(TestClassLoader.java:61)
  at loader.TestClassLoader.main(TestClassLoader.java:10)

原因是:一个类加载器不能多次加载同一个类。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WriteOnRead 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 类加载的时机
    • 1.1 类的生命周期
      • 1.2 初始化时机
        • 1.3 主动引用&被动引用
        • 2. 类加载过程
          • 2.1 加载
            • 2.2 验证
              • 2.2.1 文件格式验证
              • 2.2.2 元数据验证
              • 2.2.3 字节码验证
              • 2.2.4 符号引用验证
            • 2.3 准备
              • 2.4 解析
                • 2.4.1 符号引用
                • 2.4.2 直接引用
              • 2.5 初始化
              • 3. 类加载器
                • 3.1 类与类加载器
                  • 3.2 双亲委派模型
                    • 3.2.1 实现源码
                    • 3.2.2 优点
                  • 3.3 破坏双亲委派模型
                  • 4. 代码示例
                    • 4.1 自定义类加载器
                      • 4.2 双亲委派模型类加载
                        • 4.2 非双亲委派模型类加载
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档