概要

.class 文件定义了JVM中的一个类型,包括域、方法、继承信息、注解、其它元数据。JVM 规范对类文件的格式有详细描述。
类是 JVM 可以加载的最小程序单元。
将新的类加到 JVM 的当前运行状态中,经过加载、连接、大量验证、提供新 Class 对象给系统、创建实例。

类加载过程 - 图1

字节码

在聊 Java 类加载机制之前,需要先了解一下 Java 字节码,因为它和类加载机制息息相关。
计算机只认识 0 和 1,所以任何语言编写的程序都需要编译成机器码才能被计算机理解,然后执行,Java 也不例外。
Java 在诞生的时候喊出了一个非常牛逼的口号:“Write Once, Run Anywhere”,为了达成这个目的,Sun 公司发布了许多可以在不同平台(Windows、Linux)上运行的 Java 虚拟机(JVM)——负责载入和执行 Java 编译后的字节码。
类加载过程 - 图2
我们借助一段简单的代码来看一看,源码如下:

  1. package org.blackist.jvm;
  2. public class JvmDemo {
  3. public static void main(String[] args) {
  4. System.out.println("董亮亮的开发笔记");
  5. }
  6. }

代码编译过后,通过十六进制工具 xxd JvmDemo.class命令查看这个字节码文件:

00000000: cafe babe 0000 0034 0022 0a00 0600 1409  .......4."......
00000010: 0015 0016 0800 170a 0018 0019 0700 1a07  ................
00000020: 001b 0100 063c 696e 6974 3e01 0003 2829  .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e  V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 124c 6f63  umberTable...Loc
00000050: 616c 5661 7269 6162 6c65 5461 626c 6501  alVariableTable.

这段字节码中的 cafe babe 被称为“魔数”,是 JVM 识别 .class 文件的标志。
文件格式的定制者可以自由选择魔数值(只要没用过),比如说 .png 文件的魔数是 8950 4e47

类加载过程

JVM结束生命周期的几种情况:

  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序执行过程中遇到异常或错误而异常终止
  • 操作系统出现错误而导致JVM进程终止

Java 的类加载过程可以分为 5 个阶段:载入、验证、准备、解析和初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。
类加载过程 - 图3

加载

查找并加载类的二进制数据。
将类的.class文件中的二进制数据读入到内存,将其放在运行时数据区的方法区内,然后在堆去创建java.lang.Class对象,用来封装类在方法区内的数据结构。
类加载过程 - 图4

加载.class的方式

  • 从本地加载
  • 从网络上加载(URLClassLoader(URL[] urls))
  • 从zip, jar等归档文件中加载.class文件
  • 从专有数据库提取.class文件
  • 将Java源文件动态编译成.class文件

    连接

    验证

    确保被加载的类的正确性,符合JVM字节码规范,该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。

  • 确保二进制字节流格式符合预期(比如说是否以 cafe bene 开头、主次版本号是否在虚拟机处理范围内)。

  • 是否所有方法都遵守访问控制关键字的限定。
  • 方法调用的参数个数和类型是否正确。
  • 确保变量在使用之前被正确初始化了。
  • 检查变量是否被赋予恰当类型的值。

    准备

    JVM 会在该阶段对类变量(也称为静态变量, static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。
public String blackist = "Blackist";
public static String note = "Note";
public static final String bnote = "Note-of-Blackist";

blackist 不会被分配内存,而 note会;但 note的初始值不是“王二”而是 null
需要注意的是, static final 修饰的变量被称作为常量,和类变量不同。常量一旦赋值就不会改变了,所以 bnote 在准备阶段的值为“沉默王二”而不是 null

解析

促使 VM 检查文件中引用的类型是不是都是已知类型,如果有运行时未知的类型,那它们也需要被加载进来,再次引发类加载过程。
该阶段将常量池中的符号引用转化为直接引用。
符号引用以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。
在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 org.blackist.Quiz 类引用了 org.blackist.Bnote类,编译时 Quiz类并不知道 Bnote类的实际内存地址,因此只能使用符号 org.blackist.Bnote
直接引用通过对符号引用进行解析,找到引用的实际内存地址。

初始化

类变量已经被赋过默认初始值,而在初始化阶段为类的静态变量赋予正确的初始值,所有静态初始化代码块都会运行。

public class Test {
    // 准备阶段默认值为0,初始化阶段赋值3
    private static int foo = 3;
    // 也可写为
    private static int foo;
    static {
        foo = 3;
    }
    // 静态代码块从上到下顺序执行,foo最终等于4
    static {
        foo = 4;
    }
}

换句话说,初始化阶段是执行类构造器方法的过程。

加载和连接的最终结果是一个Class对象,用于表示加载并连接起来的新类型。 Class对象中有对成员Method和Field对象的引用,Class和反射API可以用这些对象实现对他们的简介访问。 ClassLoader及其子类会定位并连接字节流以生成新的加载类。

Java程序对类的使用

主动使用(六种)

  • 创建类的实例(new Test();)
  • 访问某个类的或接口的静态变量,或对该静态变量赋值(int b = Test.a; Test.a = b;)
  • 调用类的静态方法 (Test.foo())
  • 反射(ClassForName(“org.blackst.demo.Quiz”))
  • 初始化类的子类

    class Parent {}
    class Child extends Parent {
     public static int a = 3;
    }
    // 初始化子类,对父类进行了主动使用
    Child.a = 4;
    
  • JVM启动时被标为启动类的类(如JavaTest,java org.blackist.Test)

所有JVM实现必须在每个类或接口被Java程序 首次主动使用 时才初始化。

被动使用
除了主动使用以外的使用,都不会导致类的初始化。

类构造器

  • 类构造器()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句快可以赋值,但是不能访问。
  • 类构造器()方法与类的构造函数(实例构造函数()方法)不同,它不需要显式调用父类构造,虚拟机会保证在子类()方法执行之前,父类的()方法已经执行完毕。因此在虚拟机中的第一个执行的()方法的类肯定是java.lang.Object。
  • 由于父类的()方法先执行,也就意味着父类中定义的静态语句快要优先于子类的变量赋值操作。
  • ()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句,也没有变量赋值的操作,那么编译器可以不为这个类生成()方法。
  • 接口中不能使用静态语句块,但接口与类不太能够的是,执行接口的()方法不需要先执行父接口的()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法。
  • 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果一个类的()方法中有耗时很长的操作,那就可能造成多个进程阻塞。

    示例

    如下程序:

    class Singleton {
     private static Singleton singleton = new Singleton();
     public static int counter1;
     public static int counter2 = 0;
    
     private Singleton() {
         counter1++;
         counter2++;
     }
     public static Singleton getInstance() {
         return singleton;
     }
    }
    public class LoadTest {
    
     public static void main(String[] args) {
         Singleton singleton = Singleton.getInstance();
         System.out.println("counter1 = " + singleton.counter1);
         System.out.println("counter1 = " + singleton.counter2);
     }
    }
    

输出:
1
0
准备阶段:singleton=null, counter1=0, counter2=0
主动调用:Singleton.getInstance()触发主动调用,进行初始化
初始化阶段:singleton=new Singleton()调用构造方法[counter1=1,counter2=1],counter1不变,counter2=0.