专栏首页码的一手好代码深入理解JVM虚拟机---字节码分析

深入理解JVM虚拟机---字节码分析

java字节码整体结构和逐个分析

Java字节码分析

Java字节码整体结构

类型

名称

数量(字节)

u4

magic(魔数)

1

u2

minor_version(次版本号)

1

u2

major_version(主版本号)

1

u2

constant_pool_count(常量个数)

1

cp_info

constant_pool(常量池)

constant_pool_count-1

u2

access_flags(类的访问控制权限)

1

u2

this_class(类名)

1

u2

super_class(父类名)

1

u2

interfaces_count(接口个数)

1

u2

interfaces(接口名)

interfaces_count

u2

fileds_count(字段个数)

1

field_info

fields(字段的表)

fields_count

u2

methods_count(方法的个数)

1

method_info

methods(方法表)

methods_count

u2

attributes_count(附加属性的个数)

1

attribute_info

attributes(附加属性的表)

attributes_count

在上表中 u2、u4分别代表为占2、4个字节

上表顺序即为解析class文件的中数据所对应的顺序

可以使用javap -v命令分析一个字节码文件,就可以查看到以上信息 或者使用idea中插件jclasslib同样可以查询

代码(后续内容均通过以下代码进行分析):

Javap -v :

javap -v classname.class

jclasslib

Java字节码内容详细解析(访问标志前所有信息)

魔数、版本号、常量池

可以从表中看出java常量池前包含以下信息:魔数、版本号、常量池

下面通过一张图来介绍:

  • 第一段为魔术:所有的.class字节码文件的前4个字节都是魔数,魔数固定值为0xCAFEBABE
  • 第二段即魔数后面为版本信息,前两个字节表示minor version(次版本号),后面两个字节表示major version(主版本号),换算成十进制表示为次版本号为0,主版本号为55,对应着Java11
  • 第三段和后面一部分数据为常量池:一个Java类的很多信息都是由常量池来维护和描述的,可以将常量池看做是Class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存储在常量池中的。常量池中主要存储两类常量:字面常量与符号引用,字面常量如文本字符串,Java中声明为final的常量值等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等
  • 常量池的总体结构:常量池中常量的数量和常量池组(即常量的具体信息)。常量池的数量紧跟在主版本号后面,即图中第三段所标记,占据2个字节。常量池组则紧跟着常量数量后面。常量池组与一般的数组不同的是,常量词组中不同元素类型、长度、结构都是不同的,但是每个常量都有一个u1类型的数据来对该常量进行表示,占据一个字节。JVM在解析常量池时会根据u1类型的数据来判断该常量的类型。
    • 通过将常量池数量的数值转换为十进制(1E -> 30),但值得注意的是,常量池组中元素的个数 = 常量池数(30)-1(期中0暂时不使用),目的就是为了满足某些常量池索引值得数据在特定情况下需要表达不引用任何一个常量池的含义;根本原因在于,索引0也是一个常量(保留常量),只不过它位于常量表中,这个常量对应的就是null值,所以常量池索引是从1而非0开始的

常量池数据类型结构

有了这张表就可以对照着常量池字节码数据一一找出期中包含的信息,下面以后续7个常量为例:

期中每个颜色为一个常量

  • 0A (10),即tag值为10,CONSTANT_Methodref_info,方法信息,期中包含两个u2类型的索引项,第一个指向00 05(5),第二个指向00 15(21)即表示他们指向的第5个和第21个常量信息
  • 0A(10),同上
  • 09(9),tag值为9,CONSTANT_Methodref_info,字段信息,期中包含两个u2类型的索引项,一个为声明字段的类或结构的描述符信息的索引00 04(4),另一个为其类型类型的索引00 18(24)
  • 07(7),tag值为7,CONSTANT_Class_info,包含一个u2的类型的数据,类的全限定名索引00 19(25),即指向第25个常量
  • 07,tag值为7,同上
  • 01,tag值为1,CONSTANT_Utf8_info,一个utf-8的字符串常量,包含两个信息,一个为u2类型数据,表示字符串长度,以及紧跟着的字符串
  • 同上

P.S.:

  • 在jvm规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写的字符来表示,对象则使用字符L加对象的全限定名称来表示,例如: B - byte,C - char,D - double ,F - float ,I - int ,J - long ,S - short,Z - boolean,V - void,L-对象类型,如String - Ljava/lang/String;
  • 对于数组类型来说,每一个维度使用一个前置的[来表示,例如:int[]表示为[IString[][]表示为[[Ljava/lang/String
  • 用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为()V,方法java.lang.String toString()的描述符为”()Ljava/lang/String;,方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符为([CII[CIII)I

到此为止 常量池的所有信息就已分析完

访问标志

按照整体结构来看,在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。

具体的标志位以及标志的含义见表6-7

access_flags中一共有16个标志位可以使用,当前只定义了其中8个 ,没有使用到的标志位要求一律为0。以上述代码为例为例,MyTest1是一个普通Java类,不是接口、枚举或者注解,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK 1.2之后的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM这6个标志应当为假,因此它的access_flags的值应为:0x0001+0x0020=0x0021。从图中可以看出,access_flags标志的确为0x0021。

类索引、父类索引与接口索引集合

​ 类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。

​ 类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

对于接口索引集合,入口的第一项——u2类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。如上图所示

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。我们可以想一想在Java中描述一个字段可以包含什么信息?可以包括的信息有:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。下图列出了字段表的最终格式。

字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型,其中可以设置的标志位和含义见下图

​ 很明显,在实际情况中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java本身的语言规则所决定的。

​ 跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。

方法表集合

​ Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,见下图。这些数据项目的含义也非常类似,仅在访问标志和属性表集合的可选项中有所区别。

​ 因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对的,synchronized、native、strictfp和abstract关键字可以修饰方法,所以方法表的访问标志中增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。对于方法表,所有标志位及其取值可参见下图。

方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为”Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目,将在后面详细讲解。

​ 我们继续以代码中的Class文件为例对方法表集合进行分析,如下图所示,方法表集合的入口地址为:第一个u2类型的数据(即是计数器容量)的值为0x0003,代表集合中有三个方法(这三个方法为编译器添加的实例构造器<init>和源码中的两个方法)。第一个方法的访问标志值为0x001,也就是只有ACC_PUBLIC标志为真,名称索引值为0x0008,查常量池得方法名为”<init>”,描述符索引值为0x0009,对应常量为”()V”,属性表计数器attributes_count的值为0x0001就表示此方法的属性表集合有一项属性,属性名称索引为0x000A,即对应着,对应常量为”Code”,说明此属性是方法的字节码描述,在往后面没有用颜色进行标记的就是attribute_info,里面包含着方法的详细。

Code属性详情:

  • 00 0A:访问标记,通过查看上表6-12可以看出是public访问修饰符
  • 00 00 00 3B:attribute属性长度59,既往后面59个字节都是该属性
  • 00 02:操作数栈(Operand Stacks)深度的最大值为2。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。
  • 00 01:代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。方法参数(包括实例方法中的隐藏参数”this”,也就是这里的1)、显式异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。
  • 00 00 00 0D:字节码指令的长度为13,即后面13个字节为字节码指令
  • 2A - B1:对应的字节码指令,Oracle 指令信息以上所有字节码均可以找到对应指令信息
  • 00 00:异常相关信息,由于这里是0,所以这里没有exception_info,其实exception自身也有自己的结构体,在这里就不进行过多展开
  • 00 02:attribute_count信息以及后面2字节为attribute_info
  • 00 00 00 0A

​ 在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名 ,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。

属性表集合

​ 分析完前面的方法后就只剩下最后的属性表(attribute_info)了,结构如图所示

对应字节码:

对应jclasslib:

总结

​ 以上就是对一个简单的java程序字节码的分析,关于方发表和属性表集合这两块本身还有更多的东西还没有展示,其实也没有太多知道的必要。了解其主要结构,配置字节码查看工具能读懂即可

参考

  • 深入理解JVM-张龙
  • 深入理解JVM虚拟机(第二版)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • HDFS系统详解

    每个文件都是由一个一个的Block组成(Block默认大小128M),例如一个300M的文件会被保存成3个Block,而一个3K的文件也统一会占用一个Block...

    俺也想起舞
  • RabitMQ&Java使用说明

    RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而群集和故障转...

    俺也想起舞
  • spark源码分析————DAGScheduler实现

    DAGScheduler创建、Job提交、Stage划分、任务生成

    俺也想起舞
  • PHP 变量与常量

    本文介绍了 PHP 变量与常量。 官方文档:http://php.net/manual/zh/language.variables.php 官方文档:http:...

    康怀帅
  • 分享13个Python GUI库

    Python是一门高级编程语言。它用于通用编程。Python语言由Guido van Rossum创建,并于1991年首次发布。Python的设计哲学着重于代码...

    py3study
  • 简单使用gitHub客户端

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

    用户1451823
  • 【DB笔试面试430】在一个表中可以建立()

    ● 作者博客地址:http://blog.itpub.net/26736162/abstract/1/

    小麦苗DBA宝典
  • P002PHP开发之变量定义

    在函数体内定义的global变量,函数体外可以使用,在函数体外定义的global变量不能在函数体内使用,在全局范围内访问变量可以用特殊的 PHP 自定义 $GL...

    上善若水.夏
  • GitHub开学大礼包:近50种专业开发者工具,最高节省32万!

    GitHub推出学生礼包计划已经有6年了。至今已经为150多万学生提供了免费的最佳实践开发工具和培训。这个项目的初衷是帮助学生利用真实的工具进行实操来获取项目经...

    新智元
  • 搭建 LNMP 网站服务环境教程

    LNMP指的是一个基于CentOS/Debian编写的Nginx、PHP、MySQL一键安装包。可以在VPS、独立主机上轻松的安装LNMP生产环境。LNMP代表...

    用户6527301

扫码关注云+社区

领取腾讯云代金券