1、概述
Java虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行检验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。这个过程称为虚拟机的类加载机制。
2、类加载的时机
一个类型从被加载到内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段。其中验证、准备、解析统称为连接阶段。
其中,加载、验证、准备、初始化、卸载这五个阶段的顺序是确定的。类型的加载过程必须按照这个顺序按部就班的开始。但是解析阶段则不一定:它在某些情况下可以在初始化阶段才开始,这是为了支持Java语言的运行时动态绑定特性。
3、类加载过程
3.1、加载
“加载”是整个“类加载”(Class Loading)过程中的一个阶段。这两个不是同一个东西。
在加载阶段,Java虚拟机主要完成以下三件事情:
1、通过一个类的全限定名来获取定义此类的二进制字节流。
2、将这个字节流所代表的静态存储结构转换为方法区上的运行时数据结构。
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
3.2、验证
“验证”阶段是连接阶段的第一个阶段,这个阶段是为了保证Class文件的字节流包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
3.2.1、文件格式验证
验证阶段的第一个阶段,主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机版本处理。这一阶段可能包括下面这些验证点:
1、是否以魔数0xCAFEBABE开头。
2、主、次版本号是否在当前Java虚拟机接受范围内。
3、常量池中的常量是否有不被支持的常量类型(检查常量tag标志)。
…
这个阶段的验证是基于二进制流进行的,只有通过了这个阶段的验证之后,这段字节流才会被允许进入Java虚拟机内存的方法区中进行存储。所以后面的三个阶段都是基于方法区的存储结构进行的,不会在读取、操作字节流了。
3.2.2、元数据验证
验证阶段的第二个阶段,这一阶段对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》要求。这个阶段可能包括的验证点如下:
1、这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
2、这个类的父类是否继承了不允许被继承的类(final修饰的类)。
3、如果这个类不是抽象类,是否实现了父类或接口之中要求实现的所有方法。
…
3.2.3、字节码验证
验证阶段的第三个阶段,这一阶段主要是通过数据流分析和控制流分析,确定程序语义是否合法、符合逻辑的。在“元数据验证”阶段完成对数据类型校验完毕之后,这阶段主要对类的方法体(Class文件中的code属性)进行校验分析,保证被校验的方法在运行时不会做出对虚拟机安全的行为。例如:
1、保证任何跳转行为,都不会跳转到方法体以外的字节码指令上。
2、保证方法体中的类型转换是有效的。比如:将子类赋值给父类是有效,但是将父类赋值给子类或者赋值给完全不相干的一个类,则是危险和不合法的。
…
3.2.4、符号引用验证
验证阶段的最后一个阶段,这个阶段的检验行为发生在虚拟机将符号引用转换为直接引用的时候,这个转换动作发生在连接的第三个阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外的各种信息进行匹配性校验。通俗来说就是该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。
1、在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
2、符合引用中的类、字段、方法的可访问性(private、protected、public、)是否可被当前类访问。
…
3.3、准备
准备阶段是正是为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。从概念上来讲,这些变量所使用的内存都应当在方法区中进行分配。但是必须注意到方法区本身是一个逻辑上的区域。在JDK7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这个逻辑概念的。但是在JDK8及之后,类变量则随着Class对象一起存放在Java堆中,这时候类变量在方法区就是一种逻辑概念的描述。
在准备阶段,这时候进行的内存分配只有类变量,而不包括实例变量,实例变量将在对象实例化时随着对象一起分配在堆上。且这里所说的初始值“通常情况”下是数据类型的零值。例如一个类的变量定义为:
public static int value=123;
那变量value在准备阶段过后的初始值为0而不是123。由于这时没有执行过任何的Java方法,而把value赋值123的putstatic指令是在程序编译后,存放在类构造器()方法中的,所以把value赋值123的动作要到初始化阶段才会被执行。
Java中所有基本类型零值:
上面提到的“通常情况”下的初始值是零值,但是也存在特殊情况:如果类变量字段属性表中存在ConstantValue属性,那么在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值。假设上面的变量value定义变为:
public static final int value=123;
**编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue的设置将value赋值为123。**
3.4、解析
解析阶段是Java虚拟机将常量池中的符号引用转换为直接引用的过程。
3.5、初始化
类的初始化阶段,是类加载过程的最后一个阶段。在之前的几个类加载阶段里,除了加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。知道初始化阶段,java虚拟机才真正开始执行类中编写的Java程序代码,将主导权交给应用程序。
在准备阶段,变量已经赋过一次系统要求的零值,而在初始化阶段,才会根据程序员的编码来赋值类变量和其他资源。简答的来说,在初始化阶段才会执行()方法。()并不是程序员在Java代码中直接编写的方法,它时javac编译器自动生成物。
1、()方法是由编译器自动收集类中的所有类变量的赋值动作和静态变量语句块(static{})中的语句合并产生的,编译器收集的顺序是由语句在源文件中的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。例如:
public class Test{
static{
i=0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i=1;
}
2、()放与类的构造函数不同,它不需要显式的调用父类构造器,Java虚拟机会保证子类的()方法执行前,父类的()方法已经执行完毕。因此在Java虚拟机中第一个被执行的()方法的类型一定是java.lang.Object。
3、由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。例如:下面的代码,字段B的值是2而不是1。
static class parent{
public static int A=1;
static{
A=2;
}
}
public static class sub extends parent{
public static int B=A;
}
4、Java虚拟机必须保证一个类的()方法在多线程环境下被正确的加锁同步,如果多个线程同时去初始化同一个类,那么只会有其中一个线程执行这个类的()方法,其他线程则会阻塞等待活动线程执行完毕。如果在一个类的()方法中由耗时很长的操作,那就可能造成多个进程阻塞。例如:
public class DeadLoopClass {
static {
if (true){ //不加if语句,编译器会提示“初始化程序必须能够正常完成”
System.out.println(Thread.currentThread()+" init DeadLoopClass");
while (true){
}
}
}
public static void main(String[] args) {
Runnable s=new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread()+" start");
DeadLoopClass deadLoopClass=new DeadLoopClass();
System.out.println(Thread.currentThread()+" run over");
}
};
Thread thread = new Thread(s);
Thread thread1 = new Thread(s);
thread.start();
thread1.start();
}
}