`

【转】【深入理解JVM】:Java类继承关系中的初始化顺序

阅读更多

来源:http://blog.csdn.net/u011080472/article/details/51330114

 

Java类初始化的顺序经常让人犯迷糊,现在本文尝试着从JVM的角度,对Java非继承和继承关系中类的初始化顺序进行试验,尝试给出JVM角度的解释。

非继承关系中的初始化顺序

对于非继承关系,主类InitialOrderWithoutExtend中包含了静态成员变量(类变量)SampleClass 类的一个实例,普通成员变量SampleClass 类的2个实例(在程序中的顺序不一样)以及一个静态代码块,其中静态代码块中如果静态成员变量sam不为空,则改变sam的引用。main()方法中创建了2个主类对象,打印2个主类对象的静态成员sam的属性s。

代码1

 

package com.j2se;

public class InitialOrderWithoutExtend {
    static SampleClass sam = new SampleClass("静态成员sam初始化");
    SampleClass sam1 = new SampleClass("普通成员sam1初始化");
    static {
        System.out.println("static块执行");
        if (sam == null)
            System.out.println("sam is null");
        sam = new SampleClass("静态块内初始化sam成员变量");
    }

    SampleClass sam2 = new SampleClass("普通成员sam2初始化");

    InitialOrderWithoutExtend() {
        System.out.println("InitialOrderWithoutExtend默认构造函数被调用");
    }

    public static void main(String[] args) {
        // 创建第1个主类对象
        System.out.println("第1个主类对象:");
        InitialOrderWithoutExtend ts = new InitialOrderWithoutExtend();

        // 创建第2个主类对象
        System.out.println("第2个主类对象:");
        InitialOrderWithoutExtend ts2 = new InitialOrderWithoutExtend();

        // 查看两个主类对象的静态成员:
        System.out.println("2个主类对象的静态对象:");
        System.out.println("第1个主类对象, 静态成员sam.s: " + ts.sam);
        System.out.println("第2个主类对象, 静态成员sam.s: " + ts2.sam);
    }
}

class SampleClass {
    // SampleClass 不能包含任何主类InitialOrderWithoutExtend的成员变量
    // 否则导致循环引用,循环初始化,调用栈深度过大
    // 抛出 StackOverFlow 异常
    // static InitialOrderWithoutExtend iniClass1 = new InitialOrderWithoutExtend("静态成员iniClass1初始化");
    // InitialOrderWithoutExtend iniClass2 = new InitialOrderWithoutExtend("普通成员成员iniClass2初始化");

    String s;

    SampleClass(String s) {
        this.s = s;
        System.out.println(s);
    }

    SampleClass() {
        System.out.println("SampleClass默认构造函数被调用");
    }

    @Override
    public String toString() {
        return this.s;
    }
}
 

 

 

输出结果:

 

静态成员sam初始化
static块执行
静态块内初始化sam成员变量
第1个主类对象:
普通成员sam1初始化
普通成员sam2初始化
InitialOrderWithoutExtend默认构造函数被调用
第2个主类对象:
普通成员sam1初始化
普通成员sam2初始化
InitialOrderWithoutExtend默认构造函数被调用
2个主类对象的静态对象:
第1个主类对象, 静态成员sam.s: 静态块内初始化sam成员变量
第2个主类对象, 静态成员sam.s: 静态块内初始化sam成员变量
 

 

 

由输出结果可知,执行顺序为:

  1. static静态代码块和静态成员
  2. 普通成员
  3. 构造函数执行

当具有多个静态成员和静态代码块或者多个普通成员时,初始化顺序和成员在程序中申明的顺序一致。

注意到在该程序的静态代码块中,修改了静态成员sam的引用。main()方法中创建了2个主类对象,但是由输出结果可知,静态成员和静态代码块只进行了一次初始化,并且新建的2个主类对象的静态成员sam.s是相同的。由此可知,类的静态成员和静态代码块在类加载中是最先进行初始化的,并且只进行一次。该类的多个实例共享静态成员,静态成员的引用指向程序最后所赋予的引用。

继承关系中的初始化顺序

此处使用了3个类来验证继承关系中的初始化顺序:Father父类、Son子类和Sample类。父类和子类中各自包含了非静态代码区、静态代码区、静态成员、普通成员。运行时的主类为InitialOrderWithExtend类,main()方法中创建了一个子类的对象,并且使用Father对象指向Son类实例的引用(父类对象指向子类引用,多态)。

代码2

 

package com.j2se;

public class InitialOrderWithExtend {
    public static void main(String[] args) {
        Father ts = new Son();
    }
}

class Father {
    {
        System.out.println("父类 非静态块 1  执行");
    }
    static {
        System.out.println("父类 static块 1  执行");
    }
    static Sample staticSam1 = new Sample("父类 静态成员 staticSam1 初始化");
    Sample sam1 = new Sample("父类 普通成员 sam1 初始化");
    static Sample staticSam2 = new Sample("父类 静态成员 staticSam2 初始化");
    static {
        System.out.println("父类 static块 2  执行");
    }

    Father() {
        System.out.println("父类 默认构造函数被调用");
    }

    Sample sam2 = new Sample("父类 普通成员 sam2 初始化");

    {
        System.out.println("父类 非静态块 2  执行");
    }

}

class Son extends Father {
    {
        System.out.println("子类 非静态块 1  执行");
    }

    static Sample staticSamSub1 = new Sample("子类 静态成员 staticSamSub1 初始化");

    Son() {
        System.out.println("子类 默认构造函数被调用");
    }

    Sample sam1 = new Sample("子类 普通成员 sam1 初始化");
    static Sample staticSamSub2 = new Sample("子类 静态成员 staticSamSub2 初始化");

    static {
        System.out.println("子类 static块1  执行");
    }

    Sample sam2 = new Sample("子类 普通成员 sam2 初始化");

    {
        System.out.println("子类 非静态块 2  执行");
    }

    static {
        System.out.println("子类 static块2  执行");
    }
}

class Sample {
    Sample(String s) {
        System.out.println(s);
    }

    Sample() {
        System.out.println("Sample默认构造函数被调用");
    }
}

 

 

运行结果:

 

父类 static块 1  执行
父类 静态成员 staticSam1 初始化
父类 静态成员 staticSam2 初始化
父类 static块 2  执行
子类 静态成员 staticSamSub1 初始化
子类 静态成员 staticSamSub2 初始化
子类 static块1  执行
子类 static块2  执行
父类 非静态块 1  执行
父类 普通成员 sam1 初始化
父类 普通成员 sam2 初始化
父类 非静态块 2  执行
父类 默认构造函数被调用
子类 非静态块 1  执行
子类 普通成员 sam1 初始化
子类 普通成员 sam2 初始化
子类 非静态块 2  执行
子类 默认构造函数被调用

 

 

由输出结果可知,执行的顺序为:

  1. 父类静态代码区和父类静态成员
  2. 子类静态代码区和子类静态成员
  3. 父类非静态代码区和普通成员
  4. 父类构造函数
  5. 子类非静态代码区和普通成员
  6. 子类构造函数

与非继承关系中的初始化顺序一致的地方在于,静态代码区和父类静态成员、非静态代码区和普通成员是同一级别的,当存在多个这样的代码块或者成员时,初始化的顺序和它们在程序中申明的顺序一致;此外,静态代码区和静态成员也是仅仅初始化一次,但是在初始化过程中,可以修改静态成员的引用。

初始化顺序图示

非继承关系

非继承关系初始化顺序

继承关系

继承关系初始化顺序

类初始化顺序的JVM解释

类初始化顺序受到JVM类加载机制的控制,类加载机制包括加载、验证、准备、解析、初始化等步骤。不管是在继承还是非继承关系中,类的初始化顺序主要受到JVM类加载时机、解析和clinit()初始化规则的影响。

加载时机

加载是类加载机制的第一个阶段,只有在5种主动引用的情况下,才会触发类的加载,而在其他被动引用的情况下并不会触发类的加载。关于类加载时机和5中主动引用和被动引用详见【深入理解JVM】:类加载机制。其中3种主动引用的形式为:

  • 程序启动需要触发main方法的时候,虚拟机会先触发这个类的初始化
  • 使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、JIT时放入常量池的静态字段除外)、调用一个类的静态方法,会触发初始化
  • 当初始化一个类的时候,如果其父类没有初始化,则需要先触发其父类的初始化

代码1中触发main()方法前,需要触发主类InitialOrderWithoutExtend的初始化,主类初始化触发后,对静态代码区和静态成员进行初始化后,打印”第1个主类对象:”,之后遇到newInitialOrderWithoutExtend ts = new InitialOrderWithoutExtend();,再进行其他普通变量的初始化。

代码2是继承关系,在子类初始化前,必须先触发父类的初始化。

类解析在继承关系中的自下而上递归

类加载机制的解析阶段将常量池中的符号引用替换为直接引用,主要针对的是类或者接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用。关于类的解析过程详见【深入理解JVM】:类加载机制

而在字段解析、类方法解析、方法类型解析中,均遵循继承关系中自下而上递归搜索解析的规则,由于递归的特性(即数据结构中栈的“后进先出”),初始化的过程则是由上而下、从父类到子类的初始化顺序。

初始化clinit()方法

初始化阶段是执行类构造器方法clinit() 的过程。clinit() 是编译器自动收集类中所有类变量(静态变量)的赋值动作和静态语句块合并生成的。编译器收集的顺序是由语句在源文件中出现的顺序决定的。JVM会保证在子类的clinit() 方法执行之前,父类的clinit() 方法已经执行完毕。

因此所有的初始化过程中clinit()方法,保证了静态变量和静态语句块总是最先初始化的,并且一定是先执行父类clinit(),在执行子类的clinit()。

代码顺序与对象内存布局

在前面的分析中我们看到,类的初始化具有相对固定的顺序:静态代码区和静态变量先于非静态代码区和普通成员,先于构造函数。在相同级别的初始化过程中,初始化顺序与变量定义在程序的中顺序是一致的。

而代码顺序在对象内存布局中同样有影响。(关于JVM对象内存布局详见【深入理解JVM】:Java对象的创建、内存布局、访问定位。)

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。而实例数据是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。

无论是从父类继承还是子类定义的,都需要记录下来,这部分的存储顺序JVM参数和字段在程序源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oop,从分配策略中可以看出,相同宽度的字段总是分配到一起。满足这个条件的前提下,父类中定义的变量会出现在子类之前。不过,如果启用了JVM参数CompactFields(默认为true,启用),那么子类中较窄的变量也可能会插入到父类变量的空隙中。

 

 

 

 

继承与初始化:

 

public class Child extends parent {
	public static void main(String[] args){
	}
}
class parent{
}

 

加载过程:

1、首先访问Child.main()。于是加载器开始启动并找出Child类的编译代码(在名为Child.class的文件之中),在对它进行加载的过程中,编译期注意到它有一个基类Parent(由关键字extends得知),于是对Parent类加载。

2、如果该基类还有其自身的基类,那么第二个基类就会被加载,以此类推。

3、根基类中的static初始化即会被执行,然后是下一个导出类,以此类推。

到此为止,必要的类都已加载完毕。(加载顺序:当前类-->基类  static初始化顺序:基类-->当前类)

 

创建对象:

 

对象中的所有基本类型都会被设为默认值,对象引用被设为null。

 

基类的构造器会被调用

 

分享到:
评论

相关推荐

    java程序初始化顺序

    以下是对Java程序初始化顺序的详细说明: 1. **类加载阶段**: - **加载**:当Java虚拟机(JVM)首次遇到一个类的引用时,会通过类加载器进行加载。加载过程包括找到类的.class文件,读取其字节码,并转化为内存中...

    java中类的初始化顺序

    ### Java中类的初始化顺序详解 #### 一、概述 在Java编程语言中,类的初始化是一个非常重要的概念。类的初始化涉及到多个方面,包括静态成员变量、实例成员变量、静态初始化块、实例初始化块以及构造函数等。本文...

    java代码的初始化顺序demo

    总之,Java代码的初始化顺序是类加载的必然过程,涉及到静态和实例初始化块、构造函数、成员变量初始化以及继承关系的影响。这个demo是学习和理解这些概念的重要工具,通过实际操作可以加深对Java内存管理和对象生命...

    探究java的ClassLoader及类变量初始化顺序

    总之,深入理解Java的ClassLoader机制和类变量初始化顺序是提升Java编程技能的重要步骤。通过学习这些知识点,开发者可以更好地优化代码、设计更健壮的系统,并解决与类加载和初始化相关的复杂问题。

    JAVA面试题解惑系列——类的初始化顺序

    1. 单个类内成员的初始化顺序:静态变量、静态初始化块、变量、初始化块、构造器。 2. 在继承关系中,子类的静态成员会在父类的非静态成员初始化之前被初始化。 3. 开发者应当注意静态成员的初始化顺序,避免在继承...

    java 继承关系的加载顺序

    总结,Java中继承关系的加载顺序遵循“先父后子”的原则,从加载到初始化,都是先处理父类,再处理子类。理解这一顺序对于理解和优化代码、避免潜在问题至关重要。同时,Java的类加载机制还支持动态加载和灵活的代码...

    JAVA面试题解惑系列(一)——类的初始化顺序-JAVA程序员JAVA工程师面试必看.pdf

    本文将深入探讨Java中类的初始化顺序,特别是在有继承关系的情况下,各个初始化阶段的执行流程,以及这一顺序的重要性。 首先,让我们明确类初始化的含义。在Java中,类的初始化主要涉及以下三个阶段: 1. 静态...

    java对象的初始化顺序[参考].pdf

    Java对象的初始化顺序是一个关键的编程概念,它涉及到类加载、静态初始化、实例初始化等多个步骤。下面我们将详细探讨这些步骤。 首先,当程序运行并创建一个新的对象时,JVM(Java虚拟机)会按照特定的顺序来初始...

    “礼让”原则学习Java对象初始化顺序.pdf

    事实上,Java 对象初始化顺序并不难理解,只需要掌握“礼让”原则和 JVM 的执行过程。 一、Java 对象初始化顺序的重要性 Java 对象初始化顺序是指在 Java 程序中,对象的初始化过程中发生的顺序问题。这是 Java ...

    JVM:类加载器子系统.pdf

    在程序中,最常用的类加载器主要有三个,分别是系统类加载器(System ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader),它们之间是包含关系,不是继承关系。...

    Java中对象初始化顺序的详细介绍

    这种情况下,初始化顺序如下:首先,`Lower`类的实例创建并分配默认值,然后调用`Upper`的构造器,接着执行`Initializer.initialize()`,该方法根据对象的实际类型(`Lower`)来设置字段的值。 5. **默认值**:即使...

    Java类加载初始化的过程及顺序

    Java类加载初始化的过程是Java编程语言中一个重要的概念,它决定了Java类的加载和初始化顺序。在Java中,类的加载是通过类加载器(ClassLoader)来实现的。类加载器会将类的字节码文件(.class)加载到Java虚拟机...

    Java类的完整构造执行顺序

    2006年11月16日的更新提到了一种特殊情况,即在静态初始化块中直接或间接地创建了当前类的实例,这可能会导致初始化顺序的混乱。为了避免此类问题,应避免在静态初始化块中直接实例化当前类,而是将其延迟到类完全...

    Java类初始化顺序

    以下详细解释了Java类的初始化顺序: 1. **初始化父类**: 在子类的构造方法执行之前,首先会调用父类的构造方法。如果子类没有明确指定父类构造方法,Java会自动调用父类的无参构造器。对于没有继承的类,其父类...

    详解Java的初始化与清理

    本文将深入探讨Java中的初始化过程,包括对象的构造器初始化以及类成员的初始化顺序,同时也会涉及Java的垃圾回收机制,即内存的自动清理。 首先,让我们详细了解一下Java中的构造器初始化。构造器是Java中用于初始...

    图文详解Java中class的初始化顺序

    总结,Java中类的初始化顺序遵循以下规则: 1. 先加载父类,再加载子类。 2. 加载完成后,先初始化静态部分(静态初始化块和静态变量),再初始化实例部分(实例初始化块和构造函数)。 3. 静态部分按照源代码中的...

    Java static静态初始快

    在Java编程语言中,`static`关键字扮演着重要的角色,特别是在初始化类的静态成员时。`static`静态初始化块是用来初始化类级别的变量,也就是说,这些变量是属于类本身,而不是类的实例。当我们讨论“Java static...

    java之jvm学习笔记五(实践写自己的类装载器)

    在深入理解JVM的过程中,编写自己的类装载器(ClassLoader)是一个非常实用且有趣的实践。这个“java之jvm学习笔记五(实践写自己的类装载器)”很可能是对这一主题的详细探讨。 类装载器在Java中的主要职责是动态...

    Java中的静态块初始化块及main方法.doc

    Java编程语言中,静态块(static block)和初始化块(instance initialization block)是两种特殊的代码块,它们在程序运行的不同阶段被执行,对于类和对象的初始化有着重要作用。同时,`main`方法是Java程序的入口...

    简单了解java中静态初始化块的执行顺序

    下面是一个简单的示例代码,用于演示Java中静态初始化块的执行顺序: ```java public class Test1 { static { System.out.println("执行静态初始化块test1..."); } { System.out.println("执行初始化块test1");...

Global site tag (gtag.js) - Google Analytics