1 - 引言
在浏览『深刻了解Java虚拟机(第3版)』时看到外面的一道代码题目,书中给出了题目的解答。本人对于这个题目拓展的想了几个变式,后果有所差别,为了寻找产生差别的起因又深刻理解了一番。
2 - 类初始化机会
2.1 - 原题
在『深刻了解Java虚拟机(第3版)』的7.2章节 "类加载的机会",其代码清单7-1有这么一段代码:
public class SuperClass { static { System.out.println("SuperClass init!"); } public static int VALUE = 1;}public class SubClass extends SuperClass { static { System.out.println("SubClass init!"); }}public class Main { public static void main(String[] args) { System.out.println(SubClass.VALUE); }}
输入的后果是:
书中给出了这个后果的解答:
上述代码运行之后,只会输入“SuperClass init!”,而不会输入“SubClass init!”。对于动态字段, 只有间接定义这个字段的类才会被初始化,因而通过其子类来援用父类中定义的动态字段,只会触发父类的初始化而不会触发子类的初始化。1
所以 main()
办法里调用 SubClass.VALUE
时实际上调用了 SuperClass.VALUE
。而 SuperClass
之前还未被加载过,就触发了加载的过程, 在初始化的时候调用了 SuperClass
里的 static
动态代码块。
2.2 - 变式一
这里把下面代码稍作批改。
public class SuperClass { static { System.out.println("SuperClass init"); } // public static int VALUE = 1; public final static int VALUE = 1; // 增加一个 final 润饰}
在其余代码不变的状况下,把 SuperClass.VALUE
减少一个 final
修饰符,这时候输入后果是:
和原来的后果不同,"SuperClass init!"和"SubClass init!"都没有输入进去。
对于这个后果,我一开始猜想的是因为 VALUE
字段被 final
润饰,且又是根本数据类型,所以JVM做了一些优化,不通过 SuperClass.VALUE
而是间接援用这个字段的值。
起初看了一下IDEA反编译 Main.class
的源码:
// Main.classpublic class Main { public Main() { } public static void main(String[] args) { System.out.println(1); }}
Main
类在编译的时候间接把 SubClass.VALUE
优化成了值"1"。这和一开始的猜想还是有些出入,Main
类不是被JVM在运行时优化的,而是在编译器就间接被优化了。
对于这种状况编译器是根据什么原理优化的,在前面在深刻开展,先持续看下一种变式。
2.3 - 变式二
public class SuperClass { static { System.out.println("SuperClass init!"); } // public static int VALUE = 1; // public final static int VALUE = 1; public final static Integer VALUE = 1; // 把VALUE改成Integer包装类}
这次把之前 int
类型的 VALUE
改成 包装类Integer
,看一下运行的后果。
这次的后果又输入了"SuperClass init!"。的确,包装类其实就是一种被 final
润饰的一般类,不能像根本数据类型那样被编译器优化,所以就要调用 SubClass.VALUE
而初始化 SuperClass
。
2.4 - 变式三
public class SuperClass { static { System.out.println("SuperClass init!"); } // public static int VALUE = 1; // public final static int VALUE = 1; // public final static Integer VALUE = 1; public final static String VALUE = "1"; // 把VALUE改成String}
这次把 SubClass.VALUE
从 Integer
改成 String
,看一下运行的后果:
当初的后果和后面变式一的后果一样了,这让我有点纳闷的。String
和 Integer
不都是包装类吗,为什么能够和根本数据类型一样不会触发 SuperClass
的初始化,难道 String
有什么非凡解决吗?
我还是先去看了一下IDEA反编译的 Main.class
的源码:
// Main.classpublic class Main { public Main() { } public static void main(String[] args) { System.out.println("1"); }}
的确和变式一的状况一样,编译器间接在编译阶段就把 String
类型的 VALUE
值间接优化了。
3 - 编译器优化技术---条件常量流传
对于上文中变式一和变式三的代码运行后果,只输入了 VALUE
的值而没有输入"SuperClass init!",首要起因就是编译器优化技术。
编译器的指标尽管是做由程序代码翻译为本地机器码的工作,但其实难点并不在于能不能胜利翻译出机器码,输入代码优化品质的高下才是决定编译器优良与否的要害。
OpenJDK的官网Wiki上,HotSpot虚拟机设计团队列出了一个绝对比拟全面的、即时编译器中采纳的优化技术列表。地址:https://wiki.openjdk.java.net...。 1
官网列出了很多的编译器优化技术,其中 条件常量流传(conditional constant propagation) 就是造成上文变式一和变式三输入后果的起因。
常量流传是古代的编译器中应用最宽泛的优化办法之一,它通常利用于高级两头示意(IR)。它解决了在运行时动态检测表达式是否总是求值为惟一常数的问题,如果在调用过程时晓得哪些变量将具备常量值,以及这些值将是什么,则编译器能够在编译期间简化常数。2
3.1 - 优化常量
简略来说就是编译器会通过肯定的算法发现代码中存在的常量,而后间接替换指向它的变量值。例如:
public class Main { public static final int a = 1; // 全局动态常量 public static void main(String[] args) { final int b = 2; // 部分常量 System.out.println(a); System.out.println(2); }}
编译器编译之后:
// Main.classpublic class Main { public static final String A = "1"; public static void main(String[] args) { String b = "2"; System.out.println("1"); System.out.println("2"); }}
3.2 - 优化常量表达式
甚至一些常量的表达式,也能够事后间接把后果编译进去:
public class Main { public static void main(String[] args) { final int a = 3 * 4 + 5 - 6; int b = 10; if (a > b) { System.out.println(a); } }}
编译之后:
// Main.classpublic class Main { public static void main(String[] args) { int a = true; int b = 10; if (11 > b) { System.out.println(11); } }}
3.3 - 优化字符串拼接
还能够编译字符串的拼接,网上常常有一些题目问生成了多少个 String
对象,在JVM虚拟机的层面一顿剖析,其实都不正确,编译器间接在编译的时候就优化掉了,基本到不了运行时的内存池。
public class Main { public static void main(String[] args) { final String str = "hel" + "lo"; System.out.println(str); System.out.println("hello" == str); }}
编译后的源码,看到 str
间接被替换成了"hello"字符串,且 "hello" == str
为true,所以全程就一个String对象生成。
// Main.classpublic class Main { public static void main(String[] args) { String str = "hello"; System.out.println("hello"); System.out.println(true); }}
小拓展: 很多中央都说多个字符串拼接不能用"+"间接拼接,要用StringBuilder之类的。实际上,即应用"+"也会被编译器优化成StringBuilder的,有趣味能够本人尝试一下。
3.4 - 编译器条件常量流传带来的危险
尽管编译器优化代码能够晋升运行时的效率,然而也会带来肯定的危险
3.4.1 - 常量反射生效
尽管一些被 final
润饰的字段编译器会认定其为常量而进行优化,然而Java有反射机制,通过一些奇淫技巧能够更改这些值。然而因为被编译器优化了,可能导致被批改的值不能像预期那样失效。如:
public class Main { public static final String VALUE = "A"; public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { Class<Main> mainClass = Main.class; Field value = mainClass.getField("VALUE"); value.setAccessible(true); // 去除A的final修饰符 Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(value, value.getModifiers() & ~Modifier.FINAL); value.set(null, "B"); System.out.println(VALUE); // 理论还是输入 "A" }}
这段代码尽管一通操作把 VALUE
的值从"A"改成"B"了,然而编译器在编译的时候早就把 System.out.println(VALUE);
替换成 System.out.println("A");
,最初运行后果会和预期不同。
3.4.2 - 局部编译
如果常量和其援用的对象不在一个文件中,当批改常量之后只从新编译常量所在文件,那么未从新编译的文件就会应用旧值。如:
// Constant.javapublic class Constant { public final static String VALUE = "A";}// Main.javapublic class Main { public static void main(String[] args) { System.out.println(Constant.VALUE); }}
如果把 Constant.VALUE
的值批改成"B"而后通过 javac Constant.java
独自编译Constant.java文件,然而 Main
外面输入的值依旧会是"A"。
4 - 常量、动态常量池、动静常量池
4.1 - 常量
常量是指在程序的整个运行过程中值放弃不变的量。在Java开发的时候通常指的是被 final
润饰的变量。但从虚拟机的角度看"常量"的定义会有所不同。
在虚拟机中,常量会被寄存于常量池中,而常量池中会寄存两大类常量: 字面量(Literal)和符号援用(Symbolic
References)。字面量比拟靠近于Java语言层面的常量概念,如文本字符串、被申明为final的常量值等。1
而符号援用则属于编译原理方面的概念,次要蕴含类、字段、办法信息等,这里就不开展形容了。
4.2 - 动态常量池
(动态)常量池能够比喻为Class文件里的资源仓库,它是Class文件构造中与其余我的项目关联最多的数据,通常也是占用Class文件空间最大的数据我的项目之一,另外,它还是在Class文件中第一个呈现的表类型数据我的项目。
(动态)常量池外面存储的数据我的项目类型如下表:
1
动态常量池编译之后就写定在class文件里了,能够间接查看字节码来察看其组成构造,如以下代码:
public class Main { final static String A = "A"; final static int B = 1; final static Integer C = 2; public static void main(String[] args) { System.out.println(A); System.out.println(B); System.out.println(C); }}
编译之后通过 javap -verbose Main.class
命令查看反编译之后的字节码:
能够发现代码中的 String
和 int
型数据被存储在动态常量池中,Integer
就没有。因为前者对应常量池中的"CONSTANT\_String\_info"和"CONSTANT\_Integer\_info"
类型,而后者相当于一般的对象,只被存储了对象信息。
这就解释了上文中变式一、变式三与变式二后果不同的起因。
4.3 - 动静常量池
运行时常量池(动静常量池)绝对于Class文件常量池(动态常量池)的另外一个重要特色是具备动态性,Java语言并不要求常量肯定只有编译期能力产生,也就是说,并非预置入Class文件中常量池的内容能力进入办法区运行时常量池,运行期间也能够将新的常量放入池中,这种个性被开发人员利用得比拟多的便是String类的intern()办法。
5 - 结语
《深刻了解Java虚拟机》的确是一本很好的书,重复读过几次,每次都有新的播种,这次更是因为本人在打源码的时候的"不小心失误"有了意料之外的播种。
参考
- [1] 深刻了解Java虚拟机(第3版)
- [2] 编译优化之 - 常量流传入门
- [3] Java 中容易混同的概念:Java 8 中的常量池、字符串池、包装类对象池
- [4] 编译期常量与运行时常量
- -
原文地址:《深刻了解Java虚拟机》中一题引发的思考