java类加载器机制原理简介(ClassLoader)

java内置了多种类加载器,Bootstrap、Extensions和应用程序类加载器,有时会扩展类加载器。了解类加载器的工作原理、委托模型、可见性和唯一性很有必要。

类加载器负责在运行时,将Java类动态加载到JVM(Java虚拟机),它们是JRE(Java运行时环境)的一部分。因此,JVM不需要知道底层文件或文件系统,就可以运行Java程序这全依赖于类加载器。此外,这些Java类不会同一时刻被加载到内存中,仅在应用程序需要时,类加载器才负责将类加载到内存中。

内置的类加载器

内置类加载器的类型

首先看看如何使用各种类加载器,用一个简单示例加载不同的类:

public void printClassLoaders() throws ClassNotFoundException {
 
    System.out.println("Classloader of this class:"+ PrintClassLoader.class.getClassLoader());
 
    System.out.println("Classloader of Logging:"+ Logging.class.getClassLoader());
 
    System.out.println("Classloader of ArrayList:"+ ArrayList.class.getClassLoader());
}

执行时,上面的方法打印:

Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
Class loader of ArrayList:null

可以看到,这里有三种不同的类加载器; 应用程序(application),扩展(extension)和引导程序(bootstrap显示为null)。

第一个应用程序类加载器,加载示例方法中的类,应用程序或系统类加载器,在类路径中加载我们自己编写的文件。

第二个扩展类加载器,用于加载Logging类,Logging是Java核心类的扩展。

最后bootstrap加载器加载ArrayList类,引导类加载器(bootstrap)/原始类加载器,是所有其他类加载器的父级。

可以看到最后一个的输出,对于ArrayList,它在输出中显示为null。这是因为引导类加载器是用本地代码编写的 - 因此它不会显示为Java类。由于这个原因,引导类加载器的行为,在不同版本的JVM(不同平台)之间会有所不同。

Bootstrap类加载器

Java类由java.lang.ClassLoader的实例加载。但是,类加载器本身就是类。因此,谁加载java.lang.ClassLoader类呢?

这是bootstrap类加载器的不同之处,它负责加载JDK内部的classes,通常是rt.jar和位于$ JAVA_HOME/jre/lib目录中的核心库。此外,Bootstrap类加载器,是所有ClassLoader实例的父级。

此引导类加载器是JVM核心的一部分,使用本地代码编写,如上所示,不同的平台对于这个类加载器的实现也不尽相同。

扩展类加载器

扩展类装入器是引导类装入器(Bootstrap)的子类,负责装载Java标准扩展类,这样,平台上运行的所有应用程序都可以使用它。扩展类加载器从JDK扩展目录加载,通常是$ JAVA_HOME/lib/ext目录,或java.ext.dirs系统属性中提到的目录。

应用程序类加载器

应用程序类加载器,负责将所有应用程序级别的类加载到JVM中。它加载类路径环境变量-classpath,或-cp命令行选项中找到的文件,它是Extensions classloader的子类。

类加载器如何工作

类加载器是Java运行时环境的一部分。当JVM请求类时,类加载器会尝试使用全类名来定位类,并将类定义加载到运行时环境。

java.lang.ClassLoader.loadClass()方法,负责加载类定义到运行时环境,它尝试基于全类名的方式加载类。如果类尚未加载,它会将请求委托给父类加载器,这个过程是递归的。如果父类加载器没有找到该类,那么子类将调用java.net.URLClassLoader.findClass()方法从自己的文件系统中查找该类。

如果,最后一个子类加载器也无法加载该类,则会抛出java.lang.NoClassDefFoundError或java.lang.ClassNotFoundException。看一下抛出ClassNotFoundException时的输出示例。

java.lang.ClassNotFoundException: com.xieyonghui.classloader.SampleClassLoader    
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)    
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)    
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)    
    at java.lang.Class.forName0(Native Method)    
    at java.lang.Class.forName(Class.java:348)

通过java.lang.Class.forName()调用的事件序列,它首先尝试通过父类加载器加载,最后由自己的java.net.URLClassLoader.findClass()来查找class。当它仍然没有找到类时,会抛出ClassNotFoundException。

类加载器三个重要特性

授权模型

类加载器遵循委托模型,在请求查找类或资源时,ClassLoader实例将类,或资源的搜索委托给父类加载器。假设要把一个应用程序中的类加载到JVM中,应用程序类加载器,首先,将该类的加载工作委托给其父类(扩展类加载器),扩展类加载器又将其委托给引导类加载器。只有当引导程序和扩展类加载器,在加载类不成功后,应用程序类加载器(system class loader)才会尝试自己去加载该类。

独一无二的Classes(唯一性)

按照委托模型的推论,类加载器总是尝试向上委托,因此,很容易确保类的唯一性。如果父类加载器无法找到该类,当前实例才会尝试自己去查找。

可见性

另外,父类加载器加载的类,对子类加载器是可见的。例如,应用程序类加载器加载的类,可以看到扩展类加载器和Bootstrap类加载器加载的类,但反之则不然。

如果类A由应用程序类加载器加载,类B由扩展类加载器加载,那么,只要是由应用程序类加载器装入的类,都可以看到A类和B类。因此,对于,由扩展类加载器加载的类,只能看到B类。

自定义ClassLoader

对于,已存在于文件系统中的文件而言(包含字节码的文件),内置的类加载器就足够了。但在需要从本地硬盘,或网络中加载类,可能需要使用自定义类加载器。

自定义类加载器

自定义类加载器不仅仅是在运行时加载类,还有一些其它的应用场景:

帮助修改现有的字节码,例如织入代理,根据用户需求动态创建适合的类,例如,在JDBC中,通过动态类加载,完成不同驱动程序之间的切换。实现加载同一个类的不同版本,可以通过URL类加载器(通过URL加载jar),或自定义类加载器来完成。这里有一个自定义类加载器的具体例子。

例如,浏览器使用自定义类加载器,从网站加载可执行内容。浏览器可以使用单独的类加载器,从不同的网页加载applet。用于运行applet的applet查看器包含一个ClassLoader,用于访问远程服务器上的站点,而不是查看本地文件系统。

然后通过HTTP加载原始字节码文件,并将它们转换为JVM中的类。即使这些applet具有相同的名称,如果,由不同的类加载器加载,它们也被视为不同的组件。

既然已经搞明白了,为什么自定义类加载器是相关的,那么就可以实现ClassLoader的子类,并总结一下JVM是如何加载类的。

创建自定义类加载器

为了便于说明,假设需要使用自定义类加载器,从文件中加载类(相对于从网络中或其他方式而言)。

扩展ClassLoader类并覆盖findClass()方法:

public class CustomClassLoader extends ClassLoader {
 
    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassFromFile(name);
        return defineClass(name, b, 0, b.length);
    }
 
    private byte[] loadClassFromFile(String fileName)  {
        InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
                fileName.replace('.', File.separatorChar) + ".class");
        byte[] buffer;
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        int nextValue = 0;
        try {
            while ( (nextValue = inputStream.read()) != -1 ) {
                byteStream.write(nextValue);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        buffer = byteStream.toByteArray();
        return buffer;
    }
}

示例中,定义了一个自定义类加载器,它扩展了默认的类加载器,并从指定的文件加载一个字节数组。

java.lang.ClassLoader

先看看java.lang.ClassLoader类中的一些基本方法,以清楚地了解它是如何工作的。

loadClass()方法

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException 

此方法负责在给定name参数的情况下加载类,name参数引用完全限定的类名。Java虚拟机调用loadClass()方法来解析类引用,将resolve设置为true,但是,并不总是需要resolve一个类,如果,我们只是想确定该类是否存在,则把resolve参数设置为false。

此方法作为类加载器的入口,可以尝试从java.lang.ClassLoader的源代码中,理解loadClass()方法的内部工作机制:

protected Class<?> loadClass(String name, boolean resolve)
  throws ClassNotFoundException {
     
    synchronized (getClassLoadingLock(name)) {
        // 首先, 检查这个类是否已经加载过了
        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
                }
 
                if (c == null) {
           //如果仍没有找到,则调用findClass,查找该类
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

该方法的默认实现按以下顺序搜索类:

1、调用findLoadedClass(String)方法以查看该类是否已加载。

2、调用父类加载器上loadClass(String)方法。

3、调用findClass(String)方法查找该类。

defineClass()方法

protected final Class<?> defineClass(
  String name, byte[] b, int off, int len) throws ClassFormatError

此方法负责将字节数组转换为类的实例。在我们使用该类之前,需要解析它。如果数据不包含有效类,则会抛出ClassFormatError。此外,我们无法覆盖此方法,因为它被标记为final。

findClass()方法

protected Class<?> findClass(
  String name) throws ClassNotFoundException

此方法以全类名作为参数进行查找,自定义类加载器需要覆盖此方法,该实现遵循委托模型。如果父类加载器找不到请求的类,loadClass()将调用此方法。如果类加载器的父级没有找到该类,则默认实现会抛出ClassNotFoundException。

getParent()方法

public final ClassLoader getParent()

此方法返回父类加载器以进行委派。一些实现类似于之前看到的,使用null来表示引导类加载器。

getResource()方法

public URL getResource(String name)

此方法尝试查找给定名称的资源。它将首先委托给资源的父类加载器。如果父项为null,则搜索内置到虚拟机中类加载器的路径。如果失败,则该方法将调用findResource(String)来查找资源。输入的资源名称,可以是类路径的相对或绝对值。

它返回一个用于读取资源的URL对象,如果找不到资源,或者调用者没有足够的权限,则返回null。值得注意的是,Java会从类路径中加载资源。最后,Java中的资源加载被认为是与位置无关的,因为只要环境设置了查找资源,代码运行的位置无关紧要。

上下文类加载器(Context Classloaders)

通常,上下文类装入器,为J2SE中引入的类委托方案提供了一种替代方法。就像之前看到的一样,JVM中的类加载器遵循层次模型,这样每个类加载器都有一个父类,引导类加载器除外。但是,当JVM核心类需要动态加载,开发人员提供的类或资源时,我们可能会遇到问题。

例如,在JNDI中,核心功能由rt.jar中的引导类实现。但是这些JNDI类,可以加载由独立供应商实现的JNDI程序(部署在应用程序类路径中)。此方案要求引导类加载器(父类加载器)加载的类,可以看见应用程序加载器(子类加载器)加载的类。对于这个问题J2SE委派模型在这里并不起作用,我们需要找到类加载的替代方法,可以使用线程上下文加载器来实现。

java.lang.Thread类有一个方法getContextClassLoader(),它为指定的线程返回一个ContextClassLoader,ContextClassLoader是线程的创建者,在加载资源和类时提供的。如果未设置该值,则类加载器上下文默认为父线程的上下文。