Java字节码基础[转]

原文链接:http://it.deepinmind.com/jvm/2014/05/24/mastering-java-bytecode.html

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

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

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

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

 

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

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

Java代码  

  1. public class Main {

  2. public static void main(String[] args){

  3. MovingAverage app = new MovingAverage();

  4. }

  5. }

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

Java代码  

  1. Compiled from "Main.java"

  2. public class algo.Main {

  3. public algo.Main();

  4. Code:

  5. 0: aload_0

  6. 1: invokespecial #1

  7. 4: return

  8. // Method java/lang/Object."<init>":()V

  9. public static void main(java.lang.String[]);

  10. Code:

  11. 0: new           #2

  12. 3: dup

  13. 4: invokespecial #3

  14. 7: astore_1

  15. 8: return

  16. }

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

Java代码  

  1. public class algo.Main

  2. SourceFile: "Main.java"

  3. minor version: 0

  4. major version: 51

  5. flags: ACC_PUBLIC, ACC_SUPER

  6. Constant pool:

  7. #1 = Methodref    #5.#21         //  java/lang/Object."<init>":()V

  8. #2 = Class        #22            //  algo/MovingAverage

  9. #3 = Methodref    #2.#21         //  algo/MovingAverage."<init>":()V

  10. #4 = Class        #23            //  algo/Main

  11. #5 = Class        #24            //  java/lang/Object

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

Java代码  

  1. 0: new           #2

  2. 3: dup

  3. 4: invokespecial #3

  4. 7: astore_1

  5. 8: return

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

 

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

 

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

 

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

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

Java代码  

  1. public static void main(String[] args) {

  2. MovingAverage ma = new MovingAverage();

  3. int num1 = 1;

  4. int num2 = 2;

  5. ma.submit(num1);

  6. ma.submit(num2);

  7. double avg = ma.getAvg();

  8. }

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

Java代码  

  1. [ ] Code:?0: new  #2    // class algo/MovingAverage

  2. 3: dup

  3. 4: invokespecial #3  // Method algo/MovingAverage."<init>":()V

  4. 7: astore_1

  5. 8: iconst_1

  6. 9: istore_2

  7. 10: iconst_2

  8. 11: istore_3

  9. 12: aload_1

  10. 13: iload_2

  11. 14: i2d

  12. 15: invokevirtual #4        // Method algo/MovingAverage.submit:(D)V

  13. 18: aload_1

  14. 19: iload_3

  15. 20: i2d

  16. 21: invokevirtual #4        // Method algo/MovingAverage.submit:(D)V

  17. 24: aload_1

  18. 25: invokevirtual #5        // Method algo/MovingAverage.getAvg:()D

  19. 28: dstore     4

  20. 40LocalVariableTable:

  21. Start  Length  Slot  Name   Signature

  22. 0       31         0    args   [Ljava/lang/String;

  23. 8       23        1      ma     Lalgo/MovingAverage;

  24. 10      21         2     num1   I

  25. 12       19         3      num2   I

  26. 30       1        4    avg     D

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

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

Java代码  

  1. MovingAverage ma = new MovingAverage();

  2. for (int number : numbers) {

  3. ma.submit(number);

  4. }

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

Java代码  

  1. 0: new #2 // class algo/MovingAverage

  2. 3: dup

  3. 4: invokespecial #3 // Method algo/MovingAverage."<init>":()V

  4. 7: astore_1

  5. 8: getstatic #4 // Field numbers:[I

  6. 11: astore_2

  7. 12: aload_2

  8. 13: arraylength

  9. 14: istore_3

  10. 15: iconst_0

  11. 16: istore 4

  12. 18: iload 4

  13. 20: iload_3

  14. 21: if_icmpge 43

  15. 24: aload_2

  16. 25: iload 4

  17. 27: iaload

  18. 28: istore 5

  19. 30: aload_1

  20. 31: iload 5

  21. 33: i2d

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

  23. 37: iinc 4, 1

  24. 40: goto 18

  25. 43: return

  26. LocalVariableTable:

  27. Start  Length  Slot  Name   Signature

  28. 30       7         5    number I

  29. 12       31        2    arr$     [I

  30. 15       28        3    len     $I

  31. 18       25         4     i$      I

  32. 0       49         0     args  [Ljava/lang/String;

  33. 8       41         1    ma     Lalgo/MovingAverage;

  34. 48      1         2    avg    D

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

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

Java代码  

  1. 18: iload 4

  2. 20: iload_3

  3. 21: if_icmpge 43

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

Java代码  

  1. 37: iinc       4, 1       // increment i$

  2. 40: goto       18         // jump back to the beginning of the loop

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

 

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

Java代码  

  1. 31: iload 5
  2. ?33: i2d?
  3. 34: invokevirtual #5 // Method algo/MovingAverage.submit:(D)V

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

原创文章转载请注明出处:http://it.deepinmind.com

时间: 2024-10-13 19:30:14

Java字节码基础[转]的相关文章

从零写一个编译器(十一):代码生成之Java字节码基础

项目的完整代码在 C2j-Compiler 前言 第十一篇,终于要进入代码生成部分了,但是但是在此之前,因为我们要做的是C语言到字节码的编译,所以自然要了解一些字节码,但是由于C语言比较简单,所以只需要了解一些字节码基础 JVM的基本机制 JVM有一个执行环境叫做stack frame 这个环境有两个基本数据结构 执行堆栈:指令的执行,都会围绕这个堆栈来进行 局部变量数组,参数和局部变量就存储在这个数组. 还有一个PC指针,它指向下一条要执行的指令. 举一个例子 int f(int a, int

java字节码忍者禁术

Java语言本身是由Java语言规格说明(JLS)所定义的,而Java虚拟机的可执行字节码则是由一个完全独立的标准,即Java虚拟机规格说明(通常也被称为VMSpec)所定义的. JVM字节码是通过javac对Java源代码文件进行编译后生成的,生成的字节码与原本的Java语言存在着很大的不同.比方说,在Java语言中为人熟知的一些高级特性,在编译过程中会被移除,在字节码中完全不见踪影. 这方面最明显的一个例子莫过于Java中的各种循环关键字了(for.while等等),这些关键字在编译过程中会

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

原文出处: jaffa 很多时候我们在编写Java代码时,判断和猜测代码问题时主要是通过运行结果来得到答案,本博文主要是想通过Java字节码的方式来进一步求证我们已知的东西.这里没有对Java字节码知识进行介绍,如果想了解更多的Java字节码或对其感兴趣的朋友可以先阅读字节码基础:JVM字节码初探. String字面量可以通过’==’判断两个字符串是否相同,是因为大家都知道’==’是用来判断两个对象的值引用地址是否一致,两个值一样的字符串字面量定义是否指向同一个值内存地址呢?答案是肯定的. 1

java字节码速查笔记

java字节码速查笔记 发表于 2018-01-27 |  阅读次数: 0 |  字数统计: |  阅读时长 ≈ 执行原理 java文件到通过编译器编译成java字节码文件(也就是.class文件),这个过程是java编译过程:而我们的java虚拟机执行的就是字节码文件.不论该字节码文件来自何方,由哪种编译器编译,甚至是手写字节码文件,只要符合java虚拟机的规范,那么它就能够执行该字节码文件 字节码学习好文 http://blog.csdn.net/shenhonglei1234/articl

一文让你明白Java字节码

也许你写了无数行的代码,也许你能非常溜的使用高级语言,但是你未必了解那些高级语言的执行过程.例如大行其道的Java. Java号称是一门"一次编译到处运行"的语言,但是我们对这句话的理解深度又有多少呢?从我们写的java文件到通过编译器编译成java字节码文件(也就是.class文件),这个过程是java编译过程:而我们的java虚拟机执行的就是字节码文件.不论该字节码文件来自何方,由哪种编译器编译,甚至是手写字节码文件,只要符合java虚拟机的规范,那么它就能够执行该字节码文件.那么

一文让你明白 Java 字节码

前言 也许你写了无数行的代码,也许你能非常溜的使用高级语言,但是你未必了解那些高级语言的执行过程.例如大行其道的Java. Java号称是一门"一次编译到处运行"的语言,但是我们对这句话的理解深度又有多少呢?从我们写的java文件到通过编译器编译成java字节码文件(也就是.class文件),这个过程是java编译过程:而我们的java虚拟机执行的就是字节码文件.不论该字节码文件来自何方,由哪种编译器编译,甚至是手写字节码文件,只要符合java虚拟机的规范,那么它就能够执行该字节码文件

从1+1=2来理解Java字节码从1+1=2来理解Java字节码

编译“1+1”代码 首先我们需要写个简单的小程序,1+1的程序,学习就要从最简单的1+1开始,代码如下: 写好java类文件后,首先执行命令javac TestJava.java 编译类文件,生成TestJava.class. 然后执行反编译命令javap -verbose TestJava,字节码结果显示如下: Compiled from "TestJava.java" public class top.luozhou.test.TestJava minor version: 0 m

深入了解 Java 字节码

1.1 什么是字节码? Java 在刚刚诞生之时曾经提出过一个非常著名的口号: "一次编写,到处运行(write once,run anywhere)",这句话充分表达了软件开发人员对冲破平台界限的渴求."与平台无关"的理想最终实现在操作系统的运用层上: 虚拟机提供商开发了许多可以运行在不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的"一次编写到处运行". 各种不同平台的虚拟机与所有平台都统一使用的程序存储格

如何调教java字节码

本文地址:http://www.cnblogs.com/herbix/p/3541093.html java字节码是直接在在jvm上运行的代码.和简单易懂的java程序不同,java字节码是类似于汇编的指令串,不过比汇编的指令集要小很多,java字节码可优化的余地没有那么大,想直接编写字节码也要比编写汇编容易许多. <JAVA虚拟机规范 java SE 7>这本书上详细介绍了制作一个java虚拟机的过程,包括类文件的结构.指令集.还有一些约束等等,其余的部分是由虚拟机的编写者决定的.既然我们的