JVM 【类加载器】

2018年5月7日 作者 jacky
  • class装载验证流程
  • 什么是类装载器ClassLoader
  • JDK中ClassLoader默认设计模式
  • 打破常规模式
  • 热替换

Class装载验证流程

  • 加载
  • 链接
    • 验证
    • 准备
    • 解析
  • 初始化

加载

  • 装载类的第一个阶段
  • 取得类的二进制流
  • 转为方法区数据结构
  • 在Java堆中生成对应的java.lang.Class对象

验证 验证

链接 -> 验证

  • 目的:保证Class流的格式是正确的
    • 文件格式的验证
      • 是否以0xCAFEBABE开头
      • 版本号是否合理
    • 元数据验证
      • 是否有父类
      • 继承了final类?
      • 非抽象类实现了所有的抽象方法
    • 字节码验证 (很复杂)
      • 运行检查
      • 栈数据类型和操作码数据参数吻合
      • 跳转指令指定到合理的位置
    • 符号引用验证
      • 常量池中描述类是否存在
      • 访问的方法或字段是否存在且有足够的权限

链接 准备

链接 -> 准备

  • 分配内存,并为类设置初始值 (方法区中)
    • public static int v=1;
    • 在准备阶段中,v会被设置为0
    • 在初始化的<clinit>中才会被设置为1
    • 对于static final类型,在准备阶段就会被赋上正确的值
    • public static final int v=1;

链接 解析

链接 -> 解析

初始化

  • 执行类构造器<clinit>
    • static变量 赋值语句
    • static{}语句
  • 子类的<clinit>调用前保证父类的<clinit>被调用
  • <clinit>是线程安全的

问题:Java.lang.NoSuchFieldError错误可能在什么阶段抛出

什么是类装载器ClassLoader

  • ClassLoader是一个抽象类
  • ClassLoader的实例将读入Java字节码将类装载到JVM中
  • ClassLoader可以定制,满足不同的字节码流获取方式
  • ClassLoader负责类装载过程中的加载阶段

JDK中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都有一个Parent作为父亲

ClassLoader默认设计模式 – 协同工作

  • 直接运行以上代码:
    • I am in apploader
  • 加上参数 -Xbootclasspath/a:D:/tmp/clz
    • I am in bootloader
    • 此时AppLoader中不会加载HelloLoader
      • I am in apploader 在classpath中却没有加载
      • 说明类加载是从上往下的

问题:能否只用反射,仿照上面的写法,将类注入启动ClassLoader呢?

Thread. setContextClassLoader()

  • 上下文加载器
  • 是一个角色
  • 用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题
  • 基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例
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());
        }
......

双亲模式的破坏

  • 双亲模式是默认的模式,但不是必须这么做
  • 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;
}
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;
}

热替换

当一个class被替换后,系统无需重启,替换的类立即生效

public class CVersionA {
    public void sayHello() {
        System.out.println("hello world! (version A)");
    }
}

DoopRun 不停调用CVersionA . sayHello()方法,因此有输出:

hello world! (version A)

在DoopRun 的运行过程中,替换CVersionA 为:

public class CVersionA {
    public void sayHello() {
        System.out.println("hello world! (version B)");
    }
}

替换后, DoopRun 的输出变为

hello world! (version B)