本文收录在javaskill.cn中,内有完整的JAVA知识地图,欢迎访问
说到这几个关键字,大部分猿都能娓娓道来,说出很多它们的用法和定义
- final修饰的字段不可修改、方法不可重写、类不可继承,JVM对final有优化
- static修饰的字段所有对象共用、static{}内的代码在类加载时执行、JVM对static有优化
- volatile修饰的字段所有线程看到的值一致
问:JVM对final和static有什么优化?
我:???
问:为什么volatile各线程看到的值是一致的?
我:嘿!这个我知道,因为对volatile的写操作会直接更新到主内存(这里指堆或元空间等线程共享内存)中,不会使用TLAB(这个在字节码上看不出来,也不在本文的讨论范围中。TLAB:Thread Local Allocation Buffer,本地线程分配缓冲)
带着这些疑问,我们一起看一下Java的字节码文件,从字节码文件中,就能印证和解决上述的部分说法和问题
static关键字
准备一个测试类
public class TestStatic {
static double pi = 3.14;
static Double piO = 3.14;
static Object o1 = new Object();
Object o2 = new Object();
}
测试类很简单,四个字段都比较典型,在看字节码之前,先思考两个问题
- pi和piO在编译后有区别吗?
答:JAVA有自动拆装箱,编译后可能没有区别...吧? - o1和o2在编译后有区别吗?
答:o1是静态的,o2不是静态的(这不是废话么?)
可能大家都没有认真思考过这个问题,那么现在我们看一下TestStatic编译后的字节码文件,能否在其中找到答案
JDK提供了javap工具可以反编译字节码,使用如下命令可以方便的查看
javap -verbose F:\workspace\TestProject\out\production\TestProject\TestStatic.class
输出如下
Classfile /F:/workspace/TestProject/out/production/TestProject/TestStatic.class
Last modified 2018-4-28; size 567 bytes
MD5 checksum 73675b19fe69643d4cb69073bcbad34f
Compiled from "TestStatic.java"
public class TestStatic
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#28 // java/lang/Object."":()V
#2 = Class #29 // java/lang/Object
#3 = Fieldref #10.#30 // TestStatic.o2:Ljava/lang/Object;
#4 = Double 3.14d
#6 = Fieldref #10.#31 // TestStatic.pi:D
#7 = Methodref #32.#33 // java/lang/Double.valueOf:(D)Ljava/lang/Double;
#8 = Fieldref #10.#34 // TestStatic.piO:Ljava/lang/Double;
#9 = Fieldref #10.#35 // TestStatic.o1:Ljava/lang/Object;
#10 = Class #36 // TestStatic
#11 = Utf8 pi
#12 = Utf8 D
#13 = Utf8 piO
#14 = Utf8 Ljava/lang/Double;
#15 = Utf8 o1
#16 = Utf8 Ljava/lang/Object;
#17 = Utf8 o2
#18 = Utf8
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 LTestStatic;
#25 = Utf8
#26 = Utf8 SourceFile
#27 = Utf8 TestStatic.java
#28 = NameAndType #18:#19 // "":()V
#29 = Utf8 java/lang/Object
#30 = NameAndType #17:#16 // o2:Ljava/lang/Object;
#31 = NameAndType #11:#12 // pi:D
#32 = Class #37 // java/lang/Double
#33 = NameAndType #38:#39 // valueOf:(D)Ljava/lang/Double;
#34 = NameAndType #13:#14 // piO:Ljava/lang/Double;
#35 = NameAndType #15:#16 // o1:Ljava/lang/Object;
#36 = Utf8 TestStatic
#37 = Utf8 java/lang/Double
#38 = Utf8 valueOf
#39 = Utf8 (D)Ljava/lang/Double;
{
static double pi;
descriptor: D
flags: ACC_STATIC
static java.lang.Double piO;
descriptor: Ljava/lang/Double;
flags: ACC_STATIC
static java.lang.Object o1;
descriptor: Ljava/lang/Object;
flags: ACC_STATIC
java.lang.Object o2;
descriptor: Ljava/lang/Object;
flags:
public TestStatic();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."":()V
12: putfield #3 // Field o2:Ljava/lang/Object;
15: return
LineNumberTable:
line 1: 0
line 6: 4
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this LTestStatic;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: ldc2_w #4 // double 3.14d
3: putstatic #6 // Field pi:D
6: ldc2_w #4 // double 3.14d
9: invokestatic #7 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
12: putstatic #8 // Field piO:Ljava/lang/Double;
15: new #2 // class java/lang/Object
18: dup
19: invokespecial #1 // Method java/lang/Object."":()V
22: putstatic #9 // Field o1:Ljava/lang/Object;
25: return
LineNumberTable:
line 3: 0
line 4: 6
line 5: 15
}
SourceFile: "TestStatic.java"
javap给我们提供了贴心的注释,下面我们就一起大致的解析一下输出内容
Classfile /F:/workspace/TestProject/out/production/TestProject/TestStatic.class
Last modified 2018-4-28; size 530 bytes
MD5 checksum 9df14e179094d429736915c43214ae86
Compiled from "TestStatic.java"
这几行输出了class文件的目录、修改时间、md5、编译来源
public class TestStatic
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
接下来,描述了jdk版本,52表示JDK1.8,类访问标识,ACC_PUBLIC表示此类是的作用域是public,ACC_SUPER是超类方法的调用方式,JDK1.02后都会有
Constant pool:
#1 = Methodref #2.#28 // java/lang/Object."":()V
#2 = Class #29 // java/lang/Object
#3 = Fieldref #10.#30 // TestStatic.o2:Ljava/lang/Object;
#4 = Double 3.14d
#6 = Fieldref #10.#31 // TestStatic.pi:D
#7 = Methodref #32.#33 // java/lang/Double.valueOf:(D)Ljava/lang/Double;
#8 = Fieldref #10.#34 // TestStatic.piO:Ljava/lang/Double;
#9 = Fieldref #10.#35 // TestStatic.o1:Ljava/lang/Object;
#10 = Class #36 // TestStatic
#11 = Utf8 pi
#12 = Utf8 D
#13 = Utf8 piO
#14 = Utf8 Ljava/lang/Double;
#15 = Utf8 o1
#16 = Utf8 Ljava/lang/Object;
#17 = Utf8 o2
#18 = Utf8
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 LTestStatic;
#25 = Utf8
#26 = Utf8 SourceFile
#27 = Utf8 TestStatic.java
#28 = NameAndType #18:#19 // "":()V
#29 = Utf8 java/lang/Object
#30 = NameAndType #17:#16 // o2:Ljava/lang/Object;
#31 = NameAndType #11:#12 // pi:D
#32 = Class #37 // java/lang/Double
#33 = NameAndType #38:#39 // valueOf:(D)Ljava/lang/Double;
#34 = NameAndType #13:#14 // piO:Ljava/lang/Double;
#35 = NameAndType #15:#16 // o1:Ljava/lang/Object;
#36 = Utf8 TestStatic
#37 = Utf8 java/lang/Double
#38 = Utf8 valueOf
#39 = Utf8 (D)Ljava/lang/Double;
接下来是本类的常量池,类中用到的别的类、自身的成员变量、名字类型等都在这里
常量池是从1开始的,与JAVA的习惯不符,是因为0有特殊含义,表示不引用任何常量池项目
注意看#4和#8
#4是基本类型double的pi,值为3.14d
#8是包装类型Double的piO,值为#9.#30,
这里回答了问题1,可以清楚的看到,piO并没有拆箱操作,指向的是一个Double类型,而不是3.14这个值
有朋友问,#5去哪里了?
你问编译器去啊,我怎么知道。。。(推测:Class是一个非常严谨的文件,每一个变量占多长都是有规定的,可能是Double占据了#5的位置,所以就没有了。)
接下来看#3和#9,也就是o2和o1,仔细观察后,发现真没什么区别,暂时还回答不了问题2,继续
注:常量池的第一列表示的是类型,参考图1,出自《深入理解JAVA虚拟机:JVM高级特性与最佳实践》
static double pi;
descriptor: D
flags: ACC_STATIC
static java.lang.Double piO;
descriptor: Ljava/lang/Double;
flags: ACC_STATIC
static java.lang.Object o1;
descriptor: Ljava/lang/Object;
flags: ACC_STATIC
java.lang.Object o2;
descriptor: Ljava/lang/Object;
flags:
这一部是我们声明的成员变量,注意看pi和piO的描述符(descriptor),pi是D,是一个基本类型,piO则是一个Double类型,是一个类。o1和o2的区别在于o1有ACC_STATIC标识,o2没有,这显然与我们写的static关键字有关。
public TestStatic();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."":()V
12: putfield #3 // Field o2:Ljava/lang/Object;
15: return
LineNumberTable:
line 1: 0
line 6: 4
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this LTestStatic;
这里是构造函数(本例中,是一个默认构造函数)
()V表示void,无返回
主要看下Code中的内容
1: invokespecial #1 // Method java/lang/Object."
":()V
没错,构造函数先调用父类的构造函数,也就是super();
5: new #2 // class java/lang/Object
9: invokespecial #1 // Method java/lang/Object."":()V
12: putfield #3 // Field o2:Ljava/lang/Object;
以上三行是对o2的赋值,可推出如下结论:
虽然代码中o2是直接赋值的,但是实际上直到TestStatic这个类的构造函数执行时,才会给o2赋值。
接下来,LineNumberTable对应了源码中的行号(上面贴的源代码精简过,有可能不一致),调试的时候,断点和源码就可以对应上了
再然后,LocalVariableTable列出了用到的本地变量,只有一个this,也就是存在栈帧中的本地变量
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: ldc2_w #4 // double 3.14d
3: putstatic #6 // Field pi:D
6: ldc2_w #4 // double 3.14d
9: invokestatic #7 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
12: putstatic #8 // Field piO:Ljava/lang/Double;
15: new #2 // class java/lang/Object
18: dup
19: invokespecial #1 // Method java/lang/Object."":()V
22: putstatic #9 // Field o1:Ljava/lang/Object;
25: return
LineNumberTable:
line 3: 0
line 4: 6
line 5: 15
最后,是一个静态代码块,我们没写,显然是自动生成的,在类初始化时执行
注,有且只有在以下情况,会对TestStatic初始化
1. new TestStatic()时
2. 读取或设置piO、o1(静态变量)时
3. 反射调用TestStatic时
4. 初始化TestStatic的子类时
5. 当TestStatic中的main方法作为程序入口时
6. JDK1.7动态语言调用TestStatic时
0: ldc2_w #4 // double 3.14d
3: putstatic #6 // Field pi:D
这里是给pi赋值,直接给了3.14
6: ldc2_w #4 // double 3.14d
9: invokestatic #7 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
12: putstatic #8 // Field piO:Ljava/lang/Double;
这里是给piO复制,根本没有自动拆箱好吗?invokestatic这个指令生成了一个Double对象,然后才put给了#8
15: new #2 // class java/lang/Object
19: invokespecial #1 // Method java/lang/Object."":()V
22: putstatic #9 // Field o1:Ljava/lang/Object;
给o2赋值,也是在初始化阶段
至此,TestStatic这个类的字节码就分析完了,我们证明了包装类型为static时不会自动拆箱,static对象在初始化时赋值,非static在构造函数中赋值。
final和volitle关键字
接下来看另外两个测试类,相信读者已经掌握了基本的阅读Class文件的技巧,为了节省版面,这里不再详细分析
测试类:
public class TestA {
public final TestB finalB = new TestB();
public volatile TestB testB = new TestB();
public final TestB notInitFinalB;
public TestA(TestB notInitFinalB) {
this.notInitFinalB = notInitFinalB;
}
}
public class TestB {
}
TestA引用了TestB,finalB使用了final修饰符,testB使用了volatle修饰符,notInitFinalB使用了final并在构造函数中赋值(Spring推荐这种注入方式,在构造函数上@Autoware)
让我们一起看看TestA的Class文件
Classfile /F:/workspace/TestProject/out/production/TestProject/TestA.class
Last modified 2018-4-28; size 419 bytes
MD5 checksum f18d8c33c93237cf8e9d92783c500660
Compiled from "TestA.java"
public class TestA
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#22 // java/lang/Object."":()V
#2 = Class #23 // TestB
#3 = Methodref #2.#22 // TestB."":()V
#4 = Fieldref #7.#24 // TestA.finalB:LTestB;
#5 = Fieldref #7.#25 // TestA.testB:LTestB;
#6 = Fieldref #7.#26 // TestA.notInitFinalB:LTestB;
#7 = Class #27 // TestA
#8 = Class #28 // java/lang/Object
#9 = Utf8 finalB
#10 = Utf8 LTestB;
#11 = Utf8 testB
#12 = Utf8 notInitFinalB
#13 = Utf8
#14 = Utf8 (LTestB;)V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 LTestA;
#20 = Utf8 SourceFile
#21 = Utf8 TestA.java
#22 = NameAndType #13:#29 // "":()V
#23 = Utf8 TestB
#24 = NameAndType #9:#10 // finalB:LTestB;
#25 = NameAndType #11:#10 // testB:LTestB;
#26 = NameAndType #12:#10 // notInitFinalB:LTestB;
#27 = Utf8 TestA
#28 = Utf8 java/lang/Object
#29 = Utf8 ()V
{
public final TestB finalB;
descriptor: LTestB;
flags: ACC_PUBLIC, ACC_FINAL
public volatile TestB testB;
descriptor: LTestB;
flags: ACC_PUBLIC, ACC_VOLATILE
public final TestB notInitFinalB;
descriptor: LTestB;
flags: ACC_PUBLIC, ACC_FINAL
public TestA(TestB);
descriptor: (LTestB;)V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: new #2 // class TestB
8: dup
9: invokespecial #3 // Method TestB."":()V
12: putfield #4 // Field finalB:LTestB;
15: aload_0
16: new #2 // class TestB
19: dup
20: invokespecial #3 // Method TestB."":()V
23: putfield #5 // Field testB:LTestB;
26: aload_0
27: aload_1
28: putfield #6 // Field notInitFinalB:LTestB;
31: return
LineNumberTable:
line 6: 0
line 2: 4
line 3: 15
line 7: 26
line 8: 31
LocalVariableTable:
Start Length Slot Name Signature
0 32 0 this LTestA;
0 32 1 notInitFinalB LTestB;
}
SourceFile: "TestA.java"
过程略
结论:
1. final在字节码的表现上,只是多了ACC_FINAL标签,真正的优化在运行时
2. volatile同理,多了ACC_VOLATILE标签,运行时处理
3. 没有static变量,就不会生成static{}代码块
4. final变量直接等于值和在构造函数赋值,在字节码上表现是一样的
重新回答一下最开始的问题
问:JVM对final和static有什么优化?
我:final会在字节码中打上ACC_FINAL标签,在运行时会进行处理和优化(想要知道具体有什么优化,请查看运行时常量池的相关资料),static变量会在静态代码块中赋值,非static变量都是在构造函数中赋值,static的基本类型会在编译时把字面值放入常量池中
问:举一个字节码优化的栗子?
答:没有使用的本地变量不会编译到本地变量表(LocalVariableTable)
public class TestC {
static{
TestB bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = new TestB();
}
}
编译后静态块如下,没有bbbb什么事
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: new #2 // class TestB
3: dup
4: invokespecial #3 // Method TestB."":()V
7: astore_0
8: return
LineNumberTable:
line 3: 0
line 4: 8
LocalVariableTable:
Start Length Slot Name Signature