分享

从JVM Instructions看Java

 catph 2011-04-14

我们都知道Java程序是运行在JVM里面的一段一段字节码,JVM需要做的就是把这些字节码转换成机器语言,使得Java程序能正确的运行在计算 机上,说的更底层一点就是正确分配内存,执行CPU计算并且释放内存。所以任何一个程序如果能做到以下几件事:读入Java Class文件、分析Class文件格式、为变量对象方法动态分配内存、管理这些变量和内存的回收,都可以做为我们所谓的虚拟机为Java程序员服务。从 这个意义上来说,JVM好似一个Java程序和机器语言的中间转换装置。

        要实现转换,JVM并不是一步到位的,程序员看到的Java源代码是最接近人类语言的可读性较高,便于我们设计程序的功能和逻辑。之后经过编译这些 java文件转换成了class文件,内容全都是字节码,这些字节码被JVM认识,有的字节是变量名,有的字节是变量值,有的字节是JVM指令集中的指 令,就如同汇编语言一样,这些字节码按照一定的顺序组合起来,一个指令后面会跟着固定数量的操作数。JVM也会维护内存中的一些栈结构,不断的push和 pop引用的地址或基本类型变量的值。我们可以通过研究一下JVM的指令集帮助我们理解Java语言,或者有时还可以帮助我们分析程序的性能。

        先写一个简单的Java程序:

  1. import java.util.*;
  2. public class Demo{
  3.     private static double d = 3.14;
  4.     public static void main(String[] args){
  5.         List<String> list = new ArrayList<String>();
  6.         list.add("Hello");
  7.         int size = list.size();
  8.         String s = null;
  9.         if(size>0)
  10.             s = list.get(0);
  11.         if(s!=null)
  12.             s+=" World!";
  13.         System.out.println(d);
  14.         System.out.println(s);
  15.     }
  16. }

        这里用到一些基本语法,例如接口签名、整型赋值、String构造、String操作等。之后我们执行javac Demo.java编译成Demo.class文件,JDK提供了一个反编译指令集命令javap,执行javap -c Demo > Instructions.txt会解析class文件,排版针对Demo.class文件生成JVM字节码,再来看看这个字节码吧:

  1. Compiled from "Demo.java"
  2. public class Demo extends java.lang.Object{
  3. public Demo();
  4.   Code:
  5.    0:   aload_0
  6.    1:   invokespecial   #1//Method java/lang/Object."<init>":()V
  7.    4:   return
  8. public static void main(java.lang.String[]);
  9.   Code:
  10.    0:   new #2//class java/util/ArrayList
  11.    3:   dup
  12.    4:   invokespecial   #3//Method java/util/ArrayList."<init>":()V
  13.    7:   astore_1
  14.    8:   aload_1
  15.    9:   ldc #4//String Hello
  16.    11:  invokeinterface #5,  2//InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
  17.    16:  pop
  18.    17:  aload_1
  19.    18:  invokeinterface #6,  1//InterfaceMethod java/util/List.size:()I
  20.    23:  istore_2
  21.    24:  aconst_null
  22.    25:  astore_3
  23.    26:  iload_2
  24.    27:  ifle    41
  25.    30:  aload_1
  26.    31:  iconst_0
  27.    32:  invokeinterface #7,  2//InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
  28.    37:  checkcast   #8//class java/lang/String
  29.    40:  astore_3
  30.    41:  aload_3
  31.    42:  ifnull  65
  32.    45:  new #9//class java/lang/StringBuilder
  33.    48:  dup
  34.    49:  invokespecial   #10//Method java/lang/StringBuilder."<init>":()V
  35.    52:  aload_3
  36.    53:  invokevirtual   #11//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  37.    56:  ldc #12//String  World!
  38.    58:  invokevirtual   #11//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  39.    61:  invokevirtual   #13//Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  40.    64:  astore_3
  41.    65:  getstatic   #14//Field java/lang/System.out:Ljava/io/PrintStream;
  42.    68:  getstatic   #15//Field d:D
  43.    71:  invokevirtual   #16//Method java/io/PrintStream.println:(D)V
  44.    74:  getstatic   #14//Field java/lang/System.out:Ljava/io/PrintStream;
  45.    77:  aload_3
  46.    78:  invokevirtual   #17//Method java/io/PrintStream.println:(Ljava/lang/String;)V
  47.    81:  return
  48. static {};
  49.   Code:
  50.    0:   ldc2_w  #18//double 3.14d
  51.    3:   putstatic   #15//Field d:D
  52.    6:   return
  53. }

        有了这一段字节码,我们就可以初步开始认识JVM的指令了,每个指令都有一个对应的字节码,我就顺便在讲 解中稍微标注几个在指令后的括号里方便大家理解,首先看到的是aload_0(字节码:0x19),表示从执行栈的下标0出读取一个引用出来,接着是 invokespecial(字节码:0xb7),官方解释:Invoke the method, special handling for superclass, private, and instance initialization method invocations。这里要区别于下面出现的invokevirtual指令(字节码:0xb6),它们的区别也就是invokevirtual更加 通用一点,会调用一个实例的方法,而invokespecial是定制针对三种情况下用的:

        1、私有方法

        2、调用父类继承下来的方法

        3、每个对象的初始化

        所以<init>动作用于创建对象时进行初始化,当在JVM Heap中创建对象时,一旦在Heap中分配了空间,最先就会调用"<init>"方法,包括实例变量的赋值和初始化静态块 等。#number指明了在操作栈中各个变量的下标。其他指令比如dup是赋值整个操作栈,pop弹出操作栈顶层值,ldc是指Push item from runtime constant pool,即从常量池中取值压入操作栈中。ifle和ifnull是判断分支语句,分别表示小于和是否是null,后面跟着的是判断成功的话当前指针应该 跳转的偏移量。关于offset(偏移量)相信学过C/C++或者汇编的理解起来更轻松一些。

 

        其他指令就不一一解释了,有一点小规律是关于int类型的操作指令一般都以i开头,类似的还有float 和double、array、char、short类型等。这里我特意做了一个String类型的+=操作,通过反编译指令我们可以看到JVM内部对于程 序中的"Hello"和" World!"都是从常量池中去出来的(ldc指令的含义),然后JVM构造了一个StringBuilder(早期的JDK1.4则应该是线程安全但效 率较的低StringBuffer),通过这个类来实现String的连接+=操作,每次+=返回的都是一个新的String对象。所以我们推荐在大量操 作String的时候,直接使用StringBuilder这个类。(记住不是线程安全限制更多的StringBuffer哦)

        在一些J2EE应用中,熟悉这些指令也能帮助我们深入到框架内部,比如JPA提出对实体映射类进行的 Enhance就会直接改变class文件的字节码,我们可以通过反编译观察这些指令来帮助我们分析一些利用动态代理或者Instrument接口实现的 对Class字节码进行的修改到底原理是怎么样?性能又如何?

        关于JVM指令集全部说明可以参见官方文档:http://java./docs/books/vmspec/index.html

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多