首页 » java

Java类加载和初始化顺序

   发表于:java评论 ()   热度:64

类的初始化顺序 1

 

对于静态变量、静态初始化块、变量、初始化块、构造器,它们的初始化顺序依次是(静态变量、静态初始化块)>(变量、初始化块)>构造器。

初始化顺序图示:

 

我们也可以通过下面的测试代码来验证这一点:

package com.trs.oop;
 

public class InitialOrderTest {   
  
    // 静态变量 
  
    public static String staticField = "静态变量";
  
    // 变量 
    public String field = "变量";   
  
    // 静态初始化块 
    static {   
        System.out.println(staticField);   
        System.out.println("静态初始化块");   
    }   
  
    // 初始化块 
    {   
        System.out.println(field);   
        System.out.println("初始化块");   
    }   
  
    // 构造器 
    public InitialOrderTest() {   
        System.out.println("构造器");   
    }   
  
    public static void main(String[] args) {
        new InitialOrderTest();   
    }   
} 


运行结果:

而对于继承的情况初始化顺序又会是怎么样的呢?请看下面示例代码:

package com.trs.oop;
 
/**
 * 有继承关系的类初始化顺序
 * @author xiayunan
 * @date   2018年7月5日
 *
 */
class Parent {   
    // 静态变量 
    public static String p_StaticField = "父类--静态变量";
    protected int i = 1;
    protected int j = 8;
    // 变量 
    public String p_Field = "父类--变量";   
  
    // 静态初始化块 
    static {   
        System.out.println(p_StaticField);   
        System.out.println("父类--静态初始化块");   
    }   
  
    // 初始化块 
    {   
        System.out.println(p_Field);   
        System.out.println("父类--初始化块");   
    }   
  
    // 构造器 
    public Parent() {   
        System.out.println("父类--构造器"); 
        System.out.println("i=" + i + ", j=" + j);
        j = 9;
    }   
}   
  
public class SubClass extends Parent {
  
    // 静态变量 
    public static String s_StaticField = "子类--静态变量";
  
    // 变量 
    public String s_Field = "子类--变量";   
    
    // 静态初始化块 
    static {   
        System.out.println(s_StaticField);   
        System.out.println("子类--静态初始化块");   
    }   
    // 初始化块 
    {   
        System.out.println(s_Field);   
        System.out.println("子类--初始化块");   
    }   
  
    // 构造器 
    public SubClass() {   
        System.out.println("子类--构造器"); 
        System.out.println("i=" + i + ",j=" + j);
    }   
  
    // 程序入口 
    public static void main(String[] args) {
        new SubClass();   
    }   
}  


运行结果:

现在,结果已经不言自明了。子类的静态变量和静态初始化块的初始化是在父类的变量、初始化块和构造器初始化之前就完成了。
静态变量、静态初始化块,变量、初始化块初始化了顺序取决于它们在类中出现的先后顺序。
执行过程分析

 (1)访问SubClass.main(),(这是一个static方法),于是装载器就会为你寻找已经编译的SubClass类的代码(也就是SubClass.class文件)。在装载的过程中,装载器注意到它有一个基类(也就是extends所要表示的意思),于是它再装载基类。不管你创不创建基类对象,这个过程总会发生。如果基类还有基类,那么第二个基类也会被装载,依此类推。


(2)执行根基类的static初始化,然后是下一个派生类的static初始化,依此类推。这个顺序非常重要,因为派生类的“static初始化”有可能要依赖基类成员的正确初始化。


(3)当所有必要的类都已经装载结束,开始执行main()方法体,并用new SubClass()创建对象。


(4)类SubClass存在父类,则调用父类的构造函数,你可以使用super来指定调用哪个构造函数(也就是Beetle()构造函数所做的第一件事)。


基类的构造过程以及构造顺序,同派生类的相同。首先基类中各个变量按照字面顺序进行初始化,然后执行基类的构造函数的其余部分。


(5)对子类成员数据按照它们声明的顺序初始化,执行子类构造函数的其余部分。
 

类的初始化顺序 2

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个过程,分别为:加载,连接(验证,准备,解析),初始化,使用,卸载。

  1. 加载
    加载主要是将.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. 解析
解析是连接阶段的第三步,是虚拟机将常量池内的符号引用替换为直接引用的过程。

  1. 初始化
    初始化阶段是类加载过程的最后一步,主要是根据程序中的赋值语句主动为类变量赋值。
    当有继承关系时,先初始化父类再初始化子类,所以创建一个子类时其实内存中存在两个对象实例。
    注:如果类的继承关系过长,单从类初始化角度考虑,这种设计不太可取。原因我想你已经猜到了。
    通常建议的类继承关系最多不超过三层,即父-子-孙。某些特殊的应用场景中可能会加到4层,但就此打住,第4层已经有代码设计上的弊端了。

  2. 使用
    程序之间的相互调用。

  3. 卸载
    即销毁一个对象,一般情况下中有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种类加载器先了解一下是什么即可,由于篇幅和主题限制,类加载器将会在下篇文件详细介绍。

(。・v・。)
喜欢这篇文章吗?欢迎分享到你的微博、QQ群,并关注我们的微博,谢谢支持。