Java是一门设计为运行于虚拟机之上的编程语言,因此它需要一次编译,处处运行(当然也是一次编写,处处测试)。因此,安装到你系统上的JVM是原生的程序,而运行在它之上的代码是平台无关的。Java字节码就是你写的源代码的中间表现形式,也就是你的代码编译后的产物。你的class文件就是字节码。

简单点说,字节码就是JVM使用的代码集,它在运行时可能会被JIT编译器编译成本地代码。

你玩过汇编语言或者机器代码吗?字节码就是类似的东西,不过业界中许多人也很少会用及它,因为基本没这个必要。然而它对于理解程序运行是很重要的,如果你想在酒吧把某人PK下去,它也非常有用。

首先,我们先看一下字节码的基础知识。先拿表达式’1+2‘为例子,看下它的字节码是如何执行的。1+2可以用逆波兰式记法写成1 2 +。为什么?我们把它压到栈里你就明白了。。

OK,在字节码中我们看到了操作码(iconst1和iconst2)以及一条指令(iadd),但不是push和add,不过它们的流程是一样的。实际的指令的长度只有一个字节,所以我们把它称为字节码。一共有256种可能的字节码,但现在只用了大概200条。操作码的前缀是类型,后面是操作名。因此我们前面看到的iconst和iadd分别是整型的常量操作,以及整型的加法指令。

这些都不难理解,不过怎么读取class文件呢。通常来说,如果你用自己的编辑器直接打开class文件的话,你会看到一堆笑脸和方块,点号和一些奇奇怪怪的字符,对吧?答案是使用你的JDK提供的一个代码工具,javap。我们来看下如何使用javap。

public class Main {
public static void main(String[] args){
MovingAverage app = new MovingAverage();
}
}

一旦这个类编译成了Main.class文件后,你可以使用这个命令来解压字节码:javap -c Main

Compiled from "Main.java"
public class algo.Main {
public algo.Main();
Code:
0: aload_0
1: invokespecial #1
4: return
// Method java/lang/Object."<init>":()V
public static void main(java.lang.String[]);
Code:
0: new #2
3: dup
4: invokespecial #3
7: astore_1
8: return
}

我们可以看到字节码里有一个默认的构造方法以及main方法。顺便说一下,这就是当你没有写构造方法时,Java提供默认的构造方法的方式!构造方法中的字节码只是简单地调用了下super(),而我们的main方法会创建一个MovingAverage的实例然后返回。这个#n字符引用的是一个常量,这个我们可以通过-verbose参数看到:javap -c -verbose Main。返回结果里有意思的是下面这段:

public class algo.Main
SourceFile: "Main.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#21 // java/lang/Object."<init>":()V
#2 = Class #22 // algo/MovingAverage
#3 = Methodref #2.#21 // algo/MovingAverage."<init>":()V
#4 = Class #23 // algo/Main
#5 = Class #24 // java/lang/Object

现在我们将指令匹配到对应的常量上,可以更容易弄清楚到底发生了什么。上面的这个例子有什么看不明白的吗?没有?那每个指令前面的数字是什么呢?

 0: new           #2
3: dup
4: invokespecial #3
7: astore_1
8: return

现在糊涂了吧?:-)如果我们把这个方法体看成一个数组的话,你会得到下面这个东西:

注意每条指令都可以用16进制表示,因此我们实际会得到这个:

如果我们用16进制编辑器打开class文件的话,也能看到它:

我们可以在16进制编辑器中修改这段字节码,不过还是诚实点吧,这不是你想做的,尤其是在一个刚去完酒吧的周五下午。最好的方式就是使用ASM或者javassist。

我们继续从这个基础的例子讲起,这回增加一些本地变量来存储状态,并直接和栈进行交互。看下下面的代码:

public static void main(String[] args) {
MovingAverage ma = new MovingAverage();
int num1 = 1;
int num2 = 2;
ma.submit(num1);
ma.submit(num2);
double avg = ma.getAvg();
}

我们来看这回字节码是什么:

[ ] Code:
0: new  #2    // class algo/MovingAverage
3: dup
4: invokespecial #3 // Method algo/MovingAverage."<init>":()V
7: astore_1
8: iconst_1
9: istore_2
10: iconst_2
11: istore_3
12: aload_1
13: iload_2
14: i2d
15: invokevirtual #4 // Method algo/MovingAverage.submit:(D)V
18: aload_1
19: iload_3
20: i2d
21: invokevirtual #4 // Method algo/MovingAverage.submit:(D)V
24: aload_1
25: invokevirtual #5 // Method algo/MovingAverage.getAvg:()D
28: dstore 4
40LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 ma Lalgo/MovingAverage;
10 21 2 num1 I
12 19 3 num2 I
30 1 4 avg D

看起来更有意思了。。。我们看到这里创建了一个MovingAverage类型的对象,并通过astroe1指令(1是LocalVariableTable里面的变量槽的位置)存储到了本地变量ma里。指令 iconst1和iconst2是用来加载常量1和2到栈里,然后再通过istore2和istore_3将它们分别存储到LocalVariableTable中第2和第3的位置那。一条load指令将本地变量压到了栈里,而store指令将栈顶的元素弹出,并存储到LocalVariableTable里。很重要的一点是,如果你使用store指令的话,该元素就从栈中移出了,如果你想再操作它的话,得重新加载进来才行。

那执行中的流程控制是怎样的呢?我们看到的只是一行到下一行的顺序执行而已。我想看到GOTO 10这样的组合!我们来再看一个例子:

MovingAverage ma = new MovingAverage();
for (int number : numbers) {
ma.submit(number);
}

在这个例子中,当我们遍历for循环的时候,执行流程会不停地进行跳转。假设这个numbers变量是一个静态变量,那对应的字节码就像是下面这样:

0: new #2 // class algo/MovingAverage
3: dup
4: invokespecial #3 // Method algo/MovingAverage."<init>":()V
7: astore_1
8: getstatic #4 // Field numbers:[I
11: astore_2
12: aload_2
13: arraylength
14: istore_3
15: iconst_0
16: istore 4
18: iload 4
20: iload_3
21: if_icmpge 43
24: aload_2
25: iload 4
27: iaload
28: istore 5
30: aload_1
31: iload 5
33: i2d
34: invokevirtual #5 // Method algo/MovingAverage.submit:(D)V
37: iinc 4, 1
40: goto 18
43: return
LocalVariableTable:
Start Length Slot Name Signature
30 7 5 number I
12 31 2 arr$ [I
15 28 3 len $I
18 25 4 i$ I
0 49 0 args [Ljava/lang/String;
8 41 1 ma Lalgo/MovingAverage;
48 1 2 avg D

8到17的指令是用来设置这个循环的。LocalVariable表中有三个变量,它们在源码中是不存在的,arr$, len$以及i$。这些都是循环中会用到的变量。arr$存储的是numbers字段的引用,从它这能获取到数组的长度,len$。i$是循环的计数器,iinc指令会去增加它的值。

首先我们需要对循环的条件表达式进行测试,这个可以通过一个比较指令来完成:

18: iload 4
20: iload_3
21: if_icmpge 43

我们将4和3压到了栈里,这是循环的计数器以及循环的长度。我们检查 i$ 是不是大于等于len$。如果是的话,跳转到43处的语句,否则继续执行。我们可以在循环体中处理自己的逻辑,结束的时候会增加计数器的值,并跳转回代码中18行处的判断循环条件的语句那。

37: iinc       4, 1       // increment i$
40: goto 18 // jump back to the beginning of the loop

字节码中有许多算术运算的操作码和类型的组合,包括如下这些:

前面那个例子中我们把一个整型传递给了接收double类型的submit方法里。Java的语法是允许这样的,不过在字节码中,你可以看到实际用到了i2d操作码:

31: iload 5


33: i2d


34: invokevirtual #5 // Method algo/MovingAverage.submit:(D)V

看吧,你已经掌握了这么多了。做的不错,该喝杯咖啡犒劳一下自己了。了解这些东西真的有用吗,还是感觉更geek一些而已?其实两者都有。首先,从现在开始你可以告诉你的朋友,你就是台能处理字节码的JVM了,第二,当你在编写字节码的时候,你会更清楚自己在做些什么。比方说,当你在用ObjectWeb ASM这个广泛使用的操作字节码的工具时,你会需要自己来构造指令,这时候你会发现这些知识太有用了!

原创文章转载请注明出处:掌握Java字节码

http://www.deepinmind.com/jvm/2014/05/24/mastering-java-bytecode.html

掌握Java字节码(转)的更多相关文章

  1. 在Eclipse里查看Java字节码

    要理解 Java 字节码,比较推荐的方法是自己尝试编写源码对照字节码学习.其中阅读 Java 字节码的工具必不可少.虽然javap可以以可读的形式展示出.class 文件中字节码,但每次改动源码都需调 ...

  2. JAVA字节码解析

    Java字节码指令 Java 字节码指令及javap 使用说明 ### java字节码指令列表 字节码 助记符 指令含义 0x00 nop 什么都不做 0x01 aconst_null 将null推送 ...

  3. 【转】在Eclipse里查看Java字节码

    要理解 Java 字节码,比较推荐的方法是自己尝试编写源码对照字节码学习.其中阅读 Java 字节码的工具必不可少.虽然javap可以以可读的形式展示出.class 文件中字节码,但每次改动源码都需调 ...

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

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

  5. 通过Java字节码发现有趣的内幕之String篇(上)(转)

    原文出处: jaffa 很多时候我们在编写Java代码时,判断和猜测代码问题时主要是通过运行结果来得到答案,本博文主要是想通过Java字节码的方式来进一步求证我们已知的东西.这里没有对Java字节码知 ...

  6. Java字节码操纵框架ASM小试

    本文主要内容: ASM是什么 JVM指令 Java字节码文件 ASM编程模型 ASM示例 参考资料汇总 JVM详细指令 ASM是什么 ASM是一个Java字节码操纵框架,它能被用来动态生成类或者增强既 ...

  7. Java:从面试题“i++和++i哪个效率高?"开始学习java字节码

    今天看到一道面试题,i++和++i的效率谁高谁低. 面试题的答案是++i要高一点. 我在网上搜了一圈儿,发现很多回答也都是同一个结论. 如果早个几年,我也会认同这个看法,但现在我负责任的说,这个结论是 ...

  8. Java字节码—ASM

    前言 ASM 是什么 官方介绍:ASM is an all purpose Java bytecode manipulation and analysis framework. It can be u ...

  9. 打造一个简单的Java字节码反编译器

    简介 本文示范了一种反编译Java字节码的方法,首先通过解析class文件,然后将解析的结果转成java代码.但是本文并没有覆盖所有的class文件的特性和指令,只针对部分规范进行解析. 所有的代码代 ...

随机推荐

  1. poj 3623 Best Cow Line, Gold

    题目不算难,但是不认真想的话很容易wa,我就是wa了多次才意识到自己想法存在的缺陷. 相同的时候往后找知道出现不相同时,只能判断出当前字符的优先顺序. 这个题目如果朴素的按照这种方法做的话复杂度其实是 ...

  2. (step6.3.2)hdu 1068(Girls and Boys——二分图的最大独立集)

    题目大意:第一行输入一个整数n,表示有n个节点.在接下来的n行中,每行的输入数据的格式是: 1: (2) 4 6 :表示编号为1的人认识2个人,他们分别是4.6: 求,最多能找到多少个人,他们互不认识 ...

  3. hdu 2147 SG函数打表(手写也可以) 找规律

    kiki's game Time Limit: 5000/1000 MS (Java/Others)    Memory Limit: 40000/1000 K (Java/Others) Total ...

  4. ASP.NET 应用程序(Application)生命周期概述

    原文:ASP.NET 应用程序(Application)生命周期概述 引用MSDN:ASP.NET 应用程序生命周期概述 本 主题概述应用程序生命周期,列出重要的生命周期事件,并描述如何编写适合应用程 ...

  5. UPX 加壳工具:The Ultimate Packer for eXecutables

    UPX (the Ultimate Packer for eXecutables)是一款先进的可运行程序文件压缩器.压缩过的可运行文件体积缩小50%-70% ,这样降低了磁盘占用空间.网络上传下载的时 ...

  6. 重拾linux

    重拾linux 起因 因为想重拾起linux,同时需要用docker起几个镜像,用来学习网络知识.本来想直接去阿里云上买,后来一想自己机器上,起一个linux是个不错的选择,毕竟不花钱! 还可以用来做 ...

  7. git使用ssh密钥和https两种认证方式汇总(转)

    在版本库的SSH方式和HTTPS方式是不同的,具体来说就是url信息的不同,但是,实际的认证机制也是不同的.当建立了本机密钥之后,使用ssh方式实际上是不需要再次认证的,而https则每次需要输入密码 ...

  8. Android开发技术周报

    Android开发技术周报 原文  http://androidweekly.cn/android-dev-weekly-issue48/ 教程 深入理解Android之Gradle Gradle是当 ...

  9. hdu4521 小明系列的问题——小明序列(LIS变种 (段树+单点更新解决方案))

    链接: huangjing 题目:中文题目 思路: 1:这个题目假设去掉那个距离大于d的条件,那么必定是一个普通的LIS.可是加上那个条件后就变得复杂了.我用的线段树的解法. . .就是採用延迟更新的 ...

  10. Linux 系统文件夹结构

    登录后,在当前的命令窗口中输入命令: <span style="font-size:14px;">ls /</span> 你的终端上会出现例如以下的代码: ...