类加载器介绍
顾名思义,类加载器(ClassLoader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取Java字节代码,把类信息放到方法区,并生成 java.lang.Class 类的一个实例放到堆中。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。
获取Java字节码的途径有以下几种:
本地class字节码文件。
通过工具动态生成的,如JDK、CGLIB的动态代理实现方式。
通过网络下载的,典型的如applet。
基本上所有的类加载器都是 java.lang.ClassLoader 类的一个实例。
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
引导类加载器(BootStrap ClassLoader):它用来加载 Java 的核心库,即JRE/lib目录下的rt.jar,内嵌在java虚拟机中,用C++编写,并不继承自java.lang.ClassLoader。其父类加载器为NULL。JVM启动时,这个类就会启动。
扩展类加载器(Extensions ClassLoader):它用来加载 Java 的扩展库,即JRE/lib/ext目录下的所有jar文件。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
系统类加载器(System ClassLoader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
Java虚拟机中的所有类加载器采用具有父子关系的树形结构进行组织,在实例化每个类加载器对象时,需要为其指定一个父级类加载器对象或者默认采用系统类加载器为其父级类加载器。
类加载器树状组织结构示意图
使用类对象的class实例的getClassLoader()方法可以获得加载当前类的类加载器。
使用ClassLoader的实例方法getParent()可以获得当前类加载器的父加载器。
那么,使用下面的方法可以获得加载当前类的类加载器及所有父类加载器:
/** * 显示类加载器的顺序 * null代表是BootStrap类加载器,该加载器是顶级加载器,没有父类加载器 */ public static void printClassLoader(Class clzz) { ClassLoader loader = clzz.getClassLoader(); while (loader != null) { System.out.println(loader.getClass().getName()); loader = loader.getParent(); } System.out.println(loader); }
当我们使用入参List.class调用该方法时,输出内容为:null,也即BootStrap ClassLoader加载的该类。
当我们使用入参PKCS11.class调用该方法时(PKCS11为ext/lib目录下jar包中的类),输出内容为:
sun.misc.Launcher$ExtClassLoader
null
当我们使用我们自定义的类,如User.class作为入参调用该方法时,输出内容为:
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null
自定义类加载器
我们也可能会基于以下目的来实现自己的类加载器:
加载从网络上下载的Class字节码,典型应用如Applet。
加载加密的Class文件。
加载如JDK Proxy、CGLIB等生成的动态字节码。
热部署。
我们可以通过继承抽象类 ClassLoader 来实现自己的类加载器。此时,我们需要重写 findClass(String name) 方法,该方法默认是一个会抛出 ClassNotFoundException 异常的空实现。
下面是我写的一个简单示例,主要代码部分如下(类名MyClassLoader):
//类加载器名称 private String name; //加载类的路径 private String path = "D:/"; private final String fileType = ".class"; public MyClassLoader(String name){ //让系统类加载器成为该类加载器的父加载器 super(); this.name = name; } public MyClassLoader(ClassLoader parent, String name){ //显式指定该类加载器的父加载器 super(parent); this.name = name; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } @Override public String toString() { return this.name; } /** * 获取.class文件的字节数组 * @param name * @return */ private byte[] loadClassData(String name){ InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); name = name.replace(".", "/"); try { is = new FileInputStream(new File(path + name + fileType)); int c = 0; while(-1 != (c = is.read())){ baos.write(c); } data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally{ try { is.close(); baos.close(); } catch (IOException e) { e.printStackTrace(); } } return data; } /** * 获取Class对象 */ @Override public Class<?> findClass(String name){ byte[] data = loadClassData(name); return this.defineClass(name, data, 0, data.length); }
我们有以下几种构造类加载器的方式:
设置我们的类加载器的父类加载器为BootStrap ClassLoader,代码如下:
MyClassLoader loader = new MyClassLoader(null,"loader1"); loader.setPath("E:/workspace/test/WebRoot/WEB-INF/classes/"); Class clzz = loader.loadClass("com.classloader.Computer"); Object object = clzz.newInstance(); printClassLoader(clzz);
注:Computer为自定义的一个bean;
printClassLoader(clzz)为前边我们定义的查看某类类加载器及所有父类加载器的方法。
此时,控制台输出为:
com.classloader.MyClassLoader null
当loadClass中的字符串为java.util.ArrayList,也即JDK核心类库中的类时,控制台输出为null。
这也说明了类加载的机制:首先由父类进行加载,父类加载器在它的目录中找不到该文件时,再由子类进行加载。在我们的例子中,java.util.ArrayList为核心类库中的Class文件,BootStrap ClassLoader可以加载,所以,控制台会显示ArrayList的Class实例的类加载器是null,也即BootStrap ClassLoader。
我们可以通过MyClassLoader loader = new MyClassLoader(null,"loader1");来指定自定义类加载器的父类加载器为BootStrap ClassLoader,也可以通过MyClassLoader loader = new MyClassLoader("loader1");的方式来默认指定当前类加载器的父类加载器为System ClassLoader。也可以实例化两个类加载器,把一个作为另一个的父类加载器。
类加载器命名空间的关系
同一个命名空间内的类是相互可见的。
子加载器的命名空间包含所有父加载器的命名空间。因此子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。
由父加载器加载的类不能看见子加载器加载的类。
如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
当两个不同命名空间内的类相互不可见时,可以采用Java的反射机制来访问实例的属性和方法。