字节码(.class)文件的加载过程

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)

 
  1. public class Parent{

  2.  
  3.   static{

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

  6.  
  7.   }

  8.  
  9. }

  10.  
  11. public class Child{

  12.  
  13.   static{

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

  16.  
  17.   }

  18.  
  19. }

  20.  
  21. public class InitMain{

  22.  
  23.   public static void main(String[] args){

  24.  
  25.     Child c = new Child();

  26.  
  27.   }

  28.  
  29. }


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

 
  1. 执行InitMain,结果为:

  2.  
  3. Parent init

  4.  
  5. Child init


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

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

例2 :被动装载

 
  1. public class Parent{

  2.  
  3.   static{

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

  6.  
  7.   }

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

  10.  
  11. }

  12.  
  13. public class Child extends Parent{

  14.  
  15.   static{

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

  18.  
  19.   }

  20.  
  21. }

  22.  
  23. public class UserParent{

  24.  
  25.   public static void main(String[] args){

  26.  
  27.     System.out.println(Child.v);

  28.  
  29.   }

  30.  
  31. }

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

 
  1. 运行代码:

  2.  
  3. Parent init

  4.  
  5. 100


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

注意:虽然Child类没有被初始化,但是,此时Child类已经被系统加载,只是没有进入初始化阶段。

可以使用-XX:+ThraceClassLoading 参数运行这段代码,查看日志,便可以看到Child类确实被加载了,只是初始化没有进行

例3 :引用final常量

 
  1. public class FinalFieldClass{

  2.  
  3.   public static final String constString = "CONST";

  4.  
  5.   static{

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

  8.  
  9.   }

  10.  
  11. }

  12.  
  13. public class UseFinalField{

  14.  
  15.   public static void main(String[] args){

  16.  
  17.     System.out.println(FinalFieldClass.constString);

  18.  
  19.   }

  20.  
  21. }


运行代码: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虚拟机为各种类型变量默认的初始值如表:

类型 默认初始值
int 0
long 0L
short (short)0
char \u0000
boolean false
reference null
float 0f
double 0f

注意: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;  

 例如:

 
  1. public class SimpleStatic{

  2.  
  3.   public static int id = 1;

  4.  
  5.   public static int number;

  6.  
  7.   static{

  8.  
  9.     number = 4;

  10.  
  11.   }

  12.  
  13. }


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>()方法已经执行完毕。故父类的静态语句块会先于子类的静态语句块执行。
 
  1. public class ChildStatic extends SimpleStatic

  2. {

  3.   static{

  4.     number = 2;

  5.   }

  6.   public static void main(String[] args){

  7.     System.out.println(number);

  8.   }

  9. }

 
  1. 运行代码:

  2.  
  3. 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>()方法。

--------------------------------------------------------------------------------------------------------------------------------------------------

趁着意犹未尽,来看看对象初始化流程:包括成员变量和构造器调用的先后顺序,子类构造器和父类之间的先后顺序等等。通过字节码文件指令直接的展示这个过程

编辑几个类,包括一个子类一个父类,其中子类和父类中都包含了成员变量、非静态代码块、构造器函数以及前面讲到的静态代码块和静态变量:

 
  1. package com.classextends;

  2. public class FuZiDemo {

  3. public static void main(String[] args) {

  4. new ZiClass();//测试类,创建子类对象

  5. }

  6. }

 
  1. class FuClass {

  2. int fuOwer = 120; //成员变量一

  3. static{

  4. System.out.println("Fu clinit()"); //静态代码块

  5. }

  6. static int num = 22; //静态变量

  7. { //非静态代码块

  8. fuName = "tempValue";

  9. System.out.println(fuOwer);

  10. int c = 23;

  11. }

  12. String fuName = "dali"; //成员变量二

  13. FuClass(){ //父类构造函数

  14. System.out.println("Fu init()");

  15. fuOwer = 100;

  16. }

  17. }

 
  1. class ZiClass extends FuClass {

  2. int ziOwer = 82; //成员变量一

  3. static{ //静态代码块

  4. System.out.println("Zi clinit()");

  5. }

  6. static int num = 2; //静态变量

  7. { //非静态代码块

  8. ziName = "tempValue";

  9. System.out.println(ziOwer);

  10. int c = 23; //局部变量

  11. }

  12. String ziName = "urocle"; //成员变量二

  13.  
  14. ZiClass(){ //子类构造函数

  15. ziOwer = 23;

  16. System.out.println("Zi init()");

  17. }

  18. }

分析:

一、类的加载和初始化

首先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>函数首先会初始化类中成员变量或者执行非静态代码块(这二者执行的先后顺序依赖于在源文件中出现的顺序),然后再调用构造函数。

猜你喜欢

转载自blog.csdn.net/qq_20610631/article/details/82709187
今日推荐