老实说,类加载流程作者还是比较熟悉而且有实战经验的,因为有过一次自定义类加载器的实战经验(文章最后会和大家分享),虽然大部分小伙伴觉得这部分对coding没什么实际意义,如果你一直写CRUD并且用现有的高级语言业务框架,我可以告诉你,确实没什么用。但话说回来,你如果想多了解底层,并且在类加载时做一些手脚,那么这一块就很有必要学了。很多框架都是利用了类加载机制里的动态加载特性来搞事情,像比较出名的OSGI模块化(一个模块一个类加载器),JSP(运行时转换为字节流让加载器动态加载),Tomcat(自定义了许多类加载器用来隔离不同工程)...这里就不一一列举了。本文还是先把类加载流程先讲一讲,然后分享一下作者的一次自定义类加载的经验心得,概要如下:
文章结构
1 类加载的各个流程讲解
2 自定义类加载器讲解
3 实战自定义类加载器
作者找了下网上的图,参考着自己画了一张类生命周期流程图:
注意点:图中各个流程并不是严格的先后顺序,比如在进行1加载时,其实2验证已经开始了,是交叉进行的。
加载阶段说白了,就是把我们编译后的.Class静态文件转换到内存中(方法区),然后暴露出来让程序员能访问到。具体展开:
加载阶段获得的二进制字节流并不一定是来自.class文件,比如网络上发来的,那么如果不进行一定的格式校验,肯定是不能加载的。所以验证阶段实际上是为了保护JVM的。对于一般Javaer来说,俺们都是.java文件编译出来的.class文件,然后转换成相应的二进制流,没啥危害。所以不用太关心这一部分。
准备阶段主要是给static变量分配内存(方法区中),并设置初始值。
比如: public static Integer value =1;在准备阶段的值其实是为0的。需要注意的是常量是在准备阶段赋值的:
public static final Integer value =1 ;在准备阶段value就被赋值为了1;
解析阶段就更抽象了,稍微说一下,因为不太重要,有两个概念,符号引用,直接引用。说的通俗一点但是不太准确,比如在类A中调用了new B();大家想一想,我们编译完成.class文件后其实这种对应关系还是存在的,只是以字节码指令的形式存在,比如 "invokespecial #2" 大家可以猜到#2其实就是我们的类B了,那么在执行这一行代码的时候,JVM咋知道#2对应的指令在哪,这就是一个静态的家伙,假如类B已经加载到方法区了,地址为(#f00123),所以这个时候就要把这个#2转成这个地址(#f00123),这样JVM在执行到这时不就知道B类在哪了,就去调用了。(说的这么通俗,我都怀疑人生了).其他的,像方法的符号引用,常量的符号引用,其实都是一个意思,大家要明白,所谓的方法,常量,类,都是高级语言(Java)层面的概念,在.class文件中,它才不管你是啥,都是以指令的形式存在,所以要把那种引用关系(谁调用谁,谁引用谁)都转换为地址指令的形式。好了。说的够通俗了。大家凑合理解吧。这块其实不太重要,对于大部分coder来说,所以我就通俗的讲了讲。
这一块其实就是调用类的构造方法,注意是类的构造方法,不是实例构造函数,实例构造函数就是我们通常写的构造方法,类的构造方法是自动生成的,生成规则:
static变量的赋值操作+static代码块
按照出现的先后顺序来组装。
注意:1 static变量的内存分配和初始化是在准备阶段.2 一个类可以是很多个线程同时并发执行,JVM会加锁保证单一性,所以不要在static代码块中搞一些耗时操作。避免线程阻塞。
使用就是你直接new或者通过反射.newInstance了.
卸载是自动进行的,gc在方发区也会进行回收.不过条件很苛刻,感兴趣可以自己看一看,一般都不会卸载类.
类加载器,就是执行上面类加载流程的一些类,系统默认的就有一些加载器,站在JVM的角度,就只有两类加载器:
/lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
- <JAVA_HOME>
/lib/ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
- <JAVA_HOME>
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。见下图:
需要注意的是,自定义类加载器可以不遵循双亲委派模型,但是图中红色区域这种传递关系是JVM预先定义好的,谁都更改不了。双亲委派模型有什么好处呢?举个例子,比如有人故意在自己的代码中定义了一个String类,包名类名都和JDK自带的一样,那么根据双亲委派模型,类加载器会首先传递到父类加载器去加载,最终会传递到启动类加载器,启动加载类判断已经加载过了,所以程序员自定义的String类就不会被加载。避免程序员自己随意串改系统级的类。
上面说了半天理论,我都有点迫不及待的想上代码了。下面看看如何来自定义类加载器,并且如何在自定义加载器时遵循双亲委派模型(向上传递性).其实非常简单,在这里JDK用到了模板的设计模式,向上传递性其实已经帮我们封装好了,在ClassLoader中已经实现了,在loadClass方法中:
- protected Class < ?>loadClass(String name, boolean resolve) throws ClassNotFoundException {
- synchronized(getClassLoadingLock(name)) {
- // 1. 检查是否已经加载过。
- Class c = findLoadedClass(name);
- if (c == null) {
- long t0 = System.nanoTime();
- try {
- if (parent != null) {
- //2 .如果没有加载过,先调用父类加载器去加载
- c = parent.loadClass(name, false);
- } else {
- // 2.1 如果没有加载过,且没有父类加载器,就用BootstrapClassLoader去加载
- c = findBootstrapClassOrNull(name);
- }
- } catch(ClassNotFoundException e) {
- // ClassNotFoundException thrown if class not found
- // from the non-null parent class loader
- }
- if (c == null) {
- //3. 如果父类加载器没有加载到,调用findClass去加载
- 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;
- }
- }
从上面代码可以明显看出,loadClass(String, boolean)函数即实现了双亲委派模型!整个大致过程如下:
由上面可以知道,抽象类ClassLoader的findClass函数默认是抛出异常的。而前面我们知道,loadClass在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的findeClass函数,因此我们必须要在loadClass这个函数里面实现将一个指定类名称转换为Class对象.
如果是是读取一个指定的名称的类为字节数组的话,这很好办。但是如何将字节数组转为Class对象呢?很简单,Java提供了defineClass方法,通过这个方法,就可以把一个字节数组转为Class对象啦~
defineClass:将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组.
- protected final Class<?> defineClass(String name, byte[] b, int off, int len)
- throws ClassFormatError {
- return defineClass(name, b, off, len, null);
上面介绍了自定义类加载器的原理和几个重要方法(loadClass,findClass,defineClass),相信大部分小伙伴还是一脸蒙蔽,没关系,我先上一副图,然后上一个自定义的类加载器:
样例自定义类加载器:
- import java.io.InputStream;
- public
- class
- MyClassLoader
- extends
- ClassLoader
- {
- public MyClassLoader()
- {
- }
- public MyClassLoader(ClassLoader parent)
- {
- //一定要设置父ClassLoader不是ApplicationClassLoader,否则不会执行findclass
- super(parent);
- }
- @Override
- protected Class<?> findClass(String name) throws ClassNotFoundException
- {
- //1. 覆盖findClass,来找到.class文件,并且返回Class对象
- try
- {
- String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
- InputStream is = getClass().getResourceAsStream(fileName);
- if (is == null) {
- //2. 如果没找到,return null
- return null;
- }
- byte[] b = new byte[is.available()];
- is.read(b);
- //3. 讲字节数组转换成了Class对象
- return defineClass(name, b, 0, b.length);
- }
- catch (Exception e)
- {
- e.printStackTrace();
- }
- return null;
- }
- }
稍微说一下:
其实很简单,继承ClassLoader对象,覆盖findClass方法,这个方法的作用就是找到.class文件,转换成字节数组,调用defineClass对象转换成Class对象返回。就这么easy..
演示下效果:
- MyClassLoader mcl = new MyClassLoader();
- Class < ?>c1 = Class.forName("Student", true, mcl);
- Object obj = c1.newInstance();
- System.out.println(obj.getClass().getClassLoader());
- System.out.println(obj instanceof Student);
返回结果:
sun.misc.Launcher$AppClassLoader@6951a712
true
- MyClassLoader mcl = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent());
- Class < ?>c1 = Class.forName("Student", true, mcl);
- Object obj = c1.newInstance();
- System.out.println(obj.getClass().getClassLoader());
- System.out.println(obj instanceof Student);
返回结果:
MyClassLoader@3918d722
false
重点分析:
第一个代码和第二个代码唯一一点不同的就是在new MyClassLoader()时,一个传入的ClassLoader.getSystemClassLoader().getParent();(这个其实就是扩展类加载器)
打印结果:class sun.misc.Launcher$AppClassLoader
- MyClassLoader mcl = new MyClassLoader();
- System.out.println(mcl.getParent().getClass());
自定义类加载器就给大家说完了,虽然作者感觉已经讲清楚了,因为无非就是几个方法的问题(loadClass,findClass,defineClass),但还是给大家几个传送门,可以多阅读阅读,相互参阅一下:
www.cnblogs.com/xrq730/p/48…
www.importnew.com/24036.html
其实上面基本已经把自定义类加载器给讲清楚了,这里和大家分享一下作者一次实际的编写自定义类加载器的经验。背景如下:
我们在项目里使用了某开源通讯框架,但由于更改了源码,做了一些定制化更改,假设更改源码前为版本A,更改源码后为版本B,由于项目中部分代码需要使用版本A,部分代码需要使用版本B。版本A和版本B中所有包名和类名都是一样。那么问题来了,如果只依赖ApplicationClassLoader加载,它只会加载一个离ClassPath最近的一个版本。剩下一个加载时根据双亲委托模型,就直接返回已经加载那个版本了。所以在这里就需要自定义一个类加载器。大致思路如下图:
这里需要注意的是,在自定义类加载器时一定要把父类加载器设置为ExtentionClassLoader,如果不设置,根据双亲委托模型,默认父类加载器为ApplicationClassLoader,调用它的loadClass时,会判定为已经加载(版本A和版本B包名类名一样),会直接返回已经加载的版本A,而不是调用子类的findClass.就不会调用我们自定义类加载器的findClass去远程加载版本B了。
顺便提一下,作者这里的实现方案其实是为了遵循双亲委托模型,如果作者不遵循双亲委托模型的话,直接自定义一个类加载器,覆盖掉loadClass方法,不让它先去父类检验,而改为直接调用findClass方法去加载版本B,也是可以的.大家一定要灵活的写代码。
结语
好了,JVM类加载机制给大家分享完了,希望大家在碰到实际问题的时候能想到自定义类加载器来解决 。Have a good day .
来源: https://juejin.im/post/5a1fad585188252ae93ab953