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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
输出结果:
静态成员sam初始化
static块执行
静态块内初始化sam成员变量
第1个主类对象:
普通成员sam1初始化
普通成员sam2初始化
InitialOrderWithoutExtend默认构造函数被调用
第2个主类对象:
普通成员sam1初始化
普通成员sam2初始化
InitialOrderWithoutExtend默认构造函数被调用
2个主类对象的静态对象:
第1个主类对象, 静态成员sam.s: 静态块内初始化sam成员变量
第2个主类对象, 静态成员sam.s: 静态块内初始化sam成员变量
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
由输出结果可知,执行顺序为:
- static静态代码块和静态成员
- 普通成员
- 构造函数执行
当具有多个静态成员和静态代码块或者多个普通成员时,初始化顺序和成员在程序中申明的顺序一致。
注意到在该程序的静态代码块中,修改了静态成员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默认构造函数被调用");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
运行结果:
父类 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
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
由输出结果可知,执行的顺序为:
- 父类静态代码区和父类静态成员
- 子类静态代码区和子类静态成员
- 父类非静态代码区和普通成员
- 父类构造函数
- 子类非静态代码区和普通成员
- 子类构造函数
与非继承关系中的初始化顺序一致的地方在于,静态代码区和父类静态成员、非静态代码区和普通成员是同一级别的,当存在多个这样的代码块或者成员时,初始化的顺序和它们在程序中申明的顺序一致;此外,静态代码区和静态成员也是仅仅初始化一次,但是在初始化过程中,可以修改静态成员的引用。
初始化顺序图示
非继承关系
继承关系
类初始化顺序的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,启用),那么子类中较窄的变量也可能会插入到父类变量的空隙中。
@[TOC](这里写自定义目录标题)欢迎使用Markdown编辑器
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
新的改变
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
- 全新的界面设计 ,将会带来全新的写作体验;
- 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
- 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
- 全新的 KaTeX数学公式 语法;
- 增加了支持甘特图的mermaid语法1 功能;
- 增加了 多屏幕编辑 Markdown文章功能;
- 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
- 增加了 检查列表 功能。
功能快捷键
撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
合理的创建标题,有助于目录的生成
直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC
语法后生成一个完美的目录。
如何改变文本的样式
强调文本 强调文本
加粗文本 加粗文本
标记文本
删除文本
引用文本
H2O is是液体。
210 运算结果是 1024.
插入链接与图片
链接: link.
图片:
带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片
.
// An highlighted block
var foo = 'bar';
生成一个适合你的列表
- 项目
- 项目
- 项目
- 项目
- 项目1
- 项目2
- 项目3
- 计划任务
- 完成任务
创建一个表格
一个简单的表格是这么创建的:
项目 | Value |
---|---|
电脑 | $1600 |
手机 | $12 |
导管 | $1 |
设定内容居中、居左、居右
使用:---------:
居中
使用:----------
居左
使用----------:
居右
第一列 | 第二列 | 第三列 |
---|---|---|
第一列文本居中 | 第二列文本居右 | 第三列文本居左 |
SmartyPants
SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:
TYPE | ASCII | HTML |
---|---|---|
Single backticks | 'Isn't this fun?' |
‘Isn’t this fun?’ |
Quotes | "Isn't this fun?" |
“Isn’t this fun?” |
Dashes | -- is en-dash, --- is em-dash |
– is en-dash, — is em-dash |
创建一个自定义列表
- Markdown
- Text-to- HTML conversion tool
- Authors
- John
- Luke
如何创建一个注脚
一个具有注脚的文本。2
注释也是必不可少的
Markdown将文本转换为 HTML。
KaTeX数学公式
您可以使用渲染LaTeX数学表达式 KaTeX:
Gamma公式展示 是通过欧拉积分
你可以找到更多关于的信息 LaTeX 数学表达式here.
新的甘特图功能,丰富你的文章
- 关于 甘特图 语法,参考 这儿,
UML 图表
可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图::
这将产生一个流程图。:
- 关于 Mermaid 语法,参考 这儿,
FLowchart流程图
我们依旧会支持flowchart的流程图:
- 关于 Flowchart流程图 语法,参考 这儿.
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件或者.html文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。
注脚的解释 ↩︎
Java类初始化的顺序经常让人犯迷糊,现在本文尝试着从JVM的角度,对Java非继承和继承关系中类的初始化顺序进行试验,尝试给出JVM角度的解释。