陈同学
微服务
Accelerator
About
# Java中的类加载器 > 原文:[Class Loaders in Java](https://www.baeldung.com/java-classloaders) by baeldung > 翻译:[陈同学](https://chenyongjun.vip/) > 可以参考笔者另一篇译文 [深入JVM内幕](https://chenyongjun.vip/articles/50) 中的类装载器部分 ## 类加载器简介 Class loaders属于JRE的一部分,负责在运行时将Java类动态加载到JVM。得益于class loaders,JVM在无需知晓底层文件或文件系统时就可以运行Java程序。 此外,Java类是按需加载,并不会一次全部加载到内存中。Class loaders负责将类加载到内存。 在本教程中,我们将聊聊几种不同的内置class loaders,它们如何工作以及如何创建自定义的class loader。 ## 几种内置类加载器 我们先以一个简单例子了解下不同类被类加载器加载的区别(PrintClassLoader为当前测试类)。 ```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 ``` 可以看到,有三种不同class loader:application(系统类加载器)、extension(拓展类加载器)和bootstrap(启动类或引导类加载器,显示为null)。 application class loader加载上面样例代码所属的类,**application class loader 或(system class loader)用于加载classpath下的文件,是用于加载应用程序class的加载器。** 然后,extension class loader加载了上面的 *Logging* 类,**它用于加载Java核心库之外的拓展类。** 最后,bootstrap class loader加载了 *AarrayList* 类,**bootstrap(或 primordial) class loader是其他所有class loader的父类。** 最后一行 *ArrayList* 之所以输出值为 *null*,**这是因为bootstrap class loader是由native代码所写,所以它不会以Java类的形式体现**。由于这个原因,bootstrap class loader在不同JVM之中行为会有所不同。 让我们了解下几种不同class loader。 ### 启动类加载器(Bootstrap Class Loader) Java类由 *java.lang.ClassLoader* 的实例进行加载,不过,class loader本身也是Java类,那么 *java.lang.ClassLoader* 又是由谁加载的呢? 这就是Bootstrap class loader大显身手的地方。它主要负责加载JDK核心类,通常是 **rt.jar** 和位于 **$JAVA_HOME/jre/lib** 下的核心库。此外,它也是所有其他 ClassLoader实例的parent。 Bootstrap class loader 是JVM核心之一,由Native代码所写,这点在上述例子中提到过。不同平台Bootstrap class loader可能有不同的实现。 ### 拓展类加载器(Extension Class Loader) Extension class loader是Bootstrap class loader的子类,负责加载Java核心库外的拓展类,正因如此所有的应用程序都能够运行在Java平台上。 Extension class loader从JDK拓展目录加载类,通常是 *$JAVA_HOME/lib/ext* 目录或 *java.ext.dirs* 系统属性中配置的目录。 ### 系统类加载器(System Class Loader) System class loader是Extensions class loader的子类,负责加载所有应用程序级别的类到JVM,**它会加载classpath环境变量或 -classpath以及-cp命令行参数中指定的文件**。 ## Class Loaders是如何工作的? Class loaders是JRE的一部分。当JVM请求一个类时,class loaders会通过类的全限定名尝试加载类并将class definition加载到runtime。 **java.lang.ClassLoader.loadClass()方法负责通过类的全限定名将class definition加载到runtime**。 如果class尚未加载,class loader会将加载请求委派给父加载器,这个委派加载的处理过程会递归进行。 如果父加载器最终没有找到该类,子加载器将调用 *java.net.URLClassLoader.findClass()* 方法从文件系统中加载该类。如果最终子加载器也无法加载该类,将抛出 *java.lang.NoClassDefFoundError* 或 *java.lang.ClassNotFoundException*。 让我们看一个抛出 *ClassNotFoundException* 的例子: ```java java.lang.ClassNotFoundException: com.baeldung.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()* 再进行查找,最后抛出了 *ClassNotFoundException*。 Class Loaders有三个很重要的特性。 ### 委派模型(Delegation Model) 当需要查找class或resource时,Class loaders会遵守委派模型,它们首先会将查找请求委派给其父加载器。 假设我们需要将应用中的一个类加载到JVM,system class loader首先会将加载请求委派给extension class loader,后者又会将加载请求委派给bootstrap class loader。 只有当bootstrap class loader和extension class loader都无法加载该类时,system class loader才会尝试自行加载该类。 ### 唯一性(Unique Classes) 作为委派模型的结果,我们总是尝试向上委托,因此很容易保证类的唯一性。如果父加载器无法找到该类,当前加载器才会尝试加载该类。 ### 可见性(Visibility) 此外,父加载器加载的类对子加载器是可见的。 举个例子,system class loader可以看到extension class loader和bootstrap class loader加载的类,但是反之不行,父加载器无法看到子加载器加载的类。 为了说明这一点,假如类A由system class loader加载,类B由extension class loader加载,那么A和B对于对于system class loader来说都是可见的,extension class loader只能看到类B。 ## 自定义ClassLoader 对于文件系统中的文件来说,内置class loader已经可以满足大部分场景。然而,有些场景并不是从本机硬件设备或网络上加载类,因此我们需要自定义class loader来处理。 在本小节,我们将介绍自定义加载器的一些场景,也会介绍如何创建一个自定义加载器。 ### 自定义classloader的场景 自定义classloader不仅仅只用于在运行时加载类,还有这么一些场景: 1. 用于更新已存在的字节码,如:编织代理(weaving agent)。 2. 根据需求动态创建类,如:在JDBC中通过加载类来完成不同驱动程序之间的切换。 3. 在加载具有相同类名、包名的类的字节码时实现类的版本控制机制,可以通过URL类加载器(通过URL加载jar)或自定义加载器。 还有很多自定义加载器可以派上用场的例子。 例如,浏览器使用自定义加载器从网站加载可执行的内容。浏览器可以使用独立的class loader从不同网页加载applet,用于运行applet的applet查看器包含了一个ClassLoader,它不从本地文件系统检索类,而是访问远程服务器上的站点。然后通过HTTP加载字节码原文件,并将其转换为JVM中的类。虽然这些applet具有相同的名称,但由于它们被不同的class loader所加载,因此它们也被看作不同的组件。 现在我们理解了自定义加载器的意义,那就让我们实现一个ClassLoader的子类来总结JVM类的加载。 ### 创建我们自己的class loader 为了便于说明,假设我们需要通过FTP加载类。由于类不在classpath中,无法通过内置加载器加载这些类。 ```java public class CustomClassLoader extends ClassLoader { public CustomClassLoader(ClassLoader parent) { super(parent); } public Class getClass(String name) throws ClassNotFoundException { byte[] b = loadClassFromFTP(name); return defineClass(name, b, 0, b.length); } @Override public Class loadClass(String name) throws ClassNotFoundException { if (name.startsWith("com.baeldung")) { System.out.println("Loading Class from Custom Class Loader"); return getClass(name); } return super.loadClass(name); } private byte[] loadClassFromFTP(String fileName) { // Returns a byte array from specified file. } } ``` 在上面例子中,我们定义了一个class loader用于从包 *com.baeldung* 加载文件,拓展了默认class loader。 我们在构造器中传入了parent class loader,然后使用类的全限定名通过FTP加载类。 ## 理解java.lang.ClassLoader 让我们了解下 *java.lang.ClassLoader* 中的几个基础方法,以便对ClassLoader的工作方式有个清晰的脑图。 ### loadClass()方法 ```java public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { ``` 该方法通过给定的类全限定名加载类,如果参数 *resolve* 为 *true*,JVM将执行 *loadClass()* 解析该类。然而,我们并非总是需要解析一个类。 **如果只需要判断类是否存在,可以将 resolve参数设置为false**。 这个方法是class loader的入口,我们可以通过源码了解 *loadClass()* 的内部机制。 ```java protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded 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 thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } } ``` 方法中查找类的默认实现按照如下顺序进行: 1. 执行 *findLoadedClass(String)* 判断类是否已被加载 2. 执行父类的 *loadClass(String)* 方法 3. 执行 *findClass(String)* 方法查找类 ### defineClass()方法 ```java protected final Class<?> defineClass( String name, byte[] b, int off, int len) throws ClassFormatError ``` 该方法负责将字节数组转换为类,我们需要在使用类之前先解析类。 如果没有包含有效的类,将抛出 *ClassFormatError*。 当然,该方法由 final标记,我们不能override。 ### findClass()方法 ```java protected Class<?> findClass( String name) throws ClassNotFoundException ``` 该方法以类全限定名来查找类,在自定义的class loader中,我们需要override这个方法,并且需要遵守委派模型。 当然,如果父加载器无法找到目标类,将会执行 *loadClass()* 方法。 在默认实现中,如果所有父加载器都无法查找到该类,将抛出 *ClassNotFoundException*。 ### getParent()方法 这个方法返回父加载器用于委派。 有些实现像最上面例子中使用 *null* 来代表bootstrap class loader。 ### getResource()方法 ```java public URL getResource(String name) ``` 该方法用于查找给定名称的资源。 首先,查找请求会委托给父加载器。如果父加载器为null,则将请求交给bootstrap class loader。 如果依然失败,该方法将调用 *findResource(String)* 来查找资源。它返回一个用于读取资源的URL对象,如果没有找到资源或没有足够的权限访问资源将返回 *null*。 值得注意的是,Java会从classpath路径中加载资源。 ## 线程上下文加载器(Context Classloaders) Context Classloaders为J2SE中引入的类加载委派方案提供了另一种方式。 前面我们学到,JVM中的class loaders遵循层级模型,除bootstrap class loader外,每个类加载器都有一个父类。 然而,有时当JVM核心类需要加载由开发人员提供的类或资源时,我们可能会遇到问题。 例如,在JNDI中,其核心功能由 *rt.jar* 中的引导类实现。但是这些JNDI引导类可能需要加载由各独立服务商提供的JNDI实现类(部署在应用的classpath中),这个场景需要bootstrap class loader加载一些仅对child class loader可见的类。 J2SE委派在这里并不管用,我们需要找到一种替代方法来加载类。这可以使用线程上下文加载器来实现。 *java.lang.Thread* 类有一个 *getContextClassLoader* 方法用于返回特定线程的ContextClassLoader。在加载资源和类时,ContextClassLoader由线程的创建者提供。 ## 小结 Class loaders是执行Java程序的基础,本文我们进行了简单介绍。 我们介绍了几种不同的class loader——Bootstrap,Extension和System class loaders。Bootstrap作为所有class loader的parent,负责加载JDK核心类。Extension和System负责加载Java拓展目录和classpath中的类。 然后,我们介绍了class loader的工作方式,通过创建一个简单的自定义class loader介绍了几个特性,如:委派、可见性和唯一性。最后,我们简介了 Context class loaders。 本文的样例代码见 [github](https://github.com/eugenp/tutorials/tree/master/core-java/src/main/java/com/baeldung/classloader)。
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
Java中的类加载器
文章链接:
https://chenyongjun.vip/articles/67
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力