本博客为炼数成金JVM教程的第六课
目录
- class装载验证流程
- 什么是类装载器ClassLoader
- JDK中ClassLoader默认设计模式
- 打破常规模式
- 热替换
class 装载验证流程
- 加载
- 链接
- 验证
- 准备
- 解析
- 初始化
加载
装载类的第一个阶段
取得类的二进制流
转为方法区数据结构
在Java堆中生成对应的java.lang.Class 对象
链接 -> 验证
目的: 保证Class流的格式是正确的
1 文件格式的验证
- 是否以 0xCAFEBABE 开头
- 版本号是否合理(JDK6 可能 无法运行JDK7生成的class文件)
2 元数据验证
- 是否有父类
- 继承了final类
- 非抽象类实现了所有的抽象方法
3 字节码验证(很复杂)
- 运行检查
- 栈数据类型和操作码数据参数吻合
- 跳转指令指定到合理的位置
4 符号引用验证
- 常量池中描述类是否存在
- 访问的方法或字段是否存在且有足够的权限
链接 -> 准备
- 分配内存,并为类设置初始值(方法区中)
public static int v = 1;
在准备阶段,v会被设置为0,在初始化的clinit 中才会被设置为1
public static final int v = 1;
在准备阶段,v会被设置为1
对于static final 类型,在准备阶段就会被赋上正确的值
链接-> 解析
- 符号引用替换为直接引用
- 符号引用 -> 字符串:应用对象不一定被加载
- 直接引用 -> 指针或者地址偏移量:引用对象一定在内存中
初始化
- 执行类构造器clinit(class init)
- static 变量 赋值语句执行
- static{} 语句 执行
- 子类的clinit调用前保证父类的clinit被调用(如果子类的先被调用,对于子类来说,父类的信息就取不到)
- clinit是线程安全的(一个线程进去后,其余的线程会等待)
什么是类装载器ClassLoader
- ClassLoader 是一个抽象类
- ClassLoader的实例将读入Java字节码,将类装载到JVM中
- ClassLoader可以定制,满足不同的字节码流获取方式(可以从网络,文件等等地方加载class文件)
- ClassLoader复制装载过程中的加载阶段
CLassLoader的重要方法
- public Class<?> loadClass(String name) throws ClassNotFoundException
- 载入并返回一个Class
- protected final Class<?> defineClass(byte[] b, int off, int len)
- 定义一个类,不公开调用
- protected Class<?> findClass(String name) throws ClassNotFoundException
- loadClass回调该方法,自定义ClassLoader的推荐做法
- protected final Class<?> findLoadedClass(String name)
- 寻找已经加载的类
ClassLoader默认设计模式 - 分类
BootStrap ClassLoader (启动ClassLoader)
Extension ClassLoader (扩展ClassLoader)
App ClassLoader (应用ClassLoader/系统ClassLoader)
Custom ClassLoader(自定义ClassLoader)
除了启动ClassLoader,每个ClassLoader都有一个Parent作为父亲
JDK中ClassLoader默认设计模式 - 协同工作
首先自底向上检查类是否已经加载:
我们自己写的类都在App ClassLoader中。当我们去找一个类的时候,我们先在当前的Classloader里面找。
比如我们去找一个自己写的类,那么先在App ClassLoader中找,是否已经加载了这个类,如果没有找到,不会做加载,而是把这个请求请求到Extension ClassLoader,如果Extension ClassLoader 有,则返回该类,如果没有,那么就去找 Bootstrap ClassLoader中有没有这个类。如果有,那么返回,如果没有,那么在这个系统中都没有这个类,就会去尝试做加载。
加载的过程如下:
自顶向下的类加载方式
我们先在bootstrap ClassLoader中加载该类,如果加载成功,则返回class,如果bootstrap ClassLoader加载不成功,那么就让Extension ClassLoader 去加载,依次类推。
loadClass 原码实现
尝试加载一个类的时候,先去找这个类是否已经被加载,如果已经被加载,就返回这个类。意味着一个类不会反复被加载。如果找不到,那么就会请求父类的loader去加载。
我们看下面的例子:
这两个类在同一个文件夹下 C:\Users\Administrator\Desktop\code
public class HelloLoader {
public void print(){
System.out.println("I am in apploader");
System.out.println("===");
}
}
public class FindClassOrder {
public static void main(String args[]){
HelloLoader loader=new HelloLoader();
loader.print();
}
}
输出结果如下:
此时,我们再写一个相同的HelloLoader类
把这个类的class文件随意放一个地方C:\Users\Administrator\Desktop\code2
public class HelloLoader {
public void print(){
System.out.println("I am in bootloader");
System.out.println("===");
}
}
使用如下方法调用且结果如下:
此时,App ClassLoader中不会加载HelloLoader,因为在检查HelloLoader时,会从底层往上找,找不到,就会从顶层往下加载类,在bootstrap ClassLoader中已经找到了HelloLoader,所以不会使用到app ClassLoader中的HelloLoader。
可见,类的加载是从上往下的。
JDK中ClassLoader默认设计模式 - 问题
双亲模式的问题: 顶层ClassLoader,无法加载底层ClassLoader的类
也就是在bootstrap ClassLoader无法加载App ClassLoader中的类,没有办法生成该类的对象,该类的实例。
JDK中ClassLoader默认设计模式 - 问题解决
Thread.setContextClassLoader()
上下文加载器
是一个角色(假想为班长的身份,只是一个角色,谁都可以做。例如我们可以把App ClassLoader设置为上下文加载器)
用于解决顶层ClassLoader无法访问底层ClassLoader的类的问题
基本思想是: 在顶层ClassLoader中,传入底层ClassLoader的实例
代码来自于javax.xml.parsers.FactoryFinder
展示如何在启动类加载器加载AppLoader的类
static private Class getProviderClass(String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
try {
if (cl == null) {
if (useBSClsLoader) {
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
} else {
cl = ss.getContextClassLoader();
if (cl == null) {
throw new ClassNotFoundException();
}
else {
return cl.loadClass(className); //使用上下文ClassLoader
}
}
}
else {
return cl.loadClass(className);
}
}
catch (ClassNotFoundException e1) {
if (doFallback) {
// Use current class loader - should always be bootstrap CL
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
}
代码显示了如何在FactoryFinder这个类中,加载App ClassLoader 中的类。代码中cl 就是上下文的类加载器。在拿到了上下文加载器之后,就可以加载在App ClassLoader中的类。
上下文ClassLoader可以突破双亲模式的局限性。
双亲模式的破坏
双亲模式是默认的,但不是必须这么做
Tomcat的WebappClassloader 就会先加载自己的Class(违背了双亲模式),找不到再委托parent
OSGI的Classloader形成网状结构,根据需要自由家在Class
破坏双亲模式例子:先从底层ClassLoader加载
OrderClassLoader的部分实现
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// First, check if the class has already been loaded
Class re=findClass(name);
if(re==null){
System.out.println(“无法载入类:”+name+“ 需要请求父加载器");
return super.loadClass(name,resolve);
}
return re;
}
先会尝试自己去加载类,如果找不到,那么就去父类ClassLoader中查找。
我们看看findClass() 方法的实现
protected Class<?> findClass(String className) throws ClassNotFoundException {
Class clazz = this.findLoadedClass(className);
if (null == clazz) {
try {
String classFile = getClassFile(className);
FileInputStream fis = new FileInputStream(classFile);
FileChannel fileC = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel outC = Channels.newChannel(baos);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
省略部分代码
fis.close();
byte[] bytes = baos.toByteArray();
clazz = defineClass(className, bytes, 0, bytes.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return clazz;
}
首先去查找类(findLoaderdClass)是否被加载,如果已经加载了则直接返回。
如果找不到,自己读文件,去把二进制数据读上来。调用defineClass() 去把类真正的定义起来。
可见,findClass() 方法会去查找类定义类加载类,因此就达到了刚才说的从底层加载类的目的。
热替换
当一个class被替换之后,系统无需重启,替换的类立即生效。