zoukankan      html  css  js  c++  java
  • 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. }  
    6.    



    一旦这个类编译成了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.        4return  
    8. // Method java/lang/Object."<init>":()V  
    9. public static void main(java.lang.String[]);  
    10.      Code:  
    11.        0new           #2  
    12.        3: dup  
    13.        4: invokespecial #3  
    14.        7: astore_1  
    15.       8return  
    16. }  
    17.    



    我们可以看到字节码里有一个默认的构造方法以及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  
    12.    



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

    Java代码  收藏代码
    1. 0new           #2  
    2.       3: dup  
    3.       4: invokespecial #3  
    4.       7: astore_1  
    5.       8return  



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

     

    注意每条指令都可以用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. }  
    9.    



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

    Java代码  收藏代码
    1. [ ] Code:?0new  #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  
    27.    



    看起来更有意思了。。。我们看到这里创建了一个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. }  
    5.    



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

    Java代码  收藏代码
    1. 0new #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 41  
    24. 40goto 18  
    25. 43return  
    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  
    35.    



    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.    



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

    Java代码  收藏代码
    1. 37: iinc       41       // increment i$  
    2. 40goto       18         // jump back to the beginning of the loop  
    3.    




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

     

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

    Java代码  收藏代码
    1. 31: iload 5  
    2.    
    3. ?33: i2d?  
    4.    
    5. 34: invokevirtual #5 // Method algo/MovingAverage.submit:(D)V  
    6.    




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

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

  • 相关阅读:
    DotnetBrowser入门教程-(2)启动简单的Web服务
    DotnetBrowser入门教程-(1)浏览器控件使用
    Delphi初始化与结束化
    用友二次开发之用友备份专家[1.01]
    用友账套恢复工具
    用友二次开发之总账自定义结转
    用友二次开发之登陆界面
    用友二次开发之U810.1销售预订单导入
    表格控件表头栏目(Column)与数据表头步
    Delphi开发的IP地址修改工具
  • 原文地址:https://www.cnblogs.com/lnlvinso/p/3750137.html
Copyright © 2011-2022 走看看