浅谈Java|浅谈Java ClassLoader

Class Loaders简介 Class Loaders(类加载器)是JVM用于运行来动态加载类的,同时它们也是JRE的一部分,由于Class Loaders的存在,JVM运行Java程序的时候不需要知道底层文件或文件系统。
并不是所有的Java类都是一次性加载完的,大部分Java类在具体用到的时候才会加载。
每个Java类都有一个引用指向加载它的ClassLoader,特别的,数组类不是通过ClassLoader创建的,而是通过JVM在需要的时候自动创建的,数组类通过getClassLoader()方法获取ClassLoader的时候和该数组的元素类型的ClassLoader是一致的。
每个ClassLoader可以通过getParent()获取其父ClassLoader,如果获取到ClassLoader为null的话,那么该类是通过Bootstrap ClassLoader加载的。
Java内置Class Loaders 先看一个获取ClassLoader的例子

/** * Print The Java ClassLoader Tree */ public class PrintClassLoaderTree {public static void main(String[] args) {ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader(); StringBuilder split = new StringBuilder("|--"); boolean needContinue = true; while (needContinue){ System.out.println(split.toString() + classLoader); if(classLoader == null){ needContinue = false; }else{ classLoader = classLoader.getParent(); split.insert(0, "\t"); } } }}

在我机器上的运行结果(JDK8):
|--sun.misc.Launcher$AppClassLoader@18b4aac2 |--sun.misc.Launcher$ExtClassLoader@53bd815b |--null

从这个例子可以知道我们编写的Java类的ClassLoader是AppClassLoader,而AppClassLoader的ClassLoader是ExtClassLoader,而ExtClassLoader的ClassLoader是Bootstrap ClassLoader(从null可以得知),这个过程中涉及到3个不同的ClassLoader,下面我们来逐一了解。
Bootstrap Class Loader(引导类加载器) 我们知道,我们平时编写Java类都是通过java.lang.ClassLoader的某个实例来加载的,那么到底是谁来加载java.lang.ClassLoader呢,答案就是Bootstrap Class Loader。
Bootstrap ClassLoader用于加载JDK内部类,如rt.jar和其他JRE中lib目录的Java类库中的类。Bootstrap ClassLoader充当所有其他ClassLoader实例的父级。
Bootstrap ClassLoader是JVM核心的一部分,由原生代码编写,不同的平台可能会有不同的实现。
Extension Class Loader(扩展类加载器) Bootstrap Class Loader的子类,负载加载标准核心Java类的扩展,加载JRE中lib/ext目录中的jar,以及JVM系统属性system propertyjava.ext.dirs配置目录中的类
System Class Loader(系统类加载器) Extension ClassLoader的子类,用于加载应用级别的类到JVM,即加载classpath目录下的Java类,通过ClassLoader.getSystemClassLoader()可以获得(如,上文中的sun.misc.Launcher$AppClassLoader)
Class Loaders树形图 浅谈Java|浅谈Java ClassLoader
文章图片
image.png 工作原理 Class Loaders是JRE(Java Runtime Environment)的一部分,当JVM需要一个类的时候,Class Loader就会通过类的全名尝试定位到类文件(.class文件,字节码)的位置,并通过类文件定义成一个Java类(java.lang.Class的一个实例);另外的,ClassLoader还负责加载应用需要的资源,如配置文件,图片等等。
核心API
API 描述
getParent() 返回该ClassLoader的父ClassLoader,如果为null则该类是通过Bootstrap ClassLoader加载的
loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例
findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例,在loadClass方法中通过父ClassLoader加载类之后调用
findLoadedClass(String name) 查找名称为name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的
resolveClass(Class c) 链接指定的 Java 类
下面主要了解一下loadClass方法,java.lang.LoadClass的默认实现
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. long t1 = System.nanoTime(); 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) { resolveClass(c); } return c; } }

大致过程如下:
  • 调用findLoadedClass方法先从已经加载过class中寻找要加载的类
  • 如果要加载的类还没被加载过,通过父ClassLoader去加载该类(此过程会递归,即委托模型)
  • 如果父ClassLoader仍然么没找到该类,那么将会调用findClass方法加载该类(这个方法JDK中默认实现是java.net.URLClassLoader.findClass())
  • 如果最终没有找到,那么抛出异常java.lang.NoClassDefFoundErrorjava.lang.ClassNotFoundException
Class Loader的委托模型(Delegation Model) 先看看JDK文档中如何说描述委托模型
The ClassLoader class uses a delegation model to search for
classes and resources. Each instance of ClassLoader has an
associated parent class loader. When requested to find a class or
resource, a ClassLoader instance will delegate the search for the
class or resource to its parent class loader before attempting to find the
class or resource itself. The virtual machine's built-in class loader,
called the "bootstrap class loader", does not itself have a parent but may
serve as the parent of a ClassLoader instance.
大致意思如下:
ClassLoader类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个 相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找 类或资源之前,将搜索类或资源的任务委托给其父类加载器。虚拟机的内置类加 载器(称为 "bootstrap class loader")本身没有父类加载器,但是可以将它用作 ClassLoader 实例的父类加载器。

也就是,只有在Bootstrap ClassLoader和Extension ClassLoader没有成功加载要加载的类时,System ClassLoader尝试自己去加载
委托模型保证类的唯一性(Unique Classes) 通过委托模型,类的加载都将是从顶级的父ClassLoader(Bootstrap ClassLoader)到当前ClassLoader实例逐级加载,避免了重复加载,特别是对于Java的核心类库,如果一个类被重复加载,那么将会引来同样的Class但是类型却不一致的情况,因为JVM在判断两个Class是否相同需要同时判断类名类的ClassLoader两个条件。
委托模型提供了子ClassLoader对父ClassLoader的能见度(Visiblity) 子ClassLoader对父CLassLoader所加载的类是可见的,例如,System ClassLoader可以看见Extension ClassLoader和Bootstrap ClassLoader加载的类,但是反之则不然。
自定义ClassLoader 自定义ClassLoader从指定文件系统读取class
public class FileSystemClassLoader extends ClassLoader {private final String path; public FileSystemClassLoader(String path) { this.path = path; }@Override protected Class findClass(String name) throws ClassNotFoundException {String fileName = path + name.replace(".", "/") + ".class"; File file = new File(fileName); if(!file.exists()){ throw new ClassNotFoundException(); }else{ return this.innerDefineClass(name,file); } }private Class innerDefineClass(String name, File file) { BufferedInputStream bis = null; try { bis = new BufferedInputStream(new FileInputStream(file)); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int bufferSize = 1024; byte[] buf = new byte[bufferSize]; int len; while ((len = bis.read(buf, 0, bufferSize)) > 0){ bos.write(buf,0,len); } byte[] classByte = bos.toByteArray(); return defineClass(name, classByte,0,classByte.length); } catch (IOException e) { throw new RuntimeException(e.getMessage(),e); }finally { if(bis!=null){ try { bis.close(); } catch (IOException e) { //ignore } } } } }

FileSystemClassLoader测试
  • 自定义Class
package com.sevenlin.blueshit; public class HelloWorld { public HelloWorld() { }public static void main(String[] args) { System.out.println("Hello world"); } }

  • 测试
public void loadFileSystemClass() throws Exception {String path = "/Users/sevenlin/project/blueshit/"; String className = "com.sevenlin.blueshit.HelloWorld"; FileSystemClassLoader fileSystemClassLoader = new FileSystemClassLoader(path); Class clazz = fileSystemClassLoader.loadClass(className); System.out.println(clazz); this.printClassLoaderTree(clazz.getClassLoader()); Method main = clazz.getMethod("main", String[].class); String[] params = new String[]{}; main.invoke(null, (Object)params); }private void printClassLoaderTree(ClassLoader classLoader) { StringBuilder split = new StringBuilder("|--"); boolean needContinue = true; while (needContinue){ System.out.println(split.toString() + classLoader); if(classLoader == null){ needContinue = false; }else{ classLoader = classLoader.getParent(); split.insert(0, "\t"); } } }

  • 结果
class com.sevenlin.blueshit.HelloWorld |--com.sevenlin.blueshit.classloader.FileSystemClassLoader@15d0c81b |--sun.misc.Launcher$AppClassLoader@18b4aac2 |--sun.misc.Launcher$ExtClassLoader@4b1c1ea0 |--null Hello world

Context ClassLoader 从上文我们知道,通过ClassLoader的委托模型可以很好的加载Java类,并且保证Java类的唯一性;但是有的时候JVM需要动态地去加载第三方的类或者资源,那么通过这样就会出现问题。
例如,Java中提供了很多SPI(Service Provider Interface,服务提供者接口),允许第三方实现这些接口,常见的有:JDBC,JNDI,JCE等,而这些接口都是Java的核心类库,由Bootstrap ClassLoader来加载,这样的就存在一个问题,Bootstrap ClassLoader不关心classpath下类的加载,在委派模型下SPI的实现类则没法加载。
Java就是通过Thread.getContextLoader来解决这个问题的,下面来看下这个方法在JDK中的描述:
返回该线程的上下文ClassLoader。上下文ClassLoader由线程创建者提供,供运行于该线程中的代码在加载类和资源时使用。如果未设定,则默认为父线程的
ClassLoader 上下文。原始线程的上下文 ClassLoader 通常设定为用于加载应用程序的类加载器。
首先,如果有安全管理器,并且调用者的类加载器不是 null,也不同于其上下文类加载器正在被请求的线程上下文类加载器的祖先,则通过 >RuntimePermission("getClassLoader") 权限调用该安全管理器的checkPermission方法,查看是否可以获取上下文ClassLoader。
也就是说,通过Context ClassLoader就可以获取到当前线程的ClassLoader(通常是System ClassLoader),也就可以在classpath下加载到对于的实现类(SPI具体是如何工作的这里就不详细展开)。
那么这个Context ClassLoader是何时设置的呢?答案就在ClassLoader.getSystemClassLoader中,JVM在运行时启动序列的早期首先调用此方法,这时会创建系统类加载器并将其设置为调用 Thread 的上下文类加载器。
【浅谈Java|浅谈Java ClassLoader】具体实现是ClassLoader中这个类:
class SystemClassLoaderAction implements PrivilegedExceptionAction { private ClassLoader parent; SystemClassLoaderAction(ClassLoader parent) { this.parent = parent; }public ClassLoader run() throws Exception { String cls = System.getProperty("java.system.class.loader"); if (cls == null) { return parent; }Constructor ctor = Class.forName(cls, true, parent) .getDeclaredConstructor(new Class[] { ClassLoader.class }); ClassLoader sys = (ClassLoader) ctor.newInstance( new Object[] { parent }); Thread.currentThread().setContextClassLoader(sys); return sys; } }

参考
  • Class Loaders in Java
  • 深入分析Java ClassLoader原理
  • 深入探讨 Java 类加载器

    推荐阅读