public class ByteCodeDemo {
public static void main(String[] args) {
System.out.PRintln("Hello world");
}
}
在编译这个类后,你可以用十六进制编辑器打开.class文件然后参照虚拟机规范翻译字节码。幸运的是有更简单的方法。JDK包含一个命令行的反汇编器:javap,它可以转换字节码为一种可读的助记符形式,可以像下面这样通过传递'-c'参数给javap得到字节码列表: public class ByteCodeDemo extends java.lang.Object {
public ByteCodeDemo();
public static void main(java.lang.String[]);
}
Method ByteCodeDemo()
0 aload_0
1 invokespecial #1 <Method java.lang.Object()>
4 return
Method void main(java.lang.String[])
0 getstatic #2 <Field java.io.PrintStream out>
3 ldc #3 <String "Hello world">
5 invokevirtual #4 <Method void println(java.lang.String)>
8 return
仅仅从这个短小的列表你可以学到很多字节码的知识。从main方法的第一个指令开始: 0 getstatic #2 <Field java.io.PrintStream out>
开始的整数是方法中的指令的偏移值,因此第一个指令以0开始。紧随偏移量是指令的助记符(mnemonic)。在这个范例中,'getstatic' 指令将一个静态成员压入一个称为操作数堆栈的数据结构,后续的指令可以引用这个数据结构中的成员。getstatic 指令后是要压入的成员。在这个例子中,要压入的成员是"#2 <Field java.io.PrintStream out>" 。假如你直接检查字节码,你会看到成员信息没有直接嵌入指令而是像所有由java类使用的常量那样存储在一个共享池中。将成员信息存储在一个常量池中可以减小字节码指令的大小,因为指令只需要存储常量池中的一个索引而不是整个常量。在这个例子中,成员信息位于常量池中的#2处。常量池中的项目的顺序是和编译器相关的,因此在你的环境中看到的可能不是'#2' 。 public float add(float f, int n) {
return f + n;
}
假如你将这个方法加入上面的范例中,重新编译它,然后运行javap,你将看到的字节码类似这个: Method float add(float, int)
0 fload_1
1 iload_2
2 i2f
3 fadd
4 freturn
在方法的开始,虚拟机将方法的参数放入一个称为局部变量表的数据结构中。将像名字暗示的那样,局部变量表也包含了你声明的任何局部变量。在这个例子中,方法以三个局部变量表的项开始,这些都是add方法的参数,位置0保存this引用,而位置1和2分别保存float和int参数。 //返回 str1+str2 的串连
String concat(String str1, String str2) {
return str1 + str2;
}
//将 str2 附加到 str1
void concat(StringBuffer str1, String str2) {
str1.append(str2);
}
猜猜每个方法需要多少个方法调用。现在编译这些方法并且运行javap,你会得到类似下面的输出: Method java.lang.String concat1(java.lang.String, java.lang.String)
0 new #5 <Class java.lang.StringBuffer>
3 dup
4 invokespecial #6 <Method java.lang.StringBuffer()>
7 aload_1
8 invokevirtual #7 <Method java.lang.StringBuffer append(java.lang.String)>
11 aload_2
12 invokevirtual #7 <Method java.lang.StringBuffer append(java.lang.String)>
15 invokevirtual #8 <Method java.lang.String toString()>
18 areturn
Method void concat2(java.lang.StringBuffer, java.lang.String)
0 aload_1
1 aload_2
2 invokevirtual #7 <Method java.lang.StringBuffer append(java.lang.String)>
5 pop
6 return
concat1方法执行了5个方法调用s: new, invokespecial和三个invokevirtuals,这比concat2方法执行了更多的工作,后者只执行了一个invokevirtual调用。大多Java程序员已经得到过警告,因为String是不可变的,而使用StringBuffer进行字符串连接效率更高。使用javap分析这个使得这点变得很生动。假如你不能肯定两个语言构造在性能上是否相等,你应该使用javap分析字节码。然而,对just-in-time (JIT)编译器要小心,因为JIT编译器将字节码重新编译为本机代码而能执行一些javap不能揭示的附加优化。除非你有你的虚拟机的源代码,否则你应该补充你的字节码的基准性能分析。 public class ChangeALot {
public static final boolean debug=false;
public static boolean log=false;
}
public class EternallyConstant {
public static void main(String [] args) {
System.out.println("EternallyConstant beginning execution");
if (ChangeALot.debug)
System.out.println("Debug mode is on");
if (ChangeALot.log)
System.out.println("Logging mode is on");
}
}
假如你运行EternallyConstant,你会得到信息: EternallyConstant beginning execution
Logging mode is on
debug变量怎么了?即使你将debug设置为true,信息"Debug mode is on"并没有出现。答案在字节码中。对 EternallyConstant运行javap你会看到: Method void main(java.lang.String[])
0 getstatic #2 <Field java.io.PrintStream out>
3 ldc #3 <String "EternallyConstant beginning execution">
5 invokevirtual #4 <Method void println(java.lang.String)>
8 getstatic #5 <Field boolean log>
11 ifeq 22
14 getstatic #2 <Field java.io.PrintStream out>
17 ldc #6 <String "Logging mode is on">
19 invokevirtual #4 <Method void println(java.lang.String)>
22 return
惊异吧!在log成员上有一个'ifeq'检查,而代码根本没有检查debug成员。因为debug成员被标记为final类型,编译器知道debug成员在运行时永远不会改变,因此它通过移除'if'声明进行优化。这确实是一个非常有用的优化,因为它答应你在程序中嵌入调试代码而在将它设置为false时不用付出运行时的代价。不幸的是这个优化能够导致主要的编译时混乱。假如你改变一个final成员,你必须记住重新编译任何可能引用该成员的类。这是因为这个'reference'可能已经经过优化了。Java开发环境不能总是发现这个微妙的相关性,一些能导致非常希奇的错误。因此,古老的C++格言对于java环境仍然有效:"When in douBT, rebuild all."(有疑问,重新编译所有的代码)。 新闻热点
疑难解答