类的初始化以及静态变量优化
前言
JVM 规范中写道:在加载类的时候,分为如下几个大类:
- Creation and Loading
- Linking
- Initialization
每一步在 JVM 中都规定了具体的几个小节,但是今天本文的重点在于链接阶段,JVM 对一些静态变量做的一些优化,因此对于这里面的每一步具体是做什么的不展开讨论了。
类的初始化
首先上一段代码:
1 | public class Test1 { |
以上代码的运行结果你知道吗,如果知道的话那么就可以不用往下看了,如果不知道的话可能就涉及到你的知识盲区了。
上面的代码运行结果如下:
1 | Test1 |
是不是觉得很奇怪,为什么 Test2 的静态代码块没有执行,按照 JVM 的规范,一个类在加载的过程中,会对变量、代码块进行一些初始化赋值,那么 Test2 的代码块为什么不会被执行呢?
那么不妨将 Test2 进行调整,将 test2 的类型由 String 改成 Integer:
1 | public class TestInteger1 { |
而此时的打印结果如下:
1 | TestInteger1 |
看到这里是不是觉得奇怪,为什么一个是 String,一个是 Integer 就会导致两种结果。
静态变量
被 static 关键字修饰的变量称之为静态变量,通常一个类调用另一类的静态变量在 JVM 中是通过 getstatic
指令来实现的,而 getstatic
又是一个会触发类初始化的指令,参照 JVM规范5.5小节
明确提到了 getstatic 会使一个类进行初始化,而调用一个静态变量的字节码指令就是 getstatic,那么不妨反编译 Test1 这个类看下:
反编译出来的竟然是 ldc,ldc 表示的是从常量池中加载值,那么自然就不会引起 Test2 进行初始化了,所以就不会打印出 Test2 了,那么 TestInteger1 类又是做了什么操作呢?
看 TestInteger1 编译成的字节码发现,在调用 TestInteger2 类的时候,使用的是 getstatic
,因为此时系统内 TestInteger2 还没有被初始化,所以此时 TestInteger2 就会首次初始化初始化,因此就会导致 TestInteger2 被打印出来。
不使用 final
既然使用 final 会导致两种结果,那么通过控制变量法,将 Test2 和 TestInteger2 都进行修改,将修饰变量的 final 去掉,代码就不在这里贴出来了,直接说结果:
Test2 和 TestInteger2 都会被打印出来
那么这个说明这个优化跟 final 以及 String 有关?
final
final 在 Java 中有如下两个作用:
- 修饰的基本变量值不可变
- 修饰的对象其引用不可变
同样是由 final 修饰的 String 和 Integer,会产生如此大的差异呢?
常量池
在 Java 中,String a = “a”,会产生一个对象,但是这个对象不是在堆上,而是直接在常量池中分配的,而 jvm 加载一个常量池中的对象就是直接采用 ldc。
而 TestInteger2 这个类的 testInteger2 是一个 Integer 类型,所以是直接在堆中进行对象的分配,从而导致加载其的命令必须是 getstatic
。
这时候可能会有小伙伴有疑问了,既然加不加 final 都可以进常量池,那么为什么还需要 final 修饰?
这是因为 final 可以保证其引用不可变,如果不加 final,此时另外一个对象将 Test2 的 test2 修改为 Test3,如果 Test1 此时还用 ldc 去常量池中加载,那么肯定是会得到一个错误的结果,因此 JVM 放弃了对这种优化。
如果 String 是由 final 修饰的,那么 JVM 判断这个变量已经没有地方可以修改其引用了,因此直接在编译期间直接该引用替换为常量,具体如下:
Integer 的包装类
既然 Integer 是 int 的包装类,而基本类型也是在常量池中进行分配的,是不是只要将 Integr 修改为 int,就可以避免 TestInteger2 的初始化呢?答案是确实可以的,具体就不上代码了。
ldc
在这里顺带说一下 ldc,ldc 代表的是从常量池中加载数据,如果 Test2.test2 不是通过 String test2 = "static final test2"
这种方式赋值的话,同样也是会引起 Test2 的初始化的。
1 | public static final String test2 = new String("static final test2"); |
这种方式由于 test2 指向的并不是常量池,所以并不会采用 ldc 加载,而是通过 getstatic,所以这种情况下 Test2 依然被打印出来了。
最后
其实这些都是 JVM 的小细节,在平时的业务代码中如果遇到这种静态变量,不妨多用基本类型以及final String 来进行优化下,说不定会对系统有那么丢丢的提升。