1.面试题:static与继承的结合

“根据static代码块与继承结合的代码,输出打印结果”,属于各类面试、认证中比较高频的题目:

image-20210303230717801

大多数解析文章这样解读:

这道题考察了Java中父类先行的语法特性,即:

(1)子类的static代码块执行前会执行父类的static代码块

(2)子类的构造函数执行前会执行父类的构造函数

但,笔者想结合之前探讨过的系列文章(字节码解读、类加载子系统),更深入地探索一下这道题背后"看山不是山"的别样含义。

image-20210303232220180

2.关联:static与JVM的关联

想看透这道题,就要先透彻的理解static与JVM的关联。

我们知道,JVM的类加载子系统分为如下环节:

image-20210303232545951

在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字段,在如下两个环节都有可能赋值:

  • 连接之准备
  • 初始化

就是因为"可能"这两个字,就会分离出多种不同的场景。

也正因为对这多种不同场景的理解不足,进而导致"程序运行结果与程序猿的预期不一致"的诸多现网惨案。

image-20210303234350403

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方法的技术点吗?记下来好了!

然而,你以为就这么结束了吗。。。

image-20210303235720279

4.特殊场景:那些不会生成clinit的场景

那么,是不是所有static字段都会生成并执行clinit呢?

JVM规范描述问题的思维通常是正向的,但换成逆向的思维,就会发现很多隐藏的机关。

比如这个static,在某些场景下,就不会生成clinit方法(clinit都不会生成,当然更不会执行)。

  • 场景1:最容易想到的场景是非static的字段,显然不会生成clinit。

我们可以尝试下面这段代码(非static字段),通过查看字节码,可以证明我们的猜想是正确的:

image-20210304002309247

  • 场景2:static final字段,看起来是"常量”,应该可以在连接之准备阶段完成吧,所以不会生成clinit吧?

我们尝试下述代码(一个int类型的常量),查看字节码,似乎这个猜想是正确的:

image-20210304002433572

如果不是基本类型int,而是它的包装类型呢,查看字节码,似乎发现这个猜想不太完备:

image-20210304002521797

那我们能否修正一下对场景2的猜想:

static final修饰的基本类型,不会生成clinit

我们为基本类型int赋值时使用Integer.valueOf,查看字节码发现,依然生成了clinit:

image-20210304002613980

看来我们的场景2猜想又要完善一下了:

static final修饰的基本类型,并且不使用会通过构造器产生新对象的方式赋值,不会生成clinit

我们继续破解:如果我们用的是static final String呢?查看字节码发现,竟然也不会产生clinit:

image-20210304002643182

但为字符串赋值是采用的String.valueOf,查看字节码发现,又会产生clinit:

image-20210304002709459

我们最终完善了场景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执行顺序问题。

image-20210304003623960