JVM 类加载机制解析
简述
本文介绍了 java 虚拟机类加载机制。
类加载机制
JVM 类加载一共 7 步,前五步是类加载机制,各个步骤按照顺序进行,但是并非固定的 1,2,3 步,在实际中有可能从其中间某一步开始。
类加载机制一般分为三部分:加载 Loading -> 连接 Linking -> 初始化 Initializing
其中加载、验证、准备和初始化发生的顺序是确定的,但解析可以在初始化之后开始(java 动态绑定)
java 绑定分为静态绑定和动态绑定:
- 静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。针对 java,简单的可以理解为程序编译期的绑定。java 当中的方法只有 final,static,private 和构造方法是前期绑定的。
- 动态绑定:即晚期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。在 java 中,几乎所有的方法都是后期绑定的。
类加载机制具体过程
**I.Loading **
加载,JVM 将文件(class,jar,zip,网络等)中的二进制字节流保存到虚拟机方法区和堆中,并用该二进制表示形式创建类或者接口的过程。
Loading is the process of finding the binary representation of a class or interface type with a particular name and creating a class or interface from that binary representation
https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html (英文文档若无特殊说明都是引用官方文档,下同)
用类全限定名获取类的二进制字节流
将字节流中静态存储结构转化为方法区的运行时数据结构
在堆中生成一个代表该类的 java.lang.Class 对象,作为方法区数据的访问入口。
(
这句话存疑 ,有人说在堆中 ,也有人说在方法区 ,官方文档未相关描述2019/01/12 更新
Class 对象没有明确规定实在JAVA 堆中,对应 HotSpot 虚拟机来说,该对象在方法区中)
类加载的地方是开发人员可控性最强的地方。除了可以使用系统的 ClassLoader 外还可以自定义 ClassLoader(后文详述)。
类加载根据加载的类不同分为两种:
- 非数组类 使用系统/自定义的类加载器完成加载
- 数组类 数组类不通过类加载器创建,而是通过 JVM 直接创建,但是数组类的元素类型要通过类加载器创建
数组类的元素加载,根据数组元素的类型不同,分为两类:
- 引用类 通过普通类加载器加载,并将数组用该类加载器标识
- 非引用类 将数组与引导类加载器标识
数组类的可见性与其元素类的可见性一致。
**II.Linking **
连接,是将类或者接口组合到 java 虚拟机运行状态的过程,这样他就可以被运行。
Linking is the process of taking a class or interface and combining(组合) it into the run-time state of the Java Virtual Machine so that it can be executed(运行)
连接一般分为 3 部分:验证 Verification、准备 Preparation、解析 Resolution。
Linking a class or interface involves(包括) verifying and preparing(验证和准备) that class or interface, its direct(直接) superclass, its direct superinterfaces, and its element type (if it is an array type), if necessary. Resolution of symbolic references in the class or interface is an optional part of linking.
Verification
验证,保证 class 文件中的字节流信息符合虚拟机的要求。
Verification(§4.10) ensures that the binary representation(二进制格式) of a class or interface is structurally correct(结构正确) (§4.9). Verification may cause additional classes and interfaces to be loaded (§5.3) but need not cause them to be verified or prepared.
验证内容包括:
- 文件格式验证,验证字节流符合 class 文件格式规范;
- 元数据验证
- 字节码验证
- 符号引用验证
**Preparation **
准备,在方法区对类变量分配内存,初始化为默认值(“零值”)。
比如:static int i = 5;
在这一步只会进行到i = 0
,而i = 5
要在初始化那一步才进行;
但是如果是 final 修饰的常量,则在此分配具体值。
Preparation involves creating the static fields for a class or interface and initializing such fields to their default values (§2.3, §2.4). This does not require the execution of any Java Virtual Machine code; explicit initializers for static fields are executed as part of initialization (§5.5), not preparation.
准备工作可能在创建之后的任何时候发生,但是必须在初始化之前完成。
Resolution
解析,是在运行时常量池中动态确定符号引用的具体值的过程。
每个栈帧 frame 都有一个当前方法
到运行时常量池
的引用,用来支持方法代码 (method code) 的动态链接(dynamic linking)。
method code:要被执行的方法以及通过符号引用的变量。
Each frame (§2.6) contains a reference to the run-time constant pool (§2.5.5) for the type of the current method to support dynamic linking of the method code.
动态链接将符号引用(symbolic references)转化为具体方法的调用(concrete method references)即直接引用,根据需要加载类来解析未定义的符号,将变量访问转化为运行时内存(runtime location)。
This late binding of the methods and variables makes changes in other classes that a method uses less likely to break this code.
方法和变量的这种后期绑定,使得方法使用的其他类的更改不太可能破坏这个代码。
直接引用可以直接定位到内存中某一段地址;符号引用则与 JVM 内存无管
解析分为:
- 类,接口解析
- 字段解析
- 类方法解析
- 接口方法解析
III.Initialization
初始化 ,Initialization of a class or interface consists of executing its class or interface initialization method(执行类,接口的构造方法clinit()
)
类或接口在被初始化之前,必须先被连接 linked(verified, prepared, and optionally(可选)resolved.)。
初始化有且只有以下五种情况:
new
、读取/设置类(只有直接定义其的类才会,子类等不受影响)的静态变量(final 修饰的常量除外)、执行静态方法java.lang.reflect
反射调用类- 初始化时,如果父类未初始化,先触发父类的初始化(接口类除外)
- 虚拟机等启动时执行主类的
main()
方法时,需要先初始化主类 - JDK1.7 动态支持时,如果
java.lang.invoke.MethodHandle
实例最后解析结果REF_get/put/invokeStatic的方法句柄对应的类未被初始化时,需要先初始化对应的类
以上 5 种称为对一个类的主动引用,其余不会触发初始化,称为被动引用
clinit()
,有类变量赋值,静态语句块会由编译器合并为clinit()
方法,分为两种:
- 类 父类的
clinit()
方法会先于子类执行 - 接口 接口
clinit()
方法无需调用父类接口的clinit()
方法;接口的实现类也无需执行接口的clinit()
方法
clinit()
是线程安全的,在同一个类加载器中,多个线程的中只会有一个线程执行一次clinit()
,其余线程阻塞等待
clinit()
和init()
不同如下:
init是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行 init 方法(是在new 对象的时候初始化非静态变量);
而 clinit 是类构造器方法,也就是在 jvm 进行类加载—–验证—-解析—–初始化,中的初始化阶段 jvm 会调用 clinit 方法(是在JVM 初始化类的时候初始化静态变量)。
如果类没有静态赋值、静态语句块等则不会有clinit()
方法。
clinit()
先于init()
执行。
参考文献
Java Virtual Machine Specification Chapter 5. Loading, Linking, and Initializing
Chapter 2. The Structure of the Java Virtual Machine
Jvm 系列 3—类的加载 - Gityuan 博客 | 袁辉辉博客
类加载机制 - 深入理解 Java 虚拟机 - 极客学院 Wiki