道新闻 2017-03-25 08:26


本篇博客是对Java bytecode:这篇文章的翻译和解读,原文链接在这

http://www.ibm.com/developerworks/library/it-haggar_bytecode/index.html

如有不正之处还请各位指教,不喜勿喷,相互交流才能进步。

下面正片开始

生成java字节码:

javac Employee.java

javap -c Employee > Employee.bc

Generating bytecode

先将java源码进行编译后再用javap命令进行反编译并添加-c参数来获得类的字节码。获得字节码如下:

根据原文解读可大致猜测,Employee.java源码应该是这样的:

由此可以发现:前五行的字节码是用哪个类生成的,这个类的定义,这个类是从哪个类继承的,这个类的构造方法和其他的方法。下一步字节码将这个类的构造方法罗列出来,之后又将这个类的所有方法和与之相关的字节码用字典序罗列出来(这里我尝试了一下发现字节码中方法并没有按照字典序排序..不知是否是我的理解错误,测试图如下)。

抛开上面无关紧要的部分,继续。。。

这时候你可能会发现,特定的操作码的前缀a和i

这个操作码的前缀’a’表示的意思为:说明正在操作的是一个对象的引用。

同理’i’表示的意思为:说明正在操作的是一个整型变量

除了’a’和’i’还有以下几种操作码前缀:

‘b’:说明正在操作的是byte类型的变量

‘c’:说明正在操作的是char类型的变量

‘d’:说明正在操作的是double类型的变量

等等。。。

这里要注意:独立的代码一般会被jvm解析为操作码,多重的操作码指令一般会被解析为字节码。

The details

这里为了更深入的理解jvm是如何执行字节码的,我们要理解一下jvm。

Jvm是基于堆栈的,对于jvm来说每一个线程都会有一块独立的堆栈,堆栈中存储着栈帧,当线程中有方法被调用的时候就会创建一块栈帧并将其压入栈中,栈帧又由如下几块组成:操作数栈,局部变量区和一块当前线程所拥有的的类的引用(类的引用保存在常量池中)。

对于局部变量区来说,它既存储方法的参数,也存储方法中产生的一些中间变量。对于普通方法(静态方法)而言局部变量区首先存储的是方法的参数(从0开始),接着再存储方法中产生的局部变量。但是对于构造方法或者是实例方法而言,首先存储的是对象的引用(从0开始),接着第一个参数存1号位置,第二个参数存第二个位置,以此类推。对于静态方法来说,由于静态方法没有是类所拥有的,与对象无关,因此它的第一个参数存0号位置,第二个参数存1号位置,以此类推。。。

局部变量区的大小在编译的时候已经决定了,它主要取决于参数和中间变量的个数和大小

操作数栈用栈来push和pop值,一些特定的指令集会将操作数压入栈中,其他的指令会将这些操作数取走,并将执行的结果再次压入栈中。

上面一段是java源码,下面一段是相应的字节码。

这一段字节码由3个指令集组成。

先看第一个指令集 aload_0 首先根据之前讲的,’a’开头说明操作的是一个引用,结尾的0表示的是从局部变量区中取出放在0号位置的那个变量放入操作数栈,那么为什么要用aload而不用iload呢?我们看一下这个方法是一个实例方法,实例方法的局部变量区的0号位置存储的是对象引用(this),因此要用’a’开头。所有的一切是那么解释通了。还有一个问题,jvm为什么要取this引用呢?因为我们要用this引用来传递实例数据,名字等信息。

再看第二个指令集getfield #5,这个指令用来获取变量从一个对象中,当这个指令执行的时候,处于操作数栈顶端的this引用被拿出,和后面的#5组成一个索引去常量池中寻找相应属性(这里是找name,因为要获取name属性的引用)的引用,当这个引用找到并抓取了之后,将结果放入操作数栈中。

最后一个指令集areturn ,从这个方法中返回对应返回值的引用,并将这个返回值的引用从操作数栈中取出,压入调用方法的操作数栈中(栈帧中)。

下面讲一下最左边的数字是怎么来的,先上两张图:

相信你一定能够看懂(我不会说是因为我懒而不想翻译那么大一段英文。。。这里其实体现了java的平台无关性)

接着我们看一下构造函数的字节码:

首先第一行字节码跟我们上面分析一样,将构造方法的this引用取出来压入操作数栈中。

第二行字节码是调用父类的构造方法,因为所有的没有显示继承的类都隐式的继承的Object类,因此这里是调用了Object类的构造方法。也就是说,该构造方法的java源码其实应该是这样的:

跟之前分析的一样,当这行字节码执行完后,this引用从操作数栈中移除。

接下去的两行字节码aload_0和aload_1,就是将this引用和构造方法的第一个参数取出(不明白为什么的小伙伴请再阅读一遍局部变量区的那里哈),放入操作数栈中。

接下去的putfield #5这行字节码就是将上一步压入操作数栈中的this引用和strName值取出,并通过this和#5找到相应的strName引用将这个strName值赋给它。

下面那个给idNumber赋值的同理。

最后来看看最后5步aload_0 aload_1 iload_2 invokespecial #6 <Methodvoid storeData(java.lang.String, int)> return

前三条指令分别将this引用,strName值,idNumber值分别取出并压入操作数栈,注意this引用必须要被压入,因为这个实例方法正在被调用。如果这个方法是静态方法的话,this引用就不必被压入栈中,但是strName和idNumber必须要被压入到操作数栈中,因为他们是storedData方法的参数。因此当storedData方法执行的时候,this引用,strName和idNumebr分别占据storedData方法的局部变量区的0,1,2个索引。

Size and speed issues

这个模块就是比较两种相同功能的代码用怎么写比较快,解析成的字节码比较数量比较少。

下面看一下这两种线程同步方式解析所产生的的字节码数量的区别:

原文说道第一种方式大概要比第二种方式要快13%左右。但是我认为这仅限于方法内部都需要同步的情况,当方法只需要部分同步的时候,这时候在高并发情况下,直接加锁的方式效率显然要低于synchrnized块的方式。