Class文件加载
Class文件加载
1、导读
一个Class文件,在加载进JVM的过程中,究竟经历了些什么?加载进JVM之后又会以什么样的形式呈现?
- Class.forName究竟是怎么获取Class对象的,Class对象又是什么?
- Class文件是如何被加载到JVM里面的?
- 类变量是存在堆中还是存在方法区中?
- 类构造器
<clinit>
方法什么时候执行?
2、关于类加载器
以下是类的生命周期:
其中,如果是动态绑定或者晚期绑定,解析阶段不会再准备阶段后立刻执行。
3、加载、连接(验证,准备,解析)、初始化
3.1、加载阶段
JVM规范并没有规定java.lang.Class类的实例要放到Java堆中,对于HotSpot虚拟机,是放到方法区里面的。这个class对象作为程序访问方法区中的这些类型数据的外部接口。
如上图,加载阶段主要做以下事情:
- 通过类全限定名获取定义此类的二进制字节流;
- 将字节流代表的静态存储结构转换为方法区的运行时数据结构;
- 在内存中生成此类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
3.1.1、如何触发加载CLASS文件
- 虚拟机器启动时,main方法所在的类会首先进行初始化;
- 初始化类的时候,如果父类还没有初始化,则触发父类初始化;
- 遇到new、getstatic、putstatic或者invokestatic字节码指令的时候,如果类还没有初始化。对应场景为:
- new一个对象;
- 读取或者设置一个类的静态字段;
- 调用类的静态方法的时候;
- 使用
java.lang.reflect
包的方法对类进行反射的时候,如果类还没有初始化; - JDK1.7中使用动态语言支持的时候,如果一个java.lang.invoke.MethodHandler实例最后解析为:REF_getStatic,REF_putStatic,REF_invokeStatic方法句柄的时候,并且句柄所对应的类没有进行过初始化。
这个时候通过类的全限定名称获取类的二进制字节流。
静态存储结构 ---> 方法区的运行时数据结构
结构里面的信息详细介绍参考此文:The Java Virtual Machine
3.1.2、获取二进制流的方式
如上图描述的,JVM规范5.3. Creation and Loading并没有指定class文件二进制流需要从哪里以什么方式获取,目前主要有以下几种获取方式:
- zip包,延伸为JAR、EAR、WAR包;
- 网络,如Applet;
- 动态代理;
- JSP生成;
- 数据库获取;
3.1.3、验证二进制字节流
在加载阶段就已经开始做部分验证工作了,但是验证还是属于连接阶段的动作,下面介绍验证阶段。
3.2、连接阶段
连接阶段包括:验证,准备,解析
3.2.1、验证阶段
为了解释这一步的作用,我们先来做一个实验。
有如下一个类:
package com.itzhai.jvm.loadclass;
/**
* Created by arthinking on 4/1/2020.
*/
public class TestVerify {
public static void main(String[] args) {
System.out.println("Hello world !!!");
}
}
我们把Java文件编译为class文件,并执行之:
java com.itzhai.jvm.loadclass.TestVerify
可以发现输出:
Hello world !!!
现在我们使用前面Class文件16进制背后的秘密介绍的十六进制编辑方法,对class文件进行随意编辑,这里我们可以把常量池计数器故意调小一点,保存之后再次执行class文件:
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Invalid constant pool index 33 in class file com/itzhai/jvm/loadclass/TestVerify
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
可以发现抛出了异常:非法的常量池索引33,这正是验证阶段干的事情。
验证阶段干什么事情?
class文件是可以被认为篡改的,虚拟机如果直接拿来执行,可能会把系统给搞崩溃了,所以一定要先对Class文件做严格的验证。验证阶段主要完成以下检测动作:
3.2.1.1、文件格式验证
按照Class文件16进制背后的秘密文章中的阐述的格式,严格的进行校验。
3.2.1.2、元数据验证
主要是语义校验,保证不存在不符合Java语言规范的元数据信息,如:没有父类,继承了final类,接口的非抽象类实现没有完整实现方法等。
3.2.1.3、字节码验证
主要对数据流和控制流进行分析,确定成行语义是否合法,符合逻辑。不合法的例子:
- 操作数栈放置了int类型数据,却当成long类型使用;
- 把父类对象赋值给了子类数据类型;
- ...
3.2.1.4、符号引用验证
解析阶段发生的验证,当把符号引用转化为直接引用的时候进行验证。这主要是对类自身以外的信息进行匹配性校验。主要包括:
- 全限定名是否可以找到对应的类;
- 指定类是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;
- 校验类,字段和方法的可见性;
3.2.2、准备阶段
这个阶段还并没有开始执行类的构造方法,而只是为类变量分配内存并设置类变量初始值(零值)。这些变量所使用的内存都将在方法区中分配。
基本数据类型的零值:2.3. Primitive Types and Values
这里只分配static变量,不包括实例变量。
注意:static final类型的常量value会在准备阶段被初始化为常量指定的值。
静态变量存储在内存的PremGen(方法区域)空间中,其值存储在Heap中
3.2.3、解析阶段
主要是将符号引用替换为直接引用。
符号引用:字面量,引用目标不一定已经加载到内存中;
直接引用:直接指向目标的指针,或者相对偏移量,或是一个能简介定位到目标的句柄。直接引用和虚拟机实现的内存布局相关。
解析主要针对以下七类符号引用进行:
- 类或接口 CONSTANT_Class_info
- 字段 CONSTANT_Fieldref_info
- 类方法 CONSTANT_Methodref_info
- 接口方法 CONSTANT_InterfaceMethodref_info
- 方法类型 CONSTANT_MethodType_info
- 方法句柄 CONSTANT_MethodHandle_info
- 调用限定符 CONSTANT_InvokeDynamic_info
3.3、初始化阶段
这阶段开始执行Java程序代码,这一步主要是执行类构造器<clinit>
方法对类变量进行初始化的过程,注意,这个方法不是构造方法。
3.2.1 、<CLINIT>
方法
此方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的方法,主要是给类变量做初始化工作的方法。
关于<CLINIT>
方法的注意事项
- 顺序问题:静态语句块后面的静态变量,静态语句块中可以赋值,但不可以访问;
- 继承执行顺序:无需显示调用,虚拟机会保证子类的
<clinit>
方法执行前,父类的<clinit>
方法已经执行完毕; - 接口的
<clinit>
方法:虽然接口不能有静态语句块,但是可以给静态变量初始化值,所以也可以生成<clinit>
方法; - 接口继承:除非使用到父接口的变量,否则执行子接口的
<clinit>
方法不需要先执行父接口的<clinit>
方法; - 在并发场景,虚拟机会保证一个类的
<clinit>
方法只有一个线程执行,其他线程会阻塞,所以要确保静态代码块中不要写可能回到进程阻塞的代码。