这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。
什么情况下需要开始类加载过称的第一个阶段:加载,并没有强制约束。 但对于初始化阶段,虚拟机规范则是严格规定了有四种情况必须立即对类进行“初始化”。(而加载,验证,准备自然需要在此之前开始)。 1)遇到new,getstatic,putstatic或invokestatic时,若没有进行初始化,则需要先进行初始化。使用这4条指令最常见的java代码场景是:使用new实例化对象,读取或设置一个类的静态字段,以及调用一个静态方法的时候。 2)使用java.lang.reflect包的方法对类进行反射调用的时候。 3)初始化一个类时候,发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 4)当虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 以上四种称为对一个类的主动引用。 除此之外的行为称为被动引用,不会触发初始化。如: 1) 上述代码只会输出:“SuperClass init!”而不会输出“SubClass init”。对于静态字段,只有直接定义这个字段的类才会被初始化。
2) 运行之后发现没有输出”SuperClass init!”,说明并没有触发这个类的初始化,但是触发了一个数组类”[+类名”的初始化,它是由虚拟机自动生成的,直接继承于java.lang.Object的子类。
3) 上述代码运行后也没有进行ConstClass的初始化,这是因为虽然引用了ConstClass类的常量HELLOWORLD(这里的常量指的是static final),但是在编译阶段将此值存储到了NotInitialization类的常量池中。对ConstClass.HELLOWORLD的引用实际都被转化为对NotInitialization类对自身常量池的引用。这两个类在编译成class之后就不存在任何联系了。
接口的加载过称与类加载过称有一些不同。区别在于前面讲到的主动引用的第三种,类是会先初始化父类的,而接口不会初始化父类。
加载是类加载过程的一个阶段 虚拟机需要完成三件事: 1)通过一个类的全限定名来获取定义此类的二进制字节流。 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 3)在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
大致分为:文件格式验证,元数据验证,字节码验证,符号引用验证。
是否以魔数开头,主次版本是否与虚拟机版本匹配,常量池中常量的格式等,指针是否超范围(指针格式)。
这个类是否有父类,是否继承了不允许继承的类,是否实现了父类或接口要求实现的方法等。
类型转换是否有效,跳转指令是否有效等。
发生在虚拟机将符号引用转化为直接引用的时候,转化动作在解析阶段发生。 是否能找到符号引用对应的类,是否存在对应方法,是否允许访问等。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。 这时候进行内存分配的仅包括类变量(static),而不包括实例变量。 这里说的初始值“通常情况”为数据类型的零值。 如:public static int value = 123; value将在准备阶段过后的初始值为0而不是123,把123赋值的过程将发生在初始化阶段。 “非通常情况”。即存在ConstanValue属性,这是在准备阶段就会被赋值。拥有ConstantValue属性的条件之间提到过,为: 如果同时使用final和static来修饰一个变量,并且这个变量的数据类型是基本类型或java.lang.Sring的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被fianl修饰,或者并非基本类型及字符串,则选择在《clinit》方法中就行初始化。
解析阶段是虚拟机将常量池内的符号引用转化为直接引用的过程。 符号引用:为一组字面量,通常为指向一个utf8常量的引用。(全限定名称)。 直接引用:指向目标地址的一个指针。要求目标文件存在于内存中。 解析动作主要针对类或接口,字段,类方法,接口方法四类符号引用进行。分别对应常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info,CONSTANT_InterfaceMethodref_info。
假设当前类为D,要把符号引用N解析为C类或接口。 1)若C不是一个数组,则把N的全限定名称传递给D的类加载器去加载这个类C,若有父类,先加载父类。 2)若C是一个数组,则先加载数组中元素的类型,再由虚拟机生成一个代表次数组维度和元素的数组对象。 3)进行符号引用验证。
1)先解析出调用字段的类或接口符号C。 2)查找C中的字段,存在则结束。 3)查找C接口的字段,存在则结束。 4)查找C父类(非object)中的字段,存在则结束。 5)否则查找失败。 若同样的字段同时出现在C的接口,父类或父类的接口中,编译器会拒绝编译。
1)先解析出调用字段的类或接口符号C。 2)若C为一个接口,抛出异常后结束。 3)查找C类中的方法,存在则结束。 4)查找C类的父类,存在则结束。 5)查找C类的接口与C父类的接口,存在则结束。 6)否则查找失败。
1)先解析出调用字段的类或接口符号C。 2)若C为一个类,抛出异常后结束。 3)查找C接口中的方法,存在则结束。 4)查找C接口的父类接口,存在则结束。 6)否则查找失败。
初始化阶段是执行类构造器《clinit》()方法的过程。 《clinit》()方法是由编译器自动收集类中的所有 类变量赋值动作 与 静态语句块 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。 静态语句块中只能访问到定义在其前面的变量,定义在后面的变量只能赋值,不能访问。 《clinit》()方法对于类,需先初始化父类。 《clinit》()方法对于类,接口不是必须的。 《clinit》()方法对于接口,不用先执行父接口的《clinit》()方法。 虚拟机会保证一个类的初始化过程在多线程中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的《clinit》()方法,其他线程都需要阻塞等待。
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,被称为类加载器。 由不同类加载器加载的相同的class文件会产生不同的两个类。