浅谈 JVM 4:类加载机制

AndroidRain

共 2714字,需浏览 6分钟

 · 2022-02-18

前文中我们了解了如何阅读字节码和字节码执行引擎中的运行时栈帧结构,字节码执行引擎中的方法调用等内容还未涉及。Java 中多态的特性离不开 JVM 的动态绑定,动态绑定技术是方法动态调用的关键。而如果要介绍方法调用机制,需要先了解字节码在 JVM 中从加载到卸载的生命周期过程。

字节码在 JVM 中从加载到卸载,经历了以下过程。

我们目前掌握了如何阅读外部 class 字节码的结构,和运行时的部分内容。前者属于 “加载” 之前的内容,后者属于 “使用” 阶段。本次我们就来看看从加载到使用之前的这个过程,这就是 JVM 的类加载机制。

外部字节码的输入有多种方式,比如本地 class 文件、网络字节流、程序生成的字节码内容等。

Java 虚拟机将外部的 class 字节码加载到内存中,需要经过加载、链接、初始化三个阶段。

在了解类加载机制之前,我们先来简单回顾下 JVM 逻辑区域的划分。包括 5 部分:

方法区Java 堆程序计数器Java 栈本地方法栈

加载

加载阶段主要负责读取外部的字节流,将字节流的存储结构转化为运行时数据结构,存储在方法区中。同时在 Java 堆中创建对应类的 java.lang.Class 对象,作为方法区数据的访问入口。此时方法区中的运行时数据结构由虚拟机各自实现。

除了数组类字节流由虚拟机直接生成之外,其他类的加载主要由 ClassLoader 完成。ClassLoader 有启动类加载器、扩展类加载器、应用类加载器,其结构符合双亲委派模型,是指子类加载器在加载类时需要先交给父类加载器加载,如此层层传递,如果父类不处理则再交给子类处理。

启动类加载器由 C++ 编写,没有 Java 对象。扩展类加载器、应用类加载器等均为 java.lang.ClassLoader 子类,需要由其他类加载器,如启动类加载器,加载进来。

启动类加载器主要负责加载最为基础、重要的类,如 JRE 下 lib 目录中的 jar 包。扩展类加载器负责加载 JRE 下 lib/ext 目录中的通用、扩展类。应用类加载器主要加载应用路径下的类。

双亲委派模型保证了类加载过程的安全性和唯一性,因为任何子类加载器(包括开发者自己实现的类加载器)在加载类之前,都需要先将类上交给父类加载器。一方面这保证了不同属性类的加载功能划分,比如 Object 类总会交给启动类加载器加载,这让无论哪个类加载器加载 Object,该类都能够保证相同和唯一。另一方面,这保证了类加载的安全,开发者无法去伪造诸如 java.lang.Object 等基础类来骗过 JVM,因为内置的 ClassLoader 做了相关安全校验工作。

需要注意的是,如果同一个字节码交给不同的类加载实例加载,会得到两个不同的类。

Java 9 模块化的支持对 ClassLoader 做了些许修改,在此不再详述。

链接

链接阶段又分为验证、准备、解析三个阶段。

验证阶段

验证阶段主要负责验证上一步加载进来的字节码是否符合规范,保证不会危害 JVM 的安全。这一步是程序安全的重要保障,此阶段的工作也在整个类加载过程中占有相当的比重,主要包括字节码格式、语法、语义的验证工作。

准备阶段

准备阶段主要负责为类中的静态字段分配内存,并初始化为零值。此时仅会对类变量分配内存,在 JDK 8 及之后,类变量内存会在 Class 对象所处的 Java 堆中进行分配。实例变量的内存分配要等到对象实例化时。

解析阶段

解析阶段是将常量池内的符号引用转换为直接引用的过程。

在前文我们学习阅读字节码时,常量池的常量使用索引表示,常量间的引用关系、方法字节码对变量的获取等操作都是通过引用索引来完成,这些索引称为符号引用。符号引用只是表达变量间的逻辑引用关系,而实际运行时变量、方法所处的内存位置都不是固定的,所以在运行字节码前,需要对这些符号引用进行解析成指向目标的指针、偏移或句柄才行,这和运行时具体的内存布局有关。

当然,解析阶段并未完成所有符号引用的解析过程,对于类或接口、字段、类方法、接口方法的解析可以在此阶段完成。但是对于方法类型、方法句柄和调用点限定符的解析则和动态语言的特性支持密切相关,并不会在此阶段完成。这是因为方法的调用具体对应哪个方法在此时还并未确定,要等到真正执行时才能确定。

初始化

初始化阶段是虚拟机开始执行应用程序代码的开始,是执行类构造器  方法的过程。该方法由 Javac 编译时,通过收集类变量赋值语句和 static 代码块合并产生。

以如下 Java 代码及其对应  字节码为例,可以发现类构造器只会处理类变量和静态代码块中内容,而忽略实例变量。

// javaprivate static int a = 100;static {    a = 1000;}private int mThisIsInt = 1024;
// bytecodebipush 100putstatic #28 sipush 1000putstatic #28 return

在此我们可以提一下静态内部类单例模式的实现方式。

public class Singleton {  private Singleton() {}  private static class Holder {    static final Singleton INSTANCE = new Singleton();  }  public static Singleton getInstance() {    return Holder.INSTANCE;  }}

内部类 Holder 的初始化时机是该类的静态字段调用之时,此时触发  方法的调用,进而创建单例对象。JVM 调用该方法时会加锁处理,保证该方法只会调用一次。该特性保证了单例的延迟加载和多线程安全。

总结

本次我们掌握了 JVM 类加载过程的三个阶段:加载、链接、初始化。JVM 在逻辑上分为方法区、Java 堆、程序计数器、Java 栈、本地方法栈。方法区和 Java 堆主要用于存储数据,包括字节码对应数据结构和运行时的对象。其他部分则主要负责运行时方法调用、计算等功能。

在了解了类加载机制、JVM 逻辑区域划分和运行时栈帧及方法字节码执行过程之后,下次有时间我们来聊聊方法间的调用是如何发生的。


浏览 11
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报