美文网首页Java 杂谈
JAVA中final、static、volatile在字节码文件

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

作者: TickTock_0211 | 来源:发表于2018-04-28 11:57 被阅读0次

    本文收录在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
    

    如有错漏,欢迎指正

    相关文章

      网友评论

        本文标题:JAVA中final、static、volatile在字节码文件

        本文链接:https://www.haomeiwen.com/subject/tyiwlftx.html