我们都知道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 }
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