java中,ClassLoader负责读取class文件、加载到JVM
类加载过程
类加载包含的过程:
- 加载,根据类的完全限定查找字节码文件并加载、创建一个Class对象
- 验证,验证class文件符合JVM要求
- 准备,为static变量分配内存并初始化为初始值(不管是否显式指定了初始化,都先初始化为变量类型默认的初始值)
- 解析,为常量值创建直接引用
- 初始化,类的初始化(static变量初始化、static代码块执行)
所以说,在类加载时就初始化了static量和常量,static量和常量是和class对象一样存储在方法区的。此处要说明的是,比如A中引用了B类作为静态变量,那么在加载A类时,并不会加载B,而是作为一个引用直接默认初始化为null,在初始化A时,才会加载static变量引用的类(静态变量都是如此,也不必说成员变量了)(https://stackoverflow.com/questions/48621815/static-fields-in-jvm-loading)
另外,一个类在只引用了static变量时,会先完成static变量显式初始化和static代码块,而不会初始化成员变量、执行构造器。考虑全部的话,顺序是:父类 static 变量 –> 父类 static 代码块 –> 子类 static 变量 –> 子类 static 代码块 –> 父类显式初始化 –> 父类构造代码块 –> 父类构造器 –> 子类显式初始化 –> 子类构造代码块 –> 子类构造器
初始化的顺序就是,保证在变量可以被访问时,一定已经初始化了。所以,先static再成员变量,先初始化代码块再构造器,先父类再子类;在子类执行成员变量初始化、构造器之前,因为可能引用到父类的成员变量,所以一定先完成父类的成员变量显式初始化、构造代码块、构造器
另外一个情况明确下:比如,父类static量引用了子类static量,那么从子类static得到的是类型默认的初始值,而不会先初始化子类static量。但是这种情况可以认为不存在(子类肯定知道父类的存在,但是父类不应该知道子类的存在;这种情况并发时可能在类加载时导致死锁)
各种ClassLoader
- Bootstrap类加载器,C++实现,加载
/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,主要是java.xxx等核心类 - Extension类加载器,java实现,加载
/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,java代码中可以使用 - System类加载器/APP类加载器,加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,开发者引入的依赖和编写的类
关于父类和父加载器
APP类加载器的父加载器是Extension类加载器,Extension类加载器的父加载器是null,或者可以认为是Bootstrap类加载器(C++实现)
自定义的类加载器的父加载器不指定时默认都是APP类加载器
父加载器和父类、子类没有关系。从类继承关系上来说,Bootstrap类加载器是C++实现的,不考虑;Extension类加载器和APP类加载器都是继承自URLClassLoader,再向上都继承自抽象类ClassLoader
一些特点
延迟加载
在使用到时才加载,比如:
- 显式加载,比如Class.forName()(典型的,加载数据库jdbc驱动)、getClass().getClassLoader().loadClass()
- 隐式加载,使用到时自动加载
关于Class.forName(),要注意的是,这一方法默认会将类加载并初始化(初始化static量和static代码块)。所以加载jdbc驱动时,只需要Class.forName(“com.mysql.jdbc.Driver”),因为Driver类使用了static代码块初始化
1 | public class Driver extends NonRegisteringDriver implements java.sql.Driver { |
而loadClass()只加载、默认不解析(相关的是resolveClass()方法)、不初始化
双亲委派
在loadClass()方法中加载类,默认的实现中流程都是:
- findLoadedClass()从缓存检查是否已经加载,已经加载时直接返回
- 交由父加载器加载,父加载器继续交由父加载器,即递归的向上请求,父加载器为null时请求Bootstrap加载器加载(所以说可以认为Bootstrap加载器是Ext加载器的父加载器)
- 如果父加载器不能加载,那么自行调用findClass()方法找到类并加载
ClassLoader类中实现的findClass()方法没有具体操作,而是直接抛出了一个异常,URLClassLoader类中做出了实现
双亲委派可以保证安全性,核心类库只会由内置的类加载器加载(而且,即使自定义了一个类取包名为java.lang之类,是会报错禁止加载的)
classLoader与class对象
不考虑父加载器,在一个classLoader中,class对象是唯一的,但是在多个不同的classLoader之间,可能某个类使用不同的多个加载器加载了多个class对象,即不同的classLoader加载的实例对象都拥有不同的独立的类名称空间
使用默认的loadClass()方法中,先使用findLoadedClass()方法根据完整类名检查了类是否已经加载。因此,一个classLoader实例,只能加载一个类一次(即使重写了loadClass()方法去掉了findLoadedClass()检查,在重复加载时会报错duplicate class definition)
如果要加载一个class多次,需要每次使用一个新的自定义classLoader实例,并且这个class应该是这一个classLoader自行加载的(而不是委托给父加载器的)。对于一个代码中直接使用了的类,APP类加载器默认已经加载了,即使通过自定义classLoader加载多个class,直接调用构造器new得到的这一个类的实例,关联的仍然是APP类加载器加载的class对象(所以如果将自定义类加载器加载的类的实例cast为代码中的A,是会异常的,关联的不是同一个class对象当然不能cast)
自定义classLoader
可用于实现:
- 从一个自定义的路径加载类,或者从网络加载类
- 对于特定路径/网络中读取的字节码加密的class文件,自定义classLoader来实现解密后加载
- 热部署。假设要从一个特定的目录读取class文件并替换当前实例,可以使用多个classLoader,直接调用findClass()方法从特定目录加载某个class文件,来得到多个class对象;或者指定自定义类加载器的父加载器是null或Ext类加载器,并重写findClass()方法,调用loadClass()加载(下文有说明和实现)
自定义时,文档中不建议重写loadClass()方法,因为可能打破双亲委派模型,建议实现findClass()方法(默认流程中,先尝试父加载器加载,父加载器不能加载时findClass()找到类来加载;如果直接重写loadClass()方法放弃了使用父加载器,对于引用到的java.xxx等类(即使什么也没引用那么也默认extends了Object),自定义类加载器是禁止加载这些核心类的)
findClass()方法用于读取字节码的流,defineClass()用于将字节码的构建为class对象
自定义时,如果继承ClassLoader类,需要重写findClass()方法,在其中读取字节码,然后调用defineClass()方法构建class对象并返回;如果继承URLClassLoader类,那么已经实现了很完备的findClass()方法,可以识别传入的是一个文件路径还是URL等,一般比较简洁
还需要注意的是,一个classLoader加载类A,那么它也需要负责加载A中使用的其他类,实现中可以是委托到双亲或者自行加载
简单模拟热部署
模拟热部署,假设要从一个特定的目录读取class文件并替换当前实例,可以使用多个classLoader,直接调用findClass()方法从特定目录加载某个class文件,来得到多个class对象;或者指定自定义类加载器的父加载器是null或Ext类加载器,并重写findClass()方法,调用loadClass()加载
直接调用findClass()方法显然可以得到这个类的多个class对象
指定自定义类加载器的父加载器是null或Ext类加载器是因为,自定义classLoader默认的父加载器是APP类加载器,APP类加载器是可以加载源代码的类的。既然热部署那么当然这个类在现有代码中已经使用了,APP类加载器已经加载过了,双亲委派模型下无法加载这一个类的多个class对象。而null或Ext类加载器是无法加载源代码中的类的,那么它们将交由自定义类加载器使用findClass()加载
指定自定义类加载器的父加载器为null或者Ext类加载器带来的问题是,比如指定为Ext类加载器,那么自定义类加载器中只能委托到Ext类加载器和Bootstrap类加载器。如果加载的类A之中引用了一个源代码中其他类B,Ext和Bootstrap类加载器都无法加载B,需要自定义类加载器来处理,这种情况中,可以得到A的class对象,在初始化A时,才会加载B,此时如果自定义类加载器不能加载B,那么就会抛出异常
对这个问题,我们可以在自定义类加载器中维护一个负责加载的类的集合,集合之外的类调用APP类加载器进行加载
还存在的一个问题是,代码中直接使用的类A,是由APP类加载器加载的,那么再由自定义类加载器加载一个A时,Class.newInstance()得到的实例,不能cast为代码中的A,因为对应的不是一个class对象,这样使用自定义加载器加载的A时需要通过反射的方式。这个问题可以这样解决:让A实现一个接口C,在使用自定义类加载器加载A时,将接口类交给APP类加载器加载(实际上APP类加载器也应该早已加载过了),那么就可以将A cast为C来使用
代码如下
MyService是要热部署的类,实现了MyInterface接口:
1 | package classLoaderTest; |
1 | package classLoaderTest; |
MyClassLoader继承自ClassLoader,重写了findClass()方法:
1 | package classLoaderTest; |
以及一个模拟热部署的测试类:
1 | public class HotDeployTest { |
以上代码输出:
1 | classLoaderTest.MyService: doSomething: before hot deploy |