static:static修饰的变量被所有类实例共享,静态变量在其所在类被加载时进行初始化,静态方法中不能引用非静态变量或函数
final:final修饰的变量不可修改(基本类型值不能修改,引用类型引用不可修改),final修饰的方法,不可重写、不可继承
volatile:volatile修饰的成员变量在每次被线程访问时,都从主内存中重新读取该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到主内存
synchronized:Synchronized关键字就是用于代码同步,用于控制多线程同步访问同一变量或方法
这些Java关键字的作用,大家或多或少都听过,但是为什么会有这种效果呢?本文从Java字节码层面做简单分析
那么什么又是字节码呢?
Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。
.class文件就是Java代码编译后产生的字节码文件,看下具体实例
用Sublime Text以文本文件打开,显示如下
先写一段如下代码,非常简单
定义一个抽象类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"
大家肯定觉得很长,怎么解读呢?
minor version: 0
major version: 52
对应文本中的
0034转化到10进制就是52,52对应Java版本1.8
#1 = Methodref #7.#36 // java/lang/Object."":()V
先来看一个结构图
这是Methodref的常量池数据线字节码分布图,什么意思呢?
即第一个字节的16进制标志其tag为10,对应到下图0a即标识接下来的常量池tag=10,是methodref类型
接下来的2byte为指向声明方法的描述符索引项
0007转化到十进制也是7,即描述符下标为7,对应如图,标识其是个Object类型
最后2byte数据为指向名称及类型描述符的索引项
0024转化到10进制是36,标识调用了Object的初始化方法
当然刚才只是举例展示了MethodRef常量类型的字节码分析,常量类型很多,但思路基本都类似,先通过tag确定其常量类型,后面连续几个字节确定其具体的值含义,类型及字节含义图如下
构造方法如下:执行了各个成员变量的初始化(注意这里不包括静态变量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和局部变量
在上面方法区的解析中,发现了静态变量a并没有在构造函数中进行初始化,那么a在哪里进行初始化呢?
发现代码区多了一段static的初始化代码,其中有a变量的初始化实现,这就是Java中的静态代码块,即静态变量初始化先于成员变量
特点:随着类的加载而执行,而且只执行一次
如果静态方法能调用非静态成员变量的话,那如果别人通过类名调用静态方法时实例对象可能并不存在,导致异常出现
这就解释了,静态方法中为什么不能调用非静态本地成员变量的问题
假设我有一个静态方法呢?加上静态方法看看,代码里加上
public static void staticMethod() {
System.out.println("static method");
}
差别在哪?静态方法没有本地变量表,不持有JavaTestController的本地this指针
故 静态方法中不能出现this,super等关键字
看下Final、Volatile在字节码中的变量定义
那么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标签,在运行时会进行处理和优化
编写如下测试代码
分为三个方法
第一个为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解析出来的方法区代码
先看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对象
看下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指令就是抛出异常的地方
因此当synchronized修饰同步代码块时,会显示调用monitorenter争抢监视器锁,同步代码执行完后调用monitorexit指令释放监视器锁