深入理解类加载机制:拨开迷雾见真章

2020-09-01 09:46:12 浏览数 (1)

Java语言将封装性表现的淋漓尽致,程序员在写Java代码的时候根本不用考虑自己写的代码在后期运行时是如何被JVM加载到内存中的,但是想告别CRUD,进阶为一名高级程序员的话,JVM的类加载机制必须了然于心,本文将详解JVM的类加载机制,文中涉及的代码均为JDK8版本,所涉及的Java虚拟机均指HotSpot。

一、理解类加载机制
1.1 理解类加载阶段

我们平常写的Java代码是存储在.java文件中,这是一个文本文件,是不能直接执行的,但是这个文本文件可以被编译成为一个字节码文件(后缀为.class),这个字节码文件中描述了类的元信息,比如类的定义、属性及方法等。JVM可以将这个字节码文件从磁盘中(当然也可以来源于网络、数据库或者是动态产生的字节码二进制流)读取到内存中,并对这个字节码中的数据进行校验、解析和初始化,最后形成一个可以被JVM直接使用的类型,这个整个的读取、校验、解析、初始化的过程称为类的加载。

从Java虚拟机规范中可以了解到,类的加载通常有5个阶段,分别是加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)以及初始化(Initialization),如果将类的使用(Using)和卸载(Unloading)也归纳进来的话,7个阶段可以理解为类的生命周期。

本文主要理解类的加载机制,所以这里主要阐述类加载的五个阶段。

  • 加载

加载阶段是整个类加载机制的第一个阶段,Java虚拟机在这个阶段主要做了三件事: 1)按照指定的全限定类名来获取指定类的二进制字节流,将其读取到内存中。这里所谓的二进制流,来源方式很多,比如来自于压缩文件jar、war、zip等,也可以来源于网络,比如Web Applet,还可以来源于动态生成的字节码,比如动态代理技术生成的代理对象的字节码,这种广泛的来源方式使得Java语言编写的程序更加灵活。 2)将字节流中的数据结构转换成运行时的JVM所定义的数据结构,存放在指定区域(JDK7中存放于方法区,JDK8存放在元数据空间,元数据空间是堆外内存,可防止OOM)。 3)JVM在堆(Heap)中生成一个对应于此类的Class对象,并将一些静态变量存入到Class对象中(JDK8)。这个Class对象是面向Java程序,用于访问元数据空间的类元信息的接口。

  • 验证

验证作为连接阶段的第一步,主要目的是为了校验Class文件的二进制字节流包含的数据是否满足JVM规范,验证阶段可以防止一些恶意字节流加载到内存中,从而危害了虚拟机的安全。验证阶段的主要内容包括文件格式验证、元数据验证、字节码验证和符号引用验证。

  • 准备

准备阶段主要工作是为类变量,也就是静态变量分配内存空间并设置初始值的阶段,在这个阶段,JVM将为每个类的静态变量开辟空间,并设置初始值。JVM在哪里开辟空间,不同版本也不同,JDK7以前的HotSpot在方法区中开辟空间用来存储类变量,方法区是一个逻辑区域,具体是由永久代来实现的,在JDK8版本后,元数据空间取代了方法区,类的元数据信息存储在元数据空间,但是静态变量随着Class对象存储到了堆中。除此之外,还需要注意初始值的设置问题,比如static int value = 1,那么初始值为0,设置为1的阶段是初始化阶段,这是因为准备阶段不会去执行任何方法,而1的赋值指令放在了类构造器<clinit>()方法中,而这个方法是在初始化的阶段才被执行,所以准备阶段静态变量只有初始值而没有被正确赋值。假设这个静态变量被final修饰了,那么编译器在编译Java代码的时候,会为value生成一个标记为常量值的属性ConstantValue,准备阶段检测到这个属性后,就会将vlaue的值直接设置为1

  • 解析

解析阶段主要工作是将常量池中的符号引用替换成直接引用。理解这句话,得先理解何为符号引用,何为直接引用。所谓符号,可以是任何形式的字面量,但是要求该符号可以唯一定位目标。符号引用在Class文件中以CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_info等类型的常量出现,在解析之前,Class中引用是以这些常量来标记的,它与JVM实现的内存布局无关,且引用的内容不一定是已经加载到虚拟机内存中的数据。直接引用是指可以指向目标的指针、相对偏移量或者一个可以间接定位到目标的句柄,它与JVM实现的内存布局息息相关,表现为同一个符号引用在不同的虚拟机实例上解析出来的直接引用一般不同。直接引用锁指向的目标要求是已经加载到虚拟机内存中的内容。

  • 初始化

初始化阶段是类加载的最后一个阶段。在整个类加载的五个阶段里,第一个阶段是允许开发者开发自定义类加载器(Custom ClassLoader)去加载指定的类,其他的几个阶段都是由虚拟机来主导完成的,这些阶段不会受到任何外界因素的影响。到了初始化阶段,虚拟机开始调用开发者在类中编写的代码,去完成静态变量的赋值操作。在准备阶段,静态变量(被final修饰的除外)均被赋值为变量类型的默认值,那么在初始化阶段,这些静态变量将被赋予代码中定义的值。读者读到这里,也许有个疑问,这些静态变量的值是如何被正确赋值的呢?其实编译器在编译代码源文件的时候,会主动收集代码中的静态变量和静态代码块,编译生成一个<clinit>()方法,方法中的内容就是按照源文件从上到下的静态变量的赋值操作语句和静态代码块的执行语句编译后的代码。而初始化阶段其实就是来执行<clinit>()方法,使用这个方法来进行静态变量的赋值操作。

这里举一个案例,代码如下:

代码语言:javascript复制
package cn.itlemon.naixue.reflection.classload;

public class ClInitTest {

    private static int index;

    static {
        index = 1000;
    }

}

编译后的字节码如下所示:

代码语言:javascript复制
public class cn/itlemon/naixue/reflection/classload/ClInitTest {

  // compiled from: ClInitTest.java

  // access flags 0xA
  private static I index

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcn/itlemon/naixue/reflection/classload/ClInitTest; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 12 L0
    SIPUSH 1000
    PUTSTATIC cn/itlemon/naixue/reflection/classload/ClInitTest.index : I
   L1
    LINENUMBER 13 L1
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 0
}

字节码中生成了<clinit>()方法,初始化阶段主要就是执行这个方法。

这里额外补充关于初始化的几个特点,理解了这几个特点,有助于加深对整个类加载机制的理解。

  • <clinit>()方法是编译器收集代码中的静态变量(非final修饰)和静态代码块后生成的方法,它的主要作用是在类加载过程中的初始化阶段被调用,用于对静态变量进行赋值以及一些其他的初始化操作(这些操作可能写在静态代码块中)。不同于类的构造方法,类的构造方法在字节码层面是<init>()方法,<clinit>()方法无需显式调用父类的<clinit>()方法,虚拟机会保证在子类的<clinit>()方法,父类的<clinit>()方法已经被执行。这就解释了类加载过程中为何父类的静态变量和静态代码块优先于子类的静态变量和静态代码块执行。
  • <clinit>()方法不是必需的,如果一个类没有静态变量和静态代码块,那么在编译的时候就不会去生成这个方法。
  • 接口的初始化和普通类的初始化有点区别,当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化是,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

类加载机制的五个阶段中,只有第一个加载阶段是可以由开发者来介入的,其他阶段都是有虚拟机主导的,最后一个初始化阶段虚拟机开始调用开发者写的代码。我们将在第二节里一起深入探讨一下类加载的第一个阶段。为了文章的完整性,接下来将JVM规范中规定的6个类加载时机也写在下文中,方便读者理解。

1.2理解类加载时机

《Java虚拟机规范》没有明确规定规定类加载器在加载类的第一个阶段要遵循的规范,这个阶段可以由具体的虚拟机来自主实现,但是对类的初始化阶段有了明确的规定,《Java虚拟机规范》规定了有且仅有6种场景需要对类进行实时初始化(这里提示一下:初始化阶段就是静态变量赋值和静态代码块执行的阶段)。

  • 通过new关键字创建类的实例,或者访问、修改某个类或接口的静态变量(不包含final修饰)值,或者调用类的静态方法都会触发类的初始化。
  • 利用反射机制调用类的某些变量或方法时,如果类没有被初始化,那么需要进行初始化。
  • 初始化一个类的子类,如果父类没有初始化,那么会首先初始化父类。
  • JVM启动会去主动初始化包含main方法的类。
  • JDK7中提供了动态语言的支持,如果一个类的静态方法被解析为java.lang.invoke.MethodHandle实例,且这个类没有初始化,那么将触发其初始化。这一点比较晦涩难懂,我们来举个例子说明这一知识点。

这里提供一个类MethodHandleMainClass,代码如下:

代码语言:javascript复制
public class MethodHandleMainClass {

    private static int index;

    static {
        index = 10;
        System.out.println("MethodHandleTest init!");
    }

    public static void method(String message) {
        System.out.println("Print Message:"   message);
    }

}

这个类如果初始化,那么将打印出MethodHandleTest init!,测试类如下所示:

代码语言:javascript复制
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

/**
 * @author jiangpingping
 * @date 2020/7/16 22:20
 */
public class MethodHandleTest {

    public static void main(String[] args) {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        try {
            MethodHandle method = lookup.findStatic(MethodHandleMainClass.class, "method",
                    MethodType.methodType(void.class, String.class));
            method.invoke("Test init!");
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

}

最后控制台打印的结果如下所示:

代码语言:javascript复制
MethodHandleTest init!
Print Message:Test init!

从打印内容得知,类MethodHandleMainClass进行了初始化。

  • JDK8中接口加入了默认方法,如果这个接口的实现类发生了初始化,那么这个接口也将在实现类初始化之前进行初始化。

这6种场景下,JVM会触发类的类的初始化,这些行为称之为对类的主动引用,除此之外的场景,都不会去触发类的初始化,这些场景的行为称之为对类的被动引用。这里简单举一个例子来理解一下被动引用,我们一起设计一个场景,这个场景是在上述6种场景之外的,比如:子类引用父类的静态变量,不会引发子类的初始化。

代码语言:javascript复制
public class ParentClass {

    protected static int index = 10;

    static {
        System.out.println("Parent Class init!");
    }

}

public class SonClass extends ParentClass {

    static {
        System.out.println("Son Class init!");
    }

}

测试类代码如下所示:

代码语言:javascript复制
public class InitTest {

    public static void main(String[] args) {
        // 通过子类来引用父类静态变量
        System.out.println(SonClass.index);
    }
}

运行测试类,输出的结果如下所示:

代码语言:javascript复制
Parent Class init!
10

结果显示父类进行了初始化,但是子类没有被初始化,这是一个典型的被动引用的案例。

二、理解类加载器
2.1 理解类唯一性的确定标准

类加载器的主要作用时间段就是在类的加载阶段,每一个类都需要使用类加载器来进行加载才可以被虚拟机所识别,类加载器是JVM的一项创新,它允许开发者自定义类加载器来加载特定的类,这就给了开发者足够的自定义权限,可以应付更多的业务场景。虽然类加载器的作用阶段仅限于加载阶段,但是其作用却影响了类在JVM堆内存中的唯一性。我们都知道一点,虚拟机启动的时候会去加载一些Class文件(不仅仅是文件,也可以是网络流,动态生成的Class等)到内存中,后续基于这些类创建对象,那么一个类被加载到内存中,使用同一个类加载器多次加载是否会产生不同Class对象呢?不同加载器加载的Class文件产生的Class对象是否一样呢?我们带这个问题通过代码来验证一下。

创建一个自定义类加载器,重写父类的loadClass方法,具体代码如下:

代码语言:javascript复制
public class CustomClassLoader extends ClassLoader {

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            String fileName = name.substring(name.lastIndexOf(".")   1)   ".class";
            // 从classpath下加载指定的类
            InputStream is = getClass().getResourceAsStream(fileName);
            if (is == null) {
                return super.loadClass(name);
            }
            byte[] bytes = new byte[is.available()];
            is.read(bytes);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }
    }
}

首先我们验证第一个问题,那就是同一个类加载器多次加载同一个类,是否产生多个Class对象,测试代码如下:

代码语言:javascript复制
public class ClassLoaderTest {

    public static void main(String[] args) throws ClassNotFoundException {
        CustomClassLoader loader = new CustomClassLoader();
        Class<?> aClass1 = loader.loadClass(ClassLoaderTest.class.getName());
        Class<?> aClass2 = loader.loadClass(ClassLoaderTest.class.getName());
        System.out.println(aClass1 == aClass2);
    }

}

实验的结果是报错了,报错内容如下所示:

代码语言:javascript复制
Exception in thread "main" java.lang.LinkageError: loader (instance of  cn/itlemon/naixue/reflection/classloader/CustomClassLoader): attempted  duplicate class definition for name: "cn/itlemon/naixue/reflection/classloader/ClassLoaderTest"
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at cn.itlemon.naixue.reflection.classloader.CustomClassLoader.loadClass(CustomClassLoader.java:25)
	at cn.itlemon.naixue.reflection.classloader.ClassLoaderTest.main(ClassLoaderTest.java:14)

根据上述报错内容,可以知道,JVM不允许同一个加载器重复加载同一个类,也就是说,JVM类加载器在同一个虚拟机实例中不可以去多次加载同一个类,否则会出现错误。

我们继续验证第二个问题,那就是不同的类加载器记载同一个类,产生的Class对象是否一样。测试代码如下:

代码语言:javascript复制
public class ClassLoaderTest {

    public static void main(String[] args) throws ClassNotFoundException {
        // 首先打印出main方法中ClassLoaderTest的类加载器
        System.out.println("ClassLoaderTest默认加载器是:"   ClassLoaderTest.class.getClassLoader());
        CustomClassLoader loader = new CustomClassLoader();
        // 使用我们自定义的类加载器再次加载一遍
        Class<?> aClass = loader.loadClass(ClassLoaderTest.class.getName());
        System.out.println("ClassLoaderTest加载器是:"   aClass.getClassLoader());
        System.out.println("不同类加载器加载的ClassLoaderTest的Class是否相等:"   aClass.equals(ClassLoaderTest.class));
    }

}

在执行main方法前,ClassLoaderTest类已经加载到了内存中,且默认是由应用类加载器(Application ClassLoader)加载到内存中,我们再次使用自定义类加载器(CustomClassLoader)去加载ClassLoaderTest,产生的Class对象比较之后的结果如下所示:

代码语言:javascript复制
ClassLoaderTest默认加载器是:sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoaderTest加载器是:cn.itlemon.naixue.reflection.classloader.CustomClassLoader@6bc168e5
不同类加载器加载的ClassLoaderTest的Class是否相等:false

结果中第一行展示的是ClassLoaderTest的默认加载器,第二行打印出了我们自定义的类加载器,第三行信息表明,由不同类加载器加载同一个类产生的Class对象是不同的,这也就说明,通过不同的类加载器加载同一个类,在内存中会存在两个类,属于两个独立的类。

总结上述的实验结果:一个类需要被加载到JVM中,JVM不允许由同一个类加载器多次加载同一个Class文件,不同的类加载器加载同一个Class文件,会在JVM中产生多个独立的类。一个类在虚拟机中的唯一性由两个条件来确定:类信息来自同一个Class文件,并且由同一个类加载器加载,这样加载出来的类在虚拟机中是唯一的,如果同一个Class文件被多个类加载器加载出来的类,必然是不同的类。

2.2 理解常见的类加载器

常见的类加载器有启动类加载器(Bootstrap ClassLoader)、拓展类加载器(Extension ClassLoader)、应用类加载器(Application ClassLoader)以及自定义类加载器(Custom ClassLoader)。

上图中不仅展示了常见的类加载器,还展示了『双亲委派机制』。双亲委派机制在JVM类加载系统中有着广泛的应用,它要求除了启动类加载器以外,其他所有的类加载器都应当有自己的父类加载器,也就是说,在现有的JVM实现中(这里指代HotSpot),启动类加载器没有自己的父类加载器,扩展类加载器和应用类加载器都有自己的父类加载器,其中启动类加载器是扩展类加载器的父类加载器,扩展类加载器是应用类加载器的父类加载器,而应用类加载器是自定义类加载器的父类加载器。这里需要注意一点,那就是这里的父类加载器并不是表明加载器之间存在继承关系,而是通过组合模式来实现的父类加载器的代码复用。

在讲解双亲委派机制之前,这里补充说明一下各个类加载器负责加载的内容:

  • 启动类加载器:主要的职责是加载JVM在运行是需要的核心类库,这个类加载器不同于其他类加载器,这个类加载器是由C 语言编写的,它是虚拟机本身的一个组成部分,负责将<JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,但是并不是所有放置在<JAVA_HOME>/lib路径下的jar都会被加载,从安全角度出发,启动类加载器只加载包名为java、javax、sun等开头的类。
  • 扩展类加载器:是指Sun公司实现的sun.misc.Launcher$ExtClassLoader类,这个类加载器是由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
  • 应用类加载器:是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath-D java.class.path指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器对象。

双亲委派机制原理其实很简单,如果一个类加载器被调用去加载一个类,那么这个类加载器如果有父类加载器,它将去调用父类加载器去加载这个类,如果父类加载器还有父类加载器,那么父类加载器将继续调用它的父类加载器去加载类,直到这个类加载请求到达启动类加载器(Bootstrap ClassLoader),启动类加载器尝试去加载这个类,如果启动类加载器可以加载这个类,那么就返回成功,否则这个类加载任务将由子类加载器去尝试加载,直到加载任务重新回到最初的类加载器手中,如果其可以加载,那么返回成功,否则将抛出ClassNotFoundException异常,这其中,只要类被任何一个类加载器加载,那么都是成功的。整个过程采用的是递归思想,后面我们一起来阅读源码来证明这个过程。

在阅读源码之前,我们先一起梳理一下各个类加载器之间的继承关系,本篇文章在一开始分析类加载器的时候就说过,常见的类加载器如启动类加载器(Bootstrap ClassLoader)、拓展类加载器(Extension ClassLoader)、应用类加载器(Application ClassLoader)以及自定义类加载器(Custom ClassLoader),它们之间不是父子类继承关系,而是通过组合和递归的方式实现代码复用的。接下来将各个类加载器在代码中的关系展示如下所示:

从上图中可以看出,拓展类加载器(Extension ClassLoader)和应用类加载器(Application ClassLoader)都继承自URLClassLoader,而URLClassLoader继承自SecureClassLoaderSecureClassLoader的父类ClassLoader是Java语言实现的类加载器的顶级抽象类,这里不包括启动类加载器(Bootstrap ClassLoader),因为这个类加载器是C 语言实现的类加载器,是JVM的一部分,其他的类加载器都是JDK提供的类加载器。从图中也很清晰地看得出来,拓展类加载器(Extension ClassLoader)和应用类加载器(Application ClassLoader)并非父子类关系,这也印证了之前的观点。

2.3 理解类加载器的重要方法

作为类加载器的顶级父类,ClassLoader实现了类在加载阶段的主要逻辑,包括双亲委派机制也在ClassLoader中表现的淋漓尽致,我们一起来看看ClassLoader中比较重要的loadClass方法。

代码语言:javascript复制
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

loadClass方法是将一个指定全限定名称的二进制Class文件加载到内存中,该方法实现了双亲委派机制,不建议开发者去重写这个方法,以免开发者自行实现的类加载逻辑破坏了双亲委派机制。loadClass(String name, boolean resolve)loadClass(String name)重载方法,参数boolean resolve代表是否生成Class对象的同时进行解析相关操作。

代码语言:javascript复制
protected Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
{
    // 通过类的全限定名称来获取synchronized关键字所需的锁对象
    synchronized (getClassLoadingLock(name)) {
        // 从缓存中根据类名称来查找是否已经加载过该类,这里使用到了native方法,稍后我们一起阅读以下OpenJDK8的源码,看看从缓存中查找的基本逻辑
        Class<?> c = findLoadedClass(name);
        if (c == null) {
        	// 进入到if内,说明该类是第一次被加载
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                	// 如果父类加载器不为空,将调用父类加载器的loadClass方法去加载指定的类,这里其实利用了递归的思想,直到parent指向了Bootstrap ClassLoader,此时Bootstrap ClassLoader的父类加载器为null,将进入到else代码块中
                    c = parent.loadClass(name, false);
                } else {
               		// 这里启用Bootstrap ClassLoader去尝试加载类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
			// 如果父类加载器加载的结果是null,将进入到if方法块中由子类加载器的findClass方法去尝试加载类,这也是每个递归都要判断的一个流程,如果某个子类加载器最后加载到了类,那么c将不为null,此时它的子类加载器将不会进入到if方法块中,这就保证了每个类只能被一个类加载器进行加载一次
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                // 由子类加载器自己去尝试加载类,这个方法在ClassLoader类中并没有实现,而是直接抛出ClassNotFoundException,URLClassLoader实现了该方法,该实现也是被Application ClassLoader和Extension ClassLoader所继承共用,稍后我们一起分析一下该方法的逻辑
                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) {
        	// 如果resolve是true,那么加载后将进行解析
            resolveClass(c);
        }
        return c;
    }
}

分析了ClassLoader类的loadClass方法,其基本逻辑其实很简单,就是当该方法接收到了类加载的请求,那么首先会去缓存中查找当前类是否被加载过,如果没有被加载过,那么将这个类加载的请求委派给自己的父类加载器去加载,父类加载器也会将这个请求委派给自己的父类加载器,这是递归的一种思想,直到这个类加载请求到达了Bootstrap ClassLoader,Bootstrap ClassLoader的父类加载器是null,那么将直接由Bootstrap ClassLoader去加载,也就是使用这个findBootstrapClassOrNull(name);方法去加载类,如果父类加载器加载失败,也就是加载的结果是class对象为null,那么将调用子类加载器的findClass方法去尝试加载类,如果所有的父类加载器都没有加载成功,那么将由自定义的类加载器去加载,加载的结果是成功或者抛出ClassNotFoundException异常。

我们在一起来分析一下findLoadedClass方法,该方法的作用是在缓存中查找指定的类是否被加载过,源码如下所示:

代码语言:javascript复制
protected final Class<?> findLoadedClass(String name) {
	// 检验名称是否有效
    if (!checkName(name))
        return null;
    // 调用native方法去查找缓存
    return findLoadedClass0(name);
}

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

从缓存中查找指定名称的类,具体逻辑需要去OpenJDK中查看findLoadedClass0方法的代码实现,其具体路径为:${OPEN_JDK}/jdk/src/share/native/java/lang/ClassLoader.c这里使用OpenJDK8的代码作为示例如下所示:

代码语言:javascript复制
JNIEXPORT jclass JNICALL
Java_java_lang_ClassLoader_findLoadedClass0(JNIEnv *env, jobject loader,
                                           jstring name)
{
    if (name == NULL) {
        return 0;
    } else {
        return JVM_FindLoadedClass(env, loader, name);
    }
}

这里真正的逻辑还是在JVM_FindLoadedClass方法中完成的,该方法位于${OPEN_JDK}/hotspot/src/share/vm/prims/jvm.cpp中,具体代码如下所示:

代码语言:javascript复制
JVM_ENTRY(jclass, JVM_FindLoadedClass(JNIEnv *env, jobject loader, jstring name))
  JVMWrapper("JVM_FindLoadedClass");
  // THREAD表示当前线程
  ResourceMark rm(THREAD);
  // 处理name为空的情况
  Handle h_name (THREAD, JNIHandles::resolve_non_null(name));
  // 根据类名获取对应的Handle
  Handle string = java_lang_String::internalize_classname(h_name, CHECK_NULL);

  const char* str   = java_lang_String::as_utf8_string(string());
  // 正常性检查,为空返回null
  if (str == NULL) return NULL;
  // 检查长度是否符合要求
  const int str_len = (int)strlen(str);
  if (str_len > Symbol::max_length()) {
    // It's impossible to create this class;  the name cannot fit
    // into the constant pool.
    return NULL;
  }
  TempNewSymbol klass_name = SymbolTable::new_symbol(str, str_len, CHECK_NULL);

  // Security Note:
  //   The Java level wrapper will perform the necessary security check allowing
  //   us to pass the NULL as the initiating class loader.
  // 获取类加载器对应的Handle
  Handle h_loader(THREAD, JNIHandles::resolve(loader));
  if (UsePerfData) {
    is_lock_held_by_thread(h_loader,
                           ClassLoader::sync_JVMFindLoadedClassLockFreeCounter(),
                           THREAD);
  }
  // 查找目标类对象是否存在
  Klass* k = SystemDictionary::find_instance_or_array_klass(klass_name,
                                                              h_loader,
                                                              Handle(),
                                                              CHECK_NULL);
   // 转换Klass为jclass并返回
  return (k == NULL) ? NULL :
            (jclass) JNIHandles::make_local(env, k->java_mirror());
JVM_END

以上的C 代码基本描述了从缓存中根据类名来查找Class对象的基本逻辑,如果从缓存中找到了Class对象,那么将直接返回,否则将返回null。如果在缓存中没有找到Class对象,那么说明当前Class是第一次被加载,那么就进入到了双亲委派基本逻辑中,类加载器往上委派类加载任务,直到委派到了Bootstrap ClassLoader手中,这时候将调用findBootstrapClassOrNull(String name)方法来进行尝试加载,该方法也是一个native方法,位于${OPEN_JDK}/hotspot/src/share/vm/prims/jvm.cpp中,具体代码如下所示:

0 人点赞