JVM类加载机制

187 阅读4分钟

类加载

类加载过程

类加载到使用会经过如下几步

加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

注意,jar包中的类并不是程序启动一次性加载,而是使用到的时候才会加载

  • 加载:在磁盘上查找,通过IO读入字节码文件,使用到类的时候会被加载

  • 验证:检验字节码的正确性.(字节码的每条指令是一个16进制,4字节的字符串,看起来像是乱码,但是每条指令都是有清晰的语义,可以通过字节码手册查找,16进制字节码查看起来过于困难,也可以通过javap命令生成更可读的字节码指令,具体用法请在cmd中使用javap -help查阅)

  • 准备:给静态变量分配内存,并赋默认值

  • 解析:将符号引用替换为直接引用,(此时会把一些符号引用替换为数据所存内存的指针或者句柄,这就是所谓的静态链接过程,在类加载期间完成,在程序运行时还会有动态链接过程,具体情况请查看常量池)

  • 初始化:为静态变量初始化为指定的值,并执行静态代码块

  • 使用,卸载:详情见语义

类加载器

jvm的类加载器主要有四种,级别由高到低

  • 启动类加载器(BootstrapClassLoader):负责加载jvm运行时的核心类库,jre/lib下的jar包

  • 扩展类加载器(ExtClassLoader):加载jvm运行时的拓展类库,jre/lib/ext下的jar包

  • 应用程序类加载器(AppClassLoader):应用程序类加载器,负责加载我们自己编写的类

  • 自定义类加载器:继承java.lang.ClassLoader类,加载自定义目录下的类

双亲委派模型

双亲委派模型就是在类加载器进行类加载时不会第一时间就自己加载,而是将类委派给它的上级类加载器,父级类加载器接受到后会再次向上委派,直到最顶级的启动类加载器,然后启动类加载器会判断当前类是否是应该自己加载,如果不是向下委派回去,子级类加载器再次判断,如果不是向下委派,直到类被加载

例:

  1. 首先直接委派给父级类加载器(扩展类加载器)
  2. 父级类加载器直接委派给它的父级(启动类加载器)
  3. 启动类加载器判断这个类是否是核心类库,在jre/lib下,如果是自己加载,如果不是向下委派回去
  4. 扩展类加载器判断这个类是否是拓展类库,在jre/lib/ext下,如果是自己加载,如果不是向下委派回去
  5. 应用程序类加载器判断这个类是否是自己编写的类,在编译目录下,此时判断是,自己加载.(如果没动过编译目录,那就不可能不是了)

为什么设计双亲委派机制

  • 沙箱安全机制:防止系统核心类库被篡改
  • 避免类被重复加载:可以保证类在jvm中的唯一性

双亲委派机制在哪里实现

定义在抽象类ClassLoader中的loadClass()方法中,java定义的三种类加载器会在加载最后一步调用父类的loadClass方法.在这里会进行 if(parent != null)的判断,parent变量也是双亲委派机制中双亲的由来. 所有类加载器都直接或间接的继承自ClassLoader.(除启动类加载器除外,启动类加载器由C语言定义)

public class Launcher {
     static class AppClassLoader extends URLClassLoader 
     static class ExtClassLoader extends URLClassLoader
}
/*从这个类中可以看到AppClassLoader,ExtClassLoader都是内部类
继承自URLClassLoader*/

URLClassLoader的类图见下方

如何打破双亲委派机制

自定义类继承ClassLoader,重写loadClass方法,并去除掉双亲委派的逻辑.具体请看下一标题

注意:即便打破了双亲委派机制,类似于java.lang.String这样的类也无法加载,jvm会将其判断成非法类

实现自定义类加载器

继承ClassLoader抽象类,重写findClass方法,如果需要重写类加载逻辑就重写loadClass方法

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    name = name.replace("\\.","/");
    try {
        FileInputStream fileInputStream = new FileInputStream(classPath + name);
        int available = fileInputStream.available();
        byte[] bytes = new byte[available];
        fileInputStream.read(bytes);
        return defineClass(name,bytes,0,bytes.length);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return super.findClass(name);
}