
图3、字节码数组中的值
2A,B4和B0分别对应于aload_0,getfield和areturn。
public Employee(String strName, int num)
{
name = strName;
idNumber = num;
storeData(strName, num);
}
Method Employee(java.lang.String,int)
0 aload_0
1 invokespecial #3
4 aload_0
5 aload_1
6 putfield #5
9 aload_0 10 iload_2 11 putfield #4
14 aload_0 15 aload_1 16 iload_2 17 invokespecial #6
20 return
第一个操作码在位置0,aload_0,将引用推送到操作栈上。(记住,本地变量表用于实例方法和构造器的第一个入口就是该引用)。
下一个操作码指令在位置1,invokespecial,调用父类的构造器。因为,所有没有明确从任何其他类继承的类都隐式继承了java.lang.Object。编译器提供必需的字节码用于调用基类的构造器。在这些操作码中,操作栈的顶部值将会弹出。
下两个操作码,位于位置4和5,将本地变量表中的前两个实体推送到操作栈。第一个值是一个引用。第二个值是构造器的第一个正式的参数,strName。这些推送的值是为位于位置6的putfield操作码准备的。
putfield操作码弹出位于操作栈顶部的两个值,存储strName的一个引用到通过this引用的对象的实例属性name中。
下3个操作码指令位于9,10和11,使用第二个正常的构造器参数num,和实例变量idNumber,执行相同的操作。
接着的3个操作码指令,位于14,15和16,为storeData的方法调用准备栈数据。这些指令分别将this引用,strName和num入栈。这个引用必须入栈,因为一个实例方法被调用。如果该方法被声明为静态的,这个this引用就不需要入栈。由于strName和num是storeData方法的参数,所以它们的值需要入栈。当storeData方法执行时,this引用,strName和num,将分别占据该方法对应帧的本地变量表的0,1和2索引。
四、大小和速度问题
对于很多使用Java开发的桌面和服务端应用,性能是一个关键的问题。伴随着Java将这些系统迁移到更小的内嵌设备,大小问题也变的十分重要。了解对于一系列的Java指令将会产生什么样的字节码能帮你写更小,更高效的代码。例如,考虑Java中的同步。下面的两个方法返回一个通过数组实现的整数栈的顶部元素。两个方法都使用同步,功能上是等价的:
public synchronized int top1()
{
return intArr[0];
}
public int top2()
{
synchronized (this) {
return intArr[0];
}
}这些方法,尽管使用不同的同步方式,但效果是一致的。不明显的是,它们有不同的性能和字符数量。在这个例子中,top1大约比top2快百分之13,同时也更小。通过检查生成的字节码可以看到这些方法的不同。字节码中添加的注释用于解释每个操作码的作用。
Method int top1()
0 aload_0 //将本地变量表中索引为0的对象引用this入栈。
1 getfield #6
//弹出对象引用this,将访问常量池的intArr对象引用入栈。
4 iconst_0 //将0入栈。
5 iaload //弹出栈顶的两个值,将intArr中索引为0的值入栈。
6 ireturn //弹出栈顶的值,将其压入调用方法的操作栈,并退出。
Method int top2()
0 aload_0 //将本地变量表中索引为0的对象引用this入栈。
1 astore_2 //弹出this引用,存放到本地变量表中索引为2的地方。
2 aload_2 //将this引用入栈。
3 monitorenter //弹出this引用,获取对象的监视器。
4 aload_0 //开始进入同步块。将this引用压入本地变量表索引为0的地方。
5 getfield #6
//弹出this引用,压入访问常量池的intArr引用。 8 iconst_0 //压入0。 9 iaload //弹出顶部的两个值,压入intArr索引为0的值。 10 istore_1 //弹出值,将它存放到本地变量表索引为1的地方。 11 jsr 19 //压入下一个操作码(14)的地址,并跳转到位置19。 14 iload_1 //压入本地变量表中索引为1的值。 15 ireturn //弹出顶部的值,并将其压入到调用方法的操作栈中,退出。 16 aload_2 //同步块结束。将this引用压入到本地变量表索引为2的地方。 17 monitorexit //弹出this引用,退出监视器。 18 athrow //弹出this引用,抛出异常。 19 astore_3 //弹出返回地址(14),并将其存放到本地变量表索引为3的地方。 20 aload_2 //将this引用压入到本地变量索引为2的地方。 21 monitorexit //弹出this引用,并退出监视器。 22 ret 3 //从本地变量表索引为3的值(14)指示的地方返回。 Exception table: //如果在位置4(包括4)和位置16(排除16)中出现异常,则跳转到位置16. from to target type 4 16 16 any
top2比top1大,还慢,是因为采取的同步和异常处理方式。注意到top1使用synchronized方法修饰符,这不会产生额外的代码。相反,top2在方法体中使用synchronized语句。
在方法体中使用synchronized会产生monitorenter和monitorexit操作码的字节码,还有额外的用于处理异常的代码。如果在执行到同步锁的块(一个监视器)内部时,出现一个异常,这个锁要保证在退出同步块前被释放。top1的实现比top2略微高效些。这能获取到一小点的性能提升。
当synchronized方法修饰符出现时,就像top1中的那样,锁的获取和随后的释放不是通过monitorenter和monitorexit操作码实现的。而是在JVM调用一个方法时,它检查ACC_SYNCHRONIZED属性标识。如果有这个属性标识,正在执行的线程获取一个锁,调用方法,然后在方法返回时释放锁。如果同步方法抛出异常,在异常离开方法前锁会自动释放。
注意:如果出现synchronized方法修饰符,在方法的method_info结构中将会包含ACC_SYNCHRONIZED属性标识。
不管你使用synchronized作为方法修饰符还是作为同步块,这都有相同的含义。仅在你的代码需要同步,并且你明白它的代价时才使用同步方法。如果整个方法需要同步,我喜欢使用方法修饰符而不是同步代码块,以此来获取更小,更快的代码。
这只是使用字节码的知识来让你的代码更小,更快的一个示例。更多的信息可以参考我的书,Java实践。
五、编译选项
javac编译器提供了一些你有必要了解的可选项。第一个是-O