转自 http://blog.csdn.net/wen7280/article/details/53856790
图览全局----Class文件装载经历的各个阶段:
在Java应用程序开发中,只有被java虚拟机装载的Class类型才能在程序中使用。只要生成的字节码符合java虚拟机的指令集和文件格式,就可以在JVM上运行,这为java的跨平台性提供条件。
字节码文件的装载过程:加载 、 连接(包括三个步骤:验证 准备 解析) 、初始化,如图所示
-------------------------------------------------------------------------------------------------
类装载的条件:
Java虚拟机不会无条件的装载Class类型。
Java虚拟机规定:一个类或者接口在初次使用时,必须进行初始化。
这里的使用指的是主动使用,主动使用有以下几种情况:
当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化方式。
当调用类的静态方法时,即当使用了字节码invokestatic指令
当使用类或者接口的静态字段时(final常量除外,此种情况只会加载类而不会进行初始化),即使用getstatic或者putstatic指令(可以使用jclasslib软件查看生成的字节码文件)
当使用java.lang.reflect包中的方法反射类的方法时
当初始化子类时,必须先初始化父类
作为启动虚拟机、含有main方法的那个类
除了以上情况属于主动使用外,其他情况均属于被动使用,被动使用不会引起类的初始化,只是加载了类却没有初始化。
例1:主动使用(这是三个class文件,而不是一个,此处为方便写在一起。多说一点:因为一个Class文件只能有一个public类和文件名一样,其余类修饰符只能是非pubic)
[java]view plaincopy
publicclassParent{
static{
System.out.println("Parent init");
}
}
publicclassChild{
static{
System.out.println("Child init");
}
}
publicclassInitMain{
publicstaticvoidmain(String[] args){
Child c =newChild();
}
}
以上声明了3个类:Parent Child InitMain,Child类为Parent类的子类。若Parent被初始化,将会执行static块,会打印"Parent init",若Child被初始化,则会打印"Child init"。(类的初始化先于加载,故执行静态代码块后,就表明类已经加载了)
[java]view plaincopy
执行InitMain,结果为:
Parent init
Child init
由此可知,系统首先装载Parent类,接着装载Child类。
符合主动装载中的两个条件:使用new关键字创建类的实例会装载相关的类,以及在初始化子类时,必须先初始化父类。
例2 :被动装载
[java]view plaincopy
publicclassParent{
static{
System.out.println("Parent init ");
}
publicstaticintv =100;//静态字段
}
publicclassChildextendsParent{
static{
System.out.println("Child init");
}
}
publicclassUserParent{
publicstaticvoidmain(String[] args){
System.out.println(Child.v);
}
}
Parent中有静态变量v,并且在UserParent中,使用其子类Child去调用父类中的变量。
[java]view plaincopy
运行代码:
Parent init
100
虽然在UserParent中,直接访问了子类对象,但是Child子类并未初始化,仅仅加载了Child类,只有Parent类进行初始化。所以,在引用一个字段时,只有直接定义该字段的类,才会被初始化。
注意:虽然Child类没有被初始化,但是,此时Child类已经被系统加载,只是没有进入初始化阶段。
可以使用-XX:+ThraceClassLoading 参数运行这段代码,查看日志,便可以看到Child类确实被加载了,只是初始化没有进行
例3 :引用final常量
[java]view plaincopy
publicclassFinalFieldClass{
publicstaticfinalString constString ="CONST";
static{
System.out.println("FinalFieldClass init");
}
}
publicclassUseFinalField{
publicstaticvoidmain(String[] args){
System.out.println(FinalFieldClass.constString);
}
}
运行代码:CONST
FinalFieldClass类没有因为其常量字段constString被引用而进行初始化,这是因为在Class文件生成时,final常量由于其不变性,做了适当的优化。验证完字节码文件无误后,在准备阶段就会为常量初始化为指定的值。
分析UseFinalField类生成的Class文件,可以看到main函数的字节码为:
在字节码偏移3的位置,通过Idc将常量池第22项入栈,在此Class文件中常量池第22项为:
#22 = String #23 //CONST
#23 = UTF8 CONST
由此可以看出,编译后的UseFinalField.class中,并没有引用FinalFieldClass类,而是将FinalFieldClass类中final常量字段直接存放在自己的常量池中,所以,FinalFiledClass类自然不会被加载。(javac在编译时,将常量直接植入目标类,不再使用被引用类)通过捕获类加载日志(部分日志)可以看出:(并没有加载FinalFiledClass类日志)
注意:并不是在代码中出现的类,就一定会被加载或者初始化,如果不符合主动使用的条件,类就不会被加载或者进一步初始化。
详解类装载的整个过程
1)加载类:处于类装载的第一个阶段。
加载类时,JVM必须完成:
通过类的全名,获取类的二进制数据流
解析类的二进制数据流为方法区内的数据结构,也就是将类文件放入方法区中
创建java.lang.Class类的实例,表示该类型
2)连接
验证字节码文件:当类被加载到系统后,就开始连接操作,验证是连接的第一步。
主要目的是保证加载的字节码是符合规范的。
验证的步骤如图:
准备阶段
当一个类验证通过后,虚拟机就会进入准备阶段。准备阶段是正式为类变量(static修饰的变量)分配内存并设置类变量初始值,这些内存都将在方法区进行分配。这个时候进行内存分配的仅是类变量,不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆上。为类变量设置初始值是设为其数据类型的“零值”。
比如 public static int num = 12; 这个时候就会为num变量赋值为0
java虚拟机为各种类型变量默认的初始值如表:
类型默认初始值
int0
long0L
short(short)0
char\u0000
booleanfalse
referencenull
float0f
double0f
注意:java并不支持boolean类型,对于boolean类型,内部实现是Int,由于int的默认值是0,故对应的,boolean的默认值是false
如果类中属于常量的字段,那么常量字段也会在准备阶段被附上正确的值,这个赋值属于java虚拟机的行为,属于变量的初始化。在准备阶段,不会有任何java代码被执行。
解析类
在准备阶段完成后,就进入了解析阶段。
解析阶段的任务就是将类、接口、字段和方法的符号引用转为直接引用。
符号引用就是一些字面量的引用。比较容易理解的就是在Class类文件中,通过常量池进行大量的符号引用。
具体可以使用JclassLib软件查看Class文件的结构:::
下面通过一个简单函数的调用来讲解下符号引用是如何工作的。。。
例如:System.out.println();
生成的字节码指令:invokevirtual #24
这里使用了常量池第24项,查看并分析该常量池,可以查看到如图的结构:
常量池第24项被invokevirtual使用,顺着CONSTANT_Methodref #24的引用关系继续在常量池中查找,发现所有对于Class以及NameAndType类型的引用都是基于字符串的,因此,可以认为Invokevirtual的函数调用通过字面量的引用描述已经表达清楚了,这就是符合引用。
但是只有符合引用是不够的,当println()方法被调用时,系统需要明确知道方法的位置。java虚拟机会为每个类准备一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法时,只要知道这个方法在表中的偏移量就可以了。通过解析操作,符合引用就可以转变为目标方法在类中方法表的位置,从而使方法被成功调用。
所以,解析的目的就是将符合引用转变为直接引用,就是得到类或者字段、方法在内存中的指针或者偏移量。如果直接引用存在,那么系统中肯定存在类、方法或者字段,但只存在符合引用,不能确定系统中一定存在该对象。
3)类初始化
如果前面的步骤没有出现问题,那么表示类可以顺利装载到系统中。此时,才会开始执行java字节码。
初始化阶段的重要工作是执行类的初始化方法()。其特点:
()方法是由编译器自动生成的,它是由类静态成员的赋值语句以及static语句块合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的类变量,定义在其之后的类变量,只能被赋值,不能被访问。比如:
static{
num = 5;//这是合法的
}
static int num = 12;
static{
System.out.println(num);//这样是不合法的
}
static int num = 12;
例如:
[java]view plaincopy
publicclassSimpleStatic{
publicstaticintid =1;
publicstaticintnumber;
static{
number =4;
}
}
java编译器为这段代码生成如下的:
0 iconst_1
1 putstatic #2
4 iconst_4
5 putstatic #3
8 return
函数中,整合了SimpleStatic类中的static赋值语句以及static语句块
改段JVM指令代码表示:先后对id和number两个成员变量进行赋值
()方法与类的构造器函数()方法不同,它不需要显示的调用父类的()方法,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。故父类的静态语句块会先于子类的静态语句块执行。
[java]view plaincopy
publicclassChildStaticextendsSimpleStatic
{
static{
number =2;
}
publicstaticvoidmain(String[] args){
System.out.println(number);
}
}
[java]view plaincopy
运行代码:
2
表明父类的总是在子类之前被调用。
注意:java编译器并不是为所有的类都产生初始化函数,如果一个类既没有类变量赋值语句,也没有static语句块,那么生成的函数就应该为空,因此,编译器就不会为该类插入函数
例如:
public class StaticFinalClass{
public static final int i=1;
public static final int j=2;
}
由于StaticFinalClass只有final常量,而final常量在准备阶段被赋值,而不在初始化阶段处理,因此对于StaticFinalClass类来说,就无事可做,因此,在产生的class文件中没有该函数存在。
虚拟机保证一个类的()方法在多线程环境中被正确的加锁和同步,如果多个线程同时去初始化一个类,只有一个线程去执行这个类的()方法,其他线程都会被阻塞,直到指定线程执行完()方法。
--------------------------------------------------------------------------------------------------------------------------------------------------
趁着意犹未尽,来看看对象初始化流程:包括成员变量和构造器调用的先后顺序,子类构造器和父类之间的先后顺序等等。通过字节码文件指令直接的展示这个过程:
编辑几个类,包括一个子类一个父类,其中子类和父类中都包含了成员变量、非静态代码块、构造器函数以及前面讲到的静态代码块和静态变量:
[java]view plaincopy
packagecom.classextends;
publicclassFuZiDemo {
publicstaticvoidmain(String[] args) {
newZiClass();//测试类,创建子类对象
}
}
[java]view plaincopy
classFuClass {
intfuOwer =120;//成员变量一
static{
System.out.println("Fu clinit()");//静态代码块
}
staticintnum =22;//静态变量
{//非静态代码块
fuName ="tempValue";
System.out.println(fuOwer);
intc =23;
}
String fuName ="dali";//成员变量二
FuClass(){//父类构造函数
System.out.println("Fu init()");
fuOwer =100;
}
}
[java]view plaincopy
classZiClassextendsFuClass {
intziOwer =82;//成员变量一
static{//静态代码块
System.out.println("Zi clinit()");
}
staticintnum =2;//静态变量
{//非静态代码块
ziName ="tempValue";
System.out.println(ziOwer);
intc =23;//局部变量
}
String ziName ="urocle";//成员变量二
ZiClass(){//子类构造函数
ziOwer =23;
System.out.println("Zi init()");
}
}
分析:
一、类的加载和初始化
首先FuziDemo这个测试类要加载,然后执行main指令时会new 子类对象,故要去加载子类的字节码文件,但是会发现子类有一个直接继承类FuClass,于是就会先去加载FuClass的字节码文件,接着会初始化父类,执行FuClass类的方法:执行输出语句以及为静态成员赋值,其字节码指令为:
0 getstatic #13
3 ldc#19
5 invokevirtual #21
8 bipush22
10 putstatic#27
13 return
完成父类的初始化工作之后,紧接着加载子类的字节码文件并且执行其()方法。其字节码指令类似于父类的:
0 getstatic #13
3 ldc#19
5 invokevirtual #21//调用println()方法输出 #19也就是 Zi clinit()
8iconst_2
9 putstatic#27//为静态变量赋值
12 return
二、子类和父类成员变量初始化,以及构造函数执行顺序
测试类main函数的字节码指令:
0 new #16
3 invokespecial #18> //调用子类的初始化函数
6 return
下面看看子类ZiClass的()函数的字节码指令:
0 aload_0
1 invokespecial #32>//首先会去调用父类的()函数
4 aload_0
5 bipush82
7putfield#34//为成员变量 ziOwer赋值为82
10 aload_0
11 ldc #36
13putfield#38//执行非静态代码块,临时为成员变量ziName赋值
16 getstatic #13//调用System.out输出函数
19 aload_0
20 getfield #34//获取成员变量 ziOwer的值
23 invokevirtual #40//打印输出
26 bipush 23
28 istore_1
29 aload_0
30 ldc#43
32putfield#38//为成员变量ziName赋值为urocle
35 aload_0
36bipush 23//取出 23 ,意味着实例初始化过程中先初始化成员变量及执行非静态代码块,最后执行构造
38putfield#34//为成员变量ziOwer赋值为23
41 getstatic #13
44 ldc #45
46 invokevirtual #21
49 return
同样FuClass类的实例初始化函数()如下,此处不再解释:
0 aload_0
1 invokespecial #32>
4 aload_0
5 bipush 120
7 putfield #34
10 aload_0
11 ldc #36
13 putfield #38
16 getstatic #13
19 aload_0
20 getfield #34
23 invokevirtual #40
26 bipush 23
28 istore_1
29 aload_0
30 ldc #43
32 putfield #38
35 getstatic #13
38 ldc #45
40 invokevirtual #21
43 aload_0
44 bipush 100
46 putfield #34
49 return
三 给出程序执行的结果
Fu clinit()
Zi clinit() //静态代码块输出
120 //非静态代码块输出
Fu init() //构造函数输出
82
Zi init()
总结:
(1)父类加载初始化先于子类,父类的优先于子类的函数执行
(2)如果创建一个子类对象,父类构造函数调用先于子类构造器函数调用。在执行构造器函数首先会初始化类中成员变量或者执行非静态代码块(这二者执行的先后顺序依赖于在源文件中出现的顺序),然后再调用构造函数。