原文地址:https://blog.csdn.net/AliceSmith1/article/details/80051153

即便对那些有经验的Java开发人员来说,阅读已编译的Java字节码也很乏味。为什么我们首先需要了解这种底层的东西?这是上周发生在我身上的一个简单故事:很久以前,我在机器上做了一些代码更改,编译了一个JAR,并将其部署到服务器上,以测试性能问题的一个潜在修复方案。不幸的是,代码从未被检入到版本控制系统中,并且出于某种原因,本地更改被删除了而没有追踪。几个月后,我再次修改源代码,但是我找不到上一次更改的版本!

幸运的是编译后的代码仍然存在于该远程服务器上。我于是松了一口气,我再次抓取JAR并使用反编译器编辑器打开它......只有一个问题:反编译器GUI不是一个完美的工具,并且出于某种原因,在该JAR中的许多类中找到我想要反编译的特定类并在我打开它时会在UI中导致了一个错误,并且反编译器崩溃!

绝望的时候需要采取孤注一掷的措施。幸运的是,我对原始字节码很熟悉,我宁愿花些时间手动地对一些代码进行反编译,而不是通过不断的更改和测试它们。因为我仍然记得在哪里可以查看代码,所以阅读字节码帮助我精确地确定了具体的变化,并以源代码形式构建它们。(我一定要从我的错误中吸取教训,这次要珍惜好这些教训!)

字节码的好处是,您可以只用学习它的语法一次,然后它适用于所有Java支持的平台——因为它是代码的中间表示,而不是底层CPU的实际可执行代码。此外,字节码比本机代码更简单,因为JVM架构相当简单,因此简化了指令集,另一件好事是,这个集合中的所有指令都是由Oracle提供完整的文档。

不过,在学习字节码指令集之前,让我们熟悉一下JVM的一些事情,这是进行下一步的先决条件。

JVM 数据类型

Java是静态类型的,它会影响字节码指令的设计,这样指令就会期望自己对特定类型的值进行操作。例如,就会有好几个add指令用于两个数字相加:iadd、ladd、fadd、dadd。他们期望类型的操作数分别是int、long、float和double。大多数字节码都有这样的特性,它具有不同形式的相同功能,这取决于操作数类型。

JVM定义的数据类型包括:

1.基本类型:

数值类型: byte (8位), short (16位), int (32位), long (64-bit位), char (16位无符号Unicode), float(32-bit IEEE 754 单精度浮点型), double (64-bit IEEE 754 双精度浮点型)

布尔类型

指针类型: 指令指针。

2.引用类型:

数组

接口

在字节码中布尔类型的支持是受限的。举例来说,没有结构能直接操作布尔值。布尔值被替换转换成 int 是通过编译器来进行的,并且最终还是被转换成 int 结构。

Java 开发者应该熟悉所有上面的类型,除了 returnAddress,它没有等价的编程语言类型。

基于栈的架构

字节码指令集的简单性很大程度上是由于 Sun 设计了基于堆栈的 VM 架构,而不是基于寄存器架构。有各种各样的进程使用基于JVM 的内存组件, 但基本上只有 JVM 堆需要详细检查字节码指令:

PC寄存器: 对于Java程序中每个正在运行的线程,都有一个PC寄存器保存着当前执行的指令地址。

JVM 栈 :对于每个线程,都会分配一个栈,其中存放本地变量、方法参数和返回值。下面是一个显示3个线程的堆栈示例。

堆:所有线程共享的内存和存储对象(类实例和数组)。对象回收是由垃圾收集器管理的。

方法区 :对于每个已加载的类,它储存方法的代码和一个符号表(例如对字段或方法的引用)和常量池。

JVM堆栈是由帧组成的,当方法被调用时,每个帧都被推到堆栈上,当方法完成时从堆栈中弹出(通过正常返回或抛出异常)。每一帧还包括:

本地变量数组,索引从0到它的长度-1。长度是由编译器计算的。一个局部变量可以保存任何类型的值,long和double类型的值占用两个局部变量。

用来存储中间值的栈,它存储指令的操作数,或者方法调用的参数。

字节码探索

关于JVM内部的看法,我们能够从示例代码中看到一些被生成的基本字节码例子。Java类文件中的每个方法都有代码段,这些代码段包含了一系列的指令,格式如下:

opcode (1 byte) operand1 (optional) operand2 (optional) ...

这个指令是由一个一字节的opcode和零个或若干个operand组成的,这个operand包含了要被操作的数据。

在当前执行方法的栈帧里,一条指令可以将值在操作栈中入栈或出栈,可以在本地变量数组中悄悄地加载或者存储值。让我们来看一个例子:

为了打印被编译的类中的结果字节码(假设在Test.class文件中),我们运行javap工具:

javap -v Test.class

我们可以得到如下结果:

我们可以看到main方法的方法声明,descriptor说明这个方法的参数是一个字符串数组([Ljava/lang/String; ),而且返回类型是void(V)。下面的flags这行说明该方法是公开的(ACC_PUBLIC)和静态的 (ACC_STATIC)。

Code属性是最重要的部分,它包含了这个方法的一系列指令和信息,这些信息包含了操作栈的最大深度(本例中是2)和在这个方法的这一帧中被分配的本地变量的数量(本例中是4)。所有的本地变量在上面的指令中都提到了,除了第一个变量(索引为0),这个变量保存的是args参数。其他三个本地变量就相当于源码中的a,b和c。

从地址0到8的指令将执行以下操作:

iconst_1:将整形常量1放入操作数栈。

istore_1:在索引为1的位置将第一个操作数出栈(一个int值)并且将其存进本地变量,相当于变量a。

iconst_2:将整形常量2放入操作数栈。

istore_2:在索引为2的位置将第一个操作数出栈并且将其存进本地变量,相当于变量b。

iload_1:从索引1的本地变量中加载一个int值,放入操作数栈。

iload_2:从索引2的本地变量中加载一个int值,放入操作数栈。

iadd:把操作数栈中的前两个int值出栈并相加,将相加的结果放入操作数栈。

istore_3:在索引为3的位置将第一个操作数出栈并且将其存进本地变量,相当于变量c。

return:从这个void方法中返回。

上述指令只包含操作码,由JVM来精确执行。

方法调用

上面的示例只有一个方法,即 main 方法。假如我们需要对变量 c 进行更复杂的计算,这些复杂的计算写在新方法 calc 中:

看看生成的字节码:

main 方法代码唯一的不同在于用 invokestatic 指令代替了 iadd 指令,invokestatic 指令用于调用静态方法 calc。注意,关键在于操作数栈中传递给 calc 方法的两个参数。也就是说,调用方法需要按正确的顺序为被调用方法准备好所有参数,交依次推入操作数栈。iinvokestatic(还有后面提到的其它类似的调用指令)随后会从栈中取出这些参数,然后为被调用方法创建一个新的环境,将参数作为局域变量置于其中。

我们也注意到invokestatic指令在地址上看占据了3字节,由6跳转到9。不像其余指令那样那么远,这是因为invokestatic指令包含了两个额外的字节来构造要调用的方法的引用(除了opcode外)。这引用由javap显示为#2,是一个引用calc方法的符号,解析于从前面描述的常量池中。

其它的新信息显然是calc方法本身的代码。它首先将第一个整数参数加载到操作数堆栈上(iload_0)。下一条指令,i2d,通过应用扩展转换将其转换为double类型。由此产生的double类型取代了操作数堆栈的顶部。

再下一条指令将一个double类型常量2.0d(从常量池中取出)推到操作数堆栈上。然后静态方法Math.pow调用目前为止准备好的两个操作数值(第一个参数是calc和常量2.0d)。当Math.pow方法返回时,他的结果将会被存储在其调用程序的操作数堆栈上。在下面说明。

同样的程序应用于计算Math.pow(b,2):

下一条指令,dadd,会将栈顶的两个中间结果出栈,将它们相加,并将所得之和推入栈顶。最后,invokestatic 对这个和值调用 Math.sqrt,将结果从 double(双精度浮点型) 窄化转换(d2i)成 int(整型)。整型结果会返回到 main 方法中, 并在这里保存到 c(istore_3)。

创建实例

现在修改这个示例,加入 Point 类来封装 XY 坐标。

编译后的 main 方法的字体码如下:

这里引入了 new、dup 和 invokespecial 几个新指令。new 指令与编程语言中的 new 运算符类似,它根据传入的操作数所指定类型来创建对象(这是对 Point 类的符号引用)。对象的内存是在堆上分配,对象引用则是被推入到操作数栈上。

dup指令会复制顶部操作数的栈值,这意味着现在我们在栈顶部有两个指向Point对象的引用。接下来的三条指令将构造函数的参数(用于初始化对象)压入操作数堆栈中,然后调用与构造函数对应的特殊初始化方法。下一个方法中x和y字段将被初始化。该方法完成之后,前三个操作数的栈值将被销毁,剩下的就是已创建对象的原始引用(到目前为止,已成功完成初始化了)。

接下来,astore_1将该Point引用出栈,并将其赋值到索引1所保存的本地变量(astore_1中的a表明这是一个引用值).

通用的过程会被重复执行以创建并初始化第二个Point实例,此实例会被赋值给变量b。

最后一步是将本地变量中的两个Point对象的引用加载到索引1和2中(分别使用aload_1和aload_2),并使用invokevirtual调用area方法,该方法会根据实际的类型来调用适当的方法来完成分发。例如,如果变量a包含一个扩展自Point类的SpecialPoint实例,并且该子类重写了area方法,则重写后的方法会被调用。在这种情况下,并不存在子类,因此仅有area方法是可用的。

请注意,即使area方法接受单参数,堆栈顶部也有两个Point的引用。第一个(pointA,来自变量a)实际上是调用该方法的实例(在编程语言中被称为this),对area方法来说,它将被传递到新栈帧的第一个局部变量中。另一个操作数(pointB)是area方法的参数。

另一种方式

你无需对每条指令的理解和执行的准确流程完全掌握,以根据手头的字节码了解程序的功能。例如,就我而言,我想检查代码是否驱动Javastream来读取文件,以及流是否被正确地关闭。现在以下面的字节码为例,确认以下情况是很简单的:一个流是否被使用并且很有可能是作为try-with-resources语句的一部分被关闭的。

public static void main(java.lang.String[]) throws java.lang.Exception; descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=8, args_size=1 0: ldc #2 // class test/Test 2: ldc #3 // String input.txt 4: invokevirtual #4 // Method java/lang/Class.getResource:(Ljava/lang/String;)Ljava/net/URL; 7: invokevirtual #5 // Method java/net/URL.toURI:()Ljava/net/URI; 10: invokestatic #6 // Method java/nio/file/Paths.get:(Ljava/net/URI;)Ljava/nio/file/Path; 13: astore_1 14: new #7 // class java/lang/StringBuilder 17: dup 18: invokespecial #8 // Method java/lang/StringBuilder."":()V

21: astore_2

22: aload_1

23: invokestatic #9 // Method java/nio/file/Files.lines:(Ljava/nio/file/Path;)Ljava/util/stream/Stream;

26: astore_3

27: aconst_null

28: astore 4

30: aload_3

31: aload_2

32: invokedynamic #10, 0 // InvokeDynamic #0:accept:(Ljava/lang/StringBuilder;)Ljava/util/function/Consumer;

37: invokeinterface #11, 2 // InterfaceMethod java/util/stream/Stream.forEach:(Ljava/util/function/Consumer;)V

42: aload_3

43: ifnull 131

46: aload 4

48: ifnull 72

51: aload_3

52: invokeinterface #12, 1 // InterfaceMethod java/util/stream/Stream.close:()V

57: goto 131

60: astore 5

62: aload 4

64: aload 5

66: invokevirtual #14 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V

69: goto 131

72: aload_3

73: invokeinterface #12, 1 // InterfaceMethod java/util/stream/Stream.close:()V

78: goto 131

81: astore 5

83: aload 5

85: astore 4

87: aload 5

89: athrow

90: astore 6

92: aload_3

93: ifnull 128

96: aload 4

98: ifnull 122

101: aload_3

102: invokeinterface #12, 1 // InterfaceMethod java/util/stream/Stream.close:()V

107: goto 128

110: astore 7

112: aload 4

114: aload 7

116: invokevirtual #14 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V

119: goto 128

122: aload_3

123: invokeinterface #12, 1 // InterfaceMethod java/util/stream/Stream.close:()V

128: aload 6

130: athrow

131: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;

134: aload_2

135: invokevirtual #16 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

138: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

141: return

...

可以看到java/util/stream/Stream执行forEach之前,首先触发InvokeDynamic以引用Consumer。与此同时会发现大量调用Stream.close与Throwable.addSuppressed的字节码,这是编译器实现 try-with-resources statement 的基本代码。

这是完整的原始代码。

总结

还好字节码指令集简洁,生成指令时几乎少有的编译器优化,反编译类文件可以在没有源码的情况下检查代码,当然如没有源码这也是一种需求!

图文详解 Java 字节码,让你秒懂全过程的更多相关文章

  1. 图文详解Java对象内存布局

    作为一名Java程序员,我们在日常工作中使用这款面向对象的编程语言时,做的最频繁的操作大概就是去创建一个个的对象了.对象的创建方式虽然有很多,可以通过new.反射.clone.反序列化等不同方式来创建 ...

  2. spring security4.2.2的maven配置+spring-security配置详解+java源码+数据库设计

    最近项目需要添加权限拦截,经讨论决定采用spring security4.2.2!废话少说直接上干货! 若有不正之处,请谅解和批评指正,不胜感激!!!!! spring security 4.2.2文 ...

  3. Java字节码(.class文件)格式详解(一)

    原文链接:http://www.blogjava.net/DLevin/archive/2011/09/05/358033.html 小介:去年在读<深入解析JVM>的时候写的,记得当时还 ...

  4. 如何在IDEA里给大数据项目导入该项目的相关源码(博主推荐)(类似eclipse里同一个workspace下单个子项目存在)(图文详解)

    不多说,直接上干货! 如果在一个界面里,可以是单个项目 注意:本文是以gradle项目的方式来做的! 如何在IDEA里正确导入从Github上下载的Gradle项目(含相关源码)(博主推荐)(图文详解 ...

  5. Flume启动时报错Caused by: java.lang.InterruptedException: Timed out before HDFS call was made. Your hdfs.callTimeout might be set too low or HDFS calls are taking too long.解决办法(图文详解)

    前期博客 Flume自定义拦截器(Interceptors)或自带拦截器时的一些经验技巧总结(图文详解) 问题详情 -- ::, (agent-shutdown-hook) [INFO - org.a ...

  6. Java WebService接口生成和调用 图文详解>【转】【待调整】

    webservice简介: Web Service技术, 能使得运行在不同机器上的不同应用无须借助附加的.专门的第三方软件或硬件, 就可相互交换数据或集成.依据Web Service规范实施的应用之间 ...

  7. spark最新源码下载并导入到开发环境下助推高质量代码(Scala IDEA for Eclipse和IntelliJ IDEA皆适用)(以spark2.2.0源码包为例)(图文详解)

    不多说,直接上干货! 前言   其实啊,无论你是初学者还是具备了有一定spark编程经验,都需要对spark源码足够重视起来. 本人,肺腑之己见,想要成为大数据的大牛和顶尖专家,多结合源码和操练编程. ...

  8. 全网最详细的IDEA、Eclipse和MyEclipse之间于Java web项目发布到Tomcat上运行成功的对比事宜【博主强烈推荐】【适合普通的还是Maven方式创建的】(图文详解)

    不多说,直接上干货! IDEA [适合公司业务]全网最详细的IDEA里如何正确新建[普通或者Maven]的Java web项目并发布到Tomcat上运行成功[博主强烈推荐](类似eclipse里同一个 ...

  9. 【适合公司业务】全网最详细的IDEA里如何正确新建【普通或者Maven】的Java web项目并发布到Tomcat上运行成功【博主强烈推荐】(类似eclipse里同一个workspace下【多个子项目】并存)(图文详解)

    不多说,直接上干货! 首先,大家要明确,IDEA.Eclipse和MyEclipse等编辑器之间的新建和运行手法是不一样的. 如果是在Myeclipse里,则是File -> new -> ...

随机推荐

  1. 联表多字段update更新语句

    前言 最近需要写联表多字段update更新语句,发现不同的数据库,SQL语法也不一样,正好我这里有MySQL.Oracle.PgSQL三种数据库环境,分别练习.实操这三种数据库的联表update语句 ...

  2. 【Golang】三个点(...)用法

    众所周知,Go语言是严格类型语言,而开发的时候又遇到传入参数不定的情况,怎么办? 这里的三个点(-),就给我们编程人员带来很大的灵活性,具体如下 在Golang中,三个点一共会用在四个地方(话说三个点 ...

  3. Unity——基于UGUI的UI框架

    基于UGUI的UI框架 一.Demo展示 二.关键类 MonoSingle 继承MonoBehaviour的单例基类:做了一些特殊处理: 保证场景中必须有GameInit名称的物体,所有单例管理器脚本 ...

  4. mysql8.0.20下载安装教程

    mysql8.0.20安装教程 1.浏览器搜索mysql下载安装 地址:https://dev.mysql.com/downloads/mysql/ 2.登录或者不登录下载 3.下载的是一个压缩包,直 ...

  5. Bayou复制分布式存储系统

    本文主要参考文献[1]完成. 第1章导读 Bayou是一个复制的.弱一致性的存储系统,用于移动计算环境.为了最大化可用性,Bayou为用户提供了可以任意读写访问的副本.Bayou的设计侧重于为应用程序 ...

  6. Apache ShardingSphere 在京东白条场景的落地之旅

    京东白条使用 Apache ShardingSphere 解决了千亿数据存储和扩容的问题,为大促活动奠定了基础. 2014 年初,"京东白条"作为业内互联网信用支付产品,数据量爆发 ...

  7. linux kill信号详解

    大家对kill -9 肯定非常熟悉,在工作中也经常用到.特别是你去重启tomcat时.可是多半看来,我们对-9的理解只是表面而已. 很少有人(包括我)认真的去了解一下 kill -n 这个n到底是什么 ...

  8. 题解「2017 山东一轮集训 Day1 / SDWC2018 Day1」Set

    题目传送门 题目大意 给出一个长度为 \(n\) 的数组,选出一些数异或之和为 \(s1\),其余数异或之和为 \(s2\),求 \(s1+s2\) 最大时 \(s1\) 的最小值. 思路 你发现如果 ...

  9. 3DGIS开发使用的开源项目

    gdal proj4 vcglib assimp libjpg libpng osg libtess2 cesiumjs glm

  10. Android QMUI实战:沉浸式/适配状态栏

    近期研究QMUI换肤的实现,顺便分析了下QMUI的沉浸式. 网上已有很多关于QMUI实现页面沉浸式的文章,简而言之:复杂了. 本期,我们仅通过几行代码,即可完美实现页面沉浸式效果,并轻松匹配换肤的色彩 ...