Java 杂谈

JAVA中final、static、volatile在字节码文件

2018-04-28  本文已影响0人  TickTock_0211

本文收录在javaskill.cn中,内有完整的JAVA知识地图,欢迎访问

说到这几个关键字,大部分猿都能娓娓道来,说出很多它们的用法和定义

  1. final修饰的字段不可修改、方法不可重写、类不可继承,JVM对final有优化
  2. static修饰的字段所有对象共用、static{}内的代码在类加载时执行、JVM对static有优化
  3. 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();
}

测试类很简单,四个字段都比较典型,在看字节码之前,先思考两个问题

  1. pi和piO在编译后有区别吗?
    答:JAVA有自动拆装箱,编译后可能没有区别...吧?
  2. 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."<init>":()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               <init>
  #19 = Utf8               ()V
  #20 = Utf8               Code
  #21 = Utf8               LineNumberTable
  #22 = Utf8               LocalVariableTable
  #23 = Utf8               this
  #24 = Utf8               LTestStatic;
  #25 = Utf8               <clinit>
  #26 = Utf8               SourceFile
  #27 = Utf8               TestStatic.java
  #28 = NameAndType        #18:#19        // "<init>":()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."<init>":()V
         4: aload_0
         5: new           #2                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."<init>":()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."<init>":()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."<init>":()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               <init>
  #19 = Utf8               ()V
  #20 = Utf8               Code
  #21 = Utf8               LineNumberTable
  #22 = Utf8               LocalVariableTable
  #23 = Utf8               this
  #24 = Utf8               LTestStatic;
  #25 = Utf8               <clinit>
  #26 = Utf8               SourceFile
  #27 = Utf8               TestStatic.java
  #28 = NameAndType        #18:#19        // "<init>":()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高级特性与最佳实践》


图1
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."<init>":()V
         4: aload_0
         5: new           #2                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."<init>":()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."<init>":()V

没错,构造函数先调用父类的构造函数,也就是super();

5: new           #2                  // class java/lang/Object
9: invokespecial #1                  // Method java/lang/Object."<init>":()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."<init>":()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."<init>":()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."<init>":()V
   #2 = Class              #23            // TestB
   #3 = Methodref          #2.#22         // TestB."<init>":()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               <init>
  #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        // "<init>":()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."<init>":()V
         4: aload_0
         5: new           #2                  // class TestB
         8: dup
         9: invokespecial #3                  // Method TestB."<init>":()V
        12: putfield      #4                  // Field finalB:LTestB;
        15: aload_0
        16: new           #2                  // class TestB
        19: dup
        20: invokespecial #3                  // Method TestB."<init>":()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."<init>":()V
         7: astore_0
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

如有错漏,欢迎指正

上一篇下一篇

猜你喜欢

热点阅读