您的位置:首页 > 其它

Class文件加载及其初始化过程

2016-12-24 10:13 423 查看
该博文介绍字节码文件装载过程中的各个阶段。。。
重点需要掌握的是每个阶段中JVM需要做的工作

。。。


图览全局----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)

public class Parent{

  static{

    System.out.println("Parent init");

  }

}

public class Child{

  static{

    System.out.println("Child init");

  }

}

public class InitMain{

  public static void main(String[] args){

    Child c = new Child();

  }

}

以上声明了3个类:Parent Child InitMain,Child类为Parent类的子类。若Parent类被初始化,将会执行static块,会打印"Parent init",若Child类被初始化,则会打印"Child init"。(类的加载先于初始化,故执行静态代码块后(<cinit>),就表明类已经加载了)

执行InitMain,结果为:

Parent init

Child init

由此可知,系统首先装载Parent类,接着装载Child类。

符合主动装载中的两个条件:使用new关键字创建类的实例会装载相关的类,以及在初始化子类时,必须先初始化父类。
例2 :被动装载

public class Parent{

  static{

    System.out.println("Parent init ");

  }

  public static int v = 100; //静态字段

}

public class Child extends Parent{

  static{

    System.out.println("Child init");

  }

}

public class UserParent{

  public static void main(String[] args){

    System.out.println(Child.v);

  }

}
Parent中有静态变量v,并且在UserParent中,使用其子类Child去调用父类中的变量。

运行代码:

Parent init

100

虽然在UserParent中,直接访问了子类对象,但是Child子类并未初始化,仅仅加载了Child类,只有Parent类进行初始化。所以,在引用一个字段时,只有直接定义该字段的类,才会被初始化

注意:虽然Child类没有被初始化,但是,此时Child类已经被系统加载,只是没有进入初始化阶段。
可以使用-XX:+ThraceClassLoading 参数运行这段代码,查看日志,便可以看到Child类确实被加载了,只是初始化没有进行



例3 :引用final常量

public class FinalFieldClass{

  public static final String constString = "CONST";

  static{

    System.out.println("FinalFieldClass init");

  }

}

public class UseFinalField{

  public static void main(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 <java/io/PrintStream.println>
这里使用了常量池第24项,查看并分析该常量池,可以查看到如图的结构:



常量池第24项被invokevirtual使用,顺着CONSTANT_Methodref #24的引用关系继续在常量池中查找,发现所有对于Class以及NameAndType类型的引用都是基于字符串的,因此,可以认为Invokevirtual的函数调用通过字面量的引用描述已经表达清楚了,这就是符号引用。
但是只有符号引用是不够的,当println()方法被调用时,系统需要明确知道方法的位置。java虚拟机会为每个类准备一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法时,只要知道这个方法在表中的偏移量就可以了。通过解析操作,符号引用就可以转变为目标方法在类中方法表的位置,从而使方法被成功调用。
所以,解析的目的就是将符号引用转变为直接引用,就是得到类或者字段、方法在内存中的指针或者偏移量。如果直接引用存在,那么系统中肯定存在类、方法或者字段,但只存在符号引用,不能确定系统中一定存在该对象。

3)类初始化
如果前面的步骤没有出现问题,那么表示类可以顺利装载到系统中。此时,才会开始执行java字节码
初始化阶段的重要工作是执行类的初始化方法<clinit>()。其特点:

<clinit>()方法是由编译器自动生成的,它是由类静态成员的赋值语句以及static语句块合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的类变量,定义在其之后的类变量,只能被赋值,不能被访问。比如:
 static{
      num = 5;  //赋值操作,这是合法的,


}
static int num = 12;  
------------------------------------------------------------
static{
     System.out.println(num);  //不合法访问
}
static int num = 12;  

 例如:

public class SimpleStatic{

  public static int id = 1;

  public static int number;

  static{

    number = 4;

  }

}

java编译器为这段代码生成如下的<clinit>:

0 iconst_1

1 putstatic #2 <Demo.id> 

4 iconst_4

5 putstatic #3 <Demo.number>

8 return
<clinit>函数中,整合了SimpleStatic类中的static赋值语句以及static语句块
改段JVM指令代码表示:先后对id和number两个成员变量进行赋值

<clinit>()方法与类的构造器函数<init>()方法不同,它不需要显示的调用父类的<clinit>()方法,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。故父类的静态语句块会先于子类的静态语句块执行。

public class ChildStatic extends SimpleStatic
{
  static{
    number = 2;
  }
  public static void main(String[] args){
    System.out.println(number);
  }
}

运行代码:

2

表明父类的<clinit>总是在子类<clinit>之前被调用。

注意java编译器并不是为所有的类都产生<clinit>初始化函数,如果一个类既没有类变量赋值语句,也没有static语句块,那么生成的<clinit>函数就应该为空,因此,编译器就不会为该类插入<clinit>函数
例如:
public class StaticFinalClass{
  public static final int i=1;
  public static final int j=2;
}
由于StaticFinalClass只有final常量,而final常量在准备阶段被赋值,而不在初始化阶段处理,因此对于StaticFinalClass类来说,<clinit>就无事可做,因此,在产生的class文件中没有该函数存在。

 虚拟机保证一个类的<clinit>()方法在多线程环境中被正确的加锁和同步,如果多个线程同时去初始化一个类,只有一个线程去执行这个类的<clinit>()方法,其他线程都会被阻塞,直到指定线程执行完<clinit>()方法。

--------------------------------------------------------------------------------------------------------------------------------------------------
趁着意犹未尽,来看看对象初始化流程:包括成员变量和构造器调用的先后顺序,子类构造器和父类之间的先后顺序等等。通过字节码文件指令直接的展示这个过程
编辑几个类,包括一个子类一个父类,其中子类和父类中都包含了成员变量、非静态代码块、构造器函数以及前面讲到的静态代码块和静态变量:

package com.classextends;
public class FuZiDemo {
public static void main(String[] args) {
new ZiClass();//测试类,创建子类对象
}
}


class FuClass {
int fuOwer = 120;  //成员变量一
static{
System.out.println("Fu clinit()");  //静态代码块
}
static int num = 22; //静态变量
{				//非静态代码块
fuName = "tempValue";
System.out.println(fuOwer);
int c = 23;
}
String fuName = "dali"; //成员变量二
FuClass(){		//父类构造函数
System.out.println("Fu init()");
fuOwer = 100;
}
}

class ZiClass extends FuClass {
int ziOwer = 82;    //成员变量一
static{		    //静态代码块
System.out.println("Zi clinit()");
}
static int num = 2; //静态变量
{		    //非静态代码块
ziName = "tempValue";
System.out.println(ziOwer);
int c = 23;   //局部变量
}
String ziName = "urocle"; //成员变量二

ZiClass(){  //子类构造函数
ziOwer = 23;
System.out.println("Zi init()");
}
}


分析:

一、类的加载和初始化
首先FuziDemo这个测试类要加载,然后执行main指令时会new 子类对象,故要去加载子类的字节码文件,但是会发现子类有一个直接继承类FuClass,于是就会先去加载FuClass的字节码文件,接着会初始化父类,执行FuClass类的<clinit>方法:执行输出语句以及为静态成员赋值,其字节码指令为:
 0 getstatic #13 <java/lang/System.out>      

 3 ldc #19 <Fu clinit()>

 5 invokevirtual #21 <java/io/PrintStream.println>

 8 bipush 22

10 putstatic #27 <com/classextends/FuClass.num>

13 return

完成父类的初始化工作之后,紧接着加载子类的字节码文件并且执行其<clinit>()方法。其字节码指令类似于父类的:
 0 getstatic #13 <java/lang/System.out>

 3 ldc #19 <Zi clinit()>

 5 invokevirtual #21 <java/io/PrintStream.println>      //调用println()方法输出 #19也就是 Zi clinit()

 8 iconst_2

 9 putstatic #27 <com/classextends/ZiClass.num>        //为静态变量赋值

12 return
二、子类和父类成员变量初始化,以及构造函数执行顺序
测试类main函数的字节码指令:
0 new #16 <com/classextends/ZiClass>

3 invokespecial #18 <com/classextends/ZiClass.<init>>         //调用子类的初始化函数

6 return

下面看看子类ZiClass的<init>()函数的字节码指令:
 0 aload_0

 1 invokespecial #32 <com/classextends/FuClass.<init>>     //首先会去调用父类的<init>()函数

 4 aload_0

 5 bipush 82

 7 putfield #34 <com/classextends/ZiClass.ziOwer>        //为成员变量 ziOwer赋值为82

10 aload_0

11 ldc #36 <tempValue>

13 putfield #38 <com/classextends/ZiClass.ziName>      //执行非静态代码块,临时为成员变量ziName赋值

16 getstatic #13 <java/lang/System.out>                         //调用System.out输出函数

19 aload_0

20 getfield #34 <com/classextends/ZiClass.ziOwer>       //获取成员变量 ziOwer的值

23 invokevirtual #40 <java/io/PrintStream.println>         //打印输出

26 bipush 23

28 istore_1                                                              

29 aload_0

30 ldc #43 <urocle>

32 putfield #38 <com/classextends/ZiClass.ziName>    //为成员变量ziName赋值为urocle

35 aload_0

36 bipush 23
 //取出 23 ,意味着实例初始化过程中先初始化成员变量及执行非静态代码块,最后执行构造

38 putfield #34 <com/classextends/ZiClass.ziOwer>    //为成员变量ziOwer赋值为23

41 getstatic #13 <java/lang/System.out>

44 ldc #45 <Zi init()>

46 invokevirtual #21 <java/io/PrintStream.println>

49 return
同样FuClass类的实例初始化函数<init>()如下,此处不再解释:
 0 aload_0

 1 invokespecial #32 <java/lang/Object.<init>>

 4 aload_0

 5 bipush 120

 7 putfield #34 <com/classextends/FuClass.fuOwer>

10 aload_0

11 ldc #36 <tempValue>

13 putfield #38 <com/classextends/FuClass.fuName>

16 getstatic #13 <java/lang/System.out>

19 aload_0

20 getfield #34 <com/classextends/FuClass.fuOwer>

23 invokevirtual #40 <java/io/PrintStream.println>

26 bipush 23

28 istore_1

29 aload_0

30 ldc #43 <dali>

32 putfield #38 <com/classextends/FuClass.fuName>

35 getstatic #13 <java/lang/System.out>

38 ldc #45 <Fu init()>

40 invokevirtual #21 <java/io/PrintStream.println>

43 aload_0

44 bipush 100

46 putfield #34 <com/classextends/FuClass.fuOwer>

49 return
三  给出程序执行的结果
Fu clinit()

Zi clinit()        //静态代码块输出

120                 //非静态代码块输出

Fu init()         //构造函数输出

82

Zi init()

总结:
(1)父类加载初始化先于子类,父类的<clinit>优先于子类的<clinit>函数执行
(2)如果创建一个子类对象,父类构造函数<init>调用先于子类构造器<init>函数调用。在执行构造器<init>函数首先会初始化类中成员变量或者执行非静态代码块(这二者执行的先后顺序依赖于在源文件中出现的顺序),然后再调用构造函数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: