您的位置:首页 > 其它

JVM 之 类的加载与初始化

2015-01-12 22:54 239 查看
JVM中的类或接口的加载,连接,初始化动作都是动态的。加载主要是根据指定的名称找到并读取类或接口的二进制表示形式,然后创建类和接口。连接则是通过验证,准备,解析等动作将相关联的类或接口合并为运行时形态以便可以被JVM执行。初始化的过程主要包括类或接口初始化方法<clinit>的执行。

JVM在启动时,首先会加载和创建"启动类",该类由具体JVM实现而定,比如可以在命令行参数中指定。另外该类必须包含public static void main(String[] args)方法,JVM加载,连接,初始化该类后便会调用该方法,方法调用过程中可能会引起其他类的加载,连接操作...


一,加载

我们知道JVM是通过类加载器来完成Class文件的加载的,而类加载器总的来说分为Bootstrap Class Loader和User-defined Class Loader(继承自ClassLoader类)。(至于详细的JVM类加载机制这里我们不分析,网上有很多相关的优秀文章,读者可自行查找。)
无论哪一种类加载器,JVM都会保证对于任意一个类(如N),只会有一个初始化类加载器,也就是说,如果某一个类加载器(如L)加载了类N,那么JVM会将给类加载器L记录为类N的初始化类加载器,并保证类加载器L不会重复加载该类N。
因此,类加载的第一步便是检测类加载器L是否是将要被加载类N的初始化类加载器,如果是则抛出LinkageError。
第二步,JVM会尝试着解析指定的类的二进制表示形式,但该二进制形式不一定是合法的,因此JVM会检测一下几种可能的错误--
1)如果类格式不符合第一篇文章中分析的Class文件结构,那么抛出ClassFomatError。

2)如果类的版本不兼容于当前JVM版本,那么抛出UnsupportedClassVersionError。

3)如果指定的Class二进制表示形式不存在,那么抛出NoClassDefFoundError。
第三步,如果类N有直接父类,则检查其父类是否加载,如果没有则先加载父类。但是如果该父类实际上是个借口则抛出IncompatibleClassChangeError。如果该父类是类N本身,则抛出ClassCircularityError。

第四步,加载类N的所有接口,并且也会进行第三步中提到的异常检查。

第五步,加载完成,JVM将加载N的加载器标记为N的初始化加载器。

二,连接

一个类或接口的连接涉及该类或接口,以及其直接父类,直接父接口,元素类型(对于数组来说)的验证和准备。类或接口中的符号引用的解析则是连接中的可选操作。

JVM中并未规范连接过程的具体执行顺序,由于连接的递归性,JVM实现只需保证一下几点规范得到满足即可:

1)类或接口的连接动作必须在加载动作完全完成之后。

2)类或接口的初始化动作必须在验证和准备动作完全完成之后。

3)连接期间的任何异常都应被及时抛出。

如JVM的实现可以选择当类或接口的符号引用被使用时再进行解析(延迟解析),或者在类或接口的验证时就进行一次性的解析(静态解析)。这意味着解析过程可能在类的初始化结束后仍旧进行。无论选择哪种解析方式,解析过程中发生的异常都应当及时的抛出。

A,验证

验证的主要作用是保证类结构的正确性。验证可能会引起其他类或接口的加载,但不需要验证和准备他们。

如果类或接口的验证失败,则抛出VerfyError。如果JVM在验证某类的过程中抛出LinkageError或者其子类异常,那么该验证操作的所有子操作都将抛出相同的异常。

B,准备

准备阶段涉及类或接口中静态字段的创建以及初始化为默认值。但不会执行任何Java代码,显示的静态字段的初始化发生在初始化阶段,而不是准备阶段!

准备阶段必须在初始化前执行完毕。


三,解析

JVM的 anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface,invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, putfield, 和 putstatic 指令集均使用运行时常量池中的符号引用,任何该指令的执行都将引发符号引用的解析。
解析是将运行时常量池中符号引用转化为实际内存地址引用的过程。

对invokedynamic指令的某次执行时的符号引用解析并不表示之后invokedynamic指令被执行时的相同符号引用的解析会得到相同结果。

对于上面提到的其他指令,某次执行时的符号引用解析表示该指令以后的调用都会得到相同的结果。

对于已被解析的符号引用依然可以被再次解析,而且这种情况经常发生,但是再次解析的得到的结果与最初的结果始终相同。

如果符号引用的解析过程中发生错误,则抛出IncompatibleClassChangeError。

对于某个invokedynamic指令的调用点说明符的符号引用的解析不能早于该指令的执行。

如果invokedynamic指令的解析错误的话,那么在随后的解析中引导方法不会被再次调用。

上面某些特定指令的解析需要一些额外的连接检查,例如,为了getfield所操作的符号引用得到正确的解析,我们必须检查该变量是不是非static的,如果是static的那么抛出连接异常。

另外,为了正确解析invokedynamic指令的调用点说明符的符号引用,其中的引导方法的必须的完全正常的,并且返回恰当的调用点对象,否则抛出连接异常。

A,类或接口的解析

将类D中的名为N的符号引用解析为类或接口C的步骤如下:

1,类D的定义类加载器被用来创建名为N的类或接口C,如果创建过程中抛出异常,则该异常可以看作是类或接口解析的结果。

2,如果C是一个数组类型,并且其元素类型是引用类型,那么代表该元素类型的符号引用会按照本过程递归解析。

3,最后检查C的访问权限,如果D无法方法C,那么类或接口的解析抛出IllegalAccessError。这种情况可能发生在最初C是public的,但是D被编译后C被改为了非public的。如果1,2步成功了,但是3失败了,那么C的解析依旧有效的,不过D不可访问C。

B,变量的解析

如果从类或接口D中解析类或接口C的变量的符号引用,那么C的符号引用需要首先被解析。

当解析变量时,首先会尝试查询类或接口C以及C的父类中的变量。

1,如果C中声明了名称和描述符都相同的变量,那么查找成功,该声明的变量即为被解析的变量。

2,否则,递归查询C的父接口。

3,否则,递归查询C的父类。

4,否则,查找失败。

如果变量查找失败,那么抛出NoSuchFieldError。

如果查找成功,但是变量对D是无法访问的,那么抛出IllegalAccessError。

如果被解析变量所在的类C的类加载器是L1,而发生解析的类D的加载器是L2,那么JVM要求L1必须等于L2.

C,方法的解析

同变量的解析一样,从类或接口D中解析类C中的方法,需要先解析C。

1,方法的解析首先会检查C是否是一个接口,如果是接口则抛出IncompatibleClassChangeError。

2,然后同变量解析一样,会查询类C以及C的父类中是否存在该方法,如果存在方法名和描述符都相同的方法定义那么查找成功。

3,否则,查询类C的父接口中是否存在被解析的方法定义,如果存在查询成功。
如果方法查询失败,抛出NoSuchMethodError。

如果被查询的方法是abstract的,而类C不是abstract的,那么抛出AbstractMethodError。

如果D中无法方法该方法,那么抛出IllegalAccessError。

如果被解析方法所在的类C的类加载器是L1,而发生解析的类D的加载器是L2,那么JVM要求L1必须等于L2。
同样对于接口方法的解析大致跟上述步骤相同,只是JVM需要检查C是否是接口,如果不是接口抛出IncompatibleClassChangeError异常。

四,初始化

类或接口的初始化主要是执行类或接口的初始化方法。

只有在如下几种情况下,JVM才会初始化类或接口:

1,字节码指令new,getstatic,putstatic,invokestatic被执行时,如果相应类或接口还没有被初始化应先被初始化。

2,method handle解析后的java.lang.invoke.MethodHandle实例被初次调用时,并且该实例需要有2(REF_getStatic),4(REF_putStatic),6(REF_invokeStatic)

3,java.lang.reflect包的方法对类进行反射调用的时候。

4,当子类被初始化时,如果父类还没有被初始化,那么先初始化父类。

5,JVM启动的初始化类。

在初始化前,必须确保类的加载,连接,验证,准备动作都已经完成,至于解析则是可选的。

因为JVM是多线程的,所以类和接口的初始化需要同步,因为其他线程同一时刻可能尝试初始化同一个类或接口。因此,JVM的实现应当负责初始化的同步工作。

加入Class对象已经被验证并准备完成,那么Class对象将包含4中状态:

1,Class对象验证和准备完成,但没有被初始化

2,Class对象被一些特别的线程初始化
3,Class对象被完全初始化并可用

4,Class对象初始化失败处于一种错误状态

对于每一个类或接口C,都有一个初始化锁LC。至于C和LC的映射方式则由JVM实现灵活定制。因此,C的初始化过程如下:

1,等待并尝试获取C的初始化锁LC

2,如果C的Class对象表明其他线程正在进行C的初始化,那么释放LC,并阻塞当前线程,直到其他线程完成初始化过程。

3,如果C的Class对象表明当前线程正在进行C的初始化,那么直接完成初始化过程并释放之前获取的锁LC。

4,如果C的Class对象表明C已经被初始化完成,那么不需要其他操作,释放锁LC。

5,如果C的Class对象处于失败状态,那么无法完成初始化,释放锁LC,抛出NoClassDefFoundError。

6,否则,记录下当前线程正在执行C的Class对象的初始化过程,并释放锁LC。然后按照Class文件中ConstantValue属性表中的顺序,初始化所有final static属性。

7,接下来,如果C是个类而不是接口,并且它的父类还没有被初始化,那么先递归的初始化他的父类,如果必要,先执行验证,准备。

如果父类的初始化过程中,因为异常突然中断,然后获得锁LC,将C的Class对象标记为失败状态,通知所有等待的线程,释放锁LC,抛出异常。

8,再接下来,通过查询C的定义类加载器来判断C的断言是否可用。

9,然后执行类或接口的初始化方法。

10,如果类或接口的初始化方法执行正常完成,那么获得锁LC,将C的Class对象标记为初始化完成,通知其他线程,释放锁LC,然后正常结束该过程。
11,否则,方法执行中抛出异常E,如果E不是Error或者它的的子类,那么创建ExceptionInInitializerError,并将E作为它的参数。

12,获取锁LC,将C的Class对象标记为失败状态,通知其他等待线程,释放锁LC,然后抛出异常,中断该过程。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  JVM 初始化