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,启用),那么子类中较窄的变量也可能会插入到父类变量的空隙中。

Java类继承关系中的初始化顺序的更多相关文章

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

    尝试着仔细阅读thinking in java 看到一篇很好的文章http://blog.csdn.net/u011080472/article/details/51330114

  2. java类的加载以及初始化顺序

    类的加载和初始化的了解对于我们对编程的理解有很大帮助,最近在看类的记载方面的问题.从网上查阅了若干文章,现总结如下: 我们通过一段代码来了解类加载和初始化的顺序: package com.classl ...

  3. Programming In Scala笔记-第十一章、Scala中的类继承关系

    本章主要从整体层面了解Scala中的类层级关系. 一.Scala的类层级 在Java中Object类是所有类的最终父类,其他所有类都直接或间接的继承了Object类.在Scala中所有类的最终父类为A ...

  4. [java] java中的初始化顺序

    先看程序: package init_cls; class A{ {System.out.println("i am in the class A!");} static { Sy ...

  5. Java中的初始化顺序(静态成员、静态初始化块,普通成员、普通初始化块、构造函数)

    本文链接    http://blog.csdn.net/xiaodongrush/article/details/29170099 參考文章     http://my.oschina.net/le ...

  6. Java:验证在类继承过程中equals()、 hashcode()、toString()方法的使用

    以下通过实际例子对类创建过程汇中常用的equals().hashcode().toString()方法进行展示,三个方法的创建过程具有通用性,在项目中可直接改写. //通过超类Employee和其子类 ...

  7. Java语法专题2: 类变量的初始化顺序

    合集目录 Java语法专题2: 类变量的初始化顺序 问题 这也是Java面试中出镜率很高的基础概念问题 描述一下多级继承中字段初始化顺序 描述一下多级继承中类变量初始化顺序 写出运行以下代码时的控制台 ...

  8. Java类的加载 链接 初始化

    原文地址 Java类的加载.链接和初始化.Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是java.lang.Class类的对象.一个Java类从字节代码到能够 ...

  9. JavaSE复习日记 : 继承关系和super关键字以及继承关系中方法的覆写

    /* * 类的继承和super关键字 * * 软件开发的三大目的: * 可拓展性; * 可维护性; * 可重用性; * * 这里单说下可重用性这一项: * 为了代码复用,复用方式有: * 函数的调用复 ...

随机推荐

  1. Windows 下配置 php_imagick 扩展

    1.首先按装 imageimagick 可以去 http://imagemagick.org/script/binary-releases.php#windows 这里下载,看好自己的系统环境和选择好 ...

  2. git之merge和rebase的区别

    merge合并 # merge操作 第一步: # 先创建一个目录,在主分支提交3个txt文件 [root@luchuangao]# mkdir oldboy [root@luchuangao]# gi ...

  3. MapReduce分析流量汇总

    一.MapReduce编程规范 一.MapReduce编程规范 用户编写mr程序主要分为三个部分:Mapper,Reducer,Driver 1.Mapper阶段 (1)用户自定义Mapper类 要继 ...

  4. lvs、haproxy、nginx 负载均衡的比较分析(转)

    原文:http://blog.csdn.net/gzh0222/article/details/8540604 对软件实现负载均衡的几个软件,小D详细看了一下,从性能和稳定上还是LVS最牛,基本达到了 ...

  5. 7.Git工作区和暂存区

    Git和其他版本控制系统如SVN的一个不同之处就是有暂存区的概念. 先来看名词解释. 1.工作区(Working Directory) 就是你在电脑里能看到的目录,比如我的test文件夹就是一个工作区 ...

  6. Object-Detection中常用的概念解析

    常用的Region Proposal Selective Search Edge Boxes Softmax-loss softmax-loss层和softmax层计算大致是相同的,softmax是一 ...

  7. (0.2.6)Mysql安装——编译安装

    (0.2.6)Mysql安装——编译安装 待完善

  8. 安卓android的联系人的contacts, raw contacts, and data的区别

    https://stackoverflow.com/questions/5151885/android-new-data-record-is-added-to-the-wrong-contact/51 ...

  9. 深入理解Oracle调试事件:10046事件详解

    10046事件是SQL_TRACE的扩展,被戏称为"吃了兴奋剂的SQL_TRACE"       有效的追踪级别:              ① 0级:SQL_TRACE=FASL ...

  10. (转)VC串口小程序(用SerialPort类)

    ××××××××××××××××××××××××××××××××××××××××××××××××××××× 在MFC里面实现串口通讯有很多方式: 方案一:使用微软公司提供的 串口类,SerialPor ...