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 <init> #6 = Utf8 ()V #7 = NameAndType #5:#6 // "<init>":()V #8 = Methodref #4.#7 // java/lang/Object."<init>":()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."<init>":()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文件就是这样一个紧凑结构,不愧是为嵌入式而打造的一门语言。回到之前的问题,有没有可能自己来生成字节码?答案是肯定的,下回分解。
本文链接地址:https://dorole.com/1559/
《玩转Java字节码(上)》上有1条评论