从JVM Instructions看Java

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


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


先写一个简单的Java程序:

import java.util.*;
public class Demo{
    private static double d = 3.14;
    public static void main(String[] args){
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        int size = list.size();
        String s = null;
        if(size>0)
            s = list.get(0);
        if(s!=null)
            s+=" World!";
        System.out.println(d);
        System.out.println(s);
    }
}

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

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

有了这一段字节码,我们就可以初步开始认识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.sun.com/docs/books/vmspec/index.html



你可能感兴趣的:(从JVM Instructions看Java)