类加载类的生命周期
类的声明周期经过 加载-连接(验证-准备-解析)-初始化(解析阶段可能动态的在初始化之后)-使用-卸载
加载(与连接交叉进行,仅开始时间有先后顺序)
- 通过类全路径名获取该类的二进制流(可从本地/网络/资源包/动态生成)
- 该类字节流中的静态存储结构转换为方法区运行时数据结构
- 内存中生成该类的Class对象,作为方法区中该类各种数据入口
- 除数组类由Java虚拟机创建,其他类的加载(载入类二进制字节流)可使用系统加载或自定义
连接-验证
- 验证字节流内信息是否符合Class类文件规范,毕竟加载该字节流可从任何条件符合的地方加载.验证成功后会将该字节流存入方法区
- 类元数据语义规范验证,保证符合Java语言规范
- 字节码安全性验证,保证该类没有侵入性危害
- (解析阶段)符号引用正确性验证,验证类以外引用信息正确合法性验证(类、方法、字段、范围描述符等)
连接-准备
该阶段会为类变量分配内存和设置初始值(零值).当该类变量为final时则会直接给变量赋值(程序设置的值)
连接-解析
将常量池内符号引用(类/方法/类变量等引用)替换为直接引用的过程,将引导符号替换为指针/地址偏移量等
初始化
执行类构造器的过程,初始化类变量和类静态语句块的过程.该类构造器不是程序中定义的构造方法.而是由类静态变量和类静态块组成的<clinit>()方法.且顺序由源文件定义的顺序决定(当有父类则会先将父类初始化掉).
所以就有了这个初始化顺序父类静态成员/块 --> 子类静态成员/块 --> 父类普通成员/块 --> 父类构造函数 --> 子类普通成员/块 --> 子类构造函数. 后面四个阶段根本就不再加载过程中,而是使用过程中.</clinit>
系统ClassLoader介绍
每个ClassLoader加载各自负责的路径下的类.
BootStrap ClassLoader(C++实现,基类加载器) -> ExtClassLoader(拓展类加载器) -> AppClassLoader(应用类加载器)
BootStrapClassLoader负责加载jre\lib基础包类
ExtClassLoader负责加载jre/lib/ext拓展包类(可通过-D java.ext.dirs另行指定目录)
AppClassLoader负责加载自定义应用路径下的类(可通过-D java.class.path另行指定目录)
双亲委派模型
如下图所示,每个ClassLoader是上一个ClassLoader的子Loader(组合机制实现父子级关系,由parent属性指定),每个Loader加载完类会有对应的缓存.双亲委派机制分为向上委托查找以及向下委托加载两部分,用来防止多重名类加载导致基础类覆盖,破坏Java行为问题.
向上委托查找
当加载类请求到ClassLoader上后,该ClassLoader会查找当前加载缓存,当缓存中没有则会将加载请求向上机Loader传递,当传递到Bootstrap后还未从缓存中找到该类则向上委托结束
向下委托加载
当所有ClassLoader均未加载过该类则会进行该类的加载工作,此时加载请求在BootStrapClassLoader上,他会检查自己负责加载路径下是否有该类进行加载,没有则向下委托子Loader在他的加载路径进行查找加载,如果没有则继续向下传递至AppClassLoader结束.
源码来自于java.lang.ClassLoader的loadClass()方法
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, false); }else{ // 没有父类则需要指定由启动类加载器进行查找加载 c = findBootstrapClassOrNull(name); } }catch(ClassNotFoundException e){ // 父类加载器抛出ClassNotFoundException则表示父类加载器无法加载 } if(c == null){ //经过父类加载器向上委托查找以及向下查找加载至本级均未成功加到该类则自身尝试加载 c = findClass(name); } // 解析Class对象 if(resolve){ resolveClass(c); } return c; } }
类加载对象ClassLoader了解
上面ExtClassLoader和AppClassLoader均继承自UrlClassLoader.
自定义类加载器
根据上面的类继承机制以及loadClass方法,我们自定义ClassLoader即可继承UrlClassLoader/SecureClassLoader通过实现findClass方法,并在findClass方法中调用defineClass(name,byte,0,byte.length)来实现类定义加载
破坏双亲委派模型
破坏双亲委派模型根据上面代码解析可知我们只需要重写loadClass方法即可(因为他是protected级别的访问限制).但是还是要注意我们只需更改下加载自身实现findClass方法顺序即可,当我们加载其他系统级/未自定义的类还是要交给父级Loader来执行的
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ //检查该类是否有被加载过,是否可以直接从缓存中读取到 Class c = findLoadedClass(name); if(c == null){ // 先由自身加载 c = findClass(name); if(c == null){ try{ if(parent != null){ c = parent.loadClass(name, false); }else{ c = findBootstrapClassOrNull(name); } }catch(ClassNotFoundException e){ // 父类加载器抛出ClassNotFoundException则表示父类加载器无法加载 } } // 解析Class对象 if(resolve){ resolveClass(c); } return c; } }
动态字节码技术
笔者这些知识是学习并记录作以加强理解,对这些技术并没有很深入研究.可以参考这篇美团技术团队的文章
Java代码想要被JVM执行就要编译成字节码文件被JVM读取进行解释执行,我们想要对现有功能动态修改就要动态的改变原有的字节码生成新的字节码.
Java中可使用ASM、CGLIB(底层ASM)、BCEL、Javassist等.
ASM
目前最为强大的生成字节码技术之一,可修改类、方法或者重新定义类.核心功能类ClassVisitor、ClassWriter、ClassReader
CGLIB
Spring的AOP技术就使用到了CGLIB,当可以使用动态代理则会优先使用动态代理,否则则会使用CGLIB
热加载
热加载即在不停止服务的前提下,将已修改的类文件替换掉并将其类加载至JVM内存中.IDEA热部署可选择Spring热部署依赖devtools、JRebel热部署插件等.