类的初始化以及静态变量优化

前言

JVM 规范中写道:在加载类的时候,分为如下几个大类:

  • Creation and Loading
  • Linking
  • Initialization

每一步在 JVM 中都规定了具体的几个小节,但是今天本文的重点在于链接阶段,JVM 对一些静态变量做的一些优化,因此对于这里面的每一步具体是做什么的不展开讨论了。

类的初始化

首先上一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test1 {

static {
System.out.println("Test1");
}

public static void main(String[] args) {
System.out.println(Test2.test2);
}
}

public class Test2 {
public static final String test2 = "static final test2";

static {
System.out.println("Test2");
}
}

以上代码的运行结果你知道吗,如果知道的话那么就可以不用往下看了,如果不知道的话可能就涉及到你的知识盲区了。
上面的代码运行结果如下:

1
2
Test1
static final test2

是不是觉得很奇怪,为什么 Test2 的静态代码块没有执行,按照 JVM 的规范,一个类在加载的过程中,会对变量、代码块进行一些初始化赋值,那么 Test2 的代码块为什么不会被执行呢?
那么不妨将 Test2 进行调整,将 test2 的类型由 String 改成 Integer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestInteger1 {

static {
System.out.println("TestInteger1");
}

public static void main(String[] args) {
System.out.println(TestInteger2.testInteger2);
}
}

public class TestInteger2 {
public static final Integer testInteger2 = 1;

static {
System.out.println("TestInteger2");
}
}

而此时的打印结果如下:

1
2
3
TestInteger1
TestInteger2
1

看到这里是不是觉得奇怪,为什么一个是 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 中有如下两个作用:

  1. 修饰的基本变量值不可变
  2. 修饰的对象其引用不可变

同样是由 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 来进行优化下,说不定会对系统有那么丢丢的提升。

作者

Somersames

发布于

2021-08-01

更新于

2021-12-05

许可协议

评论