Java类加载过程简要介绍
这里就作者所学及其理解对《深入理解Java虚拟机》这本书的内容发表看法,如有错误欢迎指出。
在说类加载的时候我们先来看看一个类的生命周期,如图示:
类加载在这里包括了加载、验证、准备、解析、初始化五个部分,当一个类要加载进内存时虚拟机会依次执行这五个过程(解析过程可能会发生在初始化以后)。
一、加载阶段
这个过程需要完成三件事情:
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区这些数据的访问入口。
第一件事情中,类的二进制字节流是通过一个类的全限定名来获取的,这说明并不一定是从class文件中获取的,这也使加载类的形式可以由不同的加载器来源不同的地方。比如说从jsp文件中加载、从压缩包中加载,使得Java的部署和应用变得非常广泛。
第二件事情中,我们将所得字节流所代表的静态存储结构转化为方法区的运行时数据结构。这个过程和反序列化有点相似,我们知道方法区中存储的是类的类型数据(class信息和static变量),而我们可以在未实例化类之前通过反射获取class对象来得到这些信息,这就使得第三件事情尤为重要。
二、验证阶段
1.文件格式验证(是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理)
2.元数据验证(对字节码描述的信息进行语意分析,以保证其描述的信息符合Java语言规范要求)
3.字节码验证(保证被校验类的方法在运行时不会做出危害虚拟机安全的行为)
4.符号引用验证(虚拟机将符号引用转化为直接引用时,解析阶段中发生)
对于验证阶段,它的作用归根结底是为了虚拟机能安全和正确的加载使用包含在字节流中的类信息。而其中四钟验证的详细内容在《深入理解Java虚拟机》中已经讲的非常明确易懂,这里就不过多的赘述。
但是我们要注意,除了文件格式验证,其余三个验证都是基于方法区的存储结构进行的。因为文件格式验证主要验证字节流能不能被加载进虚拟机,这个过程当然得发生在数据存储到方法区之前。而其余三个验证我们都必须等到类的信息被加载进方法区中时,才能在方法区中提取相应数据进行验证操作。而且,我们要知道验证操作可能发生在类加载的任何阶段,并不是仅仅在准备阶段之前,比如说对符号引用的验证,可能在运行期间进行动态绑定时还要进行验证。
三、准备阶段
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。
private static int class_value = 123;
这里的类变量是指被static修饰的变量即上面的class_value,但是设置初始却并不是将class_value的值设置为123,而是设置为“零值”,所谓零值就是static修饰的变量所表示的数据类型的初始值:
如int:0;byte:(byte) 0; float:0.0f;reference:null;
而在为类变量初始化为赋给它的值这个过程发生在初始化阶段。
我们知道,当遇到new(实例化类)、getstatic(获取静态变量)、putstatic(设置静态变量的值)、invokestatic(调用静态方法)4条字节码指令时我们都必须先将类初始化,上面的代码将123赋给class_value正属于putstatic指令,这将发生在初始化阶段。
不过恰恰有一个例外的是,如果类字段的字段属性表中存在ConstantValue属性,即下面这种。
private static final int class_value = 123;
这时class_value被看成是唯一的无法被改变的常量,在准备阶段123就会被赋给它并保存在方法区的常量池中。不过还需要注意一点,像下面的这段代码,如果在静态代码块中才对class_value赋值就必须等到初始化阶段才能进行赋值。
private static final int class_value;
static {
class_value = 123;
}
四、解析阶段
解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。
那么什么是符号引用什么是直接引用呢?
符号引用存储在class文件中,它用来描述所引用的目标,这个目标可以是类也可以是方法,也可以是字段。它的表现形式可以是任何字面量,只要在转化为直接引用时不发生歧义即可,相当于说它是一份内容的临时看守人。
比如说如下代码:
class Father{
public void fun(){
System.out.println("Father");
}
}
class Son extends Father{
public void fun(){
System.out.println("Son");
}
}
public class TemFun {
public static void main(String[] args) {
Father f = new Son();
f.fun();
}
}
/*
* output:
* Son
* */
我们使用Son继承其父类Father,在子类中重写了fun()方法,这时我们使用一个指向Father的引用f当作Son类实例化对象的一个指针。
当我们在调用f.fun()方法时,关于方法的解析过程开始,我们找到符号引用指向Son类的fun()方法,然后通过对其父类或接口的递归调用寻找到拥有相同fun()方法(简单名词和描述符都相同)的哪一个,最后这个父类或接口的fun()方法(这里就是Father类的f.fun())直接引用会指向Son类的符号引用所指向的fun()方法。可以看到最后的输出结果为Son。
这就发生了动态绑定,也就是为什么解析可能会发生在初始化阶段之后。而这里作为Son类fun()方法的符号引用转变为了Father类中对fun()方法的引用。
显然直接引用是我们在运行时真正可以调用相应内容的那个引用。
五、初始化阶段
初始化阶段时加载过程的最后一步,而这一阶段也是真正意义上开始执行类中定义的Java程序代码。
初始化阶段时执行类构造器<clinit>()方法的过程,而这里的<clinit>()方法的作用是收集类中所有类变量的赋值动作和静态语句块并进行其中的初始化操作,他不同于构造函数的是在子类<clinit>()方法执行时,父类的<clinit>()方法已经执行完毕了。也就是说父类的static{}代码块(接口没有static{}代码块)会在给子类类变量赋值之前就完成其中的操作,而构造函数只能保证先执行父类的构造函数再执行子类的构造函数。
注意:这个阶段在执行<clinit>()方法时,虚拟机会给其添加锁,保证在多线程程序中相同的<clinit>()方法只有一个在执行,所以如果我们在初始化区域设置太多的复杂代码,那么就可能会使多线程程序发生阻塞引起程序错误。