Java字节码技术 static、final、volatile、synchronized关键字的字节码体现 转

出处:

static、final、volatile关键字

static:static修饰的变量被所有类实例共享,静态变量在其所在类被加载时进行初始化,静态方法中不能引用非静态变量或函数

final:final修饰的变量不可修改(基本类型值不能修改,引用类型引用不可修改),final修饰的方法,不可重写、不可继承

volatile:volatile修饰的成员变量在每次被线程访问时,都从主内存中重新读取该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到主内存

synchronized:Synchronized关键字就是用于代码同步,用于控制多线程同步访问同一变量或方法

这些Java关键字的作用,大家或多或少都听过,但是为什么会有这种效果呢?本文从Java字节码层面做简单分析

那么什么又是字节码呢?

什么是字节码

Java之所以可以”一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。

.class文件就是Java代码编译后产生的字节码文件,看下具体实例

用Sublime Text以文本文件打开,显示如下

Javap命令查看字节码文件

先写一段如下代码,非常简单

定义一个抽象类JavaTestController

变量a为静态成员变量(int)
变量b为普通成员变量(int)
变量c为volatile修饰的变量(int)
变量d为final修饰的变量(String)
变量s为字符串(String)

变量o为Object类型(Object)

public abstract class JavaTestController  {
    public static int a = 1;
    public int b = 2;
    public volatile int c = 3;
    public final int d = 4;
    private String s = "5";
    private Object o = new Object();

    public void test() {
        System.out.println("1");
    }
}

那么问题来了,文本形式看到.class文件全是十六进制的代码,有没更人性化的展示呢?

javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息

命令如下

javap -verbose class文件路径

看下这段代码使用javap命令输出的的字节码

Classfile /Users/chenyin/IdeaProjects/spring-boot-api-project-seed/target/classes/com/company/project/biz/controller/JavaTestController.class
  Last modified 2019-9-19; size 883 bytes
  MD5 checksum 9ac63f28ebe7c6a65dd6c5a12913e064
  Compiled from "JavaTestController.java"
public abstract class com.company.project.biz.controller.JavaTestController
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT
Constant pool:
   #1 = Methodref          #7.#36         // java/lang/Object."":()V
   #2 = Fieldref           #13.#37        // com/company/project/biz/controller/JavaTestController.b:I
   #3 = Fieldref           #13.#38        // com/company/project/biz/controller/JavaTestController.c:I
   #4 = Fieldref           #13.#39        // com/company/project/biz/controller/JavaTestController.d:I
   #5 = String             #40            // 5
   #6 = Fieldref           #13.#41        // com/company/project/biz/controller/JavaTestController.s:Ljava/lang/String;
   #7 = Class              #42            // java/lang/Object
   #8 = Fieldref           #13.#43        // com/company/project/biz/controller/JavaTestController.o:Ljava/lang/Object;
   #9 = Fieldref           #44.#45        // java/lang/System.out:Ljava/io/PrintStream;
  #10 = String             #46            // 1
  #11 = Methodref          #47.#48        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #12 = Fieldref           #13.#49        // com/company/project/biz/controller/JavaTestController.a:I
  #13 = Class              #50            // com/company/project/biz/controller/JavaTestController
  #14 = Utf8               a
  #15 = Utf8               I
  #16 = Utf8               b
  #17 = Utf8               c
  #18 = Utf8               d
  #19 = Utf8               ConstantValue
  #20 = Integer            4
  #21 = Utf8               s
  #22 = Utf8               Ljava/lang/String;
  #23 = Utf8               o
  #24 = Utf8               Ljava/lang/Object;
  #25 = Utf8
  #26 = Utf8               ()V
  #27 = Utf8               Code
  #28 = Utf8               LineNumberTable
  #29 = Utf8               LocalVariableTable
  #30 = Utf8               this
  #31 = Utf8               Lcom/company/project/biz/controller/JavaTestController;
  #32 = Utf8               test
  #33 = Utf8
  #34 = Utf8               SourceFile
  #35 = Utf8               JavaTestController.java
  #36 = NameAndType        #25:#26        // "":()V
  #37 = NameAndType        #16:#15        // b:I
  #38 = NameAndType        #17:#15        // c:I
  #39 = NameAndType        #18:#15        // d:I
  #40 = Utf8               5
  #41 = NameAndType        #21:#22        // s:Ljava/lang/String;
  #42 = Utf8               java/lang/Object
  #43 = NameAndType        #23:#24        // o:Ljava/lang/Object;
  #44 = Class              #51            // java/lang/System
  #45 = NameAndType        #52:#53        // out:Ljava/io/PrintStream;
  #46 = Utf8               1
  #47 = Class              #54            // java/io/PrintStream
  #48 = NameAndType        #55:#56        // println:(Ljava/lang/String;)V
  #49 = NameAndType        #14:#15        // a:I
  #50 = Utf8               com/company/project/biz/controller/JavaTestController
  #51 = Utf8               java/lang/System
  #52 = Utf8               out
  #53 = Utf8               Ljava/io/PrintStream;
  #54 = Utf8               java/io/PrintStream
  #55 = Utf8               println
  #56 = Utf8               (Ljava/lang/String;)V
{
  public static int a;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

  public int b;
    descriptor: I
    flags: ACC_PUBLIC

  public volatile int c;
    descriptor: I
    flags: ACC_PUBLIC, ACC_VOLATILE

  public final int d;
    descriptor: I
    flags: ACC_PUBLIC, ACC_FINAL
    ConstantValue: int 4

  public com.company.project.biz.controller.JavaTestController();
    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: iconst_2
         6: putfield      #2                  // Field b:I
         9: aload_0
        10: iconst_3
        11: putfield      #3                  // Field c:I
        14: aload_0
        15: iconst_4
        16: putfield      #4                  // Field d:I
        19: aload_0
        20: ldc           #5                  // String 5
        22: putfield      #6                  // Field s:Ljava/lang/String;
        25: aload_0
        26: new           #7                  // class java/lang/Object
        29: dup
        30: invokespecial #1                  // Method java/lang/Object."":()V
        33: putfield      #8                  // Field o:Ljava/lang/Object;
        36: return
      LineNumberTable:
        line 8: 0
        line 10: 4
        line 11: 9
        line 12: 14
        line 13: 19
        line 14: 25
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      37     0  this   Lcom/company/project/biz/controller/JavaTestController;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #10                 // String 1
         5: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 17: 0
        line 18: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/company/project/biz/controller/JavaTestController;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_1
         1: putstatic     #12                 // Field a:I
         4: return
      LineNumberTable:
        line 9: 0
}
SourceFile: "JavaTestController.java"

大家肯定觉得很长,怎么解读呢?

字节码文件解读

每个字节码文件都是按照如下格式产生的,下面逐个分析

1.魔数

这需要在文本模式的class文件中查看,固定的字符串”0XCAFEBABE”,标识其是一个class文件,CAFEBABE英文意为咖啡宝贝,与Java图标对应

2.版本号

minor version: 0
major version: 52

对应文本中的

Java字节码技术 static、final、volatile、synchronized关键字的字节码体现 转

0034转化到10进制就是52,52对应Java版本1.8

  1. 常量池
    常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为Final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符
    整体分为:常量池计数器以及常量池数据区
    先看常量池计数器:

Java字节码技术 static、final、volatile、synchronized关键字的字节码体现 转

Java字节码技术 static、final、volatile、synchronized关键字的字节码体现 转

002a标识共有(57-1)=56个常量
对应到javap命令中的常量池,也是56个

Java字节码技术 static、final、volatile、synchronized关键字的字节码体现 转

再看常量池的数据区,即数据如何展示的

56个常量池数据项,以第一个为例做分析,即下面的数据是如何从16进制转化而来的

#1 = Methodref          #7.#36         // java/lang/Object."":()V

先来看一个结构图

Java字节码技术 static、final、volatile、synchronized关键字的字节码体现 转

这是Methodref的常量池数据线字节码分布图,什么意思呢?

即第一个字节的16进制标志其tag为10,对应到下图0a即标识接下来的常量池tag=10,是methodref类型

Java字节码技术 static、final、volatile、synchronized关键字的字节码体现 转

Java字节码技术 static、final、volatile、synchronized关键字的字节码体现 转

接下来的2byte为指向声明方法的描述符索引项

0007转化到十进制也是7,即描述符下标为7,对应如图,标识其是个Object类型

最后2byte数据为指向名称及类型描述符的索引项

0024转化到10进制是36,标识调用了Object的初始化方法

当然刚才只是举例展示了MethodRef常量类型的字节码分析,常量类型很多,但思路基本都类似,先通过tag确定其常量类型,后面连续几个字节确定其具体的值含义,类型及字节含义图如下

访问标志

访问标识描述了类、接口的访问类型

JVM定义了如下访问标记

当前类全限定名

父类全限定名

如果有父类,后面会紧接着父类的全限定名,指向常量池中索引

接口信息

描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值。例子里没有实现接口,所以没有。

字段表

记录了当前类所定义的变量的总数量。包括类成员变量和类变量(静态变量)

方法表

描述了方法名、访问标识(ACC_PUBLIC) 等方法级别信息

构造方法如下:执行了各个成员变量的初始化(注意这里不包括静态变量a)

test()方法

Code区是具体执行的JVM指令,即Java代码转换后的JVM指令,像一些字节码增强框架,修改的就是Code区的部分
LineNumber将源码行号和字节码Code区中行号做了映射,比如test()中的code区

其中17代表Java代码中的输出Print,0对应Code区中的行号

stack=2, locals=1, args_size=1
         0: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #10                 // String 1
         5: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 17: 0
        line 18: 8
LocalVariableTable:本地变量表,包含This和局部变量

附加属性表

字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息。

static、final、volatile在字节码中的体现

Static的字节码体现

在上面方法区的解析中,发现了静态变量a并没有在构造函数中进行初始化,那么a在哪里进行初始化呢?

发现代码区多了一段static的初始化代码,其中有a变量的初始化实现,这就是Java中的静态代码块,即静态变量初始化先于成员变量

特点:随着类的加载而执行,而且只执行一次

如果静态方法能调用非静态成员变量的话,那如果别人通过类名调用静态方法时实例对象可能并不存在,导致异常出现

这就解释了,静态方法中为什么不能调用非静态本地成员变量的问题

假设我有一个静态方法呢?加上静态方法看看,代码里加上

public static void staticMethod() {
    System.out.println("static method");
}

看下字节码中方法区的解析

再看下普通的test方法的字节码解析

差别在哪?静态方法没有本地变量表,不持有JavaTestController的本地this指针

故 静态方法中不能出现this,super等关键字

Final、Volatile的字节码体现

看下Final、Volatile在字节码中的变量定义

Java字节码技术 static、final、volatile、synchronized关键字的字节码体现 转

那么Volatile又是具体如何让变量的修改直接写回主存的呢?

Final又是如何让基本类型值不能修改的呢?

其实现原理不在Java层面,而在JIT编译生成的机器码层面,这是stack-overflow上的回答

https://stackoverflow.com/questions/16898367/how-to-decompile-volatile-variable-in-java/16898432#16898432?newreg=4366ad45ce3f401a8dfa6b3d21bde635

故字节码中无法看到其实现原理,具体实现原理可以百度查

字节码层面来理解的话,只需明白:final和volatile定义的变量会在字节码中打上ACC_FINAL、ACC_VOLATILE标签,在运行时会进行处理和优化

Synchorinized的字节码体现

编写如下测试代码

分为三个方法
第一个为synchronized修饰普通方法(锁当前调用对象)
第二个为synchronized、static修饰的静态方法(锁类)
第二个为静态代码块(锁synchronized括号中的对象)

public class JavaTestController  {

    public synchronized void test() {
        System.out.println("1");
    }
    public static synchronized void test1() {
        System.out.println("1");
    }
    public void test2() {
        synchronized (new Object()) {
            System.out.println(1);
        }
    }

}

看下javap解析出来的方法区代码

synchronized修饰方法

先看test方法,可以看到flags中多了ACC_SYNCHRONIZED修饰符

public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String 1
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 11: 0
        line 12: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/company/project/biz/controller/JavaTestController;

再看test1方法,也是多了ACC_SYNCHRONIZED修饰符

public static synchronized void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String 1
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 8

所以可以看出当synchronized修饰方法时,会在字节码中加上ACC_SYNCHRONIZED修饰符

ACC_SYNCHRONIZED是获取监视器锁的一种隐式实现(没有显示的调用monitorenter,monitorexit指令)

如果字节码方法区中的ACC_SYNCHRONIZED标志被设置,那么线程在执行方法前会先去获取对象的monitor对象,如果获取成功则执行方法代码,执行完毕后释放monitor对象

synchronized同步代码块

看下test2方法的字节码实现

public void test2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #5                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."":()V
         7: dup
         8: astore_1
         9: monitorenter
        10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iconst_1
        14: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
        17: aload_1
        18: monitorexit
        19: goto          27
        22: astore_2
        23: aload_1
        24: monitorexit
        25: aload_2
        26: athrow
        27: return

指令第9行:monitorenter表示获取对象监视器锁
指令第18行:monitorexit表示释放对象监视器锁
指令第24行:monitorexit表示释放对象监视器锁

有人可能会疑问,为什么获取了一次监视器锁,却指令中有两次释放监视器锁的指令?

这是因为第二个monitorexit的位置实际是在抛出异常的时候自动调用的(防止程序异常时,监视器锁不会被释放),athrow指令就是抛出异常的地方

Java字节码技术 static、final、volatile、synchronized关键字的字节码体现 转

因此当synchronized修饰同步代码块时,会显示调用monitorenter争抢监视器锁,同步代码执行完后调用monitorexit指令释放监视器锁

Original: https://www.cnblogs.com/myseries/p/13896786.html
Author: myseries
Title: Java字节码技术 static、final、volatile、synchronized关键字的字节码体现 转

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/538826/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Mac M1 安装 Nacos 操作及问题解决

    先安装mysql ,这里使用的是8+版本,原因在于原本的 5.7 版本中并没有对 m1 的良好支持,如果启动会有报错说查询不到对应版本信息(虽然可以通过自定义 mirror 实现)…

    Java 2023年6月15日
    0103
  • 【设计模式】汉堡中的设计模式——观察者模式

    【设计模式】汉堡中的设计模式——观察者模式 【设计模式】汉堡中的设计模式——观察者模式 情景带入 为什么关注公众号就可以 发布者-订阅者模式与观察者模式 + 观察者模式(Obser…

    Java 2023年6月5日
    099
  • java并发的发布和订阅测试

    现在编码的时候,为了处理消息,大家动不动就上个重器,例如MQ之类的。但很多时候,并不是那么有必要,因为数据量和并发其实远远不够。 可以替代的方案非常多,其中一个是java.util…

    Java 2023年6月9日
    056
  • 62.可

    sdfdsf posted @2022-09-28 08:36 随遇而安== 阅读(4 ) 评论() 编辑 Original: https://www.cnblogs.com/55…

    Java 2023年6月7日
    099
  • JavaScript学习

    一、什么是JavaScript JavaScript世界上最流行的脚本语言 一个合格的后端人员,必须精通JavaScript 二、快速入门 2.1、引入JavaScript 内部引…

    Java 2023年6月8日
    086
  • jdk8函数式接口——Consumer介绍

    Consumer介绍与实例分析函数式接口:@FunctionalInterfaceConsumer(消费者)函数式接口:@FunctionalInterface自从jdk8提供了函…

    Java 2023年5月30日
    071
  • Cit 入门操作笔记

    Git操作入门 Git 是一个快速、可扩展的 分布式版本控制系统 ,它具有极为丰富的命令集,对内部系统提供了高级操作和完全访问.Git与你熟悉的大部分版本控制系统的差别是很大的。相…

    Java 2023年6月15日
    069
  • 让mysql支持emoji表情

    什么是emoji emoji就是表情符号 emoji的创造者是日本人栗田穰崇(Shigetaka Kurita) 在数据库的编码不为 utf8mb4,利用java mysql驱动保…

    Java 2023年6月16日
    087
  • SpringMVC完整学习!!!

    1.楔子 1.1、了解MVC 1.2、MVC框架的主要功能 2.初识SpringMVC 2.1、为什么要学习SpringMVC 2.2、了解SpringMVC 3.入门项目初体验!…

    Java 2023年6月8日
    0174
  • C语言快速上手

    C语言快速上手 本文旨在快速回顾C语言语法知识 char,short,int,long,long long #include int main() { printf("s…

    Java 2023年6月7日
    069
  • js console.log打印变量注意事项

    如果是基本类型变量是没有异常的 let str = ‘string’ console.log(str) // string str = ‘改&#x53…

    Java 2023年6月16日
    071
  • 深入MySQL(三):MySQL的索引的应用

    在MySQL的优化中,索引的作用绝对算是一个大头,很多时候索引使用得当可以使得一个查询的效率提高几个数量级,同时它还具有自动排序等功能。所以如果是深入MySQL,那么索引绝对是其中…

    Java 2023年6月7日
    070
  • zookeeper篇-zk的选举机制

    点赞再看,养成习惯,微信搜索「 小大白日志」关注这个搬砖人。 文章不定期同步公众号,还有各种一线大厂面试原题、我的学习系列笔记。 说说zk的选举机制 基础概念 zxid=事务id=…

    Java 2023年6月8日
    089
  • HashMap不安全后果及ConcurrentHashMap线程安全原理

    Java集合HashMap不安全后果及ConcurrentHashMap 原理 HashMap JDK7 HashMap链表循环造成死循环 HashMap数据丢失 JDK7 Con…

    Java 2023年6月16日
    084
  • quartz框架(五)-Trigger相关内容

    上篇博文,博主介绍了Job的相关内容。本篇博文,博主将介绍Trigger相关的内容。 Trigger是触发器的意思,它只定义Trigger相关属性的Get方法。一个Trigger只…

    Java 2023年6月7日
    098
  • websocket在线测试工具

    为了测试websocket, 根据网上的一些工具修改了一些, 因此得到了这个工具 源码 源码: websocket在线测试工具 WS WS WSS 发送 连接 清屏 断开 … …

    Java 2023年6月7日
    078
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球