虚拟机类加载机制 从加载到双亲委派及其破坏

虚拟机的类加载机制

类加载的时机

  • 下图是整个java类的生命周期:

虚拟机类加载机制 从加载到双亲委派及其破坏

  • 这些阶段会按部就班的开始,解析阶段某些情况下可在初始化阶段之后再开始。jvm规定有且只有六种情况必须初始化,

    (1)使用new关键字实例化对象、读取或设置一个类的静态字段(final修饰除外)、调用一个类的静态方法时。

    (2)对类进行反射调用时

    (3)父类没有初始化就先初始化父类。

    (4)先初始化main方法所在的类

    (5)参考书264页

    (6)jdk8之后,一个接口定义了被default关键字修饰的默认方法,接口实现类初始化之前接口要先初始化。

    其他被动引用方式不会触发初始化,具体见书中265页三个例子。

类加载的过程

  • 加载。其需要完成3步:

    (1)通过类的全限定名来获取此类的二进制字节流。

    (2)将字节流表示的静态存储结构转化为方法区的运行时数据结构。

    (3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类各个数据的访问接口。

    字节流的来源相当广泛,从zip包(jar,war包)到网络中获取等。

    数组类型直接由jvm生成,jvm对它的组件(去掉一层[]后如int[]->int)类型再进行递归加载。

  • 验证。分为:文件格式验证、元数据验证、字节码验证、符号引用验证。

    文件格式验证:验证是否按照约定组织的字节码信息。

    元数据验证:是否有父类,有否继承了不该继承的类,是否与父类的定义矛盾……等

    字节码验证:确定程序语义是合法、符合逻辑的。就是各种逻辑验证,十分复杂且不能保证验证通过就完全安全。jdk6之后在javac的编译阶段使用stackMapTable属性,如此在验证阶段就不需要进行逻辑推导,只检查这个属性中的记录是否合法就好。

    符号引用验证:验证被加载的类是否缺少或被禁止访问它依赖的外部类、方法、字段等资源。无法通过将抛出java.lang.IncompatibleClassChangeError的子类异常。

  • 准备。正式为类中定义的静态变量分配内存并设置变量初始值。jdk8之后,类变量会随着Class对象一起放在堆中,这里的初始值就是0值,不是程序员分配的值。如果类静态变量被final修饰,则会在这一步就分配程序员规定的值。

  • 解析。将常量池中的符号引用替换为直接引用的过程。

    这里符号引用就是类似一串字符这种,各种jvm实现必须一致。直接引用是直接可以指向目标的指针。

    jvm规范没有对解析阶段的具体时间做规定,只是在273页显示的17个字节码命令执行前进行就可以。一个符号引用的解析结果可以被缓存,之后直接使用。不过对于invokedynamic命令就不同了,它的目的就是在动态的在程序运行到某条指令时才会解析。对应于java 的lambda表达式功能。

    解析主要对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符进行。

    (1)类或接口解析

    分为需要解析的类或接口C是不是数组类型,不是就先对C进行加载,是数组就先自动生成数组对象再递归加载。最后判断本地类D的符号引用有没有对C的访问权限。即:

    a. C是public的且与D处于同一模块。

    b. C是public,且不是同一模块,但C的模块允许被D所在的模块访问。

    c. C不是public,但与D在同一个包中。

    (2)字段解析

    字段包括类级变量与实例级变量。具体见275页内容,主要就是先在字段所属的类C中找,找不到就找C实现的接口或父接口,再不行找C的父类中递归找,否则查找失败,抛出:java.lang.IllegalAccessError异常。

    (3)方法解析

    先找到方法所属的类或接口并对其进行解析,用C表示这个类。发现C是接口就直接抛异常->在C中查找这个方法->否则,查找C的父类->否则,查找继承的接口,找到了说明C是抽象类,抛相关异常->否则,找不到,抛异常。

    (4)接口解析

    与类解析相似。

  • 初始化。初始化阶段按照程序员的意愿给类变量赋值,也就是执行类构造器:<clinit>()的过程。它由javac编译器自动收集类中所有的类变量赋值动作与静态语句块中的语句合并产生。static中只能访问它之前定义的static变量,之后的可以赋值但不能访问。

    子类的<clinit>()调用之前会自动调用父类的<clinit>()。

    接口也有<clinit>(),但只有父接口变量被引用时才会被调用<clinit>()函数。

    jvm确保多线程情况下只有一个线程可以访问<clinit>()初始化一个类,其他必须阻塞等待。但这个线程退出后,其他线程并不会再次进入<clinit>(),一个类加载器下一个类型只会被初始化一次。

类加载器

  • **类加载器的功能就是通过一个类的全限定名来获取相应的二进制字节流。**这个是在jvm外部的。
  • 每一个类加载器都有一个独立的类名称空间,两个类是否相等,只有在同一个类加载器下才有意义。

双亲委派机制

  • 如下图

    虚拟机类加载机制 从加载到双亲委派及其破坏

    • 工作过程是:一个类加载器收到了加载请求,它不会自己尝试加载,会把请求发送给父类加载器加载,每一层加载器都是如此,只有父类加载器的搜索范围内没有所需的类时才会再让子加载器完成。

    • 这样做的好处是保证什么层次的类由什么层次的加载器加载,不会破坏java的原生代码。

    • 破坏双亲模型

      有3次破坏,具体见书上p286.

    java模块化系统

    • java的模块定义包含:

      依赖其他模块的列表、导出的包列表即其他模块可以使用的列表、开放的包列表即其他模块可以反射访问的列表、使用的服务列表、提供服务的实现列表。

      jvm可以通过导出信息在验证阶段就能判断依赖的类是否可访问。

      下图是模块化下的类加载器模型

      虚拟机类加载机制 从加载到双亲委派及其破坏