1.面试题:static与继承的结合
“根据static代码块与继承结合的代码,输出打印结果”,属于各类面试、认证中比较高频的题目:
大多数解析文章这样解读:
这道题考察了Java中父类先行的语法特性,即:
(1)子类的static代码块执行前会执行父类的static代码块
(2)子类的构造函数执行前会执行父类的构造函数
但,笔者想结合之前探讨过的系列文章(字节码解读、类加载子系统),更深入地探索一下这道题背后"看山不是山"的别样含义。
2.关联:static与JVM的关联
想看透这道题,就要先透彻的理解static与JVM的关联。
我们知道,JVM的类加载子系统分为如下环节:
在JVM规范中,这样定义"连接之准备"环节:
Preparation involves creating the static fields for a class or interface and initializing such fields to their default values (§2.3, §2.4). This does not require the execution of any Java Virtual Machine code; explicit initializers for static fields are executed as part of initialization (§5.5), not preparation.
就是说在这个阶段,JVM会对static字段赋值——赋值特指"默认值”。
在JVM规范中,描述"初始化"环节时,又提到了诸多"触发赋值"的可能性:
A class or interface C may be initialized only as a result of:
The execution of any one of the Java Virtual Machine instructions new, getstatic, putstatic, or invokestatic that references C (§new, §getstatic, §putstatic, §invokestatic). These instructions reference a class or interface directly or indirectly through either a field reference or a method reference.
……
根据JVM规范,说明对于static字段,在如下两个环节都有可能赋值:
- 连接之准备
- 初始化
就是因为"可能"这两个字,就会分离出多种不同的场景。
也正因为对这多种不同场景的理解不足,进而导致"程序运行结果与程序猿的预期不一致"的诸多现网惨案。
3.主要工作:生成并执行clinit方法
JVM在初始化环节,最主要的工作是执行clinit方法。
JVM规范中,这样描述clinit方法:
The name
<clinit>
is supplied by a compiler. Because the name<clinit>
is not a valid identifier, it cannot be used directly in a program written in the Java programming language. Class and interface initialization methods are invoked implicitly by the Java Virtual Machine; they are never invoked directly from any Java Virtual Machine instruction, but are invoked only indirectly as part of the class initialization process.
我们可以这样理解:
(1)clinit是Javac自动生成的:意味着程序猿无法自定义一个同名方法,更无法在程序中主动调用。
(2)clinit由"类的静态成员的赋值语句"和"static代码块"产生。
阅读到这里,您可能会想:以前我所知道的static,在JVM层上,不就多了个clinit方法的技术点吗?记下来好了!
然而,你以为就这么结束了吗。。。
4.特殊场景:那些不会生成clinit的场景
那么,是不是所有static字段都会生成并执行clinit呢?
JVM规范描述问题的思维通常是正向的,但换成逆向的思维,就会发现很多隐藏的机关。
比如这个static,在某些场景下,就不会生成clinit方法(clinit都不会生成,当然更不会执行)。
- 场景1:最容易想到的场景是非static的字段,显然不会生成clinit。
我们可以尝试下面这段代码(非static字段),通过查看字节码,可以证明我们的猜想是正确的:
- 场景2:static final字段,看起来是"常量”,应该可以在连接之准备阶段完成吧,所以不会生成clinit吧?
我们尝试下述代码(一个int类型的常量),查看字节码,似乎这个猜想是正确的:
如果不是基本类型int,而是它的包装类型呢,查看字节码,似乎发现这个猜想不太完备:
那我们能否修正一下对场景2的猜想:
static final修饰的基本类型,不会生成clinit
我们为基本类型int赋值时使用Integer.valueOf,查看字节码发现,依然生成了clinit:
看来我们的场景2猜想又要完善一下了:
static final修饰的基本类型,并且不使用会通过构造器产生新对象的方式赋值,不会生成clinit
我们继续破解:如果我们用的是static final String呢?查看字节码发现,竟然也不会产生clinit:
但为字符串赋值是采用的String.valueOf,查看字节码发现,又会产生clinit:
我们最终完善了场景2:
static final修饰的,基本类型或String类型,不会采用构造器方式赋值的场景,不会生成clinit
其实,跳出推断过程和结论,我们可以朴实地猜想一下JVM实现者的设计思路:
在连接阶段,并不会执行Java类中的任何代码,因此只要会产生对象的代码,都无法执行。
因此,JVM才会设计出一个"初始化"环节——生成clinit方法,为static字段赋值、运行static代码块。
如果static字段能够满足不产生对象的要求,JVM索性将这种字段在连接阶段就给处理掉。
5.总结
本文分析了Java实战中极为常用的关键词:static:
- static与JVM的关联:static修饰的字段、代码块与JVM的初始化环节、连接环节有密切联系
- JVM的初始化环节会执行javac自动生成clinit
- javac自动生成clinit的原则:被static修饰的字段和代码块都可能触发自动生成clinit。
- 不生成clinit的场景本质:被static final修饰、基础数据类型或String类型、赋值不涉及对象生成的场景,都不会触发自动生成clinit
理解了不生成clinit的场景,那么肯定不会与"开篇提到的static执行顺序"扯上关系;
下一步,我们会进一步探讨生成clinit的场景的static执行顺序问题。