JVM自定义类加载器及问题
在看神书《深入理解Java虚拟机》时,看到类加载部分,对如何自定义一个类加载器来,加载自己想要的加载的类产生了兴趣,所以研究了一下。参考了一下网上的其他的文章 ,记录一下。
-
首先为什么要自定义类加载器呢
-
我们需要的类不一定存放在已经设置好的classpath下(由系统类加载器AppClassLoader加载的路径),对于自定义路径中的class类文件的加载,我们需要自己的ClassLoader
-
有时我们不一定是从类文件中读取类,可能是从网络的输入流中读取类,这就需要做一些 加密和解密操作,这就需要自己实现加载类的逻辑,当然其他的特殊处理也同样适用。
-
可以定义类的实现机制,实现类的热部署,如OSGi中的bundle模块就是通过实现自己的ClassLoader实现的。
-
一些软件设计时为了实现更好的功能和设计思想。如tomcat
tomcat使用自定义类加载器的原因:
- 要保证部署在tomcat上的每个应用依赖的类库相互独立,不受影响。
- 由于tomcat是采用java语言编写的,它自身也有类库依赖,为了安全考虑,tomcat使用的类库要与部署的应用的类库相互独立。
- 有些类库tomcat与部署的应用可以共享,比如说servlet-api,使用maven编写web程序时,servlet-api的范围是provided,表示打包时不打包这个依赖,因为我们都知道服务器已经有这个依赖了。
- 部署的应用之间的类库可以共享。这听起来好像与第一点相互矛盾,但其实这很合理,类被类加载器加载到虚拟机后,会生成代表该类的class对象存放在永久代区域,这时候如果有大量的应用使用spring来管理,如果spring类库不能共享,那每个应用的spring类库都会被加载一次,将会是很大的资源浪费。
-
-
类是如何被加载到JVM的?
在jdk中,是通过ClassLoader来加载jdk的系统类和用户类路径下(classpath)的类的。
在ClassLoader中的loadClass方式,实现了类的加载逻辑,即非常有名的双亲委类加载模式。工作过程如下:
-
当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则 直接返回原来已经加载的类。
-
如果没有找到,就去委托父类加载器去加载。父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托父类的父类去加载,一直到启动类加载器。因为如果父加载器为空了,就代表使用启动类加载器作为父加载器去加载。
-
如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。
代码如下,可简单看下:
protected synchronized Class<?> loadClass ( String name , boolean resolve ) throws ClassNotFoundException{ //检查指定类是否被当前类加载器加载过 Class c = findLoadedClass(name); if( c == null ){//如果没被加载过,委派给父加载器加载 try{ if( parent != null ) c = parent.loadClass(name,resolve); else c = findBootstrapClassOrNull(name); }catch ( ClassNotFoundException e ){ //如果父加载器无法加载 } if( c == null ){//父类不能加载,由当前的类加载器加载 c = findClass(name); } } if( resolve ){//如果要求立即链接,那么加载完类直接链接 resolveClass(); } //将加载过这个类对象直接返回 return c; }
如果想要加载的类不是jdk的系统类或者classpath下的用户类,即上面第1条中说的情况,那么类加载器是无法记载的,就会进入如下逻辑。
//父类不能加载,由当前的类加载器加载 if( c == null ){ c = findClass(name); }
附上findClass方法
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
从此段代码可以看出,如果没有重写此方法,那么会直接抛出ClassNotFoundException异常。类加载失败。所以这时候如果要加载这个类,那么就需要重写findClass方法。
-
-
如何重写findClass来实现一个自己的类加载器?
-
首先创建一个非jdk的系统类路径和非classpath路径和的外部class文件。
为了方便,在D盘根目录下创建一个.java文件。
注意:不要在你的ide中创建,否则会在运行程序的时候被编译到用户类路径(classpath)下,那么还是会被AppClassLoader加载,除非创建此类生成此类class文件后,删掉此类的.java和.class文件,再运行自定义类加载器
// 定义一个包路径 package com.jvm.test; /** * @author: yhl * @DateTime: 2019/12/5 9:41 * @Description: 定义类 */ public class People { }
在此目录下shift+鼠标右击,选择打开cmd(win10为Powershell)。运行java命令生成.class文件。
javac -encoding utf-8 .\Test.java
查看此目录,已经生成了Test.class文件
-
创建自己的类加载器加载Test.class文件。 代码如下:
/** * Author: yhl * DateTime: 2019/12/4 21:12 * Description: 自定义类加载 * 需要继承ClassLoader,重写findClass */ public class TestClassLoader extends ClassLoader { /** * * * 此路径即时想要加载的外部class路径 */ private String filePath; TestClassLoader(String filePath){ this.filePath = filePath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { File file = new File(filePath); try { byte[] bytes = getClassBytes(file); // 如果读取的.class文件为空,则抛出ClassNotFoundException异常 if (bytes == null) { throw new ClassNotFoundException(); } //defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class Class<?> c = this.defineClass(name, bytes, 0, bytes.length); return c; } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } private byte[] getClassBytes(File file) throws Exception { // 这里要读入.class文件的字节,因此要使用字节流 FileInputStream fis = new FileInputStream(file); FileChannel fc = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel wbc = Channels.newChannel(baos); ByteBuffer by = ByteBuffer.allocate(1024); while (true) { int i = fc.read(by); if (i == 0 || i == -1) break; by.flip(); wbc.write(by); by.clear(); } fis.close(); return baos.toByteArray(); } public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { // 创建自定义加载器实例 TestClassLoader mcl = new TestClassLoader("D:/Test.class"); // classPackage为自定义的class中的包路径 String classPackage = "com.jvm.test.Test"; // 通过自定义类加载器加载此class, // Class<?> obj = Class.forName(classPackage, true, mcl).newInstance(); Object obj = mcl.loadClass(classPackage).newInstance(); // 打印加载的类对象 System.out.println(obj); // 打印加载此类的类加载器 System.out.println(obj.getClass().getClassLoader());//打印出我们的自定义类加载器 } }
打印结果如下:
com.jvm.test.Test@63961c42 com.boot.demo.jvm.TestClassLoader@85ede7b
TestClassLoader即为我们自定义的类加载器。
-
-
注意事项
在尝试自定义的过程中,遇到了一些问题,花了些许时间,在此记录一下。
1.加载类的必须是非jdk系统类路径和用户类路径下(classpath)的class文件,否则还是会使用AppClassLoader来加载,不是自定义的类加载。所以在上面第3条第i点的注意事项很重要。
2.其实重写ClassLoader中的loadClass也可以使用自定义类加载器,而且不用考虑上面第1条要注意的问题。但是,这样会破坏双亲委派模型,是不可取的。如下面代码
/** * @author: yhl * @DateTime: 2019/12/4 15:26 * @Description: */ public class ClassLoaderTest extends ClassLoader{ public static void main(String[] args) throws Exception { /** * 重写loadClass,违背了双亲委派原则。 * 因为双亲委派的逻辑是在loadClass中通过递归实现 * 正确操作为重写findClass */ ClassLoader load = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream resourceAsStream = getClass().getResourceAsStream(fileName); if (resourceAsStream == null) return super.loadClass(name); byte[] b = new byte[resourceAsStream.available()]; resourceAsStream.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException(); } } }; ClassLoader find = new ClassLoader() { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream resourceAsStream = getClass().getResourceAsStream(fileName); if (resourceAsStream == null) { return super.findClass(name); } else { byte[] b = new byte[0]; try { b = new byte[resourceAsStream.available()]; resourceAsStream.read(b); } catch (IOException e) { e.printStackTrace(); } return defineClass(name, b, 0, b.length); } } }; // 重写loadClass,破坏双亲委派原则,但会使用自定义的类加载器 Object loadObject = load.loadClass("com.boot.demo.jvm.ClassLoaderTest").newInstance(); System.out.println("load:"+ loadObject.getClass().getClassLoader()); System.out.println(loadObject instanceof com.boot.demo.jvm.ClassLoaderTest); System.out.println("------------------------------------------------------"); // 重写findClass,运行时由于要加载的类在classpath下已经编译好,则会用AppClassLoader来加载, // 如果要使用自定义累加器的加载,则应该使用类路径classpath外的一个class文件,并且类路径下不能包含此class // 才能使用到自定义的类加载器 Object findObject = find.loadClass("com.boot.demo.jvm.ClassLoaderTest").newInstance(); System.out.println("find:"+ findObject.getClass().getClassLoader()); System.out.println(findObject instanceof com.boot.demo.jvm.ClassLoaderTest); }
打印结果如下
load:com.boot.demo.jvm.ClassLoaderTest$1@65b54208 false ------------------------------------------------------ find:sun.misc.Launcher$AppClassLoader@18b4aac2 true