Java ClassLoader代码热替换
lolxxs 人气:5总结
- 类加载器是负责加载类的对象。类ClassLoader是一个抽象类。给定类的全限定类名,类加载器应尝试查找或生成构成该类定义的数据Class文件。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的类文件
- 每个Class对象都包含一个Class.getClassLoader()方法可以获取到定义它的ClassLoader
- 数组类的Class对象不是由类加载器创建的,而是根据Java运行时的要求自动创建的。getClassLoader()返回的数组类的类装入器与其元素类型的类装入器相同,如果元素类型是基础类型,则数组类没有类装入器。
- 除了加载类之外,类加载器还负责定位资源。资源是一些数据(例如 .class文件、配置数据或图像)。资源通常与应用程序或库一起打包,以便可以通过应用程序或库中的代码找到它们。
- ClassLoader类使用委托模型来搜索类和资源。ClassLoader的每个实例都有一个关联的父类加载器。当请求查找类或资源时,ClassLoader实例通常会在尝试查找类或资源本身之前,将对该类或资源的搜索委托给其父类装入器。
- Java内置类加载器
加载器名 | 方法名 | 作用 |
---|---|---|
Bootstrap class loader | 无 | 虚拟机的内置类加载器,底层是用C++实现的,没有父加载器。主要加载系统环境的一些jar包和.class文件。C/C++语言编写,是虚拟机的一部分,无法在Java代码中直接获取它的引用。可以通过 System.getProperty(“sun.boot.class.path”)获取其加载路径下的文件 |
Platform class loader (也称为ExtClassLoader) | getPlatformClassLoader() | 平台类加载器,负责加载JDK中一些特殊的模块。主要加载java.ext.dirs下的.class文件 |
System class loader (也称为AppClassLoader) | getSystemClassLoader() | 系统类加载器,负责加载用户类路径上所指定的类库。主要加载java.class.path下的.class文件,是面向用户编写类的类加载器,即自己写的类或者引入的第三方库通常由此加载器加载 |
- 通常Java虚拟机以依赖于平台的方式从本地文件系统加载类。但是,有些类可能不是源于文件;它们可能来自其他来源,如网络,也可能由应用程序构建。 方法defineClass(String name, byte[] b, int off, int len),将字节数组转换为类class的实例。这个新定义的类的实例可以使用Class.newInstance()方法创建
- 类加载器创建的对象的方法和构造函数可以引用其他类。为了确定引用的类,Java虚拟机调用最初创建该类的类加载器的loadClass方法
ClassLoader 虚拟类方法
方法名 | 作用 |
---|---|
protected ClassLoader(String name, ClassLoader parent) | 创建指定名称name的新类加载器,并使用指定的父类加载器parent进行委派 |
public String getName() | 返回此类加载器的名称,如果此类加载器未命名,则返回null |
public Class loadClass(String name, boolean resolve) | 加载具有指定类名称的类,resolve为true表示解析类引用。loadClass方法会先调用getClassLoadingLock方法获取锁,再调用findLoadedClass方法检查类是否已加载,如果未加载,则往父加载器一直递归调用loadClass加载该类,如果父加载器也加载不了该类,才调用findClass方法获取Class对象,而findClass是虚拟方法由子类实现。其实现使用defineClass(String name, byte[] b, int off, int len)方法可以将class文件的字节数组转为Class对象,最后使用resolveClass方法进行类的链接。而由于获取该字节数组的方法是很多样的,所以类加载的方式也非常多样,如本地加载、网络加载、压缩包中加载、自己构建Class文件 |
protected Object getClassLoadingLock(String className) | 返回类加载操作的锁对象。 如果此ClassLoader对象注册为支持并行,则该方法返回与指定类名 className关联的专用对象。否则该方法将返回此ClassLoader对象,即同一时间一个ClassLoader只能加载一个类 |
protected final Class findLoadedClass(String name) | 如果Java虚拟机已将此加载程序记录为具有给定全限定类名称的类的初始加载程序,则返回具有给定全限定类名称的类。否则返回 null |
protected Class findClass(String name) | 查找具有指定全限定类名的类。这个方法是空方法应该被子类重写,并且被调用在检查请求类的父类加载器之后,loadClass方法将调用这个方法 |
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) | 将字节数组转换为具有给定保护域ProtectionDomain的类Class的实例。 如果指定的name以“java.”开头,它只能由getPlatformClassLoader()获取到的平台类加载器或其祖先定义(define),否则将抛出SecurityException。如果name不是null,则它必须等于字节数组b指定的类的全限定类名称,否则将抛出NoClassDefFoundError |
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b, ProtectionDomain protectionDomain) | 将字节缓冲区ByteBuffer转换为具有给定保护域ProtectionDomain的类Class的实例。其余和上面一样 |
protected final void resolveClass(Class c) | 链接指定的类。类加载器可能会使用此方法链接类。如果类c已经被链接,那么这个方法直接返回。 否则,将按照Java语言规范执行一章中的描述链接该类 |
实现代码热替换
通过上面我们可以知道类加载流程是
- loadClass方法会先调用getClassLoadingLock方法获取锁
- 再调用findLoadedClass方法检查类是否已加载,如果已经加载则直接获取到该Class类对象,再判断该Class类对象是否需要链接(resolve),如果要链接进入resolveClass,链接完后直接返回。链接就是执行类加载过程中的验证、准备、解析这些过程
- 如果未加载,则往父加载器一直递归调用loadClass加载该类
- 如果父加载器也加载不了该类,才调用findClass方法获取Class对象而findClass是空方法由子类重写
- 其实现中会使用defineClass(String name, byte[] b, int off, int len)方法,该可以将class文件的字节数组转为Class对象
- 如果需要链接,最后使用resolveClass方法进行类的链接
实现
- 如果我们要实现代码热替换,那么就要使用defineClass方法加载新的类,所以最简单的实现就是直接使用defineClass方法将新的Class文件字节数组转为Class对象,再使用反射创建新对象并执行新方法
- 但是defineClass是protected方法,所以我们只能继承ClassLoader虚拟类才能调用该方法
- 最好的规范就是继承ClassLoader虚拟类,并实现其loadClass方法和findClass方法,并在loadClass方法中调用findClass方法,在findClass方法中再调用defineClass方法
- 为了便于理解,直接抛弃规范,直接自己写一个方法直接调用defineClass方法实现代码热替换
项目结构,out是编译出的class文件目录,由于Test就在src目录下,没有包名,则其全限定类名为Test
public class Test extends ClassLoader { public static void main(String[] args) throws Exception { while(true) { try { Test test = new Test(); // 编译后的class文件位置 ./表示代码根目录 String classFile = "./out/production/Java_hot_replace/Test.class"; FileInputStream fis = new FileInputStream(classFile); byte[] bytes = new byte[1024*10]; int len = fis.read(bytes); //将字节数组转为Class类对象 Test为全限定类名 Class clazz = test.defineClass("Test", bytes, 0 ,len); //使用反射根据新的Class对象创建新对象,并执行其printStr方法 Object object = clazz.newInstance(); Method m = object.getClass().getMethod("printStr", new Class[] {}); m.invoke(object, new Object[] {}); Thread.sleep(2000); } catch(Exception e) { e.printStackTrace(); try { Thread.sleep(2000); } catch(InterruptedException ex) { } } } } public void printStr() { System.out.println("A"); } }
启动后修改代码,然后点重新编译
可以看到代码被热替换了
改进思考
- 正常实现流程应该为继承ClassLoader虚拟类,并重写其loadClass方法和findClass方法,并在loadClass方法中调用findClass方法,在findClass方法中再调用defineClass方法
- 实现热替换应该是替换修改过的代码,则应当维护一个Map<String, Long> 存储从全限定类名到上次文件修改时间的映射,每次定时扫描Class文件目录或检测到保存快捷键Ctrl+s时触发扫描,文件的属性也有上次修改时间,拿我们存储的和文件的属性比较即可知道文件是否修改,即是否需要重新加载Class类
- 热替换产生了大量类信息都存储在jdk1.7的永久代,jdk1.8的元空间,如果无用的类信息过多则会造成OOM,我们自定义类加载器和其产生的Class类对象,都可以通过置空(= null)使其不可达,然后调用System.gc()就可以卸载,即类似如下代码
public class Test extends ClassLoader { public static void main(String[] args) throws Exception { MyClassLoader classLoader = new MyClassLoader(); Class classLoaded = classLoader.loadClass("MyClass"); classLoaded = null; classLoader = null; System.gc(); } }
加载全部内容