JVM的类加载机制: 加载、连接、初始化。

来源:互联网 发布:软件源大全 编辑:程序博客网 时间:2024/05/29 19:48

在Java语言里面,类型的加载、链接、初始化过程都是在程序运行期间完成的。 

类从被夹在到虚拟机内存中开始,到卸载出内存为止。他的生命周期包括:加载、链接(链接包括验证、准备、解析)、初始化。


Q1:什么时候开始类加载的第一个阶段:加载。

JVM没有进行任何强制的约束,这可以交给虚拟机的具体实现来自由把握,但是可以根据初始化来推断加载、连接等操作已经完成。但是对于初始化阶段,虚拟机规定了  有且只有5种 情况必须对类初始化(加载、验证等在此之前已经执行完)。

①:遇到new、getstatic、putstatic、或invokestatic这四条字节码指令时。对应的java代码场景:使用new实例化对象;获取/设置一个类的静态字段;调用一个类的静态方法;

②:使用java.lang.reflect的方法对类进行反射调用时,若为对类进行初始化,则需先触发其初始化。

③:初始化一个子类时,若其父类未初始化,则先初始化其父类。

④:当VM启动,用户需要制定一个要执行的主类,虚拟机会先初始化这个主类。

⑤:当使用JDK1.7的动态语言相关操作。(Ps:这部分很少用到,知道一下就行)

这5中场景称为对一个类进行主动引用。

主动引用:会自动的初始化一个类。

被动引用:不会自动的初始化一个类;

下面看三个特殊被动引用的例子:

package PassiveReference;/** * 通过子类引用父类的静态字段,不会导致子类初始化。 */class superclass{static{System.out.println("superclass init !");}public static int value =123;}class subclass extends superclass{static{System.out.println("subclass init !");}}public class demo1 {public static void main(String[] args) {// 输出superclass inti ! 和 123//因为123是静态字段,当访问静态字段,只有定义这个字段的类才会被初始化,通过子类引用父类中的定义的静态字段,仅出发父类的初始化。System.out.println(subclass.value);}}


package PassiveReference;public class demo2 {/** * @param args */public static void main(String[] args) {// 这里发现运行之后没有输出 superclass inti !//new生成了一个由虚拟机自动生成的、直接继承于java.lang.object的子类,因此不能实例化。数组的创建动作由newarray触发superclass[] s = new superclass[10];}}


package PassiveReference;class ConstClass{static{System.out.println("ConstClass init ..");}public static final String HELLOWORLD = "hello world";}public class demo3 {public static void main(String[] args) {// 只输出hello world//因为常量在  编译 阶段已经将此常量的值 hello world 存储到了demo3类的常量池里面System.out.println(ConstClass.HELLOWORLD);}}


Q2: 加载的作用是什么:

加载阶段,JVM需要完成以下三件事情:

①:通过一个类的全限定名来获取定义此类的二进制字节流。(这里从哪里获取、怎么获取都是开放的,没有统一的规定)

②:将这个字节流所代表的 静态存储结构转为方法去的运行时数据结构

③:在内存中生成一个代表这个类的java.lang.object对象,作为方法区这个类的各种数据的访问入口。

注意:在加载阶段,加载一个普通类和加载一个数组的情况是不一样的。

加载一个数组时,数组类本省不通过类加载器创建,它是由Java虚拟机直接创建的。(这也是上面第二个demo产生的原因。)数组的创建过程遵循以下原则:

a. 如果数组的组件类型(就是单个元素的类型)为引用类型,则地柜采用本节中定义的加载过程家在这个组建类型。

b. 如果数组的组件类型为基本类型,jvm会把数组标记为与引导类加载器关联(引导类加载器后面讲)。

c. 数组类的可见性与它的组件的可见性一致,如果组建类型不是引用类型,那数组类的可见性默认为public。


Q3:连接的作用:

上文已经说到,连接阶段细分为:  验证、准备、解析。

★验证:确保class文件的字节流符合当前虚拟机的要求。

主要完成下面四个阶段的验证:

文件格式验证:验证类文件结构结构等。

元数据验证:验证字节码描述的信息。

字节码验证:验证语义是否合法等。

符号引用验证:在符号引用转为直接引用时验证。

★准备:正式为变量分配内容并且设置变量的初始值阶段。

首先:这里进行内存分配的只是类变量(被static修饰),而不包括实例变量,实例变量在对象初始化时候随对象一起分配在堆中。

其次:这里所说的初始值,指的是数据类型的零值。 int 为 0 ; boolean 为 false ; long为0L; 而不是赋值的那个初始值。比如,public static int  value =1;在准备阶段过后,他的初始值为0而不是1,因为此时尚未开始执行任何java方法;真正的赋值操作是程序被编译后,存放于类构造器<clinit>()方法之中。所以value赋值的操作应该是在初始化阶段才执行。特例:如果类字段是常量,则在准备阶段变量的值就会被赋给指定的值。即:public static final int value =1 ;准备阶段过后,value = 1而不是0;

  

★解析:JVM将常量池内的符号引用替换为直接引用的过程

发生时间:jvm未指定,但是要在new、putstatic、putfiled等16个字节码指令之前。

符号引用:用一组符号描述所引用的目标,符号可以使任何形式的字面量,只用可以无歧义的定位到目标即可。

直接引用:直接指向目标的指针。相对偏移量或者间接定位到目标的句柄。

差别:直接引用是和vm实现的内存布局相关的,同一个符号引用在不同的VM上翻译出来的直接引用也不相同。

解析动作主要针对:类或接口、字段、类方法、方法类型。、方法句柄、调用点限定符号引用进行;

a>、类或接口的解析

假设代码所处的类为D,吧一个从未解析过的符号引用N解析为一个类或者接口C的直接引用,需要完成下面三部。

c不是一个数组,JVM吧N的全限定名传给D的类加载器去加载C

c是一个数组且元素为对象,先按照上限的规则加载数组元素类型,然后由jvm生成一个代表此数组未读和元素的数组对象。

上述步骤未出现异常,对符号引用进行验证,确认D是否对C有访问权限。


b>、字段解析

先对字段表内的class_index项中索引的CONSTANT_Class_info符号引用先找字段所属类或者接口c;如果正常找到,然后按照如下步骤进行搜索字段:

1.c本身包含字段,直接返回;

2.c中实现了接口,按照继承关系从下往上搜索父接口。如果找到返回直接引用。

3.c不是接口,会按照继承关系从下往上搜素父类,如果找到返回直接引用。

4.未找到,抛出异常

c>、类方法解析

第一步如字段解析相同,找到所属的类或接口的符号引用。接下来如下进行:

1.类方法 和 接口方法 符号引用的常量类型定义是分开的。如果在类方法中发现c是个接口,抛出异常。

2.通过了第一步,如果在类c中找到该方法,返回直接引用。

3.在类c的父类中递归查找,找到返回直接引用。

4.否则在c实现的接口以及接口的父接口查找。如果查到,说明c是抽象类。(如果不是抽象类,在类c中必有实现。)抛出异常。

5.否则,无此方法。

d>、接口方法查找

第一步如字段解析相同,找到所属的类或接口的符号引用。接下来如下进行:

1.如果在接口方法表中发现c是一个类而不是接口,抛出异常。

2.在接口c中查找时候匹配的方法,找到返回方法的直接引用。

3.在接口c的父接口中递归查找,找到返回方法的直接引用。

4.未找到无此方法。


Q3:初始化的作用:

初始化就是执行类构造器<clinti>()方法( 类构造器不是实例构造器,注意 )的过程。

<clinit>()方法:由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句并且合并产生。收集顺序有语句在源文件中出现的的顺序决定。静态语句块只能访问定义在静态语句块之前的变量。但是却可以给它赋值。见下面例子:

package staticblock;public class test {static{i = 0;//System.out.println(i);  注释掉这里,程序正常运行; 不注释这里,程序报错。}static int i = 1;public static void main(String[] args) {// TODO Auto-generated method stubSystem.out.println(i);}}

关于<clinit>()方法与类构造器<init>()方法,一个类初始化 就会调用<clinit> (),并且只调用一次。 而类构造器<init>()每次新建一个对象,就会调用一次。 他们的区别如下所示:

class A{static{System.out.println("this is clinit...");}public A(){System.out.println("this is init..");}}public class test {public static void main(String[] args) {/**输出: * this is clinit... * this is init.. * this is init.. *  */A a = new A();A b = new A();}}


<clinit>()方法与类构造器<init>()方法不同,他不需要显示的调用父类的构造器,虚拟机保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。所以在虚拟机中第一个执行的是<cliinit>()方法是java.lang.Object的。

由于父类的<clinit方法>首先执行,因此父类中定义的静态语句块先于子类的变量赋值操作。见下:

public class test {static class parent{public static int A =1;static{A = 2;}}static class sub extends parent{public static int B = A;}public static void main(String[] args) {// 输出为2System.out.println(sub.B);}}

-------摘自<深入理解Java虚拟机:JVM高级特性与最佳实践>

0 0
原创粉丝点击