开启Java程序员进阶之旅,比较难啃的JVM。

字节码

在聊 Java 类加载机制之前,需要先了解一下 Java 字节码,因为它和类加载机制息息相关。

计算机只认识 0 和 1,所以任何语言编写的程序都需要编译成机器码才能被计算机理解,然后执行,Java 也不例外。

Java 在诞生的时候喊出了一个非常牛逼的口号:“Write Once, Run Anywhere”,为了达成这个目的,Sun 公司发布了许多可以在不同平台(Windows、Linux)上运行的 Java 虚拟机(JVM)——负责载入和执行 Java 编译后的字节码。

我们借助一段简单的代码来看一看,源码如下:

1
2
3
4
5
6
7
package org.blackist.jvm;

public class JvmDemo {
public static void main(String[] args) {
System.out.println("董亮亮的开发笔记");
}
}

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

1
2
3
4
5
6
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 文件的魔数是 89504e47

类加载过程

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

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

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

加载

查找并加载类的二进制数据。

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

加载.class的方式

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

连接

验证

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

  • 确保二进制字节流格式符合预期(比如说是否以 cafe bene 开头)。
  • 是否所有方法都遵守访问控制关键字的限定。
  • 方法调用的参数个数和类型是否正确。
  • 确保变量在使用之前被正确初始化了。
  • 检查变量是否被赋予恰当类型的值。

准备

JVM 会在该阶段对类变量(也称为静态变量, static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。

1
2
3
public String blackist = "Blackist";
public static String note = "Note";
public static final String bnote = "Note-of-Blackist";

blackist不会被分配内存,而 note会;但 bnote的初始值不是“王二”而是 null

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

解析

该阶段将常量池中的符号引用转化为直接引用。

符号引用以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。

在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 org.blackist.Quiz 类引用了 org.blackist.Bnote类,编译时 Quiz类并不知道 Bnote类的实际内存地址,因此只能使用符号 org.blackist.Bnote

直接引用通过对符号引用进行解析,找到引用的实际内存地址。

初始化

类变量已经被赋过默认初始值,而在初始化阶段为类的静态变量赋予正确的初始值。

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

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

Java程序对类的使用

主动使用(六种)

  • 创建类的实例(new Test();)
  • 访问某个类的或接口的静态变量,或对该静态变量赋值(int b = Test.a; Test.a = b;)
  • 调用类的静态方法 (Test.foo())
  • 反射(ClassForName(“org.blackst.demo.Quiz”))
  • 初始化类的子类
1
2
3
4
5
6
class Parent {}
class Child extends Parent {
public static int a = 3;
}
// 初始化子类,对父类进行了主动使用
Child.a = 4;
  • JVM启动时被标为启动类的类(如JavaTest,java org.blackist.Test)

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

被动使用

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

示例

如下程序:

输出:

1
2
1
0

参考

(完)