曾经小小少年,到如今风度翩翩!曾几何时,每次想了解Java中volatile关键字的实现原理时,小编都会去百度找博客看,翻遍了许许多多的博客,有讲的深入的,有讲的浅显的,反正小编脑子是有点乱了。其中很多博客讲到其底层是给变量加了一条lock
指令,真的是这样的吗?确实是。
下面我们就来验证下到底这个lock
指令是如何得出来的,以及介绍下查看windows字节码的相关工具的使用,由于小编看得懂的字节码指令寥寥无几,因此,暂时还不能每条指令具体分析,留到下篇博客介绍。
一、Java字节码及class文件反汇编
我们都知道,Java源代码文件想要执行,会被编译器(javac
)编译为.class
文件,Java字节码文件具备了相应的格式,而且非常严格(具体的class文件格式,可以查阅《Java虚拟机规范》)。由于.class
文件为二进制文件,因此我们无法直接使用文本文件打开查看,如果要打开,我们可以使用诸如Java Decompiler
这类的工具来反编译.class
文件。Java虚拟机与传统汇编语言不同,它不直接使用底层的寄存器,而是设计成一台基于栈的虚拟机,在Java方法中,前面指令的执行结果先push进操作数栈,后面的指令如果需要使用到先前的结果,则从操作数栈中将值pop出来。而这些操作,底层Java虚拟器在读取
、存储
、出栈
、入栈
等方面拥有许许多多的字节码指令支持,我们可以这样子理解,如果说汇编指令属于底层操作系统指令,那么Java字节码指令属于Java虚拟机的指令,要想查看.calss
文件中的字节码指令,我们可以使用JDK提供的工具javap
进行反汇编。
Javap
反汇编示例:
Java源代码
/**
* The class Hello.
*
* Description:反汇编、汇编测试用例
*
* @author: huangjiawei
* @since: 2018年8月14日
* @version: $Revision$ $Date$ $LastChangedBy$
*
*/
public class Hello {
private static volatile String name;
public Hello() {}
public static void say() {
for (int i = 0; i <= 1000; i++) {
System.out.println(i);
name = "huangjiawei";
System.out.println(name);
}
}
public static void main(String[] args) {
for (int i = 0; i <= 100; i++) {
say();
}
}
}
复制代码
执行完javac Hello.java javap -c Hello.class
之后得到下面字节码指令:
Compiled from "Hello.java"
public class Hello {
public Hello();// 构造方法字节码指令
Code:// 在x86架构中,汇编语言.code标识代表指令代码区,.data表示数据区
0: aload_0 // 这里指令aload_0表示将this引用压入栈中
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void say();// say方法
Code:
0: iconst_0
1: istore_0
2: iload_0
3: sipush 1000
6: if_icmpgt 36
9: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
12: iload_0
13: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
16: ldc #4 // String huang
18: putstatic #5 // Field name:Ljava/lang/String;
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: getstatic #5 // Field name:Ljava/lang/String;
27: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: iinc 0, 1
33: goto 2
36: return
// main 方法指令区
public static void main(java.lang.String[]);
Code:
0: iconst_0 // int 常量i
1: istore_1
2: iload_1
3: bipush 100
5: if_icmpgt 17
8: invokestatic #7 // Method say:()V
11: iinc 1, 1
14: goto 2
17: return
}
复制代码
安装HSDIS
深入学习这些字节码指令,对于JVM调优非常有帮助,比如说判断槽位是否复用、逃逸分析栈上分配等等。具体的字节码指令学习,可以查阅《Java虚拟机规范》一书,英语好的话,建议线上官方地址:点我就好啦!
那么,我们了解了Java的字节码指令,我们想一想,我们开发的Java应用程序最后还不是在Linux或者Windows上面进行执行,而我们又知道,和底层硬件最接近的就是汇编语言了,那么,我们能不能将class文件转换成特定平台的汇编代码呢?答案是肯定的。下面我将介绍几种查看汇编代码的工具及其使用。
二、Hsdis 结合 JITWatch 查看机器汇编代码
HSDIS
是由Project Kenai(kenai.com/projects/ba… VM JIT编译代码的反汇编插件,作用是让HotSpot的-XX:+PrintAssembly
指令调用它来把动态生成的本地代码还原为汇编代码输出,同时还生成了大量非常有价值的注释,这样我们就可以通过输出的代码来分析问题。
windows上进行反汇编需要hsdis-amd64.dll
这个插件,因此我们需要生成这个插件,然后将该插件放置到我们的jreDir/bin/server
目录下,然后使用-XX:+PrintAssembly
即可输出汇编代码。这里有个官方标准教程,由于是英文的,在这里我将其中的步骤做一个简单的总结:
-
1、安装
Cygwin
unix模拟环境安装的过程中记得在
select package
窗口将下面的几个包给加上:gcc-core
mingw64-i686-gcc-core
mingw64-x86_64-gcc-core
patch
make
-
2、下载
GNU binutils 2.28
,注意官方推荐是2.30版本,但是2.30版本后期make
会有问题 -
3、下载OpenJDK,详细见官网。
-
4,5,6步见官网描述吧!没有坑,哈哈!
为了防止官网后面访问不了,小编将html文件下载保存在github上了,详见How to build hsdis-amd64.dll and hsdis-i386.dll on Windows
当你在命令行执行java -XX:+PrintAssembly -XX:+UnlockDiagnosticVMOptions Hello >> code.txt
就会输出大量汇编代码,如下图:
但是,有没有这样一种更加直观的方式,能够具体查看某个方法的汇编代码呢?答案是肯定的,下面出场的是jitwatch
,它是一个开源项目,其github地址为:jitwatch
相对来说,jitwatch
的安装相对比较简单,我们可以直接克隆项目,该项目支持三种编译方式:
- 如果使用ant编译,请使用
ant clean compile run
执行 - 如果使用gradle编译构建,请使用
gradlew clean build run
- 如果使用maven构建,请使用
mvn clean compile exec:java
启动完项目之后大概就是这么一个界面:
我们大概有两种方式查看我们的汇编代码:
- 1、使用
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=code.log
指令执行,然后点击界面的Open Log
按钮将日志文件导入,再start
- 2、点击界面的
Sandbox
,配置相关参数,然后start
Java字节码文件中有一个叫做行号表
的属性,存在于Code属性中, 它建立了字节码偏移量到源代码行号之间的联系。我们可以点击LNT
按钮,进行调试:
最后,如果您正在使用JDK8,那么您需要确保你写的Java方法被调用的次数足够多,以触发C1(客户端)编译,并大约10000次触发C2(服务器)编译器并打开高级优化。换句话说,你要像查看汇编代码,你写的Java源代码文件不能太过于简单,要足够复杂,但我们第一节的Hello.java
已经足够了,同时jitwatch本身也提供了很多学习样例,可以在JITWatchDir\sandbox\sources
中获得。
还记得最开始我们讨论的volatile底层汇编代码lock指令吗?查看我们的汇编代码可以发现有这么一行代码:
是吧!我们终于自己将lock指令找出来了,至于为什么lock指令能够保证内存一致性,我们首先需要从汇编语言层面对lock指令的功能进行一番了解。在所有的 X86 CPU 上都具有锁定一个特定内存地址的能力,当这个特定内存地址被锁定后,它就可以阻止其他的系统总线读取
或修改
这个内存地址。这种能力是通过 LOCK 指令前缀再加上下面的汇编指令来实现的。当使用 LOCK 指令前缀时,它会使 CPU 宣告一个 LOCK# 信号,这样就能确保在多处理器系统或多线程竞争的环境下互斥地使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。注意由于是内存互斥的,因此这个临界区除了当前lock的线程拥有,其他线程都不能进入该临界区。