java里面类的加载顺序(秋招必考的知识点)
知识点总结
总结Java里类的加载顺序_面试高频考点之一
总结Java面向对象特点及基础_面试高频考点之一
总结Java集合类_面试考点之一
总结Java(23模式)工厂模式_面试高频考点之一
简介:对于一个java程序的执行流程,首先编写生成java的源代码文件,接着调用java的编译器对源代码进行解析,其中包括语法分析、语义分析、词法分析等等一系列的过程,如下图。
编译结束之后生成java的class字节码文件,字节码也定义了一系列的相关标准。最后将字节码运到JVM中去解析为可在各个平台所运行。接下来讲讲本篇文章的重点——类加载器(类装载子系统)。要想弄明白类的加载顺序及过程,首先来写两个例子。
类装载过程
1.将class文件加载到类加载子系统
2.开辟一个包含 堆、方法区、栈、本地方法栈和程序计数器的空间。
3.字节码引擎开始执行
静态块,加载步骤
一.装载
1.通过类型的完全限定名,产生一个代表该类型的二进制数据流
2.解析这个二进制数据流为方法区内的内部数据结
3.构创建一个表示该类型的java.lang.Class类的实例
另外如果一个类装载器在预先装载的时遇到缺失或错误的class文件,它需要等到程序首次主动使用该类时才报告错误。
二.连接
1.验证,确认类型符合Java语言的语义,检查各个类之间的二进制兼容性(比如final的类不用拥有子类等),另外还需要进行符号引用的验证。
2.准备,Java虚拟机为类变量分配内存,设置默认初始值。
3.解析(可选的) ,在类型的常量池中寻找类,接口,字段和方法的符号引用,把这些符号引用替换成直接引用的过程。
三.初始化
1.当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
2.当调用某个类的静态方法时
3.当使用某个类或接口的静态字段时
4.当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
5.当初始化某个子类时
6.当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)
Java编译器会收集所有的类变量初始化语句和类型的静态初始化器,将这些放到一个特殊的方法中:clinit。
例子
父类Numberr
package com.hello.world;
public class Numberr {
public static final int num = 999999;
static{//静态代码块
System.out.println("我是父类静态代码块");
}
{//普通代码块
System.out.println("我是父类代码块");
}
public Numberr(){
System.out.println("我是父类构造方法");
}
}
子类BigNumber
package com.hello.world;
public class BigNumber extends Numberr{
private static Numberr bg = new BigNumber();//父类引用指向子类对象,运行时多态的表现形式之一
private static int max;//静态成员变量
private static int small;//静态成员变量
public int number1 = 1;//实例变量
public static int number2 = 1;
public static int number3 = 1;
static{//静态代码块
max = 0x7fffffff;
small = 0x80000000;
System.out.println("我是子类静态代码块");
}
{//代码块
number3++;
System.out.println("我是子类代码块");
}
public BigNumber(){//子类构造方法
super();//父类对象的引用必须放在第一行,不写会默认调用无参父类构造方法
number1++;
number2++;
System.out.println("我是子类的构造方法");
}
public void show(){
System.out.println("max:"+max+"small:"+small+"number1:"+number1+"number2:"+number2+"number3:"+number3);
}
}
测试类
package com.hello.world;
public class Test {
public static void main(String[] args) {
BigNumber bgn = new BigNumber();//实例化一个对象
Numberr nub = new BigNumber();//实例化父类的一个对象,与直接创建父类对象的不同点在于会调用子类重写父类的方法
Numberr nu = new Numberr();//实例化父类对象
bgn.show();
}
}
先看结果在分析
实例化父类对象,显示结果如下:
分析:
由执行结果分析可以很简单的得出结论,静态代码块最先执行,普通代码块优先于构造方法先执行。其实不仅如此静态代码块优先于主方法先进行加载。而代码块只有创建或声明一个对象的时候,该类的代码块才会被加载。
实例化父类引用指向子类对象结果如下:
分析
在父类的基础上加入了父类引用指向子类对象,属于向上转型小类型转大类型自动转换,此时的加载的顺序是父类的静态代码块接着是子类的静态代码块,父类代码块,父类代码块,父类构造方法,子类代码块,子类的构造方法的一个顺序。由此可以看出创建子类时会优先调用父类的静态代码块、父类代码块和构造方法。且普通的代码块只会在构造方法之前调用。
同时实例化父类对象和子类对象时,如下图:
分析:
首先声明的是子类对象,与先前的情况一致。不同点在于父类代码块和父类构造方法加载了两次,而静态代码块只加载了一次。同时也可以说明,静态代码块只会加载一次,执行完便销毁。
将子类里的静态变量的注释去掉并输出信息如下(去掉所有注释):
分析:
发现和先前的情况都相同,特别是开头的父类静态代码块、父类的代码块、父类的构造方法是连着一起执行的这是为什么呢?其实不然这种情况出现在子类中定义了 private static Numberr bg = new BigNumber();子类的一个对象,在此之前并没有静态的代码块,所以不会在父类静态代码块之后去执行子类的静态代码块,而是按照创建子类对象的顺序去走,先是父类的静态代码块在是父类的代码块、父类构造方法,在是子类的代码块,子类构造方法。此时的 private static Numberr bg = new BigNumber();算加载完成。这样说明一个问题,静态变量和静态代码块的加载优先级相同,谁在前面谁会被优先执行。之后才会加载子类的静态代码块,在此之前都没有加载main主方法,从下图可以看出:
在此之后进入main主方法依次加载,和之前完全相同。
总结
普通类无继承关系的加载顺序是静态代码-块代码块-构造方法
含有继承的类加载顺序父类静态代码块-子类静态代码块-父类代码块-父类构造方法-子类代码块-子类构造方法
含有静态成员变量为子类对象的类加载顺序是父类静态代码块-父类的代码块-父类构造方法-子类代码块-子类构造方法-子类的静态代码块在此之前还未执行main主方法。
被static修饰的变量和代码块的加载优先级相同,且优先于主方法就会被提前加载,它会在类的初始化的时候执行一次,执行完之后便销毁,它仅能初始化类变量,static修饰的成员变量。