关闭

Java虚拟机----类的加载过程

标签: classloaderclass虚拟机java
394人阅读 评论(0) 收藏 举报
分类:

上一篇《Java虚拟机----类加载器简单讲述了什么是类加载器、抽象类ClassLoader、类加载机制中的双亲委派模型以及自定义类加载器,这篇将分析-------类的加载过程。

        类加载器的主要任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM内部,然后转化为一个与目标类对应的java.lang.Class对象实例。当然类加载器所执行的加载操作仅仅属于JVM中加载过程的一个阶段而已,一个完整的类加载过程必须经历加载、连接、初始化这3个步骤。如图1所示:

 

                                                  图 1 完整的类加载过程

类加载过程各阶段的任务,见图2所示:

                                                                                     图 2 各阶段的任务

类的生命周期:

  加载 loading

  验证 verification

  准备 preparation

  解析 resolution

  初始化 initialization

  使用 using

        卸载 unloading

        Java虚拟机规范在类的加载和连接的时机上提供了较大的灵活性,但Java虚拟机规范却明确规定了类的初始化时机,且分为主动和被动引用两种;也就是说,一个类或者接口应该在首次主动使用时进行初始化操作:

有且只有以下几种情况必须立即对类进行初始化”(称为对一个类进行主动引用)

(1)遇到newgetstaticputstaticinvokestatic这四条字节码指令时(使用new实例化对象的时候、读取或设置一个类的静态字段、调用一个类的静态方法)

(2)使用java.lang.reflet包的方法对类进行反射调用的时候。

(3)当初始化一个类的时候,如果发现其父类没有进行过初始化,则需要先触发其父类的初始化。

(4)当虚拟机启动时,虚拟机会初始化主类(包含main方法的那个类)

(5)调用一个类或接口的静态字段,或者对这些静态字段进行赋值操作的时候(即节码中,调用getstatic()putstatic()方法指令),不过用final关键字修饰的静态字段除外,它被初始化为一个编译时的常量表达式。

被动引用:

(1)通过子类引用父类的静态字段,不会导致子类初始化(对于静态字段,只有直接定义这个字段的类才会被初始化)

(2)通过数组定义类应用类:ClassA [] array=new ClassA[10]。触发了一个名为[LClassA的类的初始化,它是一个由虚拟机自动生成的、直接继承于Object的类,创建动作由字节码指令newarray触发。

(3)常量会在编译阶段存入调用类的常量池。

        在此大家需要注意,尽管一个类在初始化之前必须要求它的父类提前完成初始化操作,但对于接口而言,这条规则却显得并不适用。编译器会为接口生成<clinit>()构造器,用于初始化接口中定义的成员变量。一个接口在初始化时,并不要求其父类接口全部完成了初始化,只有在真正使用到父接口的时候才会初始化。

一、加载Loading

        加载任务是由类加载器所负责的,当然类加载器所执行的加载操作仅仅属于JVM中加载过程的一个阶段,同样也是类加载过程的第一个阶段,而后续还需要连接和初始化阶段的配合才能构成一个完整的类加载过程。加载:就是根据一个类的全限定名来读取此类的二进制字节流到JVM内部,然后转化为一个与目标类对应的java.lang.Class对象实例。又可细分以下3步:

(1)通过一个类的全限定名来获取此类的二进制字节流。可以是class文件,可以是jar

(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

(3)在java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。

        参考《Java虚拟机规范(Java SE7版)》的描述,创建数组的类的情况稍微有些特殊,简单来说,数组类本身并不是类加载器负责创建,而是由JVM在运行时根据需要而直接创建,但数组的元素类型仍然是依靠类加载器创建。

二、连接Linking

        连接Linking阶段要做的事情就是将已经加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,然后该连接阶段则由验证、准备、解析3个阶段构成。

1.验证

        这个阶段验证读取到的二进制字节流是否符合虚拟机规范中Class文件的存储格式,如果不符合,抛出java.lang.VerifyError异常或其子类的异常。

        验证阶段确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段中的一些验证操作将会和加载阶段一起执行,而不会等待加载完成后才会执行某些验证操作,比如当一个类型的二进制信息被加载到JVM内部之前,就必须做文件格式验证,以免一些二进制信息对JVM虚拟机产生不良影响,或者造成JVM进程崩溃。验证阶段大致可以划分为:文件格式验证、语义分析、操作验证、符号引用验证。

验证阶段分为四步:

(1)文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这个阶段的验证时给予字节流进行的,经过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储所以后面的验证阶段都是给予方法区的存储结构进行的。关于字节码文件有效性验证,可参阅:http://blog.csdn.net/ljheee/article/details/52224015

      有以下这些验证点:

            *是否已魔数0xCAFEBABE开头。

            *主次版本号是否能被当前虚拟机处理。

            *常量池的常量中是否有不被支持的类型。

            *常量池里的项是否执行不存在的常量或不符合类型的常量。

(2)元数据验证:对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息。

      有以下这些验证点:

            *是否 有父类(除java.lang.Object外,所有类都应有父类)。

            *是否继承了final类。

            *如果不是抽象类,是否实现了所有需要实现的方法。

(3)字节码验证:进行数据流和控制流分析,对类的方法体进行校验分析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。

(4)符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),对常量池中的各种符号引用的信息进行匹配性的校验。

2.准备

        连接阶段的验证操作完成后,接下来JVM要做的事情就是对存放在方法区中类数据信息的类变量执行初始化,这里所指的初始化仅仅是为类中的静态变量分配内存,并将其初始化为默认值(由于没有产生对象,因此实例变量将不在此操作范围内),而非用户手动赋值。如下表所示:

数据类型

默认初始值

int

0

long

0L

short

(short)0

char

'\u0000'

byte

(byte)0

boolean

false

float

0.0f

double

0.0d

reference

null

        在此需要注意,JVM实现其实并不支持boolean类型,因此在JVM内部,boolean类型往往被实现为一个int类型,初始值为0也就代表着false。当然boolean类型的变量,尽管在JVM中当作int来实现,但在初始化时也总会被初始化为一个false值。

3.解析

        解析阶段是在虚拟机将常量池内的符号引用替换为直接引用的过程。

        什么是符号引用呢?符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

        直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

要执行解析操作的内容包括:类或接口、类字段、类方法、接口中的方法。

(1)解析类或接口

 

类或接口(对应于常量池的CONSTANT_Class_info类型)的解析:

假设当前代码所处的类为D,需要将一个从未解析过的符号引用N解析为一个类或接口C的直接引用:

i.如果C不是一个数组类型,虚拟机将会把代表C的全限定名传递给D的类加载器去加载这个类。

ii.如果C是一个数组类型,并且数组的元素类型为对象(N的描述符类似[Ljava.lang.Integer),将会加载数组元素类型(java.lang.Integer),接着由虚拟机生成一个代表此数组维度和元素的数组对象。

iii.如果以上过程没有发生异常,则C在虚拟机中已经成为了一个有效的类和接口了,之后还要进行的是符号引用验证,确认D是否具有对C的访问权限,如果没有,将抛出java.lang.IllegalAccessError异常。

(2)解析类字段

字段(对应于常量池的CONSTANT_Fieldref_info类型)解析:

i.对字段表中的class_index项中索引的CONSTANT_Class_info符号引用进行解析。用C表示这个字段所属的类或接口。

ii.如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。

iii.否则,如果C实现了接口,则会按照继承关系从下往上递归搜索各个接口和他的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。

iv.否则,如果C不是java.lang.Object类型的话,将会按照继承关系从下往上递归的搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。

v.否则,查找失败,抛出java.lang.NoSuchFieldError异常。

虚拟机的编译器实现可能会更严格:如果一个同名字段同时出现在C实现的接口和父类中,或者同时在自己或父类的多个接口中出现,编译器将可能拒绝编译。

(3)解析类方法

  类方法(对应于常量池的CONSTANT_Methodref_info类型)解析:

i.对方法表中的class_index项中索引的CONSTANT_Class_info符号引用进行解析。用C表示这个方法所属的类或接口。

ii.类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,则抛出java.lang.IncompatibleClassChangeError

iii.在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。

iv.否则,在C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。

v.否则,在C实现的接口列表及它们的父接口中递归的查找是否有简单名称和描述符都与目标相匹配的方法,如果有说明C是个抽象类,查找结束,抛出java.lang.AbstractMethodError异常。

vi.否则,查找失败,抛出java.lang.NoSuchMethodError异常。

vii.如果查找返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对这个方法的访问权限,则抛出java.lang.IllegalAccessError异常。

(4)解析接口方法

 接口方法(对应于常量池的CONSTANT_InterfaceMethodref_info类型)

i.对方法表中的class_index项中索引的CONSTANT_Class_info符号引用进行解析。用C表示这个方法所属的类或接口。

ii.如果在接口方法表中发现class_index中索引的C是个类,则抛出java.lang.IncompatibleClassChangeError

iii.否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。

iv.否则,在接口C的父接口中递归查找,知道java.lang.Object(包括在内),看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。

v.否则,查找失败,抛出java.lang.NoSuchMethodError

三、初始化Initiazation

        初始化是类加载过程的最后一个阶段,前面的类加载动作,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正执行类中定义的Java程序代码(或者说是字节码)。在初始化阶段,JVM会将一个类的所有被static关键字修饰的代码统统执行一遍,如果执行的是静态变量,那么会使用程序员指定的值覆盖掉之前在准备阶段中JVM为其设置的初始值,当然如果程序中没有为静态变量显示的指定赋值操作,那么所持有的值仍然是之前的初始值;反之如果执行的是static代码块,那么在初始化阶段中,JVM就会将执行static代码块中的操作。

        所有的类变量[静态变量]初始化语句和静态代码块都会在Java源码执行字节码编译时,被前端编译器放在收集器里,存放到一个特殊的方法中,这个方法就是<client>()方法。对于类来说,这个方法可称为类初始化方法,而对于接口来说,则可称为接口初始化方法。简单来说,<client>()方法的作用就是初始化一个类中的静态变量,使用程序员指定的值覆盖掉之前在准备阶段中JVM为其设置的初始值。

<clinit>()方法执行过程--特点:

1.<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后变量,在前面的静态语句块中可以赋值,但是不能访问。

2.<clinit>()方法与类的构造器<init>()不同,它不需要显示地调用父类类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行<clinit>()方法的类肯定是java.lang.Object

3.由于父类的<clinit>()方法先执行,所就意味着父类中定义的静态语句块要优先于子类的类变量赋值操作。

4.<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这类生成<clinit>()方法。

5.接口中不能使用静态语句块,但仍然可以有变量初始化的同仁操作,因此接口与类一样都会生成<clinit>()方法,但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会初始化。另外,接口的实现类在初始化时也不会执行接口的<clinit>()方法。

6.虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其它线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有很耗时的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

7.<clinit>()是线程安全的。如果多个线程同时去初始化一个类,只有一个线程会去执行初始化,其他的线程都会阻塞,仅仅允许其中一个线程对其初始化操作,完成后才会通知正在等待的其他线程。

8.任何invoke之类的字节码指令也无法调用<clinit>()方法,因为该方法只能在类加载的过程中被JVM所调用。

 

 

《Java虚拟机----类加载器http://blog.csdn.net/ljheee/article/details/52353017

1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:267831次
    • 积分:4543
    • 等级:
    • 排名:第6803名
    • 原创:165篇
    • 转载:13篇
    • 译文:1篇
    • 评论:44条
    最新评论