类加载的过程
(1)加载
加载是类加载的一个阶段,在加载阶段虚拟机需要完成以下三件事:
(1)通过一个类的全限定名来获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类访问所有数据结构的访问入口。
其中二进制流可以从以下方式中获取:
ZIP包读取,称为JAR,EAR,WAR格式的基础
从网络中获取,最典型的应用是Applet。
运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
由其他文件生成,例如由JSP文件生成对应的Class类。
(2)验证
验证确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致完成下面4个阶段的检验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前虚拟机处理。
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
字节码验证:主要是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。
符号引用验证:符号引用验证可以看做是对类自身以外的信息进行匹配性校验。
(3)准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配,类变量就是被static修饰的变量。实例变量将在对象实例化时随对象一起分配在java堆中。类加载发生在所有实例化操作之前,并且类加载只能进行一次,实例化可以进行多次。
初始值一般为0值,例如下面的类变量value被初始化为0而不是123。
public static int value=123;
将value的值赋为123的动作将在初始化阶段才会执行。
如果类字段的字段属性表中存在ConstantValue属性,那么在准备阶段变量value就会被初始化为ConstantValue属性所指的值。
public static final int value=123
编译时,javac会将value生成ConstantValue属性,在准备阶段虚拟机就将value赋值为123。
(4)解析
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持Java的动态绑定。
(5)初始化
类初始化阶段是类加载过程的最后一步。
初始化阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。初始化阶段是执行类构造器clinit()方法的过程,在准备阶段,类变量已经赋过一次系统所要求的初始值,而初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
clinit()是由编译器自动收集类中所有类变量赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在文件中出现的顺序决定。特别注意的是,静态语句只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
public class Test{
static {
i=0; //给变量赋值可以编译通过
System.out.println(i);//这句编译器会提示“非法向前引用”
}
static int i=1;
}
由于父类的clinit()方法先执行,所以父类中定义的静态语句块的执行要先于子类的变量赋值操作。例如下面代码中B的值是2而不是1。
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);
}
接口中不可以使用静态语句块,但仍然有类变量初始化赋值操作,因此接口和类一样都会生成clinit()方法。但接口与类不同的是,执行接口的clinit()方法不需要先执行父接口的clinit()方法。只有当父接口中的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 clinit() 方法。
虚拟机会保证一个类的 clinit() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 clinit() 方法,其它线程都会阻塞等待,直到活动线程执行 clinit() 方法完毕。如果在一个类的 clinit() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。