JVM类加载机制
什么是类加载
在JVM虚拟机实现规范中,通过classLoader类加载器把*.class字节码文件(文件流)加载到内存,并对字节码文件内容进行验证、准备、解析和初始化,最终形成可以被虚拟机直接使用的java.lang.class 对象,这个过程被称作类加载。
类是在运行期间第一次使用时,被类加载器动态加载至JVM。JVM不会一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。
类的生命周期
类的声明周期包括7个阶段:①加载(Loading) ②验证(Verification) ③准备(Preparation) ④解析(Resolution) ⑤初始化(Initialization) ⑥使用(Using) ⑦卸载(Unloading)
结束类声明周期的几种场景:①执行System.exit()方法 ②程序正常执行结束 ③程序执行中遇到了异常或错误而异常终止 ④ 操作系统出现错误或强制结束程序而导致JVM虚拟机进程终止。
类加载的过程
加载->验证->准备->解析->初始化
加载
在加载阶段,JVM主要完成以下3件事:
- 通过类的完全限定名称获取定义该类的*.class字节码文件的二进制字节流。
- 将该字节流表示的静态存储结构转换为Metaspace元空间区的运行时存储结构。
- 在内存中生成一个代表该类的Class对象,作为元空间区中该类各种数据的访问入口。
由于JVM虚拟机对加载*.class字节码文件的来源并未做限制,所以出现了以下的*.class字节码文件加载方式:
- 本地文件系统直接读取;
- 从网络中通过服务器响应读取。例如:web Applet技术;3.从JAR、EAR、WAR等压缩文件中读取;
- 运行时通过动态代理技术生成字节码文件。例如:在 java.lang.reflect.Proxy使用ProxyGenerator.generateProxyClass的代理类的二进制字节流。
- 由其他文件或容器生成。例如:由tomcat将*.jsp文件翻译成*.java文件后,编译生成对应的*.class字节码文件。
在加载阶段完成之后,*.class字节码文件的类信息数据就会存储在元空间,同时在JVM虚拟机堆区生成—个该类的Class 对象。
验证
在验证阶段,JVM主要保证xxx.class字节码文件中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的安全。在此阶段主要完成四个步骤的验证:
- 文件格式验证:验证字节流是否符合xxx.class字节码文件格式的规范,且能被当前版本的虚拟机处理。
- 是否以魔数CAFEBABE开头。
- 主、次版本号是否在当前虚拟机可处理的范围之内
- 常量池中的常量是否包含不被支持的常量类型
- 指向常量的各种索引值中是否有执行不存在的常量或不符合装型的常量
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
- xxx.class文件中各个部分及文件本身是否有被删除的或附加的其他信息
- 元数据验证:对字节码描述的信息进行语义分析,以保证描述的信息符合Java语言规范的要求
- 这个类是否有父类(除java.lang.Object之外,所有的类都应当有父类)
- 这个类的父类是否继承了不被允许被继承的类(言外之意就是被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- 类中的字段、方法是否与父类产生了矛盾,例如:覆盖了父类的 final字段。出现不符合规则的方法重载,例如:方法参数都一致,但返回值类型却不同等。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 保证跳转指令不会跳转到方法体以外的字节码指令上
- 保证方法体中的类型转换是有效的,例如:可以把子类对象赋值给父类数据装型,这是安全的;但把父类对象意赋值给子类数据类型,甚至把对象赋值给毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
- 保证任意时刻操作数栈的数据装型与指令代码序列都能配合工作,例如:不会出现在操作栈中放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中
- 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段——解析阶段中发生。确保解析动作能正常执行。
- 符号引用中通过字将串描述 的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用中的类、字段和方法的访问性( private .protected 、public . default)是否可被当前类访问
为什么需要进行验证:
Java语言本身是相对安全的语言,但*.class字节码文件并不一定要求用Java源码编译而来,可以使用任何途径,甚至可用十六进制编译器直接编写来产生*.class字节码文件。
类的加载是JVM针对*.class字节码文件的读取加载机制,所以虚拟机如果不检查输入的字节流,可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
另外,通过类加载机制的验证环节,可以增强解释器的运行期执行性能。因为,解释器在运行期间无需再对每条执行指令进行检查。
准备
- 类变量是被static修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是元空间区的内存。
- 实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
- 初始值一般为0值。
例如:静态的int型变量的初始值为0,而不是初始的赋值;而静态常量的初始值为初始赋值
解析
将常量池的符号引用替换为直接引用
初始化
初始化阶段才真正开始执行类中定义的Java程序代码。初始化阶段是虚拟机执行类构造器()方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
()是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。所以,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。
/*
以下代码中静态变量i只能赋值,不能访问,因为i定义在静态代码块的后面
*/
public class Test{
static {
i = 0; //给变量赋值可以正常编译通过
System.out.print(i);//编译器会提示“非法向前引用”
}
static int i = 1;
}
/*
由于父类的<clinit()>方法先执行,也就意味着父类中定义的静态代码块的执行要优先于子类
*/
static class Parent{
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent{
public static int B = A;
}
public static void main(String[] args){
System.out.println(Sub.B); //2
}
线程安全
虚拟机会保证一个类的()方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的()方法,其它线程都会阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中,该阻塞非常隐蔽,几乎不会被察觉。