1.Class文件基础 (1)文件格式 Class文件的结构不像XML等描述语言那样松散自由。由于它没有任何分隔符号, 所以,以上数据项无论是顺序还是数量都是被严格限定的。哪个字节代表什么 含义,长度是多少,先后顺序如何,都不允许改变。 (2)数据类型 仔细观察上面的Class文件格式,可以看出Class文件格式采用一种类似于C语言 结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。 无符号数就是u1、u2、u4、u8来分别代表1个、2个、4个、8个字节。表是由 多个无符号数或其他表构成的复合数据类型,以“_info”结尾。在表开始位置, 通常会使用一个前置的容量计数器,因为表通常要描述数量不定的多个数据。 下图表示的就是Class文件格式中按顺序各个数据项的类型: (3)兼容性 高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件, 即使文件格式未发生任何变化。举例来说,JDK 1.7中的JRE能够执行JDK 1.5编译 出的Class文件,但是JDK 1.7编译出来的Class文件不能被JDK 1.5使用。这就是 target参数的用处,可以在使用JDK 1.7编译时指定-target 1.5。 2.一个简单的例子 编译成Class文件后的样子: 3.逐个字节分析 (1)魔数和版本号 前四个字节(u4)cafebabe就是Class文件的魔数,第5、6字节(u2)是Class文件的 次版本号,第7、8字节(u2)是主版本号。十六进制0和32,也就是版本号为50.0, 即JDK 1.6。之前介绍的target参数会影响这四个字节的值,从而使Class文件兼容不同 的JDK版本。 (2)常量池 常量池是一个表结构,并且就像之前介绍过的,在表的内容前有一个u2类型的计数器, 表示常量池的长度。十六进制23的十进制值为35,表示常量池里有下标为1~34的表项。 下标从1开始而不是0,是因为第0个表项表示“不引用常量池中的任意一项”。每个表项 的第一个字节是一个u1类型,表示12中数据类型。具体含义如下: 以第一项07 00 02为例,07表示该常量是个CONSTANT_Class_info类型,紧接着一个u2 类型的索引执行第2项常量。再看第二项01 00 24 63 6f 6d 2f ... 65表示的就是字符串 类型,长度为36(十六进制00 24),紧接着就是UTF-8编码的字符串"com/cdai/jvm/bytecode /ByteCodeSample"。很容易读懂吧?常量池主要是为后面的字段表和方法表服务的。 下面是通过javap解析后常量池的全貌(执行javap -c -l -s -verbose ByteCodeSample) Constant pool: const #1 = class #2; // com/cdai/jvm/bytecode/ByteCodeSample const #2 = Asciz com/cdai/jvm/bytecode/ByteCodeSample; const #3 = class #4; // java/lang/Object const #4 = Asciz java/lang/Object; const #5 = Asciz msg; const #6 = Asciz Ljava/lang/String;; const #7 = Asciz <init>; const #8 = Asciz ()V; const #9 = Asciz Code; const #10 = Method #3.#11; // java/lang/Object."<init>":()V const #11 = NameAndType #7:#8;// "<init>":()V const #12 = String #13; // hello world const #13 = Asciz hello world; const #14 = Field #1.#15; // com/cdai/jvm/bytecode/ByteCodeSample.msg:Ljava/lang/String; const #15 = NameAndType #5:#6;// msg:Ljava/lang/String; const #16 = Asciz LineNumberTable; const #17 = Asciz LocalVariableTable; const #18 = Asciz this; const #19 = Asciz Lcom/cdai/jvm/bytecode/ByteCodeSample;; const #20 = Asciz say; const #21 = Field #22.#24; // java/lang/System.out:Ljava/io/PrintStream; const #22 = class #23; // java/lang/System const #23 = Asciz java/lang/System; const #24 = NameAndType #25:#26;// out:Ljava/io/PrintStream; const #25 = Asciz out; const #26 = Asciz Ljava/io/PrintStream;; const #27 = Method #28.#30; // java/io/PrintStream.println:(Ljava/lang/String;)V const #28 = class #29; // java/io/PrintStream const #29 = Asciz java/io/PrintStream; const #30 = NameAndType #31:#32;// println:(Ljava/lang/String;)V const #31 = Asciz println; const #32 = Asciz (Ljava/lang/String;)V; const #33 = Asciz SourceFile; const #34 = Asciz ByteCodeSample.java; (3)访问标志 显然,00 21表示的就是公有的类。 (4)类、父类、接口 这三个u2类型的值分别表示类索引1、父类索引3、接口索引集合0。查看之前的常量池, 第1项为"com/cdai/jvm/bytecode/ByteCodeSample",第3项为"java/lang/Object"。第0项 表示此类没有实现任何接口,这也就是常量池第0项的作用! (5)字段表 00 01表示有1个字段。00 02是字段的访问标志,表示private权限的。00 05是字段的名称 索引,指向常量池里第5项"msg"。00 06是字段的描述符索引,指向常量池里的第6项 "Ljava/lang/String"。最后的00 00表示该字段没有其他属性表了。 描述符的作用就是用来描述字段的数据类型、方法的参数列表和返回值。而属性表就是为 字段表和方法表提供额外信息的表结构。对于字段来说,此处如果将字段声明为一个static final msg = "aaa"的常量,则字段后就会跟着一个属性表,其中存在一项名为ConstantValue, 指向常量池中的一个常量,值为的"aaa"。 属性表不像Class文件中的其他数据项那样具有严格的顺序、长度和内容,任何人实现的编译器 都可以向属性表中写入自己定义的属性信息,JVM会忽略掉它不认识的属性。后面的方法表中 还要用到属性表的Code属性,来保存方法的字节码。 (6)方法表 00 02表示有两个方法。00 01是方法的访问标志,表示公有方法。00 07和00 08与字段表中的名称 和描述符索引相同,在这里分别表示"<init>"和"()V"。00 01表示该方法有属性表,属性名称为00 09 即我们前面提到的Code属性。 要注意的是:Code属性表也可以有自己的属性,如后面的LocalVariableTable和LineNumberTable。 它们分别为JVM提供方法的栈信息和调试信息。 以下是javap解析后的结果: public com.cdai.jvm.bytecode.ByteCodeSample(); Signature: ()V LineNumberTable: line 3: 0 line 5: 4 line 3: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcom/cdai/jvm/bytecode/ByteCodeSample; Code: Stack=2, Locals=1, Args_size=1 0: aload_0 1: invokespecial #10; //Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #12; //String hello world 7: putfield #14; //Field msg:Ljava/lang/String; 10: return LineNumberTable: line 3: 0 line 5: 4 line 3: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcom/cdai/jvm/bytecode/ByteCodeSample; public void say(); Signature: ()V LineNumberTable: line 8: 0 line 9: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcom/cdai/jvm/bytecode/ByteCodeSample; Code: Stack=2, Locals=1, Args_size=1 0: getstatic #21; //Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #14; //Field msg:Ljava/lang/String; 7: invokevirtual #27; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 10: return LineNumberTable: line 8: 0 line 9: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcom/cdai/jvm/bytecode/ByteCodeSample; 4.小结 怎么样并不难吧!接下来我们将要学习下如何用字节码工具如ASM、CGLIB来手写 字节码,从而加深对字节码的理解。 |
|