一、 前言
本篇文章主要是通过解析类加载过程来验证子父类之间的静态方法、静态代码块、普通方法、代码块、构造器的执行顺序。
二、 类加载过程
类加载指的是在程序运行期将类数据从Class文件加载到内存中,最终形成可以被虚拟机直接使用的Java类型,整个过程包括加载、连接(验证、准备、解析)、初始化5个阶段。
类加载的时机
在开始类加载过程讲述之前,先聊一聊类加载是什么时候开始的。虚拟机没有规定什么时候进行类的加载,但规定了在什么情况下需要立即对类进行初始化。(当然在这之前需要对该类完成加载、验证、准备、解析)
以下五种情况为主动引用,需要加载类:
1.遇到new、getStatic、putstatic、invokeStatic这四条字节码指令时,new(实例对象),getStatic(读取一个类的静态字段),putstatic(设置一个类的静态字段),invokeStatic(调用一个类的静态方法)。
2.使用Java.lang.reflect包的方法对类进行反射调用时,如果此时类没有进行init,会先init。
3.当初始化一个类时,如果其父类没有进行初始化,先初始化父类。
4.jvm启动时,用户需要指定一个执行的主类(包含main的类)虚拟机会先执行这个类。
5.当使用JDK1.7的动态语言支持的时候,当java.lang.invoke.MethodHandler实例后的结果是REF-getStatic/REF_putstatic/REF_invokeStatic的句柄,并且这些句柄对应的类没初始化的话应该首先初始。
以下三种情况称作被动引用,不需要加载类:
1、通过子类引用父类的静态字段或者父类特有静态方法,不会导致子类初始化(子类是否会进行加载、连接等过程取决于虚拟机的参数设置)。
2、通过数组定义来引用类,不会触发此类的初始化。
3、调用类B使用类A的常量变量时,不会触发常量所在的类A的初始化,A类的常量在编译阶段会存入调用类B的常量池中,类B看起来是访问类A的常量,实际上是在自身的常量池(B类的常量池)中访问该常量。
注意: 接口加载过程和类加载基本相同,虽然接口不能使用static方法块,但还是会初始化接口成员变量(默认且只能为public static final,不可修改)。接口初始化时不要求父接口都完成初始化,只要在用到时完成即可。
加载
加载过程完成以下三件事情:
1、使用类加载器来通过一个类的全限定名来获取其定义的二进制字节流。
2、将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。
3、在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
特别注意:
1、类加载器使用的是双亲委托机制,从下向上进行检查、从上向下进行尝试加载,保证了核心类不会被篡改,防止了类被重复加载。
2、通过类的全限定名进行加载,表明并不一定非要从Class文件中获取二进制字节流,而可以通过多种其它途径,例如ZIP包、网络、其它文件、数据库,由此产生了许多中Java技术。
3、数组类本身不是通过类加载器加载,而是由虚拟机直接创建,但数组类中的元素类型需要通过类加载器去加载,引用类型元素使用相应的类加载器进行加载,基本类型元素,将其标记为与引导类加载器关联。
4、加载开始时间和后面的连接阶段开始时间是固定先后顺序的,但两个过程往往是交叉进行的,加载还未完成,连接阶段已经开始。
5、加载阶段用户可通过自定义加载器就行类加载,连接阶段完全由虚拟机主导和控制,初始阶段才开始真正的执行java的程序代码(字节码)。
验证
验证是连接的第一个阶段,验证的对象是字节流,主要是确保字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身安全。包括文件格式验证、元数据验证、字节码验证、符号引用验证四个阶段。
1、文件格式的验证:验证Class文件字节流是否符合Class文件的格式的规范,并且能够被当前版本的虚拟机处理,确保字节流能正确地解析并存储于方法区中。这里面主要对魔数、主次版本号、常量池等等的校验。
(注意: 完成文件格式验证之后,字节流进入了方法区,之后的三个验证阶段都是基于方法区的存储结构进行的,不再直接操作字节流。)
2、数据验证:对数据类型进行校验。主要是对字节码描述的信息进行语义分析,语义校验元数据信息,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突、是否继承了final类、重载不符合规则等等。
3、字节码验证:对方法体进行校验。这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出危害虚拟机安全的事。比如:保证方法体的类型转换是否有效、保证操作数栈的数据类型与指令代码能正常工作等。
4、符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对常量池中的各种符号引用进行匹配校验。目的是确保解析动作能够完成。比如:校验通过字符串全限定名是否能找到该类、当前类是否有权限访问符号引用中的类、字段、方法,校验类中是否存在符号引用所描述的方法和字段。
注意: 可通过-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
准备阶段开始在方法区中为类变量(静态变量)分配内存,并设置变量初始值,需要注意的是这里的初始值,通常情况下并不是自己在代码中赋予的值,而是虚拟机默认的零值。如下:
但当类变量是一个常量时,就会被赋予代码中的初始值。因为当该类进行编译时,编译器会为常量生成ConstantValue属性,并按照代码初始化的值进行指定ConstantValue属性的值。当一个类字段的字段属性中有ConstantValue属性,在准备阶段,就按照ConstantValue属性的值为该类变量赋值。
public static int value=666;
以上代码,在准备阶段,value的值为0。
public static final int value=666;
以上代码,在准备阶段,value的值为666。
解析
解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用。
符号引用:相当于一个逻辑引用。以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好。因为符号引用的字面量形式明确定义在了Java虚拟机规范的Class文件格式中,因此各种虚拟机虽然内存布局可以不同,但能接受的符号引用必须一致。符号引用同虚拟机实现的内存布局无关,符号引用的目标不一定加载到了内存中。
直接引用:相当于物理引用,直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。转换为直接引用时,引用的目标必然已经在内存中存在。
注意:
1、虚拟机并没有规定解析阶段开始的具体时间,只是当需要执行某条用于操作符号引用的指令(16种)时,我们要在指令执行之前完成对该符号引用的转换。因此虚拟机可以根据需要来决定何时来进行符号解析。
2、除了invokeddynamic指令外,虚拟机第一次执行其它指令时,会将其操作的符号引用解析的结果缓存在运行时常量池中,避免重复解析。并且无论真正解析多少次,虚拟机需要保证成功一次之后,接下来的解析都得成功;解析失败时,其它指令对该符号得解析请求会收到异常。
3、解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符合限引用。分别对应常量池中的七种常量类型。
初始化
初始化阶段是通过执行构造器clinit()来进行类变量赋值和静态代码块的执行。虚拟机会保证在子类的构造器clinit()执行之前,父类的构造器clinit()会先得到执行,因此父类的静态变量赋值语句和静态代码块会在子类的之前执行(执行顺序按照代码顺序执行,定义在静态代码块之后的静态变量,静态代码块只能赋值,不能访问)。
因为构造器clinit()是由编译器自动收集类中的所有类变量的赋值动作和静态代码块的语句合并产生的,因此当类中无类变量的赋值动作和静态代码块时,变量不会为该类生成构造器clinit()。
注意: 在深入理解Java虚拟机这本书上,描述接口虽然不能有静态代码块,但因为有变量初始化的赋值操作,所以接口也会生成构造器clinit()。
(个人疑惑,接口中的变量是静态常量,若该常量值是在编译器就编译到类的常量池中,则在准备阶段就进行了赋值操作,若不是编译时常量,静态变量是一个引用,比如一个随机生成数,那么应该是在初始化阶段进行赋值,然后加入运行时常量池,这是个人的理解,不是特别确定,求大神指点证实一下,万分感谢!)
public static final int FINAL_VALUE_INT = new Random(66).nextInt();
三、Java的静态变量、静态函数、静态代码块
Java中存在着一种伪全局变量,即为静态变量,它与静态函数、静态代码块一同只属于类本身,而不属于类的实例对象,但对象可以访问类变量,我们可以通过类名或者对象名对它进行访问或者修改。
静态变量
静态变量在类加载的准备阶段,进行内存分配,设置默认值值;在类加载的初始化阶段进行真正的初始化赋值。
静态变量是属于类本身的变量,因此又被称作类变量,所有的实例共用这一份变量,所有实例都可以访问或者修改该变量。
子类引用父类的静态变量,不会造成子类的类加载。
静态代码块
1、静态代码块在类加载的初始化阶段开始自动执行,多个静态代码块会按照代码顺序进行顺序执行,且只能执行一次。
2、静态代码块对于定义在它后面的静态变量,只能赋值不能访问。
3、静态代码块内不能直接访问非静态成员变量或者成员方法。非静态成员变量和方法属于对象实例,静态代码块在执行时,还未实例化对象。但可通过对象间接访问非静态成员变量和方法。(这里的实例对象可以是类本身的对象,如类A的静态代码块中可以通过类A的实例对象B访问其非静态方法或者变量,个人理解,到了初始化这一阶段,除了类变量赋值和静态代码块没执行,其它都已经准备好,因此这里可以实例化自己的对象)。
4、静态代码块只能定义在类里面,不能定义在方法里面。
5、静态代码块里的变量都是局部变量,只在块内有效。
6、静态代码块多用于对类的初始化。
static{
InheritFoundation ih= new InheritFoundation ("11","11");
ih.notAbstract();
}
静态方法
1、静态方法属于类,在内存中只有一份,所有的实例共用这个方法。
2、静态方法同静态代码块一样,只能直接访问静态成员变量,不能直接访问非静态成员变量和方法,可通过对象间接访问。
3、静态方法可通过类名和对象名进行访问。
4、静态方法内不能出现this,因为this关键字属于对象。
5、静态方法不能被重写,而是被根据引用类型被调用,表现为重写。若子父类都有静态方法A(),使用子类调用方法A()时,调用子类的静态方法,使用父类调用方法A()时,调用父类的静态方法。
6、若是子类调用父类的特有静态方法,则不会产生子类的类加载。
public static void resr (){
InheritFoundation ih= new InheritFoundation ("22","33");
ih.notAbstract();
}
四、执行顺序解析
父类静态方法、静态代码块、代码块、构造器以及子类静态方法、静态代码块、代码块、构造器的执行顺序如下:
1、先执行父类静态代码块(多个静态代码块按代码先后顺序执行);
2、再执行子类静态代码块(多个静态代码块按代码先后顺序执行);
(注: 这两个步骤是在类加载的初始阶段完成。)
3、再执行父类方法块(方法块与静态方法块的区别是方法块属于对象而不是类本身);
4、再执行父类构造器完成父类对象实例化;
5、再执行子类方法块;
6、最后执行子类构造器,完成子类对象实例化。
(重点注意: 子类静态方法以及父类静态方法的执行顺序是怎样往往取决于你在哪里调用该方法。)
7、若你在对象实例化之前,用父类进行调用静态方法,则它出现在父类静态代码块之后(因为只有完成了类加载之后,才能调用静态方法)。
8、若你在对象实例化之前,用子类进行调用静态方法,则出现在子类静态代码块之后(加载子类,必须加载父类,那父类静态代码块先执行,子类代码块后执行,完成加载再执行子类静态方法)。
(特殊情况: 若子类调用的是父类的特有静态方法,则不会加载子类,因此发生在父类代码块之后。)
9、若在对象实例化之后,则该静态函数调用会发生在对象构造方法执行之后。
(静态方法只要在父子类的静态代码块之后即可,因为需要完成类加载)
代码示例
public class StaticMethodDemo {
public static void main(String[] args){
int age=22;
StaticMethodTest.ParentUniqueStatic();
Parent.ParentStatic();
StaticMethodTest.ParentStatic();
// System.out.println(""+StaticMethodTest.high);
StaticMethodTest test=new StaticMethodTest(22);
// test.high=5;
//System.out.println(StaticMethodTest.high);
StaticMethodTest.ParentStatic();
Parent.ParentStatic();
StaticMethodTest.ParentUniqueStatic();
test.childMethod();
/* StaticMethodTest.ParentStatic();
System.out.println();*/
}
}
class StaticMethodTest extends Parent{
private static String children="children";
public static int childHigh=4;
private int childAge;
public StaticMethodTest(int Age){
super(Age+1);
this.childAge=Age;
System.out.println("子类构造器");
}
public static void ParentStatic(){
System.out.println("子类静态方法调用");
}
static{
System.out.println("子类静态方法块调用");
}
static{
System.out.println("子类静态方法块2调用");
}
{
System.out.println("子类方法块调用");
}
{
System.out.println("子类方法块2调用");
}
/* @Override*/
public void childMethod(){
System.out.println("子类普通方法");
}
}
class Parent {
private static String parent= "parent";
public static int high=15;
private int parentAge;
public Parent(int i) {
this.parentAge=i;
System.out.println("父类构造器");
}
public static void ParentUniqueStatic(){
System.out.println("父类特有静态方法调用,子类没有哟");
}
public static void ParentStatic() {
System.out.println("父类静态方法调用");
}
static {
System.out.println("父类静态方法块调用");
}
static {
System.out.println("父类静态方法1块调用");
}
{
System.out.println("父类方法块调用");
}
{
System.out.println("父类方法块1调用");
}
public void childMethod() {
System.out.println("父类普通方法");
}
}
执行结果:
父类静态方法块调用
父类静态方法1块调用
父类特有静态方法调用,子类没有哟
父类静态方法调用
子类静态方法块调用
子类静态方法块2调用
子类静态方法调用
父类方法块调用
父类方法块1调用
父类构造器
子类方法块调用
子类方法块2调用
子类构造器
子类静态方法调用
父类静态方法调用
父类特有静态方法调用,子类没有哟
子类普通方法
五、总结
1、无论是静态方法还是非静态方法都是在类加载完成之后才能进行调用。
2、静态代码块和静态变量的初始化完成是在类加载的最后一个阶段初始化阶段。静态代码块只执行一次,多用于对类的初始化。
3、静态方法只是表现为重写,其实是根据引用类型不同而进行不同的调用,子类调用,则调用子类的静态方法,若父类调用则调用父类的静态方法。
4、子类调用父类的静态属性或者父类的特有静态方法不会造成子类的类加载。
5、执行顺序要点:子类加载之前,必须加载父类,因此父类的执行会在子类之前。调用静态方法或者构造对象实例之前,必须完成类加载,因此静态代码块肯定先执行;普通代码块属于实例,在实例构造器执行之前执行。静态方法或者非静态方法执行需要看是谁执行的、在何时执行来决定顺序。
6、类加载过程其实是个比较自由的过程,虽然名义上是有着前后顺序的五个阶段,但其实往往是交叉执行,且往往只规定了某个阶段需要在什么情况下必须触发完成,而不是需要五个阶段必须像流水一样,一旦开始加载必须依次立马完成。
最后抛出个个人疑惑,希望能有大神解答下:
疑惑: 在深入理解Java虚拟机这本书上,描述接口虽然不能有静态代码块,但因为有变量初始化的赋值操作,所以接口也会生成构造器clinit()。
(个人疑惑,接口中的变量是静态常量,若该常量值是在编译器就编译到类的常量池中,则在准备阶段就进行了赋值操作,若不是编译时常量,静态变量是一个引用,比如一个随机生成数,那么应该是在初始化阶段进行赋值,然后加入运行时常量池,这是个人的理解,不是特别确定,求大神指点证实一下,万分感谢!)