看懂一个Class文件
准备条件
魔数
版本号
常量池
访问标志
类索引,父类索引,接口索引集合
字段表集合
方法表集合
属性表集合
准备条件
1 Hex Editor工具用于将Class文件转换成16进制字节
2 https://tool.oschina.net/hexconvert 16进制转换网站
3 http://www.bejson.com/convert/ox2str/ 16进制转换字符串网站
4 源文件
5 本文内容大部分出自《深入理解Java虚拟机JVM高级特性与最佳实践》一书。本文的内容除了Class文件和书中不一致,其他内容以本书为标准。在这里我把这本书的电子版分享给大家,但是希望大家购买正版书籍支持作者不要学我天天白嫖
链接:https://pan.baidu.com/s/1TCLCzZp3cql4uCHP8w3dXw
提取码:8nzf
6 Hex Editor工具选择File->Open ->OpenFile 选择Class文件打开
左边的栏目是行号,右边的是Class的字节,右第一行是字节的下标。通过行号和下标可以确定字节的位置。
Class文件是一组以8位字节伪基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件中,中间没有添加任何的分隔符,这使得整个Class文件中存储得内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上的数据项,则会按照高位在前的方式分割若干个8位字节进行存储。根据Java虚拟机规范规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种类型:无符号数和表,后边解析都要以这两种数据为基础,所以这里要先介绍这两个概念。无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1,2,4,8个无符号数,无符号数可以用来描述数字,索引引用,数量值,或者按照UTG-8编码构成的字符串。表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯地以_info结尾。表用于描述有层次关系的复合结构的数据,一个完整的Class文件本质上就是一张表。
上一段是对Class文件的描述需要注意的 u1 u2 u4 u8代表 1 2 4 8个字节。Class内容分为两部分:1无符号数和表,无符号数可以是基本数据类型和字符串或者引用,表则是由多个无符号数组成的对象。我们还可以通过javap命令查看Class的描述。
一 魔数
每个Class的头4个字节称为魔数,它的唯一作用是确定这个文件是不是Class文件。它的位置是行00000000 列 00-03 它的值是固定的,cafebabe(咖啡宝贝)
二 版本号
魔数后边的4个字节是Class的版本号,也就是JDK的版本号。但是我的是JDK1.8在书中并没有显示。
三 常量池
版本号后是常量池,在javap命令显示的Constant pool是常量池描述常量池是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。常量池的入口有一个u2长度的数据,它代表常量池中的常量项总数。位置在行0列08-09值为0014转换10进制是20,计数器从1开始而并发0。既常量项总数是19。
往后的字节一大部分都是常量项,我把常量项简单的分为两种也是最好理解的。字符串字面量和符号引用。如果是字面量则表示可直接将字节转换成字符串,符号引用则是一个对象,里边可能包含字面量或者其他的符号引用。现在说这个可能不太理解,不要紧,解析几个常量项就明白了。每一个常量项的第一个字节都标识该常量项是什么类型,这个类型可能是字面量或者是符号引用。
关于JVM常量类型结构我这推荐一个前辈总结的博客,这个博客和书中所描述的一致,但书中是分散在各个章节不方便大家统一查看。这里贴出来前边的博文地址。https://my.oschina.net/roccn/blog/1544507
这里要额外的说明,一个常量项类型如果是1则表示它后边的字节是UTF-8类型的字符串否则都是符号引用,如果符号引用中包含字面量,那也是该字面量的引用。
行0列0a值0a是第1个常量项的类型转换成十进制是10是CONSTANT_Methodref它是一个符号引用也就是一个对象类型。它的结构是:
这个类型分为 u1 u2 u2三部分u1部分就是oa这个字节表示常量项的类型。剩余两部分都是符号引用。我们说过主要不是直接的字面量剩下的全部都是符号引用。它们的位置在行0列0b-0e。值是0004 0010转换十进制是4和16表示它们引用常量项#4和常量项#16。
行0列0f是第2项的类型它的值是09转换10进制是9对应的类型是CONSTANT_Fieldref它是一个符号引用也就是一个对象类型。它的结构是:
这个类型分为 u1 u2 u2三部分u1部分就是9这个字节表示常量项的类型。剩余两部分都是符号引用。它们的位置在行1列00-03值是0003 0011转换十进制是3,17表示它们引用常量项#3和常量项#17。
行1列04是第3个常量项07十进制是7它的类型是CONSTANT_Class_info。它的结构是:
它包含两部分u1是类型u2是一个CONSTANT_Utf-8_info上边说过,只要不是直接是CONSTANT_Utf-8_info类型的其余的都是符号引用。u2的位置在行1列05-06值是0012转换十进制是18引用常量项#18。
行1列07是第4个常量项07转换10进制是7类型是它还是CONSTANT_Class_info类型,u2的位置在行1列08-09 0013转换10进制是19表示引用#19
行1列0a是第5个常量项01转换10进制是1类型是CONSTANT_Utf8,注意这个类型是字面量也就是说,这个常量项是包含字符串的,它的结构是:
有一个u2的长度,长度后边的字节数组就是具体值。u2的位置在行1列0b-0c值是0004转换十进制是4表示后边4个字节是字符串。从行1列0d到行2列00值是6e 61 6d 65 转换成字符串是name。关于字符串的字节数量这里需要注意从 '\u0001'到'\u0007f'之间的字符其实就是1-127的ASCLL码的缩略字符占位1个字节,从'\u0080'到'\u07ff'之间的字符用两个字节表示,从'\u0800'到'\ufff'之间的是标准UTF-8使用三个字节。我这里的字符串只是英文字母在ASCLL范围内,如果你有汉字或者其他字符你又不知道怎么算一个字符几个字节。可以直接从length后查找length个字节使用文章开始推荐的16进制转换字符串的网站。
行2列01是第6个常量项值是01转换10进制是1它的类型是CONSTANT_Utf8行2列02-03是它的长度0012转换10进制是18这个字符串有18个字节。从行2列04到行3列05值是4c 6a 61 76 61 2f
6c 61 6e 67 2f 53 74 72 69 6e 67 3b转换字符串后是Ljava/lang/String;
行3列06是第7个常量项值是01转换10进制是1是CONSTANT_Utf8行3列07-08是它的长度0006转换10进制是6,它的值从行3列09-0e值是3c 69 6e 69 74 3e转换字符串是<init>行
行3列0f是第8个常量项值是01转换10进制它的类型是CONSTANT_Utf8行4列00-01是它的长度0003转换10进制是3,它的值从行4列02-04值是28 29 56转换字符串是()V
行4列05是第9个常量项值是01转换10进制是1类型是CONSTANT_Utf8行4列06-07是它的长度0004转换10进制是4,它的值从行4列08-0b值是43 6f 64 65转换字符串是Code
行4列0c是第10个常量项值是01转换10进制它的类型是CONSTANT_Utf8行4列0d-0e是它的长度000f转换10进制是15它的值从行4列0f到行5列0d值是4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65转换字符串是LineNumberTable
行5列0e是第11个常量项值是01转换10进制它的类型是CONSTANT_Utf8行5列0f到行6列07是它的长度是0007转换10进制是7,它的值从行6列01-07值是67 65 74 4e 61 6d 65转换字符串是getName
行6列08是第12个常量项值是01转换10进制1它的类型是CONSTANT_Utf8行6列09-0a是它的长度0014转换10进制是20,它的值从行6列0b到行7列0e值为28 29 4c 6a 61 76 61 2f
6c 61 6e 67 2f 53 74 72 69 6e 67 3b转换字符串是()Ljava/lang/String;
行7列0f是第13个常量项值是01转换10进制1它的类型是CONSTANT_Utf8行8列00-01是它的长度0007转换10进制是7,它的值从行8列02-08值为73 65 74 4e 61 6d 65转换字符串是setName
行8列09是第14个常量项值是01转换10进制1它的类型是CONSTANT_Utf8行8列0a-0b是它的长度000a转换10进制是10,它的值从行8列0c到行9列05值为53 6f 75 72 63 65 46 69 6c 65转换字符串是SourceFile
行9列06是第15个常量项值是01转换10进制1它的类型是CONSTANT_Utf8行8列07-08是它的长度000b转换10进制是11,它的值从行9列09到行a列03值为50 65 72 73 6f 6e 2e 6a 61 76 61转换字符串是Person.java
行a04是第16个常量项值是0c转换10进制是12它的类型是CONSTANT_NameAndType它的结构是:
它包含u1 u2 u2 后边两个u2在行a列05-08值是0007 0008转换10进制是7,8它们指向的常量项是#7#8
行a04是第17个常量项值是0c转换10进制是12它的类型是CONSTANT_NameAndType和上边一样它的两个u2在行a列0a-0d值是0005 0006转换10进制是5,6它们指向的常量项是#5#6
行a列0e是第18个常量项值是01转换10进制1它的类型是CONSTANT_Utf8行a列0f到行b列00是它的长度0011转换10进制是17,它的值从行b列01到行c列01值为63 6f 6d 2f 64 61 74 61 6e 67 2f 50 65 72 73 6f 6e转换字符串是com/datang/Person
行c列02是第19也是最后一个常量项01转换10进制是1它的类型是CONSTANT_Utf8行c列03-04是它的长度0010转换10进制是16它的值从行c列05到行d列04值为6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74转换字符串是java/lang/Object
访问标志
常量池结束后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口:是否定义为public类型;是否定义为abstract类型;如果是类的话,是否呗声明为final。
行d列05-06是访问标志0021可以但是这个0021在上图并没有对应类型。这里需要注意访问表示就是4个字节,它的计算方式是,如果满足定义则累加。当前我的Person类满足ACC_PUBLIC和ACC_SUPER两个定义。0x0001和0x0020将它们转换10进制分别是1和32而0021转换10进制是33。
类索引,父类索引,接口索引集合
在访问标志后描述了类的索引信息,其中类索引u2,父类索引也是u2,接着是一个u2长度的接口计数器,如果该类没有实现任何接口则该计数器值为0,计数器后边也没有接口索引表。如果有接口则计数器后边跟着N个u2类型的接口索引。在我这个Person类中,没有接口但是类索引和父类索引肯定是有的。行d列07-08是类索引值是0003转换10进制是3表示引用常量池#3位置的值,行d列09-0a是父类索引值是0004转换10进制是4表示引用常量池#4位置的值,行d列0b-0c是接口索引计数器值是0000表示没有实现接口。这里着重看#3#4这两个常量项。这两个的值我们已经计算过的,可以往上翻翻看这里我提出来javap -v显示的信息,其实和我们手动算出来的是一致的。#3#4引用的是#18#19对应的字面量是com/datang/Person和java/lang/Object也就是我们的全类路径和父类全路径。
字段表集合
这里描述的字段为成员变量,不包括局部变量。在接口索引集合后跟着的是字段表的count。行d列0d-0e值0001转换10进制是1表示只有一个成员变量。count后跟的就是字段,字段也是一个表,它的结构如下:
u2类型的access_flags是字段访问标志,它的可选择如下:
行d列0f到行e列00值为0002对应的就是name字段的访问标志private。访问标志后是u2长度的name_index它是字段的简单名称,它对应的是常量池引用。行e列01-02值为0005转换10进制是5对应的是常量池#5的引用也就是name字面量。
u2长度的descriptor_index表示字段或者方法描述符,在这里就是字段的描述符。描述符的作用是用来描述字段的数据类型,方法的参数列表(包括数量,类型以及顺序 )和返回值。根据描述符规则,基本数据类型(byte,char,double,float,int,long,short,boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表述如下表:
对于数组类型,每一维度将使用一个前置的[字符来描述,如定义一个java.lang.String[][]类型的二维数组,将被记录为[[Ljava/lang/String;一个整数数组int[]将被记录为[I方法描述符描述方法时,按照先参数列表,后返回值的顺序描,参数列表按照参数的严格顺序放在一组()之内。如方法void setName()的描述符为()V,方法java.lang.Stirng.toString()的描述符为()Ljava/lang/String;方法int indexOf(char[]source,int sourceOffset,int sourceCount,
sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符
为([CII[CIII)
行e列03-04是u2的descriptor_index值是0006转换10进制是6对应的是字段的类型,在常量池#6的位置是Ljava/lang/String;
u2类型的attributes_count是属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零项或者多项的额外信息。对于本例中的字段name它的属性计数器在行e列05-06位置是0000也就是没有额外的描述信息,但是如果字段name的声明改成String name='zs';那么就会存在一个项目为ConstantValue的属性其值指向常量'zs'。最后一个属性attribute_info在该属性表中并没有实际作用,等下在方法表集合中会用到,目前它不占字节。
方法表集合
方法表集合的结构和字段的结构基本类似。
在行e列07-08值为0003转换10进制是3它是方法表计数器,其中两个方法是getName,setName还有一个是编译器添加的实例构造器方法<init>
方法访问标志如下:
u2长度的access_flags在行e列09-0a位置值为0001转换10进制是1是第一个方法的访问标志public。
u2长度的name_index在行e列0b-0c值为0007转换10进制是7它是方法的名称,指向#7也就是<init>是编译器生成的构造器方法。
u2长度的descriptor_index是方法的参数列表和返回值描述符号它的位置在行e列0d-0e值为0008指向的是常量池#8()V这个规则在字段描述表中说过的,先是参数然后是返回值。
u2长度的attributes_count是方法的属性表集合在行e列0f-行f列0值是0001转换10进制是1表示有一个属性。
属性表集合
属性表在字段表集合和方法表集合中都有,在咱们的例子中字段表集合中没有使用到属性表,而在方法表集合中,行e列0f-行f列0位置标记<init>方法是存在一个属性表的。
与Class文件中其他的数据项目要求严格的顺序,长度和内容不同,属性表集合的限制稍微宽松了一些,不在要求各个属性表具有严格的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。为了能正确解析Class文件《Java虚拟机(第二版)规范》中预定义了9项虚拟机应当能识别的属性,而在最新《Java虚拟机规范(Java SE 7)》版本中,预定义的属性已经增加到了21项。如下图:
现在我们需要确定的是当前<init>方法的属性表是什么类型。对于这21种数据类型来说它们有一个共性的表结构如下:
u2长度的attribute_name_index表示属性表的名称,u4长度的attribute_length表示该属性的总长度,而剩余的info则根据每个属性表的不同展示的内容,长度也不同。在本文中,不会对21中类型都涉及,只会拿代码中使用到的类型来解释。
行f列1-2值为0009转换10进制是9它是attribute_name_index也就是属性表的名称,查询这个名称总是对应常量池中的引用#9的值是Code。也就是说,当前这个属性表是Code属性类型。它的结构如下:
行f列3-7值为0000001d转换10进制是29是attribute_lenth的值。这里需要说明下,这个长度是除去attribute_name_index和attribute_length的。也就是说剩下的类型总长度是29个字节。
行f列7-8为0001转换10进制是1它是属性max_stack的值,max_stack代表了操作数栈深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行时需要根据这个值来分配栈帧中操作栈的深度。
行f列9-0a值为0001转换10进制是1它是属性max_locals的值,max_locals代表了局部变量表所需要的存储空间。在这里max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte,char,float,int,short,boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Solt来存放。方法参数(包括实例方法中的隐藏参数this),显式异常处理器的参数(Exception Handler Parameter就是try-catch语句中catch块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。
行f列0b-0e的值是00000005转换10进制是5它是属性code_length的值,code_length后面跟的是u1长度的code这意味着,如果code_length的长度是5则后边跟的5个字节就是code,它们在行f列0f到行10列03值为2a b7 00 01 b1。关于code它的每一个长度是u1表示最多的可选值是0-255也就是最多有257中code指令,目前Java虚拟机规范已经定义了其中约200条编码值对应的指令这里我贴出来图片:
指令太多导致截图篇幅太大这里我们来看看<init>方法的code指令2a b7 00 01 b1
2a:将第一个引用类型本地变量推送至栈顶。
b7:调用超累构造方法,实例初始化方法,私有方法。
00:什么都不做。
01:将null推送至栈顶。
b1:从当前方法返回void。
这五个指令看完后仿佛一团迷雾,因为这个<init>方法不是我们写的,不着急,分析这个方法后,有两个是我们自己写的。
code分析结束后还有四个属性我这里为了大家看着方便再把图贴出来(PS:是为了我自己编辑方便)
u2长度的exception_table_length它的位置在行10列4-5值为0000该<init>方法没有异常。
u2长度的attributes_count是Code结构中最后一个属性在行10列6-7值是0001转换10进制是1表示有一个attribute_info,这个attribute_info也是21中属性集合中的一种,这很好理解,就是对象里边包对象。
在行10列08-09值为000a转换10进制是10它指向常量池#10的位置字面量是LineNumberTable它的结构如下图:
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必须的属性,但默认会生成Class文件之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行时产生的主要影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
行10列0a-0d值为00000006转换10进制是6是u2长度的attribute_length表示剩下的两个属性只有6个字节。
行10列0e-0f值为0001转换10进制是1它记录了line_number_info的属性的个数。
最后一个line_number_info的格式是固定的,它包括两个u2长度的数据项。start_pc和line_number,前者是行号,后者是Java源码行号。在行11列0-3值为00000002,尴尬的是当前这个Code是<init>方法的这个方法不是我们写的,所以这些也无法推算出来对不对。
到现在为止我们只是解析了三个方法表中的一个,还剩下setName()和getName()用上边的方式我们来翻译剩余的字节码。
行11列4-5值是0001转换10进制是1它是u2长度的access_flag表示方法的访问标志,1是public。
行11列6-7值是000b转换10进制是11它是u2长度的name_index是方法的简单名称,它指向常量池的#11也就是getName()方法。
行11列8-9值是000c转换10进制是12它是方法的参数列表和返回值的描述指向常量池#12的位置字面量字符串是()Ljava/lang/String;没有入参,返回值是String。
行11列0a-0b值是0001转换10进制是1是u2长度attribute_count的值,它记录了attribute_info的个数1个。
行11列0c-0d值是0009转换10进制是9它指向常量池#9值是Code下边还是看Code的结构:
行11列0e到行12列1值为0000001d转换10进制为29它是u4长度attribute_length的值,表示剩余的属性长度为29。
行12列2-3值为0001转换10进制是1是u2长度的max_stack这个栈的最大深度,其实可以理解为当前方法有没有在进入其他方法,每进入一个方法就会增加一个深度。在getName()方法中,并没有进入其他方法,所以它的栈深度就是1。
行12列4-5值为0001转换10进制是1是u2长度的max_locals。
行12列6-9值是00000005转换10进制是5是u4长度code_length的值,它是下边code的长度。
行12列0a-0e值是 2a b4 00 02 b0是5个字节码指令,字节码指令在上边有图这里分析下5个步骤。
2a:将第一个引用类型本地变量推送至栈帧。
b4:获取指定类的实例域,并将其值压入栈顶。
00:不做任何操作。
02:将int型-1推送至栈顶。
b0:从当前方法返回对象引用。
只有最后一个b0能看出返回了一个对象的引用,估计就是String类型的name。
行12列0f到行13列0值为0000转换10进制是0它是u2长度exception_table_length的长度,没有异常。
行13列1-2值为0001转换10进制为1是u2长度attributes_count的长度,表示Code中还有一个对象级别的属性,也就是还有一个attribute_info。
行13列3-4值为000a转换10进制是10它的类型是LineNumberTable,其实这个是和<init>方法一样的。
行13列5-8值是00000006转换10进制是6是u4长度的attribute_length的长度有6个字节。
行13列9-0a值是0001转换10进制是1是u2长度的line_number_table_length它标记有1个line_number_info。
行13列0b-0e值是0000006它包括两个u2长度的数据项。start_pc和line_number,前者是行号,后者是Java源码行号。可以看出行号这个是对的上的是在第6行。
最后一个方法setName()。
行13列0f到行14列00值为0001转换10进制是1它是方法的访问类型public。
行14列1-2值为000d转换10进制是13它是方法名指向的是常量池#13字面量为setName。
行14列3-4值为0008转换10进制是8它是方法的参数列表和返回值它指向常量池#8字面量为()V。哎,这里犯了个错误。。。。set方法没有放入参,淦!我说怎么是没有入参呢。将错就错吧。
行14列5-6值是0001转换10进制是1它是attributes_count计数器,表示有1个attribute_info。
行14列7-8值是0009转换10进制是9它是attribute_info的类型Code,看来每一个方法都会包含一个Code。
行14列9-0c值是00000025转换10进制是37表示剩余的属性字节有37个。
行14列0d-0e值是0002转换10进制是2它表示方法栈的最大深度是2。
行14列0f到行15列0值为0001转换10进制是1是u2长度的max_locals。
行15列1-4值是00000009转换10进制是9是u4长度code_length的值,它是下边code的长度。
code:2a 2a b4 00 02 b5 00 02 b1
2a:将第一个引用类型本地变量推送至栈顶。
2a:将第一个引用类型本地变量推送至栈顶。
b4:获取指定类的实例域,并将其压入栈顶。
00:什么都不做。
02:将int型-1推送至栈顶。
b5:为指定的类的实例域赋值。
00:什么都不做。
02:将int型-1推送至栈顶。
b1:从当前方法返回void。
行15列0e-0f值为0000转换10进制是0它是u2长度exception_table_length的长度,没有异常。
行16列0-1值为0001转换10进制为1是u2长度attributes_count的长度,表示Code中还有一个对象级别的属性,也就是还有一个attribute_info。
行16列2-3值为000a转换10进制是10它的类型是LineNumberTable,其实这个是和<init>方法一样的。
行16列4-7值是0000000a转换10进制是6是u4长度的attribute_length的长度有10个字节。
行16列8-9值是0002转换10进制是2是u2长度的line_number_table_length它标记有2个line_number_info。
行16列0a-0d值是00000009它包括两个u2长度的数据项。start_pc和line_number,前者是行号,后者是Java源码行号。
行16列0e到行17列01值是0008000a它包括两个u2长度的数据项。start_pc和line_number,前者是行号,后者是Java源码行号。
字节码文件的最后几个字节,是额外附加的属性表。
行17列2-3值为0001转换10进制是1表示有一个额外属性。
行17列4-5值为000e转换10进制是14对应常量池#14字面量为SourceFile这是它的属性,它的结构是:
SourceFile属性用于记录生成这个Class文件的编码文件名。这个属性也是可选的,可以使用javac的-g:none或者-g:source选项来关闭或要生成这项信息。在Java中对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况入内部类。如果不生成这个属性,当抛出异常时,堆栈中将不会显示错误代码所属的文件名。
行17列6-9值为00000002转换10进制是2它是u2长度attribute_length的值。最后两个字节000f转换10进制是15对应常量池#15的字面量Person.java
至此整个Class文件解析完毕。