深入理解JVM(8)——字节码文件结构(1)

目录

1.Java的跨平台与多语言

2.示例程序

3.字节码文件结构剖析

3.1 魔数

3.2 版本号

3.3 常量池

(1)常量池内容

(2)常量池结构

(3)容量计数器中数量与常量的个数的关系

(4)常量池数组中元素的14种数据类型

(5)分析示例中的常量池


1.Java的跨平台与多语言

  • Java一个最重要的特性就是跨平台——一次编写,到处运行
  • 而支撑上述特性的根基就是:
    • 1.Java虚拟机
      • JVM本身提供了根据操作系统的不同的不同版本,JVM本身不是跨平台的
      • 我们下载jdk的时候,针对不同的操作系统要下载不同版本,而不同的jdk中就包含了不同的针对于操作系统的JVM
    • 2.字节码文件
      • Java不管是在什么平台上去编译,都生成的是.class文件
      • .class文件就是跨平台的文件,在Windows上和在Linux上编译出来的.class文件的内容是一样的
  • Java提供了两种规范
    • Java语言规范
    • JVM规范
      • Java字节码规范是JVM规范的一部分
  • 现在,在JVM之上可以运行不止Java以外的多种语言,如Scala,Jython,JRuby,而它们最终均能在JVM上运行的原因就是它们都可以被编译为字节码文件(即.class文件),遵守着Java字节码的规范,最终被JVM执行
  • Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义能力肯定会比Java语言本身更加强大,因此,有一些Java语言本身无法有效支持的语言特性不代表字节码本身无法有效支持,而其他语言实现一些有别于Java的语言特性也是基于此。
  • 注意:任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件中,下述的字节码文件并非一定以磁盘文件格式存在

2.示例程序

以下示例程序用于后面分析字节码文件的结构

package ByteCode;

public class ByteCodeDemo01 {
    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }

    private int a = 1;
}

生成它的字节码文件

使用下面的命令来在Terminal中反编译出字节码文件的内容

javap -verbose 字节码文件

  • 可以看到使用javap -verbose命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法,类中的方法信息、类变量与成员变量等信息

使用装了Hex Editor插件的Notepad++查看16进制格式的该字节码文件

3.字节码文件结构剖析

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

class文件中的两种数据类型:

  • 无符号数
    • 以u1,u2,u4,u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数
    • 无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值
    • 由多个基本数据或者其他表按照既定的顺序组成的大的数据集合
    • 所有的表都习惯以“_info”结尾
    • 表是有结构的,它的结构体现在:组成表的成分所在的位置和顺序都是已经严格定义好的
    • 整个class文件本质上就是一张表

总览图:

每个字节码文件都由以上10种结构构成,以下使用上述示例中的class文件一一讲解

3.1 魔数

  • 所有的class文件的前4个字节都是魔数,魔数为固定值,为0xcafebabe(咖啡宝贝,从上述16进制文件就可以看到)
  • 作用:
    • 确定这个文件是否为一个能被虚拟机接受的class文件
  • 使用魔数而不是扩展名来进行识别class文件是出于基于安全方面的考虑,因为文件扩展名可以随意地改动

3.2 版本号

  • 魔数之后的4个字节存储的是class文件的版本号:第5和第6个字节是次版本号,第7和第8个字节是主版本号
  •     52对应到十六进制就是上述的34
  • 而Java的部分版本号对应如下:
编译器版本十六进制版本号十进制版本号
JDK1.5.000 00 00 3149.0
JDK1.6.000 00 00 3250.0
JDK1.7.000 00 00 3351.0
JDK1.8.000 00 00 3452.0

 

 

 

 

 

 

  • 所以上述我的class文件是使用JDK1.8.0编译的
  • 高版本的JDK能向下兼容以前版本的class文件,但不能运行以后版本的class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的class文件,所以上述文件的版本号说明这个文件是可以被JDK1.8以上版本虚拟机执行的class文件

3.3 常量池

  • 紧跟着主板本号之后的就是常量池入口

一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池理解为class文件的资源仓库

(1)常量池内容

常量池中主要存放两大类常量:

  • 字面量:
    • 文本字符串
    • Java中声明为final的常量
  • 符号引用:
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

(2)常量池结构

不同的class文件的常量池中字节数量是不固定的,那么常量池到哪一个字节结束怎么确定呢?

  • 常量池采用的是容量计数器+若干个连续数据项的形式(当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器+若干个连续数据项的形式,这时称这一连续的某一类型的数据为某一类型的集合,后面还会多次使用)
    • 1.即常量池的入口先放置一项u2类型的数据,代表常量池容量计数值,即常量池中包含多少个字节
    • 2.然后紧跟的是常量池“数组”(也即上述的表结构类型)

(3)容量计数器中数量与常量的个数的关系

上述文件中第9、第10个字节为(十进制为24),代表常量池中有24个常量?

但在javap -verbose反编译出来的个数却只有23个?

  • 其实上述的24只能代表有23个常量,即索引值的范围为1~23 
  • 设计者将第0项常量空出来有特殊考虑
    • 目的:
      • 满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义
    • 根本原因:
      • 索引为0也是一个常量,它是一个保留常量,只不过它不位于常量数组中,这个常量就对应null值
  • 而class文件中只有常量池的容量计数从1开始,而后面的其他集合类型(包括接口索引集合,字段表集合,方法集合、附加属性集合)的容量计数都是从0开始

总结:常量池数组中元素个数 = 常量池数 - 1  (其中0暂不使用)

(4)常量池数组中元素的14种数据类型

  • 常量池数组与一般的数组不同的是,常量池中不同的元素的类型、结构是不同的所以长度当然也就不同,那JVM怎么解析不同的元素呢?
    • 常量池中每一个元素的第一种类型是u1类型,该字节是个标志位,占据1个字节,JVM在解析常量池时,会根据这个标志位来获取该标志对应的常量类型
    • 所以常量池种每一项都是一个表结构
    • 在jdk1.7以前有11种结构各不相同的表结构即常量类型,但在jdk1.7为了更好地支持动态语言调用,又额外增加了3种,总体如下表所示:

关于JVM规范中的描述信息说明:

  • 在JVM规范中,每个变量/字段都有描述信息
  • 描述信息的主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值
  • 根据描述符规则,
    • 基本数据类型和代表无返回值的void类型都用一个大写字符来表示:
      • B:byte
      • C:char
      • D:double
      • F:float
      • I:int
      • J:Long
      • S:short
      • Z:boolean
      • V:void
    • 对象类型:L加对象的全限定类型名称来表示
      • 如字符串类型为Ljava/lang/String;
    • 数组类型:每一个维度使用一个前置的[来表示
      • int[]被记录为[I
      • String[][]被记录为[[java/lang/String
  • 使用上述表示,是为了压缩字节码文件的体积

用描述符来描述方法:

  • 按照先参数列表,后返回值的顺序来描述,参数列表按照参数的严格顺序放在一组()之内
    • 如方法String getRealName(int id,String name)的描述符为:(I,Ljava/lang/String;)Ljava/lang/String;

(5)分析示例中的常量池

接下来我们一步一步分析上述示例中常量数组中每一项元素:

常量池中第1个常量:

 (10)

  • 即tag为10,对应CONSTANT_Methodref_info,表示该元素是一个类中方法的符号引用,可以看到接下为两个u2类型的索引指向常量池中描述该方法的元素

(4)

  • 查上述表可知,这里表示索引,即4表示指向常量池中第4个元素
  • 表示它是一个java/lang/Object类的方法

(20)

  • 索引,即20表示指向第20个元素
  • 第20个元素又指向第7和第8个元素
  • <init>表示构造方法,()V表示方法描述符

所以第一个元素总体为

  • 表示该方法是一个构造方法,返回值为空,是Object类的构造方法(因为我们没有为该类定义构造方法,这个类的构造方法是编译器帮我们生成出来的,而这个类的父类是Object,所以最终调用的就是Object类的构造方法)

常量池中第2个常量:

(9)

  • 即tag为9,对应CONSTANT_Fieldref_info,表示该元素是一个字段的符号引用,可以看到接下为两个u2类型的索引指向常量池中描述该字段的元素

(3)

  • 查上述表可知,这里表示索引,即3表示指向第3个元素
  • 表示该字段是ByteCode/ByCodeDemo01类的字段

(21)

  • 索引,即21表示指向第21个元素
  • 第21个常量又指向第5和第6个元素
  • 表示该字段的名字是a,类型为int

所以第2个元素总体为

  • 表示该元素是类ByteCode/ByteCodeDemo01的一个字段,字段名为a,类型为int

常量池中第3个元素:

(7)

  • 即tag为7,对应CONSTANT_Classref_info,表示该元素是一个类或接口的符号引用,可以看到接下为一个u2类型的索引指向描述该类的字面量的元素

(22)

  • 索引,即22表示指向常量池中第22个元素
  • 表示该类的全限定类名是ByteCode/ByteCodeDemo01

所以第3个元素总体为

  • 表示类ByteCode/ByteCodeDemo01

常量池中第4个元素:

(7)

  • 即tag为7,对应CONSTANT_Classref_info,表示该元素是一个类的符号引用,可以看到接下为一个u2类型的索引指向描述该类的字面量的元素

(23)

  • 索引,即23表示指向常量池中第23个元素
  • 表示类的全限定类名是java/lang/Object

所以第4个元素总体为

  • 表示类java/lang/Object

常量池中第5个元素:

(1)

  • 即tag为1,对应CONSTANT_Utf8_info,表示该元素是一个UTF-8缩略编码的字符串

(1)

  • 表示字符串占用1个字节的长度

(97)

  • 表示97对应的字符,即为a

所以第5个元素总体为

  • 表示字符串a

补充:UTF-8缩略编码

  • '\u0001'~'\u007f'之间的字符的缩略编码使用一个字节表示(相当于1~127的ASCII码)
  • '\u0080'~'\u07ff'之间的所有字符的缩略编码用两个字节表示
  • '\u800'~'\uffff'之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示

常量池中第6个元素:

同理

第6个元素总体为(备注:0x49为73)

  • 表示字符串I

常量池中第7个元素:

同理

第7个元素总体为

  • 0x3c:60   对应字符为:< 
  • 0x69:105  对应字符为:i
  • 0x6e:110   对应字符为:n
  • 0x69:105  对应字符为:i
  • 0x74:116  对应字符为:t
  • 0x3e:62  对应字符为:>

  • 表示字符串<init>

常量池中第8个元素:

同理

第8个元素总体为

  • 表示字符串()V

..........

剩余的均同理

其实在javap -verbose的输出结果的后面将上述信息已经帮我们在右边做了整理和翻译,剩余元素如下

  • 可以发现上述的所有index项最终都指向CONSTANT_Utf8_info类型常量

最终解析如下:

接下一篇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值