代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

由于最近十年内虚拟机以及大量建立在虚拟机之上的程序语言如雨后春笋般出现并蓬勃发展,将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语言选择了操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。

无关性的基石

  • Java刚诞生的宣传口号:一次编写,到处运行(Write Once, Run Anywhere)。其最终实现在操作系统的应用层:Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码。
  • 字节码(ByteCode)是构成平台无关的基石;
  • 另外虚拟机的语言无关性也越来越被开发者所重视,JVM设计者在最初就考虑过实现让其他语言运行在Java虚拟机之上的可能性,如今已发展出一大批在JVM上运行的语言,比如Clojure、Groovy、JRuby、Jython、Scala;
  • 实现语言无关性的基础仍是虚拟机和字节码存储格式,Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,这使得任何语言的都可以使用特定的编译器将其源码编译成Class文件,从而在虚拟机上运行。

Java虚拟机提供的语言无关性

Class类文件的结构

  • Class文件是一组以8个字节为基础单位的二进制流(可能是磁盘文件,也可能是类加载器直接生成的),各个数据项目严格按照顺序紧凑地排列,中间没有任何分隔符;
  • Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,其中只有两种数据类型:无符号数和表;
  • 无符号数属于基本的数据类型,以u1、u2、u4和u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值;
  • 表是由多个无符号数获取其他表作为数据项构成的复合数据类型,习惯以“_info”结尾;
  • 无论是无符号数还是表,当需要描述同一个类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据未某一类型的集合。

下面我以自己本机写的一个简单的Java文件来学习其中各个部分的含义:

使用javac编译成TestClass.class文件,使用16进制打开:

使用javap命令输出Class文件信息:

魔数和版本(magic、version)

  • Class文件的头4个字节,唯一作用是确定文件是否为一个可被虚拟机接受的Class文件,固定为“0xCAFEBABE”。
  • 第5和第6个字节是次版本号,第7和第8个字节是主版本号(0x0034为52,对应JDK版本1.8);能向下兼容之前的版本,无法运行后续的版本;

常量池(constant_pool)

  • 常量池可以理解为Class文件之中的资源仓库,是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项之一;
  • 由于常量池中的常量数量不固定,因此需要在常量池前放置一项u2类型的数据来表示容量,该值是从1开始的,上图的0x0013为十进制的19,代表常量池中有18项常量,索引值范围为1~18;
  • 常量池主要存放两大类常量:字面量(Literal,比较接近Java的常量概念,比如文本字符串和final常量等)和符号引用(Symbolic References,主要包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符);
  • Java代码在javac编译时不会有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接;所以在Class文件不会保存各个方法、字段和最终内存布局信息;当虚拟机运行时需要从常量池获取对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中;
  • JDK 1.7中常量池共有14种不同的表结构数据,这些表结构开始的第一位是一个u1类型的标志位,代表当前常量的类型,具体如下图所示:

  • 之所以说常量池是最繁琐的数据就是因为这14种常量类型都有自己的结结构。可以结合下图中各个表结构的说明和之前使用javap解析的文件内容一起看。

  • 第1项:0x0A(15标志为方法句柄),0x0004(指向第4项的类描述符),0x000F(指向第15项的名称及类型描述符);
  • 第2项:0x09(9标志为字段符号引用),0x0003(指向第3项类描述符),0x0010(指向第16项的名称及类型描述符);
  • 第3项:0x07(7标志为类符号引用),0x0011(指向第17项全限定名常量项);
  • 第4项:0x07(7标志为类符号引用),0x0012(指向第18项全限定名常量项);
  • 第5项:0x01(1标志为UTF-字符串常量),0x0001(字符串占用1个字节),6D(字符“m”);
  • 第6项:0x01(1标志为UTF-字符串常量),0x0001(字符串占用1个字节),49(字符“I”);
  • 第7项:0x01(1标志为UTF-字符串常量),0x0006(字符串占用6个字节),3C 69 6E 69 74 3E(字符“”);
  • 第8项:0x01(1标志为UTF-字符串常量),0x0003(字符串占用3个字节),28 29 56(字符“()V”);
  • 第9项:0x01(1标志为UTF-字符串常量),0x0004(字符串占用4个字节),43 6F 64 65(字符“Code”);
  • 第10项:0x01(1标志为UTF-字符串常量),0x000F(字符串占用15个字节),4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65(字符“LineNumberTable”);
  • 第11项:0x01(1标志为UTF-字符串常量),0x0003(字符串占用3个字节),69 6E 63(字符“inc”);
  • 第12项:0x01(1标志为UTF-字符串常量),0x0003(字符串占用3个字节),28 29 49(字符“()I”);
  • 第13项:0x01(1标志为UTF-字符串常量),0x000A(字符串占用10个字节),53 6F 75 72 63 65 46 69 6C 65(字符“SourceFile”);
  • 第14项:0x01(1标志为UTF-字符串常量),0x000E(字符串占用14个字节),54 65 73 74 43 6C 61 73 73 2E 6A 61 76 61(字符“TestClass.java”);
  • 第15项:0x0C(12标志为名称和类型符号引用),0x0007(指向第7项名称常量项), 0x0008(指向第8项描述符常量项);
  • 第16项:0x0C(12标志为名称和类型符号引用),0x0005(指向第5项名称常量项), 0x0006(指向第6项描述符常量项);
  • 第17项:0x01(1标志为UTF-字符串常量),0x001F(字符串占用31个字节),63 6F 6D 2F 67 69 6E 6F 62 65 66 75 6E 6E 79 2F 63 6C 61 7A 7A 2F 54 65 73 74 43 6C 61 73 73(字符“com/ginobefunny/clazz/TestClas”);
  • 第18项:0x01(1标志为UTF-字符串常量),0x0010(字符串占用16个字节),6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74(字符“java/lang/Object”);

访问标志

  • 紧接在常量池后面的是两个字节的访问标志,用于标识类或接口的访问信息;
  • 访问标志一个有16个标志位,但目前只采用了其中8位,本例子中的0x0021标识为一个public的普通类;

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

  • 类索引:u2类型的数据,用于确定类的全限定名。本例子中为0x0003,指向常量池中第3项;
  • 父类索引:u2类型的数据,用于确定父类的全限定名。本例子中为0x0004,指向常量池中第4项;
  • 接口索引计算器:u2类型的数据,用于表示索引集合的容量。本例子中为0x0000,说明没有实现接口;
  • 接口索引集合:一组u2类型的数据的集合,用于确定实现的接口(对于接口来说就是extend的接口)。本例子不存在。

字段表集合

  • 用于描述接口或者类中声明的变量,包括类级变量和实例级变量,但不包括方法内部声明的局部变量;它不会列出从父类和超类继承而来的字段;
  • 0x0001表示这个类只有一个字段表数据;
  • 字段修饰符放在access_flag中,是一个u2的数据类型,0x0002表示为private的属性;
  • 字段名称name_index,是一个u2的数据类型,0x0005表示该属性的名称为常量池的第5项;
  • 字段描述符descriptor_index,是一个u2的数据类型,0x0006表示该属性的描述符为常量池的第6项,其值“I”表示类型为整形;
  • 字段属性计算器和属性集合:0x0000表示该例子中不存在;

方法表集合

  • 和字段表集合的方式几乎一样;
  • 方法里面的代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面;
  • 0x0002表示这个类有两个方法表数据,分别是编译器添加的实例构造器和源码中的方式inc();
  • 第一个方法的访问标志是0x0001(public方法),名称索引值为0x0007(常量池第7项,“”),描述符索引值为0x0008(常量池第8项,“()V”),属性表计算器为0x0001(有一项属性),属性名称索引为0x0009(常量池第9项,“Code”);
  • 根据“6.3.7.1 Code属性”说明,属性值的长度为23(0x0000001D表示29,但需要减去属性名称索引和属性长度固定的6个字节长度),操作数栈深度的最大值为1(0x0001,虚拟机运行时根据这个值来分配栈帧中操作栈深度),局部变量表所需要的存储空间为1个Slot(0x0001,Slot是内存分配的最小单位),字节码长度为5(0x00000005),分别为2A(aload_0,将第0个Slot中为reference类型的本地变量推送到操作数栈顶)、B7(invokespecial,以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它父类的方法,后面接着一个u2的参数指向常量池的方法引用)、0x0001(表示常量池的第1项,即Object类的方法)、B1(对应的指令为return,返回值为void);显式异常表为空(0x0000,计数器为0);该Code属性还内嵌1个属性(0x0001),属性的名称索引为0x000A(即“LineNumberTable”属性,用于记录对应的代码行数),该内嵌属性的长度为6(0x00000006),对应的行数信息为源码的第3行(0x000100000003);
  • 第二个方法的访问标志是0x0001(public方法),名称索引值为0x000B(常量池第11项,“inc”),描述符索引值为0x000C(常量池第12项,“()I”),属性表计算器为0x0001(有一项属性),属性名称索引为0x0009(常量池第9项,“Code”);
  • 根据“6.3.7.1 Code属性”说明,属性值的长度为25(0x0000001F表示31,但需要减去属性名称索引和属性长度固定的6个字节长度),操作数栈深度的最大值为2(0x0002),局部变量表所需要的存储空间为1个Slot(0x0001),字节码长度为7(0x00000007),分别为2A(aload_0)、B4(getfield,后面接着一个u2的参数指向常量池的属性引用)、0x0002(表示常量池的第2项,即TestClass类的m属性)、04(对应的指令为iconst_1)、60(对应的指令为iadd,整形求和)、AC(对应的指令为ireturn,返回值为整形);显式异常表为空(0x0000,计数器为0);该Code属性还内嵌1个属性(0x0001),属性的名称索引为0x000A(即“LineNumberTable”属性,用于记录对应的代码行数),该内嵌属性的长度为6(0x00000006),对应的行数信息为源码的第8行(0x000100000008);

属性表集合

  • 在Class文件、字段表、方法表都可以携带自己的属性表集合;
  • 属性表集合的限制较为宽松,不再要求严格的顺序,只要属性名不重复即可;
  • 以下是Java虚拟机规范里预定义的虚拟机实现应当能识别的属性:

虚拟机规范预定义的属性2

  • 接着我们的例子的Class文件还有最后一段:0x0001表示该Class有一个属性,0x000D表示属性名索引为第13项(对应“SourceFile”),0x00000002表示该属性长度为2,0x000E表示该类的SourceFile名称为第14项(对应“TestClass.java”)。

Code属性

Java程序方法体中的代码经过javac编译后,字节码指令存放在Code属性,其属性表结构如下:

Exceptions属性

方法描述时throws关键字后面列举的异常,和Code属性里的异常表不同。其属性表结构如下:

LineNumberTable属性

用于描述Java源码行号与字节码行号之间的对应关系,它不是必须的,可以通过javac -g:none取消该信息。没有该信息的影响是运行时抛异常不会显示出错的行号,在代码调试时无法按照源码行来设置断点。

LocalVariableTable属性

用于描述栈帧中局部变量与Java源码中定义的变量之间的关系,它不是运行时必须的,可以通过javac -g:none取消该信息。如果没有这个属性,所有的参数名称都会丢失,取之以arg0、arg1这样的占位符来替代。

其中local_variable_info项代表了一个栈帧与源码中局部变量的关联,如下所示:

SourceFile属性

用于记录生成这个Class的源码文件名称,这个属性也是可选的。

ConstantValue属性

作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量才可以用这个属性。对于非static类型的变量的赋值是在实例构造器方法中进行的;而对于类变量有两种方式:在类构造器方法中或者使用ConstantValue属性。目前Sun javac编译器的选择是:同时使用final和static修饰的变量且为基本数据类型或String类型使用ConstantValue属性初始化,否则使用初始化

InnerClass属性

用于记录内部类与宿主类之间的关联。

其中number_of_class代表需要记录多少个内部类信息,每个内部类的信息都由一个inner_class_info表进行描述。

Deprecated及Synthetic属性

Deprecated(不推荐使用)和Synthetic(不是由Java源码直接产生编译器自行添加的,有两个例外是实例构造器和类构造器)这两个属性都属于布尔属性,只存在有和没有的区别,没有属性值的概念。在属性结构中attribute_length的数据值必须为0x00000000。

StackMapTable属性

这是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

Signature属性

一个可选的定长属性,在JDK 1.5发布后增加的,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signature属性会为它记录泛型签名信息。这主要是因为Java的泛型采用的是擦除法实现的伪泛型,在字节码中泛型信息编译之后统统被擦除,在运行期无法将泛型类型与用户定义的普通类型同等对待。通过Signature属性,Java的反射API能够获取泛型类型。

BootstrapMethods属性

一个复杂的变长属性,位于类文件的属性表中,用于保存invokedynamic指令引用的引导方法限定符。

字节码指令简介

Java虚拟机的指令由一个字节长度的、代表着特定操作含义的数字(操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。

在指令集中大多数的指令都包含了其操作所对应的数据类型信息,如iload指令用于从局部变量表中加载int类型的数据到操作数栈中。

  • 加载和存储指令:iload/iload_等(加载局部变量到操作栈)、istore/istore_等(从操作数栈存储到局部变量表)、bipush/sipush/ldc/iconst_(加载常量到操作数栈)、wide(扩充局部变量表访问索引);
  • 运算指令:没有直接支持byte、short、char和boolean类型的算术指令而采用int代替;iadd/isub/imul/idiv加减乘除、irem求余、ineg取反、ishl/ishr位移、ior按位或、iand按位与、ixor按位异或、iinc局部变量自增、dcmpg/dcmpl比较;
  • 类型转换指令:i2b/i2c/i2s/l2i/f2i/f2l/d2i/d2l/d2f;
  • 对象创建与访问指令:new创建类实例、newarray/anewarray/multianewarray创建数组、getfield/putfield/getstatic/putstatic访问类字段或实例字段、baload/iaload/aaload把一个数组元素加载到操作数栈、bastore/iastore/aastore将一个操作数栈的值存储到数组元素中、arraylength取数组长度、instanceof/checkcast检查类实例类型;
  • 操作数栈管理指令:pop/pop2一个或两个元素出栈、dup/dup2复制栈顶一个或两个数组并将复制值或双份复制值重新压力栈顶、swap交互栈顶两个数值;
  • 控制转移指令:ifeq/iflt/ifnull条件分支、tableswitch/lookupswitch复合条件分支、goto/jsr/ret无条件分支;
  • 方法调用和返回指令:invokevirtual/invokeinterface/invokespecial/invokestatic/invokedynamic方法调用、ireturn/lreturn/areturn/return方法返回;
  • 异常处理指令:athrow
  • 同步指令:monitorenter/monitorexit

公有设计和私有实现

  • Java虚拟机的实现必须能够读取Class文件并精确实现包含在其中的Java虚拟机代码的含义;
  • 但一个优秀的虚拟机实现,通常会在满足虚拟机规范的约束下具体实现做出修改和优化;
  • 虚拟机实现的方式主要有两种:将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集或宿主主机CPU的本地指令集。

Class文件结构的发展

  • Class文件结构一直比较稳定,主要的改进集中向访问标志、属性表这些可扩展的数据结构中添加内容;
  • Class文件格式所具备的平台中立、紧凑、稳定和可扩展的特点,是Java技术体系实现平台无关、语言无关两项特性的重要支柱;

本章小结

本章详细讲解了Class文件结构的各个部分,通过一个实例演示了Class的数据是如何存储和访问的,后面的章节将以动态的、运行时的角度去看看字节码在虚拟机执行引擎是怎样被解析执行的。

参考资料

  • 《深入理解Java虚拟机——JVM高级特性与最佳实践》-周志明

原文:https://meandni.com/2019/01/14/jvm_note4/

《深入理解Java虚拟机》笔记4——类文件结构相关推荐

  1. Java虚拟机:class类文件结构

    一.平台无关性: Java的无关性的实现,是由Java源代码编译后生成的字节码class文件和Java虚拟机实现的.无关性包括:平台无关性以及语言无关性. (1)平台无关性,是指java代码可以运行在 ...

  2. 深入理解java虚拟机-笔记

    java内存区域与内存溢出异常 java虚拟机自动内存管理机制,不用像C/C++为每一个new操作去写配对delete/free代码 java虚拟机在执行java程序的过程中 会把内存划分为若干个不同 ...

  3. java虚拟机内存分为,深入理解Java虚拟机笔记(一)----内存划分

    Java内存划分 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,如下图 一.程序计数器 程序计数器(Program Counter Register)是一块很小 ...

  4. 深入理解Java虚拟机--笔记1

    Java内存区域与内存溢出异常 运行时数据区域 Java虚拟机在执行Java程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域. 1 程序计数器--Program Counter Regis ...

  5. 深入理解java虚拟机 新生代_深入理解java虚拟机:笔记

    1.运行时数据区域 1.程序计数器 当前线程执行字节码的行号指示器,字节码解释器工作通过改变这个计数器的值来选取下一条需要执行的字节码指令,每一个线程拥有独立的程序计数器,线程私有的内存 2.虚拟机栈 ...

  6. 深入理解Java虚拟机 笔记

    对象内存布局: 对象头 实例数据 对齐填充 对象头: Mark Word:存储自身的运行时数据,如hashcode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳 类型指针,指向其类 ...

  7. 深入理解Java虚拟机笔记之六内存分配与回收策略

    对象的内存分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配.少数情况下也可能会直接分配在老年代中,分配的规则并不是百分百固定的,其细节取决于当前使用的 ...

  8. 2020-5-9 开始阅读深入理解java虚拟机

    深入理解java虚拟机笔记 day1 读完前三章 关于Jit编译器和解释器和关系,见博客:https://www.cnblogs.com/insistence/p/5901457.html HotSp ...

  9. 《深入理解JAVA虚拟机》详细解读(第二章 ):JAVA内存区域与内存溢出异常

    目录 一.JAVA内存区域与内存溢出异常 1. 概述 2. 运行时数据区域 2.1 程序计数器 2.2 Java虚拟机栈 2.3本地方法栈 2.4 堆 2.5 方法区 2.6 运行时常量池 2.7直接 ...

最新文章

  1. springboot 事务手动回滚_来,讲讲Spring事务有哪些坑?
  2. 图灵LAMP类图书精彩荟萃
  3. tomcat端口占用后的解决办法
  4. tcp/ip 建立过程
  5. 蓝牙BLE ATT剖析(一)
  6. php中var_dump是什么意思,php中的var_dump()方法的詳細說明
  7. mysql连表查询on条件_mysql 外连接的时候,条件在on后面和条件在where后面的区别...
  8. php 将数据库导出为csv,php – 将数据库转换为CSV并将文件保存到服务器上的文件夹 - 程序园...
  9. java实现傅里叶变换
  10. 调用GPU进行神经网络的训练 GPU环境的搭建
  11. Linux 终端显示 Git 当前所在分支 1
  12. Eclipse 中如何设置字体大小与样式
  13. Boost Asio run() run_one() poll() poll_one()的区别
  14. 止步智能手机,网易的移动互联网冷静剂
  15. BFS - CH2906 - 武士风度的牛
  16. mac上如何提取图片上的文字?几款不错的OCR文字识别工具推荐
  17. QuantumultX 初学者傻瓜教程
  18. ARM固件开发(LPC1768启动初始化代码)
  19. ​如何通过微信打新债?
  20. windows11 + linux 蓝牙连接问题

热门文章

  1. spring-data-redis 使用
  2. linux 清空文件内容命令
  3. 怎样复制粘贴windows命令行中的内容
  4. 基于deep learning的快速图像检索(Deep Learning of Binary Hash Codes for Fast Image Retrieval)
  5. 解密谷歌机器学习忍者项目,如此培养人工智能人才
  6. 清单革命:为什么不仅是工具革命
  7. php swiper 下拉刷新,SwipeRefreshLayout的使用(下拉刷新)
  8. Modular Arithmetic 模算术
  9. Linux执行yum不显示时间,Linux停的yum命令详解(朝花夕拾)
  10. python中key的意思_有朋友问Python 中实例对象为啥能按照key赋值。