【JVM】Java 的类加载机制

发布时间 2023-10-17 19:20:12作者: LARRY1024

字节码

Java 源文件通过编译后,就会生成字节码:

image

类加载过程

Java 的类加载过程可以分为 5 个阶段:载入验证准备解析初始化

这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。

Loading

JVM 在该阶段的主要目的是:将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象。

Verification

JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查:

  • 确保二进制字节流格式符合预期

    比如,字节码是否以 cafe babe 开头。

  • 是否所有方法都遵守访问控制关键字的限定。

  • 方法调用的参数个数和类型是否正确。

  • 确保变量在使用之前被正确初始化了。

  • 检查变量是否被赋予恰当类型的值。

Preparation

JVM 会在该阶段对类变量分配内存并初始化,对应数据类型的默认初始值,如 0、0L、null、false 等。

也就是说,假如有这样一段代码:

public String a = "jack";
public static String b = "john";
public static final String c = "lee";

其中,a 不会被分配内存,而 b 会分配内存,但 b 的初始值不是“jack”,而是 null。

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

Resolution

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

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

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

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

Initialization

该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。

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

String user = new String("Jack");

上面这段代码使用了 new 关键字来实例化一个字符串对象,那么,这时候,就会调用 String 类的构造方法对 user 进行实例化。

类加载器

对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。

也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等,比如两个类的 Class 对象不 equals。

Java 类加载器可以分为三种。

  • 启动类加载器(Bootstrap Class-Loader):加载 jre/lib 包下面的 jar 文件,比如说常见的 rt.jar。

  • 扩展类加载器(Extension or Ext Class-Loader):加载 jre/lib/ext 包下面的 jar 文件。

  • 应用类加载器(Application or App Clas-Loader):根据程序的类路径(classpath)来加载 Java 类。

每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader() 可以获取到此引用;通过 loader.getParent() 可以获取类加载器的上层类加载器。

【示例】

public class Test {
    public static void main(String[] args) {
        ClassLoader loader = Test.class.getClassLoader();
        while (loader != null) {
            System.out.println(loader);
            loader = loader.getParent();
        }
    }
}

示例输出:

sun.misc.Launcher$AppClassLoader@b4aac2
sun.misc.Launcher$ExtClassLoader@16d3586

双亲委派模型

如果前面提到的三种类加载器都不能满足要求的话,程序员还可以自定义类加载器(继承 java.lang.ClassLoader 类),它们之间的层级关系如下图所示。

image

这种层次关系被称作为双亲委派模型:如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。

使用双亲委派模型有一个很明显的好处,那就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,这对于保证 Java 程序的稳定运作很重要。


参考: