Java字节码(Byte-code)是指Java源代码编译而成的,供JVM虚拟机执行的代码。用文本编辑器打开将是一团乱码,用十六进制编辑器打开能勉强看懂头部一些规范,例如魔数,主次版本。而用/bin/javap“反编译”之后可以得到一个人类可读的代码段,类似于用Wireshark来分析cap数据包。
要看懂这个文件必须要知道字节码规范,主要有以下两个表格内容构成。其中表1是Class文件的组成部分,各段依次排列,排列紧密,无多余的分隔符。其中u1~u8表示占用字节数,1表示1个字节,2表示2个字节,4字节,8字节。例如魔数:仅有1个,占用4个字节,位于文件的前4个字节。紧接着4个字节是minor_version,major_version各占用2个字节。接下来2个字节是常量池数量统计,意味着一个Class不能有超过65535个常量池(maybe)。接下来是常量池,常量池的格式由表2来定义,下一段再来分析。依次类推可以将整个Class文件分析出来。
常量池中的每一个常量项通常有2~3个项目组成。例如:CONSTANT_Utf8_info这一项,第1个字节是表示定义(tag),值为1,紧接着2个字节表示该项将占用的长度(length),意味着接下来的几个字节长度将是字符串的实际内容。
再来分析下CONSTANT_Integer_info项,从下表可以看出头一个字节tag值为3,接下来4个字节安高位在前编码表示int值,所以在Java中int数据类型的最大值是去掉一个符号位的0x7FFFFFFF。Utf8,Integer,Float,Long,Double这5个都是类似的,直接以值的形式存储。其余常量项都是存储着引用值(index),分别指向这5个值或其它地方,这里不一一介绍了。
等等java不是八大基本数据类型吗,这里怎么只有提到了4个,没错,小于int的都当作int处理了。也就是boolean,byte,char,short都“拉长”至int级别来对待。例如:boolean a = “true” 对应的字节码指令是:iconst_1,putfield a,将int型常量值1进栈赋值给变量a。
介于编译器可能会做一些性能优化,例如int值超过2字节(32768)才会加入到常量项,小于2字节由sipush指令在运行时分配。
分析这玩意实在有些无聊,既然本文是以玩转为主题,自然就说点有趣的。首先一个简单的Java代码如下:
package classloader;
/**
* Created by dorole.com on 2016/6/13.
*/
public class Hello {
public String sayHello(String name) {
System.out.println(this.getClass().getName() + " -> " + name);
return name;
}
}
这段代码自然再简单不过了,随便一个文本编辑器敲进去,javac编译,找个main方法调用完事。那能不能不编译直接调用?当然没问题,先不谈这个,先来看看编译后究竟是个什么样子。我们用WinHex来打开编译好的Class文件,截图如下:
黑色标记的是全部的常量项tag值(一个个标出来,该是有多蛋疼~),自己可以对照表2慢慢分析。当然也可以借助javap来“反编译”下,输出更为直观的表格:
Constant pool:
#1 = Utf8 classloader.Hello
#2 = Class #1 // "classloader.Hello"
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8
#6 = Utf8 ()V
#7 = NameAndType #5:#6 // "":()V
#8 = Methodref #4.#7 // java/lang/Object."":()V
#9 = Utf8 sayHello
#10 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#11 = Utf8 java/lang/System
#12 = Class #11 // java/lang/System
#13 = Utf8 out
#14 = Utf8 Ljava/io/PrintStream;
#15 = NameAndType #13:#14 // out:Ljava/io/PrintStream;
#16 = Fieldref #12.#15 // java/lang/System.out:Ljava/io/PrintStream;
#17 = Utf8 java/lang/StringBuilder
#18 = Class #17 // java/lang/StringBuilder
#19 = Methodref #18.#7 // java/lang/StringBuilder."":()V
#20 = Utf8 getClass
#21 = Utf8 ()Ljava/lang/Class;
#22 = NameAndType #20:#21 // getClass:()Ljava/lang/Class;
#23 = Methodref #4.#22 // java/lang/Object.getClass:()Ljava/lang/Class;
#24 = Utf8 java/lang/Class
#25 = Class #24 // java/lang/Class
#26 = Utf8 getName
#27 = Utf8 ()Ljava/lang/String;
#28 = NameAndType #26:#27 // getName:()Ljava/lang/String;
#29 = Methodref #25.#28 // java/lang/Class.getName:()Ljava/lang/String;
#30 = Utf8 append
#31 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#32 = NameAndType #30:#31 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#33 = Methodref #18.#32 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#34 = Utf8 ->
#35 = String #34 // ->
#36 = Utf8 toString
#37 = NameAndType #36:#27 // toString:()Ljava/lang/String;
#38 = Methodref #18.#37 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#39 = Utf8 java/io/PrintStream
#40 = Class #39 // java/io/PrintStream
#41 = Utf8 println
#42 = Utf8 (Ljava/lang/String;)V
#43 = NameAndType #41:#42 // println:(Ljava/lang/String;)V
#44 = Methodref #40.#43 // java/io/PrintStream.println:(Ljava/lang/String;)V
#45 = Utf8 Code
这里我仅放上常量池部分,这下很好对了把。Class文件就是这样一个紧凑结构,不愧是为嵌入式而打造的一门语言。回到之前的问题,有没有可能自己来生成字节码?答案是肯定的,下回分解。