《深入理解Java虚拟机:JVM高级特性与最佳实践》读书笔记(三)

虚拟机把描述类数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机识别的Java类型,这就是Java虚拟机的类加载机制,并且类型的加载、链接和初始化都是在程序运行期间完成的,这篇文章就来谈谈类加载机制。

类加载的时机

  • 类从加载到虚拟机内存开始,到卸载出内存为止,类的生命周期包括:加载、验证、准备、解析、初始化、使用和卸载等7个阶段。其中验证、准备、解析阶段称为链接。类的生命周期如图:

图中的加载、验证、准备、初始化和卸载等几个阶段是确定的,而解析这个阶段并不确定。

  • 虚拟机规范严格规定了有且只有5种情况下必须对类进行“初始化”:

    • 使用new关键字示例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候;
    • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化;
    • 当初始化一个类时,若其父类还没有初始化则需要先触发其父类的初始化;
    • 当虚拟机启动时,用户需要指定一个要执行的主类(包含mian()的方法的那个类),虚拟机会先初始化这个主类;
    • 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_in-vokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

    上面5种情况称为对一个类进行主动引用。除此之外的所有引用类都不会触发初始化,称为被动引用。下面是对被动引用的演示代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class SuperClass{
    static{
    System.out.println("SuperClass init!");
    }
    public static int = 123;
    }
    public class SubClass extends SuperClass{
    static{
    System.out.println("SubClass init!");
    }
    }
    public class ConstClass{
    public static final String HELLOWORLD = "Hello world";
    static{
    System.out.println("ConstClass init!");
    }
    }
    public class NotInitialization{
    public static void main(String[] args){
    //通过子类引用父类的静态字段,不会导致子类初始化,因为对于静态字段只有定义这个字段的类才会初始化
    System.out.println(SuperClass.value);
    //通过数组定义来引用类,不会触发该类的初始化
    SuperClass[] sca = new SuperClass[10];
    //常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,不会触发该类的初始化
    System.out.println(ConstClass.HELLOWORLD);
    }
    }
  • Java虚拟机中类加载的过程包括加载、验证、准备、解析和初始化这5个阶段:

    • 加载阶段,虚拟机需要完成的事情:

      • 通过一个类的全限定名获得定义此类的二进制字节流;
      • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
      • 在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问接口。

      数组类本身不通过类加载器创建,而是由Java虚拟机直接创建。

    • 验证是连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。大体上,验证阶段会完成文件格式、元数据、字节码和符号引用等验证;

    • 准备阶段是正式为类变量分配内存(此时内存分配仅包括类变量——被static修饰的变量,实例变量将会随着对象一起分配在Java堆中)并设置类变量初始值(即数据类型的零值,当然ConstantValue属性会设置为指定的值)的阶段;

    • 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的的过程;

    • 初始化阶段真正执行类中定义的Java程序代码,并执行类构造器<clinit>()方法,按照程序初始化类和其他资源。

类加载器

  • 实现”通过一个类的全限定名获得定义此类的二进制字节流”这个动作的代码模块成为”类加载器“;
  • 一个类在Java虚拟机中的唯一性需要由这个类本身和加载该类的类加载器共同决定,即比较两个类是否相等的前提是这两个类由同一个类加载器加载。这里的”相等“,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法返回的结果,也包括使用instanceof关键字做对象所属关系判定情况;
  • 从Java虚拟机的角度讲,只存在两种不同的类加载器:启动类加载器,这个类加载器由C++语言实现,是虚拟机的一部分;另一种是是所有其他类加载器,由Java语言实现,并且独立于虚拟机外部;
  • 从Java程序员的角度来看,类加载器可以分为如下3类:
    • 启动类加载器(Bootstrap ClassLoader):是用本地代码实现的类装入器,它负责将 Java_Home/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作;
    • 标准扩展类加载器(Extension ClassLoader):是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Home >/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器;
    • 应用程序类加载器(Application ClassLoader):是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将用户类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。由于这个类加载器是ClassLoader中的getSystemClassLoader()返回值,所以也成为系统类加载器。
  • 某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。这就是双亲委派机制。