最近工作中需要处理到常量定义文件,发现有时会load class,有时又不会,探索下JAVA编译时对静态变量的处理。
什么叫编译
简单讲就是把人类发明的编程语言转化成机器理解的语言。计算机专业的应该都学过《编译原理》,当然不会一步到01二进制,肯定又是复杂的多层架构,最终翻译成CPU可以执行的指令。
下图是JAVA运行过程,.java编译成.class,既JRE可以理解的语言,JRE再翻译为OS可以执行的指令。JAVA是为跨平台而生的,所以多层架构自然增加JRE层.
JAVA静态编译
1.编译代码示例
package com.wesleyshaw.staticcompile;
class StaticA {
public static final String varA = "StaticA.varA";
}
package com.wesleyshaw.staticcompile;
class RefA {
public RefA() {
String strA = StaticA.varA;
System.out.println(strA);
}
}
2.使用javap -verbose *.class查看
Last modified 2019-4-14; size 424 bytes
MD5 checksum 9ecc5450edc65bb15a9aa6360e06cf50
Compiled from "RefA.java"
class com.wesleyshaw.staticcompile.RefA
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #7.#14 // java/lang/Object."<init>":()V
#2 = Class #15 // com/wesleyshaw/staticcompile/StaticA
#3 = String #16 // StaticA.varA
#4 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Class #21 // com/wesleyshaw/staticcompile/RefA
#7 = Class #22 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 SourceFile
#13 = Utf8 RefA.java
#14 = NameAndType #8:#9 // "<init>":()V
#15 = Utf8 com/wesleyshaw/staticcompile/StaticA
#16 = Utf8 StaticA.varA
#17 = Class #23 // java/lang/System
#18 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 com/wesleyshaw/staticcompile/RefA
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public com.wesleyshaw.staticcompile.RefA();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: ldc #3 // String StaticA.varA
6: astore_1
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: aload_1
11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: return
LineNumberTable:
line 4: 0
line 5: 4
line 6: 7
line 7: 14
}
SourceFile: "RefA.java"
执行序列4 ldc入栈#3,#3在Constant pool引用#16,#16对应UTF-8的字符串,过程中StaticA.java被“架空”了,某些情况下删除StaticA.java也不影响。
那么为什么呢?
人类语言不可避免的会引入一些“浪费”,比如很多公司编程规范会禁止重复定义常量,避免出现变量修改不完整不一致的情况。对机器来说,常量初始话就不会修改了,再增加一次“引用”到定义常量的类,明显会花更多的指令,简单的办法就是在自己函数空间中直接定义一个变量,逻辑和上面class文件对应(那会造成内存浪费吧?是的,针对这种情况,runtime时内存会有复用机制,这是另外的话题,不在这里探讨啦。)。
另外:JAVA是为网络而生的,从A发送到B的网络流需要可以直接运行。网络最大的问题就是等待,基于这两个原因,也必须减少class大小,防止过多的引用。
JAVA引用编译
上面这种处理有没有问题?假设有两个团队,A团队提供StaticA.java编译后jar,B团队按上面方法使用,功能release后,A团队为解决问题Patch修改了变量,而B团队又没有重新编译,大家使用的常量就“不常”了。
能不能引用常量?
1.编译代码示例
package com.wesleyshaw.staticcompile;
class StaticA {
public static final String varA1 = "StaticA.varA";
public static final String varA2 = "StaticA.varA".substring(0);
}
package com.wesleyshaw.staticcompile;
class RefA {
private RefA() {
String strA1 = StaticA.varA1;
System.out.println(strA1);
String strA2 = StaticA.varA2;
System.out.println(strA2);
}
public static void main(String[] args){
// empty
new RefA();
}
}
2.使用javap -verbose *.class查看
{
public com.wesleyshaw.staticcompile.RefA();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: ldc #3 // String StaticA.varA
6: astore_1
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: aload_1
11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: getstatic #6 // Field com/wesleyshaw/staticcompile/StaticA.varA2:Ljava/lang/String;
17: astore_2
18: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
21: aload_2
22: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
LineNumberTable:
line 4: 0
line 5: 4
line 6: 7
line 8: 14
line 9: 18
line 10: 25
}
编译器被“欺骗”了,我们成功引用了StaticA.java中的常量。
3.性能劣化
大家嫌JAVA慢,主要在JUST-IN-TIME编译阶段,而上面的方式会引发另外的问题。
还拿上面的团队举例,假如团队B只是使用了团队A提供的几个常量,而StaticA.java却包含所有CDEF…团队注册的几千的常量。
java -verbose:class com.wesleyshaw.staticcompile.RefA | grep wesleyshaw
[Loaded com.wesleyshaw.staticcompile.RefA from file:/mnt/D696EBF896EBD751/source/github/JAVA/StaticCompile/]
[Loaded com.wesleyshaw.staticcompile.StaticA from file:/mnt/D696EBF896EBD751/source/github/JAVA/StaticCompile/]
运行时会load整个文件,对B来说时间和内存消耗浪费了。
总结
个人感觉好的处理办法:
1.不要定义超级静态常量类,可以根据模块划分为不同的Inner static class,避免加载过多内存,消耗过多时间。
2.当不采用引用常量的方式时,需要在CI集成编译规划好编译依赖。