Java诞生之始就提出一个口号叫“Write once,Run anywhere”,即代码编写一次、编译一次之后就可在所有平台上通用。实现平台无关性的基石是各种平台不同的虚拟机和所有平台统一使用的程序存储格式——字节码。这里的虚拟机相当于手机充电的转换头。这些字节码是存储在class文件中的,了解class文件结构是了解jvm的基础。
class文件是不包含任何的分隔符的,因此想要虚拟机读懂它就必须严格规定文件中数据项的顺序数量以及数据存储的字节序(class文件的字节序为Big-Endian),总体结构如下表。class文件的数据类型就两种:无符号数和表。无符号数用于描述数字、索引引用、数量值、或者是utf-8编码的字符串值,我们用u1、u2、u4、u8代表相应数量的无符号数。表就是有一定结构的无符号数,表名称习惯以“_info”结尾,其实整个class文件就能看做是一张表。当需要描述同一类型的不定数量的多个数据时,经常使用一个前置容量计数器加若干个连续数据项,我们称之为某一类型的集合。比如下表中的constant_pool常量池前的constant_pool_count。
下面我们结合一个类分析它的class文件结构,java文件和class文件内容如下。class文件用两种颜色区分每个部分
package com.cq.test; public class TestClass{ private int m; public int inc(){ return m+1; } }
①:magic魔数,唯一作用就是辨别是否class文件。作用真不大...class文件的魔数是0xCAFEBABE(咖啡?宝贝)
②:文件版本,第5、6字节是次版本号这里是0000,7、8字节是主版本号。jdk1.1的主版本号为45,之后大版本发布一次往上加1,0x0034即52说明笔者使用的是jdk1.8
③:常量池集合,0x0013(19)是常量池容量计数器。即有18个常量,从1开始计数,索引值范围为1~18,还有一个索引值为0的用于在特定情况下表达“不引用任何一个常量池项目”,合起来19个。
常量池中每一项都是一个表,都有各自的结构,长度也不尽相同,因此必须有个标志来区别属于那种表。上面紧接0x0013的是0x0A表示第一个常量是CONSTANT_Methodref_info表,其表结构是:tag u1、class_index u2(指向类或接口描述符的索引项)、name_and_type_index u2(指向名称及类型描述符的索引项)。所以第一项常量的内容为0x0A0004000F,表示该方法所属类的是第4个常量所表示的类,第15个常量表示了方法的简单名称和描述符(包括参数和返回类型等)
④:访问标志,这16位的二进制数用于描述类的访问信息,包括这个是否是接口;是否定义为public;是非是abstract等等。16位中一共使用了8位来描述1则为是0则为否。0x0021中0x0001表示为public类型的类,0x0020是jdk1.0.2之后都必须为真的值。
⑤:类索引、父类索引、接口索引集合,0x0003表示第3个常量描述了这个类,父类同。由于这个类没有继承接口则interface_count为0x0000
⑥:字段表集合,字段表的结构:access_flag u2;name_index u2;descriptor_index u2;attributes_count u2;arrtibutes 属性表具体长度看表结构 (字段值应该是在实例生成时由构造方法赋值的)
集合长度:0x0001 表示该类就一个字段
access_flag:类似访问标志0x0002表示字段是private的
name_index:索引值指向常量池,代表字段名称,这里指向第5项常量值为“m”
descriptor_index:索引值指向常量池,代表字段的类型,第6项常量值为“I”,表示这个字段是int类型的值。其他的F表示float类型,Z表示boolean类型,对象类型则用L加对象全限定名表示如:Ljava/lang/Object,数组的话则前置一个“[”来表示,如一个String[]数组,将被记录为“[Ljava/lang/String”
attributes_count:属性数量,这个例子中字段没有属性
attribute:属性
⑦:方法表集合,方法表结构与字段表结构一样。当然access_flag位所表示的含义与字段是不一样的。另外所有包含方法体的方法都有一个属性Code,方法体的字节码就存放在Code中
⑧:其他属性,最后一部分的0x0001表示还有一个其他属性0x000D是索引值表示属性名称,查询得第13个常量是值为“SourceFile”的utf8表。“SourceFile”属性的表结构为:attribute_name_index u2;attribute_length u4;sourcefile_index u2。
当然这样直接看字节码是不直观的,还可以通过jdk的javap工具的-verbose参数输出class文件字节码内容,现将该示例的字节码内容粘贴如下
Classfile /D:/TestClass.class Last modified 2016-10-8; size 287 bytes MD5 checksum 89c5dc104dfcc5ee3da3c336334615d9 Compiled from "TestClass.java" public class com.cq.test.TestClass minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#15 // java/lang/Object."<init>":()V #2 = Fieldref #3.#16 // com/cq/test/TestClass.m:I #3 = Class #17 // com/cq/test/TestClass #4 = Class #18 // java/lang/Object #5 = Utf8 m #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 inc #12 = Utf8 ()I #13 = Utf8 SourceFile #14 = Utf8 TestClass.java #15 = NameAndType #7:#8 // "<init>":()V #16 = NameAndType #5:#6 // m:I #17 = Utf8 com/cq/test/TestClass #18 = Utf8 java/lang/Object { public com.cq.test.TestClass(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public int inc(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field m:I 4: iconst_1 5: iadd 6: ireturn LineNumberTable: line 7: 0 } SourceFile: "TestClass.java"