这篇文章,可以让你对Java中的类加载器了解很多。转载本文请联系程序新视界公众号。前言对于每个开发者来说,java.lang.ClassNotFoundExcetpion异常几乎都遇到过,而如果要寻找异常的来源,就不可避免地会谈到Java类加载器。本文将基于启动类加载器、扩展类加载器、系统类加载器和自定义类加载器为您补充这些知识。类加载器简介Java程序经过编译器编译,成为一个字节码文件(.class文件)。当程序需要某个类时,虚拟机会加载相应的类文件,并创建相应的Class对象。而这个将类文件加载到虚拟机内存中的过程就是类加载。类加载器负责在运行时将Java类动态加载到JVM(Java虚拟机)中,是JRE(JavaRuntimeEnvironment)的一部分。由于类加载器的存在,JVM不需要知道底层文件或文件系统来运行Java程序。Java类不会一次性全部加载到内存中,而是仅在应用程序需要它们时才加载。此时,类加载器负责将类加载到内存中。一个类加载过程类的生命周期通常包括:加载、链接、初始化、使用、卸载。上图包含类加载的三个阶段:加载阶段、链接阶段和初始化阶段。如果将这三个阶段进一步划分细化,则包括:加载、验证、准备、解析和初始化。关于这几个阶段的作用已经有很多文章写过,这里简单介绍一下:加载:通过完全限定的类找到类字节码文件,在方法区转换成运行时数据结构,并创建一个具有代表性的此类的类对象。验证:确保Class文件的字节流所包含的信息符合当前虚拟机的要求,不会危及虚拟机本身的安全。准备工作:为类变量(即static修饰的字段变量)分配内存,并设置类变量的初始值。final修饰的静态变量不包括在内,因为它已经在编译时分配。解析:将常量池中的符号引用转换为直接引用的过程。如果符号引用指向一个没有被加载的类,或者是一个没有被加载的类的字段或方法,那么解析就会触发那个类的加载。初始化:在类加载的最后阶段,如果类有父类,则对其进行初始化,执行静态初始化器,静态初始化成员变量。在上述类加载过程中,虚拟机内部提供了三个类加载器:引导类加载器、扩展(Extension)类加载器和系统(System)类加载器(也称为应用程序类加载器)。下面讨论不同类型的内置类加载器如何工作,以及如何自定义类加载器。内置类加载器让我们从一个简单的例子开始,看看如何使用不同的加载器来加载不同的类:out.println("ClassloaderofLogging:"+Logging.class.getClassLoader());System.out.println("ClassloaderofArrayList:"+ArrayList.class.getClassLoader());}执行上面的程序,打印如下内容:Classloaderofthisclass:sun.misc.Launcher$AppClassLoader@18b4aac2ClassloaderofLogging:sun.misc.Launcher$ExtClassLoader@2f0e140bClassloaderofArrayList:null以上三行输出分别对应三种不同的类加载器:系统(System)类加载器、扩展(Extension)类加载器和Bootstrap类加载器(显示为null)。系统程序类加载器加载包含示例方法的类,即我们自己的文件加载到类路径中。扩展类加载器加载Logging类,即标准核心Java类的扩展类。启动类加载器加载ArrayList类,它是所有其他类的父类。对于ArrayList的类加载器,输出为null。这是因为启动类加载器是用本机代码而不是Java实现的,所以它不会显示为Java类。启动类加载器在不同的JVM中运行不同。以上三个类加载器,加上自定义类加载器,它们的直接关系可以用下图来表示:下面我们来详细了解一下这几个类加载器。Bootstrap类加载器Java类由java.lang.ClassLoader的实例加载。然而,类加载器本身就是类。那么,谁来加载java.lang.ClassLoader呢?没错,就是启动类加载器。启动类加载器主要负责加载JDK内部类,通常是$JAVA_HOME/jre/lib目录下的rt.jar等核心库。此外,Bootstrap类加载器还充当所有其他ClassLoader实例的父类。启动器类加载器是Java虚拟机的一部分,用本机代码(例如C++)编写,并且可以在不同平台上以不同方式实现。出于安全考虑,Bootstrap启动类加载器只加载包名以java、javax、sun等开头的类。扩展类加载器扩展类加载器是启动类加载器的子类,用Java语言编写,由sun实现.misc.Launcher$ExtClassLoader。父类加载器是启动类加载器,负责加载标准核心Java类的扩展。扩展类加载器自动从JDK扩展目录(通常是$JAVA_HOME/lib/ext目录)或java.ext.dirs系统属性中指定的任何其他目录加载。系统类加载器系统类加载器负责将所有应用程序级类加载到JVM中。它加载在类路径环境变量、-classpath或-cp命令行选项中找到的文件。它是扩展类加载器的子类。系统类加载器,又称应用程序加载器,是指Sun公司实现的sun.misc.Launcher$AppClassLoader,负责加载系统类路径-classpath或-Djava指定路径下的类库。class.path,也就是我们经常使用的classpath路径,开发者可以直接使用系统类加载器。一般这个类加载器是程序中默认的类加载器,可以通过ClassLoader#getSystemClassLoader()方法获取类加载器。类加载器如何工作类加载器是Java运行时环境的一部分。当JVM请求一个类时,类加载器将尝试定位该类并使用完全限定名将类定义加载到运行时中。java.lang.ClassLoader.loadClass()方法负责将类定义加载到运行时,它尝试通过其完全限定名称加载类。如果没有加载到类中,那么它将请求委托给父类加载器。依次向上重复这个过程。最后,如果父类加载器找不到指定的类,子类将调用java.net.URLClassLoader.findClass()方法在文件系统本身中查找类。如果最后一个子类加载器也无法加载该类,则会抛出java.lang.NoClassDefFoundError或java.lang.ClassNotFoundException。抛出ClassNotFoundException时的示例输出:java.lang.ClassNotFoundException:com.baeldung.classloader.SampleClassLoaderatjava.net.URLClassLoader.findClass(URLClassLoader.java:381)atjava.lang.ClassLoader.loadClass(ClassLoader.java:424)atjava。lang.ClassLoader.loadClass(ClassLoader.java:357)atjava.lang.Class.forName0(NativeMethod)atjava.lang.Class.forName(Class.java:348)以上过程通常称为双亲委托机制。双亲委派机制要求除了顶层的启动类加载器,其余的类加载器都应该有自己的父类加载器。请注意,双亲委托机制中的父子关系并不是通常所说的类继承关系,而是复用父类加载器相关代码的组合关系。此外,类加载器还具有三个重要功能:委托模型、类的唯一性和可见性。委托模型类加载器遵循委托模型,在该模型中,根据请求查找类或资源,ClassLoader实例将类或资源的搜索委托给父类加载器。假设我们有一个将应用程序类加载到JVM中的请求。系统类加载器首先将类的加载委托给它的父扩展类加载器,后者又将其委托给引导类加载器。只有在启动类加载器和扩展类加载器都没有成功加载类时,系统类加载器才会尝试加载类本身。类的唯一性作为委托模型的结果,很容易确保类的唯一性,因为总是尝试向上委托。如果父类加载器找不到该类,则只有当前实例本身会尝试查找并加载它。可见性此外,子类加载器加载的类对其父类加载器是可见的。例如,系统类加载器加载的类对扩展和引导类加载器加载的类具有可见性,反之亦然。比如A类是通过系统类加载器加载的,B类是通过扩展类加载器加载的,那么A类和B类对于系统类加载器加载的其他类都是可见的。但是类B是扩展类加载器加载的其他类唯一可见的类。自定义类加载器在大多数情况下,如果文件已经在文件系统中,内置类加载器就足够了。但是,在需要从本地硬盘或网络加载类的情况下,可能需要使用自定义类加载器。下面介绍自定义类加载器的使用。自定义类加载器示例自定义类加载器不仅有助于在运行时加载类,还有一些特殊的场景:帮助修改现有的字节码,比如编织代理;动态创建适合用户需要的类。例如,在JDBC中,不同驱动程序实现之间的切换是通过动态类加载完成的。当为具有相同名称和包的类加载不同的字节码时,实现类版本控制机制。这可以通过URL类加载器(通过URL加载jar)或自定义类加载器来完成。举一个更具体的例子,比方说,浏览器使用自定义类加载器从网站加载可执行内容。浏览器可以使用不同的类加载器从不同的网页加载小程序。用于运行applet的applet查看器包含一个ClassLoader,它可以在不查看本地文件系统的情况下访问远程服务器上的网站。然后通过HTTP加载原始字节码文件并在JVM中转换为类。即使这些小程序具有相同的名称,但如果它们由不同的类加载器加载,它们将被视为不同的组件。现在我们明白了为什么自定义类加载器是相关的,让我们实现一个ClassLoader的子类来扩展和总结JVM如何加载类。创建自定义类加载器。自定义类加载器通常继承java.lang.ClassLoader类并重写findClass()方法:length);}privatebyte[]loadClassFromFile(StringfileName){InputStreaminputStream=getClass().getClassLoader().getResourceAsStream(fileName.replace('.',File.separatorChar)+".class");byte[]buffer;ByteArrayOutputStreambyteStream=newByteArrayOutputStream();intnextValue=0;try{while((nextValue=inputStream.read())!=-1){byteStream.write(nextValue);}}catch(IOExceptione){e.printStackTrace();}buffer=byteStream.toByteArray();returnbuffer;}}在上面的示例中,我们定义了一个自定义类加载器,它扩展了默认类加载器并从指定的文件数组加载字节。如果没有复杂的需求,可以直接继承URLClassLoader类,重写loadClass方法。具体请参考AppClassLoader和ExtClassLoader。了解java.lang.ClassLoader让我们看一下java.lang.ClassLoader类中的一些基本方法,以便更清楚地了解它的工作原理。loadClass方法publicClass>loadClass(Stringname,booleanresolve)throwsClassNotFoundException{此方法负责加载给定名称参数的类。name参数是类的完全限定名称。Java虚拟机调用loadClass()方法来解析类引用并将resolve设置为true。但是,并不总是需要解析一个类。如果您只需要确定该类是否存在,请将resolve参数设置为false。此方法用作类加载器的入口点。我们可以尝试从java.lang.ClassLoader的源码中了解loadClass()方法的内部工作原理:>c=findLoadedClass(name);if(c==null){longt0=System.nanoTime();try{if(parent!=null){c=parent.loadClass(name,false);}else{c=findBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){//ClassNotFoundExceptionthrownifclassnotfound//fromthenon-nullparentclassloader}if(c==null){//Ifstillnotfound,theninvokefindClassinorder//tofindtheclass.c=findClass(name);}}if(resolve){resolveClass(c);}returnc;}}此方法的默认实现按以下顺序搜索类:调用findLoadedClass(String)方法以查看该类是否已加载。在父类加载器上调用loadClass(String)方法。调用findClass(String)方法来查找类。defineClass方法protectedfinalClassdefineClass(Stringname,byte[]b,inoff,intlen)throwsClassFormatError该方法负责将字节数组转换为类的实例。如果数据不包含有效类,则会抛出ClassFormatError。另外,由于这个方法被标记为final,我们不能覆盖这个方法。findClass方法protectedClass>findClass(Stringname)throwsClassNotFoundException此方法查找具有标准名称作为参数的类。我们需要在遵循用于加载类的委托模型的自定义类加载器实现中覆盖此方法。此外,如果父类加载器找不到请求的类,loadClass()将调用此方法。如果类加载器的父类都找不到该类,默认实现将抛出ClassNotFoundException。getParent方法publicfinalClassLoadergetParent()该方法返回委托的父类加载器。一些实现使用null来表示启动类加载器。getResource方法publicURLgetResource(Stringname)此方法尝试查找具有给定名称的资源。它会先委托给资源的父类加载器,如果父类为null,则搜索虚拟机内置类加载器的路径。如果失败,该方法将调用findResource(String)来查找资源。指定为输入的资源名称可以相对于类路径或相对于绝对路径。它返回用于读取资源的URL对象,如果未找到资源或调用者没有足够的权限返回资源,则返回null。需要注意的是,Java是从classpath加载资源的。最后,Java中的资源加载被认为是与位置无关的,因为只要设置环境以查找资源,代码运行的位置都无关紧要。上下文类加载器通常,上下文类加载器提供了J2SE中引入的类加载委托方案的替代方案。JVM中的类加载器遵循分层模型,因此每个类加载器都有一个父类,启动类加载器除外。但是,当JVM核心类需要动态加载应用程序开发人员提供的类或资源时,有时会遇到问题。例如,在JNDI中,核心功能由rt.jar中的引导类实现。但是这些JNDI类可能会加载由独立供应商实现的JNDI提供程序(部署在应用程序类路径中)。这种情况就需要启动类加载器(父类加载器)加载相应程序加载器(子类加载器)可见的类。线程上下文类加载器(contextclassloader)是从JDK1.2引入的。Java.lang.Thread中的方法getContextClassLoader()和setContextClassLoader(ClassLoadercl)用于获取和设置线程的上下文类加载器。如果不通过setContextClassLoader(ClassLoadercl)方法设置,线程会继承父线程的上下文类加载器。运行Java应用程序的初始线程的上下文类加载器是系统类加载器,线程中运行的代码可以通过这个类加载器来加载类和资源。线程上下文类加载器从根本上解决了一般应用不能违反双亲委托模型的问题,使java类加载系统更加灵活。上面提到的问题正是线程上下文类加载器所擅长的。如果不做任何设置,Java应用程序的线程上下文类加载器默认为系统类加载器。因此,在SPI接口的代码中使用线程上下文类加载器,可以成功加载到SPI实现的类中。总结类加载器对于Java程序的执行是必不可少的。我们首先了解了类加载器的不同类型,即引导类加载器、扩展类加载器和系统类加载器。Bootstrap类加载器作为所有类加载器的父类,负责加载JDK内部类。扩展类加载器和系统类加载器分别从Java扩展目录和类路径加载类。然后,我们了解了类加载器的工作原理、它们的特性以及如何创建自定义类加载器。
