Java字节码结构剖析一:常量池

Class文件的结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间地数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

每一个 Class 文件对应于一个如下所示的 ClassFile 结构体。

ClassFile{u4magic;u2minor_version;u2major_version;u2constant_pool_count;cp_infoconstant_pool[constant_pool_count-1];u2access_flags;u2this_class;u2super_class;u2interfaces_count;u2interfaces[interfaces_count];u2fields_count;field_infofields[fields_count];u2methods_count;method_infomethods[methods_count];u2attributes_count;attribute_infoattributes[attributes_count];}

这种数据结构,类似C语言结构体。这个结构体中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础,所以这里要先介绍这两个概念。

无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质就是一张表。

下面是我的案例代码,本章将以此代码生成的字节码文件作为例子来分析。

publicclassMyTest2{    String str ="Welcome";privateintx =5;publicstaticIntegerin=10;publicstaticvoidmain(String[] args){        MyTest2 myTest2 =newMyTest2();        myTest2.setX(8);in=20;    }publicvoidsetX(intx){this.x = x;    }}

对应生成的字节码文件格式如下:(数据内容较多,只是截了部分)

上面的数字是以16进制表示的。我们可以按照之前的结构一项项去解读它。

Class文件解析

magic

魔数,u4类型的数据,占4个字节。魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的 Class 文件。魔数值固定为 0xCAFEBABE (咖啡宝贝),不会改变。

minor_version、major_version

紧接着魔数之后的4个字节为Java版本信息:第5和第6个字节是次版本号(minor_version),第7和第8个字节是主版本号(major_version)。

就看当前这个字节码,次版本号是0×0000=0,主版本号是0×0034=52。我本地机器用的是JDK1.8,所以可生成的Class文件主版本号最大值为52.0。

下面给出了Java各个主版本号,以供参考。

constant_pool_count

常量池计数器,u2类型的数据。它是常量池的入口,表示紧跟着它后面的常量池的元素个数。算一下,0x002F=47,即常量池里的元素有47个。这里我用jdk的内置工具javap,反编译一下,可以输出常量池的信息以及元素个数。执行命令:javap -verbose。输出结果如下:

Constant pool:#1= Methodref#10.#34// java/lang/Object."<init>":()V#2=String#35// Welcome#3= Fieldref#5.#36// com/shengsiyuan/jvm/bytecode/MyTest2.str:Ljava/lang/String;#4= Fieldref#5.#37// com/shengsiyuan/jvm/bytecode/MyTest2.x:I#5=Class#38// com/shengsiyuan/jvm/bytecode/MyTest2#6= Methodref#5.#34// com/shengsiyuan/jvm/bytecode/MyTest2."<init>":()V#7= Methodref#5.#39// com/shengsiyuan/jvm/bytecode/MyTest2.setX:(I)V#8= Methodref#40.#41// java/lang/Integer.valueOf:(I)Ljava/lang/Integer;#9= Fieldref#5.#42// com/shengsiyuan/jvm/bytecode/MyTest2.in:Ljava/lang/Integer;#10=Class#43// java/lang/Object#11= Utf8              str#12= Utf8              Ljava/lang/String;#13= Utf8              x#14= Utf8              I#15= Utf8in#16= Utf8              Ljava/lang/Integer;#17= Utf8              #18= Utf8              ()V#19= Utf8              Code#20= Utf8              LineNumberTable#21= Utf8              LocalVariableTable#22= Utf8              this#23= Utf8              Lcom/shengsiyuan/jvm/bytecode/MyTest2;#24= Utf8              main#25= Utf8              ([Ljava/lang/String;)V#26= Utf8              args#27= Utf8              [Ljava/lang/String;#28= Utf8              myTest2#29= Utf8              setX#30= Utf8              (I)V#31= Utf8              #32= Utf8              SourceFile#33= Utf8              MyTest2.java#34= NameAndType#17:#18// "<init>":()V#35= Utf8              Welcome#36= NameAndType#11:#12// str:Ljava/lang/String;#37= NameAndType#13:#14// x:I#38= Utf8              com/shengsiyuan/jvm/bytecode/MyTest2#39= NameAndType#29:#30// setX:(I)V#40=Class#44// java/lang/Integer#41= NameAndType#45:#46// valueOf:(I)Ljava/lang/Integer;#42= NameAndType#15:#16// in:Ljava/lang/Integer;#43= Utf8              java/lang/Object#44= Utf8              java/lang/Integer#45= Utf8              valueOf#46= Utf8              (I)Ljava/lang/Integer;

可是,我们得到的常量池里的元素个数是46。我们看常量池第一个元素,它的索引是从1开始的。所以索引值范围是1~46。设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中。这个常量就对应Null值,所以常量池的索引从1而非0开始。

常量池结构剖析

紧接其后的就是常量池了。一个Java类中定义的很多信息都是由常量池维护和描述的。可以将常量池看作是Class文件的资源库。比如:Java类中定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面常量和符号引用。字面量,如文本字符串,Java中声明为常量值,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。

注:常量池中存储的不一定是不变的量!如, private int x = 5 ,x是变量,但“x”这个变量名字依然存在常量池中。

我们也可以把常量池当做一个数组(常量池中的每一项常量都是一个表),与一般数组不同的是,常量池数组中不同的元素类型,结构都是不同的,长度当然也不相同;但是每一个元素的第一个数据都是u1类型,该字节是个标志位,占一个字节。JVM在解析长量池时,会根据这个u1类型来获取元素的具体类型。目前,常量池中出现的常量类型有14种,如下表:

有了这张表就可以继续剖析常量池的内容了,常量池第一个字节就是一个标志位,0x000A=10,说明第一个常量类型是CONSTANT_Methodref_info。这是一个表类型,它对应的结构是:

CONSTANT_Methodref_info{u1tag;u2class_index;u2name_and_type_index;}

可知,该类型常量占1+2+2=5个字节。所以我们从常量池前5个字节就是第一个常量元素了。紧接后面就是第二个常量,同样的,开始是一个标志位,即0x008=8。可知,第二个常量是CONSTANT_String_info类型。CONSTANT_String_info 用于表示 java.lang.String 类型的常量对象,格式如下:

CONSTANT_String_info{u1tag;u2string_index;}

所以常量池的第二个元素占3个字节。按照这个套路,我们就可以找出每一个常量了。一直数到第46个常量,常量池就结束了。此处是常量池中的 14种常量项的结构总表 。感兴趣的可以对照这个表,去把剩下的常量对照出来。

常量项分析

第一个常量是CONSTANT_Methodref_info类型的,它描述了类中方法的符号引用。class_index 项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Class_info结构,表示一个类或接口。

class_index表示的索引值是0x000A=10。根据之前 javap -verbose  输出的常量池信息,我们可以知道常量池的#10项是CONSTANT_Class_info类型的常量。该类型常量用于表示类或接口,格式如下:

CONSTANT_Class_info{u1tag;u2name_index;}

name_index 项的值,必须是对常量池的一个有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,代表一个有效的类或接口二进制名称的内部形式。

name_index 表示的索引值是43(这里我直接从上面的量池信息读出,如果从字节码里看,此处的值为0x002B=43)。所以接着找常量池第43项的常量类型,是CONSTANT_utf8_info类型,用于表示字符串常量的值,结构如下:

CONSTANT_Utf8_info{u1tag;u2length;u1bytes[length];}

其中,length 项的值指明了 bytes[]数组的长度,bytes[]是表示字符串值的byte数组。在这里,我把字节码常量池中#43处常量的16进制值单独拿出来来看。下图有背景色的部分就是完整的CONSTANT_Utf8_info类型常量表示。

第一个字节是标志位,0×0001=1。说明此常量类型是CONSTANT_Utf8_info。后面2个字节是0×0010=16,表示后面bytes[]长度为16。所以往后数16个字节就是整个它表示的字符串常量。

bytes[]第一个字节值,0x006A。根据 ASCII码对照表 ,代表的字符串是”j”。依次的,第二个字节0×0061,代表“a”,等等。把16个字节看完你就得到了字符串常量表示“java/lang/Object”。好了这表示一个类的全限定名。饶了一大圈,终于找到最终要表示的常量信息了。

到此,我们把第一个常量的结构中的class_index就解析完了,还剩一个name_and_type_index。它表示了常量池在该索引处的项必须是 CONSTANT_NameAndType_info结构,它表示当前字段或方法的名字和描述符。后面大家可以根据 常量池中的14种常量项的结构总表 ,并结合javap得到的常量池信息,自己去分析每个常量在常量池里是怎么个回事。

总结

这篇文章介绍了,字节码文件的结构组成,并分析了魔数、次主版本号和常量池。尤其带大家深入分析了常量池的组成结构,并拿例子中的常量池第一个常量作为案例,完整解析它在常量池中的各项引用。套路都是一样的,常量池后面的常量,大家可以自己去分析了。你会发现类中有用的信息都存在了我们的常量池里,然后以索引的形式,给代码使用。这也就是常量池作为class文件的资源仓库的原因了。

 在此我向大家推荐一个架构学习交流群。交流学习群号:938837867 暗号:555 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容