Java类加载机制中最重要的就是程序初始化过程,其中包含了静态资源,非静态资源,父类子类,构造方法之间的执行顺序。这类知识经常会出现在面试题中,如果没有搞清楚其原理,在复杂的开源设计中可能无法梳理其业务流程,是java程序员进阶的阻碍。
首先通过一个例子来分析java代码的执行顺序:
public class CodeBlockForJava extends BaseCodeBlock {
{
System.out.println("这里是子类的普通代码块");
}
public CodeBlockForJava() {
System.out.println("这里是子类的构造方法");
}
@Override
public void msg() {
System.out.println("这里是子类的普通方法");
}
public static void msg2() {
System.out.println("这里是子类的静态方法");
}
static {
System.out.println("这里是子类的静态代码块");
}
public static void main(String[] args) {
BaseCodeBlock bcb = new CodeBlockForJava();
bcb.msg();
}
Other o = new Other();
}
class BaseCodeBlock {
public BaseCodeBlock() {
System.out.println("这里是父类的构造方法");
}
public void msg() {
System.out.println("这里是父类的普通方法");
}
public static void msg2() {
System.out.println("这里是父类的静态方法");
}
static {
System.out.println("这里是父类的静态代码块");
}
Other2 o2 = new Other2();
{
System.out.println("这里是父类的普通代码块");
}
}
class Other {
Other() {
System.out.println("初始化子类的属性值");
}
}
class Other2 {
Other2() {
System.out.println("初始化父类的属性值");
}
}
这个例子比较简单,在运行代码之前分析一下:带有static关键字的代码块应该是最先执行,其次是非static关键字的代码块以及类的属性(Fields),最后是构造方法。带上父子类的关系后,上面的运行结果为:
这里是父类的静态代码块
这里是子类的静态代码块
初始化父类的属性值
这里是父类的普通代码块
这里是父类的构造方法
这里是子类的普通代码块
初始化子类的属性值
这里是子类的构造方法
这里是子类的普通方法
注意的是类的属性与非静态代码块的执行级别是一样的,谁先执行取决于书写的先后顺序。
结论1:父类的静态代码块->子类的静态代码块->初始化父类的属性值/父类的普通代码块(自上而下的顺序排列)->父类的构造方法->初始化子类的属性值/子类的普通代码块(自上而下的顺序排列)->子类的构造方法。
注:构造函数最后执行。
上面的例子只是小试牛刀,接下来再看一个比较复杂的例子:
public class ClassloadSort1 {
public static void main(String[] args) {
Singleton.getInstance();
System.out.println("Singleton value1:" + Singleton.value1);
System.out.println("Singleton value2:" + Singleton.value2);
Singleton2.getInstance2();
System.out.println("Singleton2 value1:" + Singleton2.value1);
System.out.println("Singleton2 value2:" + Singleton2.value2);
}
}
class Singleton {
static {
System.out.println(Singleton.value1 + "\t" + Singleton.value2 + "\t" + Singleton.singleton);
//System.out.println(Singleton.value1 + "\t" + Singleton.value2);
}
private static Singleton singleton = new Singleton();
public static int value1 = 5;
public static int value2 = 3;
private Singleton() {
value1++;
value2++;
}
public static Singleton getInstance() {
return singleton;
}
int count = 10;
{
System.out.println("count = " + count);
}
}
class Singleton2 {
static {
System.out.println(Singleton2.value1 + "\t" + Singleton2.value2 + "\t" + Singleton2.singleton2);
}
public static int value1 = 5;
public static int value2 = 3;
private static Singleton2 singleton2 = new Singleton2();
private String sign;
int count = 20;
{
System.out.println("count = " + count);
}
private Singleton2() {
value1++;
value2++;
}
public static Singleton2 getInstance2() {
return singleton2;
}
}
这个用例相比第一个,知识点更深了一层。如果你用结论1是没法分析出正确答案的,但这并不代表结论1就是错误的。
运行结果:
Singleton value1:5
Singleton value2:3
Singleton2 value1:6
Singleton2 value2:4
Singleton中的value1,value2并没有受到构造方法中自加操作的影响。然而Singleton2中的代码也相同,为什么执行出来的效果就不一样呢?
要想知道原因,必须先搞清楚Java类加载中具体做了些什么。
JAVA类的加载机制
Java类加载分为5个过程,分别为:加载,连接(验证,准备,解析),初始化,使用,卸载。
- 加载
加载主要是将.class文件(也可以是zip包)通过二进制字节流读入到JVM中。 在加载阶段,JVM需要完成3件事:
1)通过classloader在classpath中获取XXX.class文件,将其以二进制流的形式读入内存。
2)将字节流所代表的静态存储结构转化为方法区的运行时数据结构;
3)在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2.1. 验证
主要确保加载进来的字节流符合JVM规范。验证阶段会完成以下4个阶段的检验动作:
1)文件格式验证
2)元数据验证(是否符合Java语言规范)
3)字节码验证(确定程序语义合法,符合逻辑)
4)符号引用验证(确保下一步的解析能正常执行)
2.2. 准备
准备是连接阶段的第二步,主要为静态变量在方法区分配内存,并设置默认初始值。
2.3. 解析
解析是连接阶段的第三步,是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化
初始化阶段是类加载过程的最后一步,主要是根据程序中的赋值语句主动为类变量赋值。
当有继承关系时,先初始化父类再初始化子类,所以创建一个子类时其实内存中存在两个对象实例。
注:如果类的继承关系过长,单从类初始化角度考虑,这种设计不太可取。原因我想你已经猜到了。
通常建议的类继承关系最多不超过三层,即父-子-孙。某些特殊的应用场景中可能会加到4层,但就此打住,第4层已经有代码设计上的弊端了。使用
程序之间的相互调用。卸载
即销毁一个对象,一般情况下中有JVM垃圾回收器完成。代码层面的销毁只是将引用置为null。
通过上面的整体介绍后,再来看Singleton2.getInstance()的执行分析:
1)类的加载。运行Singleton2.getInstance(),JVM在首次并没有发现Singleton类的相关信息。所以通过classloader将Singleton.class文件加载到内存中。
2)类的验证。略
3)类的准备。将Singleton2中的静态资源转化到方法区。value1,value2,singleton在方法区被声明分别初始为0,0,null。
4)类的解析。略(将常量池内的符号引用替换为直接引用的过程)
5)类的初始化。执行静态属性的赋值操作。按照顺序先是value1 = 5,value2 = 3,接下来是private static Singleton2 singleton2 = new Singleton2();
这是个创建对象操作,根据 结论1 在执行Singleton2的构造方法之前,先去执行static资源和非static资源。但由于value1,value2已经被初始化过,所以接下来执行的是非static的资源,最后是Singleton2的构造方法:value1++;value2++。
所以Singleton2结果是6和4。
以上除了搞清楚执行顺序外,还有一个重点->结论2:静态资源在类的初始化中只会执行一次。不要与第3个步骤混淆。
有了以上的这个结论,再来看Singleton.getInstance()的执行分析:
1)类的加载。将Singleton类加载到内存中。
2)类的验证。略
3)类的准备。将Singleton2的静态资源转化到方法区。
4)类的解析。略(将常量池内的符号引用替换为直接引用的过程)
5)类的初始化。执行静态属性的赋值操作。按照顺序先是private static Singleton singleton = new Singleton(),根据 结论1 和结论2,value1和value2不会在此层执行赋值操作。所以singleton对象中的value1,value2只是在0的基础上进行了++操作。此时singleton对象中的value1=1,value2=1。
然后, public static int value1 = 5; public static int value2 = 3; 这两行代码才是真的执行了赋值操作。所以最后的结果:5和3。
如果执行的是public static int value1; public static int value2;结果又会是多少?结果: 1和1。
注:为什么 Singleton singleton = new Singleton()不会对value1,value2进行赋值操作?因为static变量的赋值在类的初始化中只会做一次。
程序在执行private static Singleton singleton = new Singleton()时,已经是对Singleton类的static变量进行赋值操作了。这里new Singleton()是一个特殊的赋值,类似于递归里层,外层已经是赋值操作了,所以里层会自动过滤static变量的赋值操作。但非static的变量依然会被赋值。
结论3:在结论2的基础上,非静态资源会随对象的创建而执行初始化。每创建一个对象,执行一次初始化。
掌握结论1,2,3基本对java类中程序执行的顺序了如指掌。这还不够,ClassLoader还没有介绍呢。
类加载器
JVM提供了以下3种系统的类加载器:
- 启动类加载器(Bootstrap ClassLoader):最顶层的类加载器,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
- 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
- 应用程序类加载器(Application ClassLoader):也叫做系统类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径(classpath)上的类库。如果没有自定义类加载器,一般这个就是默认的类加载器。
这3种类加载器先了解一下是什么即可,由于篇幅和主题限制,类加载器将会在下篇文件详细介绍。