美文网首页
08 虚拟机类加载机制

08 虚拟机类加载机制

作者: 格林哈 | 来源:发表于2020-09-16 15:11 被阅读0次

1 类加载机制

  • 定义: 数据从Class文件加载到内存,并对数据进行验证,准备,解析和初始化,最终形成可以被虚拟机直接使用的java类型的这一过程。

1.1 类的生命周期

  • image.png
  • 一个类型从加载到虚拟机内存中,到卸载出内存为止,它的生命周期
    • 加载
    • 验证
    • 准备
    • 解析
    • 初始化
    • 使用
    • 卸载
    • 加载、验证、准备、初始化和卸载 这五个阶段 顺序确定的, 其他阶段可以交叉混合进行。
java虚拟机规范 严格规定了6种情况必须立即进行初始化。
  • 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时。这四个指令场景
    • new关键字实例化对象
    • 读取或者设置静态字段的时候(final修饰,已在编译器把结果放到常量池的静态字段除外)
    • 调用静态方法。
  • 使用java.lang.reflect包的方法对类型进行反射调用的时候。
  • 当初始化类的时候,如果发现其父类还没有进行过初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
  • 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

1.2 类加载的过程

  • java虚拟机规范规定: 通过一个类的全限定名来获取定义此类的二进制字节流

  • 没有指定要从哪里获取,如何获取。常见获取方式

    • 从ZIP压缩包中读取,如 JAR、EAR、WAR格式的基础。
    • 从网络中获取,如 Web Applet。
    • 运行时计算生成,如 ,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
    • 由其他文件生成,如 JSP应用,由JSP文件生成对应的Class文件。
    • 从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP-Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发
    • 可以从加密文件中获取,这是典型的防Class文件被反编译的保护措施,通过加载时解密Class文件来保障程序运行逻辑不被窥探
  • 非数组类型的加载阶段

    • 可以使用Java虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员通过定义自己的类加载器去控制字节流的获取方式(重写一个类加载器的findClass()或loadClass()方法)
  • 数组类

    • 数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来的,但是数组类的元素类型(Element Type,指的是数组去掉所有维度的类型)最终还是要靠类加载器来完成加载
    • 数组类创建遵循一下规则:
      • 如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型)是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将被标识在加载该组件类型的类加载器的类名称空间上
      • 如果数组的组件类型不是引用类型(例如int[]数组的组件类型为int),Java虚拟机将会把数组C标记为与引导类加载器关联
      • 数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的可访问性将默认为public,可被所有的类和接口访问到
  • 加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了。

1.3 验证

  • 目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
  • 验证阶段是非常重要的,从代码量和耗费的执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重。
  • 验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证
    • 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
      • 是否以魔数0xCAFEBABE开头。
      • 主、次版本号是否在当前Java虚拟机接受范围之内
      • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
      • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
      • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据
      • class文件中各个部分及文件本身是否有被删除的或附加的其他信息。 ...
      • 目的:保证输入的字节流能正确地解析并存储于方法区之内,只有通过了这个阶段的验证之后,这段字节流才被允许进入Java虚拟机内存的方法区中进行存储,后面的三个验证阶段全部是基于方法区的存储结构上进行的。
    • 元数据验证
      • 对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求
      • 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
      • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
      • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
      • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。
      • 目的: 对类的元数据信息进行语义校验。保证不存在与《java语言规范》不符信息
    • 字节码验证
      • 通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
      • 对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
    • 符号引用验证
      • 发生在虚拟机将符号引用转化为直接引用的时候,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。
      • 符号引用中通过字符串描述的全限定名是否能找到对应的类
      • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
      • 符号引用中的类、字段、方法的可访问性(private、protected、public、<package>)是否可被当前类访问
      • 目的:确保解析行为能正常执行

1.4 准备

  • 正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
  • 从概念上讲,这些变量所使用的内存都应当在方法区中进行分配
    • jdk7 永久代来实现方法区
    • jdk8以后类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了
  • 分配内存仅包括类变量,初始值一般是0
    • 特殊情况 ConstantValue属性所指定的初始值 final 修饰的值,就是你定义的值。

1.5 解析

  • Java虚拟机将常量池内的符号引用替换为直接引用的过程
    • 符号引用
      • 以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,不一定加载到内存。
      • 符号引用与虚拟机实现的内存布局无关
    • 直接引用
      • 直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄
      • 直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同
      • 如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在
  • Java虚拟机规范执行下面字节码指令之前,先对其符号引用进行解析
    • anewarray 创建一个引用型(如类, 接口, 数组)的数组, 并将其引用值压入栈顶
    • checkcast 检验类型转换, 检验未通过将抛出 ClassCastException
    • getfield 获取指定类的实例域, 并将其压入栈顶
    • getstatic 获取指定类的静态域, 并将其压入栈顶
    • instanceof 检验对象是否是指定类的实际,
    • invokedynamic 调用动态方法
    • invokeinterface 调用接口方法
    • invokespecial 用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
    • invokestatic 调用静态方法
    • invokevirtual 用于调用非私有实例方法。
    • ldc 8bit 索引,将int,float或String型常量值从常量池中推送至栈顶
    • ldc_w 16 bit索引,将int,float或String型常量值从常量池中推送至栈顶
    • ldc2_w 将long或double型常量值从常量池中推送至栈顶
    • multianewarray 创建指定类型和指定维度的多维数组(
    • new 创建一个对象, 并将其引用引用值压入栈顶
    • putfield 为指定类的实例域赋值
    • putstatic 为指定类的静态域赋值

1.6 初始化

  • 类加载过程的最后一个步骤
  • 目的: 初始化阶段就是执行类构造器<clinit>()方法的过程
    • clinit Javac编译器自动
      • 是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的
      • 顺序是由语句在源文件中出现的顺序决定的
      • 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
public class Test {
    static {
        i = 0;  //  给变量复制可以正常编译通过
        System.out.print(i);  // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}

2 类加载器

2.1 类与类加载器

  • 对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间
    • 比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义
      • 相等 是指equals()方法、isAssignableFrom()方法、isInstance()
public class ClassLoaderTest {

    public static void main(String[] args) throws Exception {

        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };

        Object obj = myLoader.loadClass("com.mg.javaxnj.chapter07.ClassLoaderTest").newInstance();

        System.out.println(obj.getClass());
        System.out.println(obj instanceof  com.mg.javaxnj.chapter07.ClassLoaderTest);
        System.out.println(obj.getClass().getClassLoader());
        System.out.println(com.mg.javaxnj.chapter07.ClassLoaderTest.class.getClassLoader());
    }
}

// 输出
class com.mg.javaxnj.chapter07.ClassLoaderTest
false
com.mg.javaxnj.chapter07.ClassLoaderTest$1@4f023edb
sun.misc.Launcher$AppClassLoader@18b4aac2

2.2 双亲委派模型

三层类加载器
  • 启动类加载器(Bootstrap Class Loader)
    • 负责加载存放在<JAVA_HOME>\lib目录 或者 被-Xbootclasspath参数所指定的路径中存放的
    • 而且Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)
    • 自定义类加载器如果需要把加载请求委派给 启动类加载器,直接null代替
  • 扩展类加载器(Extension Class Loader)
    • sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的
    • 负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库
  • 应用程序类加载器
    • 由sun.misc.Launcher$AppClassLoader来实现
    • 负责加载用户类路径(ClassPath)上所有的类库
  • image.png
  • JDK9 之前 java应用都是这三种类加载器进行相互配合完成的,也可以自定义类加载器
    • 如增加除了磁盘位置之外的Class文件来源
    • 通过类加载器实现类的隔离、重载等功能
    • 对class文件进行加密
双亲委派模型的工作过程
  • 一个类加载器收到类加载请求,自己先不尝试加载这个类,而是把这个请求委派给父类加载器去完成。

  • 每一层类加载器都是如此,因此所有类加载请求最后都应该传送到最顶层启动类加载器中。

  • 当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试去完成加载。

  • 好处

    • Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。
      • 例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类
破坏双亲委派模型
  • 双亲委派模型出现之前,类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码

  • 模型自身的缺陷导致的,越基础的类由越上层的加载器进行加载,如果有基础类型又要调用回用户的代码

    • 如 JNDI服务,它的代码由启动类加载器来完成加载,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口,启动类加载器范围内搜索不到
  • 用户对程序动态性的追求而导致的,

    • 动态性 是指
      • 代码热替换
      • 模块热部署
      • OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换
  • 来源 深入理解java虚拟机

相关文章

网友评论

      本文标题:08 虚拟机类加载机制

      本文链接:https://www.haomeiwen.com/subject/cerfyktx.html