Java 反汇编、反编译、volitale解读

曾经小小少年,到现在风度翩翩!曾几什么时候,每次想了解Java中volatile关键字的实现原理时,小编都会去百度找博客看,翻遍了许许多多的博客,有讲的深刻的,有讲的浅显的,反正小编脑子是有点乱了。其中不少博客讲到其底层是给变量加了一条lock指令,真的是这样的吗?确实是。html

下面咱们就来验证下到底这个lock指令是如何得出来的,以及介绍下查看windows字节码的相关工具的使用,因为小编看得懂的字节码指令寥寥无几,所以,暂时还不能每条指令具体分析,留到下篇博客介绍。java

1、Java字节码及class文件反汇编

咱们都知道,Java源代码文件想要执行,会被编译器(javac)编译为.class文件,Java字节码文件具有了相应的格式,并且很是严格(具体的class文件格式,能够查阅《Java虚拟机规范》)。因为.class文件为二进制文件,所以咱们没法直接使用文本文件打开查看,若是要打开,咱们可使用诸如Java Decompiler这类的工具来反编译.class文件。Java虚拟机与传统汇编语言不一样,它不直接使用底层的寄存器,而是设计成一台基于栈的虚拟机,在Java方法中,前面指令的执行结果先push进操做数栈,后面的指令若是须要使用到先前的结果,则从操做数栈中将值pop出来。而这些操做,底层Java虚拟器在读取存储出栈入栈等方面拥有许许多多的字节码指令支持,咱们能够这样子理解,若是说汇编指令属于底层操做系统指令,那么Java字节码指令属于Java虚拟机的指令,要想查看.calss文件中的字节码指令,咱们可使用JDK提供的工具javap进行反汇编。nginx

Javap反汇编示例:git

Java源代码github

/**
 * 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以后获得下面字节码指令:windows

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文件转换成特定平台的汇编代码呢?答案是确定的。下面我将介绍几种查看汇编代码的工具及其使用。bash

2、Hsdis 结合 JITWatch 查看机器汇编代码

HSDIS是由Project Kenai(kenai.com/projects/ba… VM JIT编译代码的反汇编插件,做用是让HotSpot的-XX:+PrintAssembly指令调用它来把动态生成的本地代码还原为汇编代码输出,同时还生成了大量很是有价值的注释,这样咱们就能够经过输出的代码来分析问题。服务器

windows上进行反汇编须要hsdis-amd64.dll这个插件,所以咱们须要生成这个插件,而后将该插件放置到咱们的jreDir/bin/server目录下,而后使用-XX:+PrintAssembly便可输出汇编代码。这里有个官方标准教程,因为是英文的,在这里我将其中的步骤作一个简单的总结:多线程

  • 一、安装Cygwinunix模拟环境

    安装的过程当中记得在select package窗口将下面的几个包给加上:

    • gcc-core
    • mingw64-i686-gcc-core
    • mingw64-x86_64-gcc-core
    • patch
    • make
  • 二、下载GNU binutils 2.28,注意官方推荐是2.30版本,可是2.30版本后期make会有问题

  • 三、下载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

启动完项目以后大概就是这么一个界面:

咱们大概有两种方式查看咱们的汇编代码:

  • 一、使用-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=code.log指令执行,而后点击界面的Open Log按钮将日志文件导入,再start
  • 二、点击界面的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的线程拥有,其余线程都不能进入该临界区。

2019年8月23日更新

总线加锁lock是早起CPU的一种实现方式,最新的实现采用EMSI缓存一致性协议来实现VOLITALE

大概流程就是多个线程都去嗅探(监听)总线上的某个变量,一旦有线程将变量协会主内存时,其余线程将会第一时间监听到变量的改变,性能比lock总线加锁高不少。

相关文章
相关标签/搜索