JVM

类的加载

通俗来讲,类的加载就是指将 .class 文件中的字节码读入内存,将其放在运行时数据区的方法区(method code)内,最终在堆区(heap)中创建一个 java.lang.Class 对象。
JVM类初始化机制 - 图1
Class 对象封装了类在方法区内的数据结构,并且向提供了访问方法区内的数据结构的接口。

何时进行类加载

一般来说,只有在第一次 主动调用 某个类时才会去进行类加载。如果一个类有父类,会先去加载其父类,然后再加载其自身。
上面这段话有两个关键词:第一次主动调用第一次 是说只在第一次时才会有初始化过程,以后就不需要了,可以理解为每个类 有且仅有一次 初始化的机会。那么什么是 主动调用 呢?JVM 规定了以下六种情况为 主动调用,其余的皆为 被动调用

  1. 一个类的实例被创建(new操作、反射、cloning,反序列化)
  2. 调用类的static方法
  3. 使用或对类/接口的static属性进行赋值时(这不包括final的与在编译期确定的常量表达式)
  4. 当调用 API 中的某些反射方法时
  5. 子类被初始化
  6. 被设定为 JVM 启动时的启动类(具有main方法的类)

关于主动加载与被动加载的区别,可以参考下面这个例子:

  1. class NewParent {
  2. static int hoursOfSleep = (int) (Math.random() * 3.0);
  3. static {
  4. System.out.println("NewParent was initialized.");
  5. }
  6. }
  7. class NewbornBaby extends NewParent {
  8. static int hoursOfCrying = 6 + (int) (Math.random() * 2.0);
  9. static {
  10. System.out.println("NewbornBaby was initialized.");
  11. }
  12. }
  13. public class ActiveUsageDemo {
  14. // Invoking main() is an active use of ActiveUsageDemo
  15. public static void main(String[] args) {
  16. // Using hoursOfSleep is an active use of NewParent,
  17. // but a passive use of NewbornBaby
  18. System.out.println(NewbornBaby.hoursOfSleep);
  19. }
  20. static {
  21. System.out.println("ActiveUsageDemo was initialized.");
  22. }
  23. }

上面的程序最终输出:

  1. ActiveUsageDemo was initialized.
  2. NewParent was initialized.
  3. 1

之所以没有输出NewbornBaby was initialized.是因为没有主动去调用NewbornBaby,如果把打印的内容改为NewbornBaby.hoursOfCrying 那么这时就是主动调用NewbornBaby了,相应的语句也会打印出来。

类加载的生命周期

JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization。
JVM类初始化机制 - 图2
下面分别介绍这三个过程:

Loading

Loading 过程主要工作是由ClassLoader完成。该过程具体包括三件事:

  1. 根据类的全名,生成一份二进制字节码来表示该类
  2. 将二进制的字节码解析成方法区对应的数据结构
  3. 最后生成一 Class 对象的实例来表示该类

JVM类初始化机制 - 图3
JVM 中除了最顶层的Boostrap ClassLoader是用 C/C++ 实现外,其余类加载器均由 Java 实现,可以用getClassLoader方法来获取当前类的类加载器:

  1. public class ClassLoaderDemo {
  2. public static void main(String[] args) {
  3. System.out.println(ClassLoaderDemo.class.getClassLoader());
  4. }
  5. }
  6. # sun.misc.Launcher$AppClassLoader@30a4effe
  7. # AppClassLoader 也就是上图中的 System Class Loader

此外,在启动java传入-verbose:class来查看加载的类有那些。

  1. java -verbose:class ClassLoaderDemo
  2. [Opened /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
  3. [Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
  4. [Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
  5. [Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
  6. [Loaded java.lang.CharSequence from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
  7. ....
  8. ....
  9. [Loaded java.security.BasicPermissionCollection from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
  10. [Loaded ClassLoaderDemo from file:/Users/liujiacai/codes/IdeaProjects/mysql-test/target/classes/]
  11. [Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
  12. [Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
  13. [Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
  14. sun.misc.Launcher$AppClassLoader@2a139a55
  15. [Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]
  16. [Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/jre/lib/rt.jar]

ClassLoader 还具有一重要特性:双亲委派模型。具体来说就是:

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

Linking

Verification

Verification 主要是保证类符合 Java 语法规范,确保不会影响 JVM 的运行。包括但不限于以下事项:

  • bytecode 的完整性(integrity)
  • 检查final类没有被继承,final方法没有被覆盖
  • 确保没有不兼容的方法签名

    Preparation

    在这个阶段,JVM 会为 类成员变量(不包括实例变量)分配内存空间并且赋予默认初始值,需要注意的是这个阶段不会执行任何代码,而只是根据变量类型决定初始值。如果不进行默认初始化,分配的空间的值是随机的,有点类型c语言中的野指针问题。
    1. Type Initial Value
    2. int 0
    3. long 0L
    4. short (short) 0
    5. char '\u0000'
    6. byte (byte) 0
    7. boolean false
    8. reference null
    9. float 0.0f
    10. double 0.0d
    另一个需要注意的是实例 在这个阶段,JVM 也可能会为有助于提高程序性能的数据结构分配内存,常见的一个称为method table的数据结构,它包含了指向所有类方法(也包括也从父类继承的方法)的指针,这样再调用父类方法时就不用再去搜索了。

    Resolution

    Resolution 阶段主要工作是确认类、接口、属性和方法在类run-time constant pool的位置,并且把这些符号引用(symbolic references)替换为直接引用(direct references)。

    locating classes, interfaces, fields, and methods referenced symbolically from a type’s constant pool, and replacing those symbolic references with direct references.

这个过程不是必须的,也可以发生在第一次使用某个符号引用时。

Initialization

经过了上面的loadlink后,就到了 Initialization。这个阶段会去真正执行代码,具体包括:代码块(static与static)、构造函数、变量显式赋值。
这些代码执行的顺序遵循以下两个原则:

  1. 有static先初始化static,然后是非static的
  2. 显式初始化,构造块初始化,最后调用构造函数进行初始化

    示例

    属性在不同时期的赋值

    1. class Singleton {
    2. private static Singleton mInstance = new Singleton();// 位置1
    3. public static int counter1;
    4. public static int counter2 = 0;
    5. // private static Singleton mInstance = new Singleton();// 位置2
    6. private Singleton() {
    7. counter1++;
    8. counter2++;
    9. }
    10. public static Singleton getInstantce() {
    11. return mInstance;
    12. }
    13. }
    14. public class InitDemo {
    15. public static void main(String[] args) {
    16. Singleton singleton = Singleton.getInstantce();
    17. System.out.println("counter1: " + singleton.counter1);
    18. System.out.println("counter2: " + singleton.counter2);
    19. }
    20. }

    mInstance在位置1时,打印出

    1. counter1: 1
    2. counter2: 0

    mInstance在位置2时,打印出

    1. counter1: 1
    2. counter2: 1

    Singleton中的三个属性在Preparation阶段会根据类型赋予默认值,在Initialization阶段会根据显示赋值的表达式再次进行赋值(按顺序自上而下执行)。根据这两点,就不难理解上面的结果了。

    首次主动调用才会初始化

    1. public class Alibaba {
    2. public static int k = 0;
    3. public static Alibaba t1 = new Alibaba("t1");
    4. public static Alibaba t2 = new Alibaba("t2");
    5. public static int i = print("i");
    6. public static int n = 99;
    7. private int a = 0;
    8. public int j = print("j");
    9. {
    10. print("构造块");
    11. }
    12. static {
    13. print("静态块");
    14. }
    15. public Alibaba(String str) {
    16. System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
    17. ++i;
    18. ++n;
    19. }
    20. public static int print(String str) {
    21. System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
    22. ++n;
    23. return ++i;
    24. }
    25. public static void main(String args[]) {
    26. Alibaba t = new Alibaba("init");
    27. }
    28. }

    上面这个例子是阿里巴巴在14年的校招附加题

    1. j i=0 n=0
    2. 构造块 i=1 n=1
    3. t1 i=2 n=2
    4. j i=3 n=3
    5. 构造块 i=4 n=4
    6. t2 i=5 n=5
    7. i i=6 n=6
    8. 静态块 i=7 n=99
    9. j i=8 n=100
    10. 构造块 i=9 n=101
    11. init i=10 n=102

    上面是程序的输出结果

  3. 由于Alibaba是 JVM 的启动类,属于主动调用,所以会依此进行 loading、linking、initialization 三个过程。

  4. 经过 loading与 linking 阶段后,所有的属性都有了默认值,然后进入最后的 initialization 阶段。
  5. 在 initialization 阶段,先对 static 属性赋值,然后在非 static 的。k 第一个显式赋值为 0 。
  6. 接下来是t1属性,由于这时Alibaba这个类已经处于 initialization 阶段,static 变量无需再次初始化了,所以忽略 static 属性的赋值,只对非 static 的属性进行赋值,所有有了开始的:

    1. j i=0 n=0
    2. 构造块 i=1 n=1
    3. t1 i=2 n=2
  7. 接着对t2进行赋值,过程与t1相同

    1. j i=3 n=3
    2. 构造块 i=4 n=4
    3. t2 i=5 n=5
  8. 之后到了 static 的 in

    1. i i=6 n=6
  9. 到现在为止,所有的static的成员变量已经赋值完成,接下来就到了 static 代码块

    1. 静态块 i=7 n=99
  10. 至此,所有的 static 部分赋值完毕,接下来是非 static 的 j

    1. j i=8 n=100
  11. 所有属性都赋值完毕,最后是构造块与构造函数

    1. 构造块 i=9 n=101
    2. init i=10 n=102

    经过上面这9步,Alibaba这个类的初始化过程就算完成了。这里面比较容易出错的是第3步,认为会再次初始化 static 变量或代码块。而实际上是没必要,否则会出现多次初始化的情况。