JAVA常量static final编译探索

最近工作中需要处理到常量定义文件,发现有时会load class,有时又不会,探索下JAVA编译时对静态变量的处理。

什么叫编译

简单讲就是把人类发明的编程语言转化成机器理解的语言。计算机专业的应该都学过《编译原理》,当然不会一步到01二进制,肯定又是复杂的多层架构,最终翻译成CPU可以执行的指令。

下图是JAVA运行过程,.java编译成.class,既JRE可以理解的语言,JRE再翻译为OS可以执行的指令。JAVA是为跨平台而生的,所以多层架构自然增加JRE层.
JAVA运行过程

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集成编译规划好编译依赖。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值