类加载器的继承层次
java类加载器按照如下图所示的结构组织,各加载器各司其职只加载自己管辖范围内的类。
引导类加载器(Bootstrap):java虚拟机内置的加载器,在虚拟机启动的时候会用这个类加载器来加载 JDK安装目录下的 /JRE/LIB/rt.jar ,也就是系统默认导入的一些类(如下图所示)。不能通过代码直接获取引导类加载器的引用,获取的都是null。
扩展类加载器(ExtClassLoader):这个类加载器加载JDK安装目录下的/JRE/LIB/ext 目录中的类:
系统类加载器(AppClassLoader):根据java程序的classpath加载对应的类。可以通过ClassLoader.getSystemClassLoader()来取得。
public class Demo {
public static void main(String[] args) {
//系统类加载器
ClassLoader demoClassLoader = Demo.class.getClassLoader();
System.out.println(demoClassLoader); //sun.misc.Launcher$AppClassLoader@5e87512
//扩展类加载器
ClassLoader demoClassLoaderParent = demoClassLoader.getParent();
System.out.println(demoClassLoaderParent); //sun.misc.Launcher$ExtClassLoader@605df3c5
//引导类加载器
ClassLoader objectClassLoader = Object.class.getClassLoader();
System.out.println(objectClassLoader); //null
System.out.println(demoClassLoaderParent.getParent()); //null
}
}
JVM判断两个类相同的准则
只有当类的全限定名相同且加载此类的类加载器(类的定义加载器)相同,jvm才认为两个类相同。不同类加载器加载的类之间不兼容,类A的同一份A.class文件被ClassLoaderA和ClassLoaderB加载后会定义出两个表示类A的java.lang.Class实例,这两个实例是不同的。
代理加载类
如果由用户自定义的类加载器来加载核心库,那么系统中同一个核心类就可能存在多个版本,互相之间不兼容。如果加载核心类的工作交由引导类加载器来完成,就不会出现多版本的问题。试想com.lang.Object由两个不同的类加载器加载,则jvm中存在两个表征Object的Class,因此可能实例化出两种Object对象,但是彼此不相等,terrible! 另一个问题,如果用户自定义java.lang.Object类,且用自定义的类加载器来加载,那么核心库提供的类就被屏蔽了,同样用户也可以对核心库进行任意修改,这都将导致java核心库处于不安全的状态。为了解决这些问题,java类加载器使用向上代理的方式加载类,即发起加载任务的类加载器优先将加载任务代理给其父类加载器,如果父类加载器加载不到就继续代理给上层类加载器,直至引导类加载器,如果还是未加载到,最初发起加载任务的类加载器就自己加载类,这样核心库的类只会被jvm指定的类加载器加载,自定义的java.lang.Object类永远不会被加载。类加载器工作的核心源码如下
类加载器优先将加载任务代理给其父类,所以真正完成加载任务的加载器(类的定义加载器)和发起加载任务的加载器( 初始加载器 )可能不是同一个。类的定义加载器才是jvm判断两个类是否相同的一个准则。如下图, 类A的类定义加载器会发起类B的加载,因此类A的类定义加载器是类B的初始化加载器。
类的初始加载器通过调用loadClass( ) 来启动类加载,此过程可能抛出java.lang.ClassNotFoundException,类的定义加载器通过调用其defineClass( )来加载类,此过程可能抛出java.lang.NoClassDefFoundError。类加载器在成功加载某个类之后,会把得到的 java.lang.Class
类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载,也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即loadClass()
方法不会被重复调用。每个Class对象对保留有对定义他的类加载器的引用,通过对类调用getClassloader( )来获取该引用。
当自定义类加载器时,为了保证委托机制,建议重写 findClass(String binaryName) 而不是重写loadClass(String binaryName,Boolean resolve)。重写findClass主要是实现另一种获取字节码的方式。
线程上下文类加载器
java提供了很多服务提供者接口(Servie Provider Interface),例如jdbc、jndi、jaxp等,这些spi接口定义在java核心库中,但是spi的实现(服务提供者)由第三方提供(例如mysql提供mysql数据库驱动,oralce提供oracle提供oracle的驱动),以应用依赖的jar的形式存在,接口由引导类加载器加载,实现由系统类加载器加载,因为引导类加载器(加载器的鼻祖)只加载java核心库,无法向下代理给系统类加载器加载spi的实现类,因此在spi接口中无法加载到spi的实现。怎么解决这个问题呢?为了解决这个问题,java提供了线程上下文类加载器,java应用程序的主线程上下文类加载器是系统类加载,子线程默认继承父线程的上下文的类加载器。不同与向上代理的模式,线程上下文类类加载器相当于父类加载器委托子类加载器代理加载父类加载不到的类。有了线程上下文类加载器,引导类加载器就可以委托它为spi加载相应的实现。可以通过Thread.currentThread().getContextClassLoader( )类获得当前线程的上下文类加载器。
自定义类加载器
自定义类加载器通常是为了通过特殊途径获取字节码或者对字节码进行特殊处理,通常只需要重写Class<?> findClass(String name) 方法即可。以下demo通过自定义列加载器来加载经过DES加密算法加密后的字节码文件,生成相应的类。public class MatrixClassLoader extends ClassLoader { //字节码文件路径 private String clazzFilePath; MatrixClassLoader(String clazzFilePath){ this.clazzFilePath = clazzFilePath; } /** * 重写findClass(String name),实现自己的获取字节码的方式 。 * 此处通过des解密算法对加密的字节码文件解密,并返回解密后的字节码文件的字节数组 */ @Override protected Class<?> findClass(String name) { byte[] clazzByte = null; try { DES des = new DES("123"); //用123解密字节码,用字节数据返回解密后的字节码数据 clazzByte = des.decryp(clazzFilePath); return defineClass(name, clazzByte, 0, clazzByte.length); //将字节码数据交给ClassLoader处理 } catch (Exception e) { e.printStackTrace(); } return null; } public static void main(String[] args) throws Exception { String path = "C:/Users/Administrator/Desktop/bin/Clazz.class"; //加密前的字节码文件 DES des = new DES("123"); //用123加密字节码,返回加密文件的路径 String encryptedFilePath =des.encryFile(path); MatrixClassLoader classLoader = new MatrixClassLoader(encryptedFilePath); Class clazz = classLoader.loadClass("com.hsh.cl.Clazz"); } }