在线.class文件转换.java_带你逐个字节地搞明白.class文件

a52953bbc18c69a129251a0d8352168a.png

Class文件概述

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。根据Java虚拟机规范的规定,Class文件结构采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。

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

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

788f440c4caa4c508b1ee4668313ef5e.png

实例分析

上面的东西你看了肯定也头疼,话不多说,直接结合代码来说明。下面通过一段简短的代码,以实例说明字节码的含义,代码如下:

package 

使用javac命令将上述代码编译,生成HelloWorld.class文件,我们使用Binary Viewer打开.class文件,得到如下内容(下面这张图会在后续讲解中多次用到,建议单独保存下来,以便和后续的指令代码图做对比分析)

073d72b95410d316191a79038313fde5.png

这个二进制文件是方便JVM解析用的,为了能够帮助程序员理解这个二进制文件,Java为我们提供了一种能够将二进制内容翻译成类似汇编语言指令码的工具,我们在HelloWorld.class所在的目录下输入:

javap 

会将二进制内容翻译成以下指令代码文件(建议把指令代码文件也保存下来,方便和二进制进行比对)如下:

lassfile /D:/WorkSpace/test/src/main/HelloWorld.class
  Last modified 2019-1-6; size 296 bytes
  MD5 checksum 8f119111c39951f21cef8ffe2d5542ef
  Compiled from "HelloWorld.java"
public class main.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // main/HelloWorld.a:I
   #3 = Class              #17            // main/HelloWorld
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               increase
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               HelloWorld.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // a:I
  #17 = Utf8               main/HelloWorld
  #18 = Utf8               java/lang/Object
{
  int a;
    descriptor: I
    flags:

  public main.HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 3: 0
        line 5: 4

  public int increase();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 8: 0
}
SourceFile: "HelloWorld.java"

既然javap命令可以将HelloWorld.class文件翻译成上述的指令代码,那么就说明二者有一定的联系,我们可以逐个字节码来翻译。

魔数

首先是字节码文件的开头部分"CA FE BA BE”,这是Java设置的固定字符,也称为魔数,它的主要作用是标记当前文件为一个可供JVM识别和解析的字节码文件,即便有人将.class后缀名修改为其他后缀名,也不妨碍JVM的识别它,简单了解即可。

版本信息

紧接下来是4个字节:00 00 00 34,这4个字节描述的是Class文件的版本号,前两个字节(00 00)表示的是Minor Version,后两个字节(00 34,十六进制)表示的是主版本号(Major Version),34转化为十进制为52,表示这个Class文件的主版本是JDK1.8.0,对应javap翻译过来的指令代码为:

65ba1673a0271d46fbfa92303caaaf0c.png
版本号

顺便提一下,Java过往版本号如下:

6eca389eac64f9498b978070804025ab.png

常量池信息

版本号之后紧接着就是常量池信息。我们可以将常量池理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据项,也是占用Class文件空间最大的数据项目之一。由于常量池中的常量数量并不固定,因此在常量池的入口需要放置一项u2类型的数据,这个容量计数从1开始而不是从0开始。二进制文件中的 00 13 换算成十进制为19,对应指令代码中的#1-#18个项,如下图所示:

99c9cfda4995acfa16b18b282251e771.png

然后就是常量池的正文内容,常量池的正文内容主要由两类组成,一个是字面量,一个是符号引用。字面量比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:

1、类和接口的全限定名

2、字段的名称和描述符

3、方法的名称和描述符

Java代码在进行Javac编译时,并不像C和C++那样有“连接”的步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

JDK1.7中,总共有14种类型的常量,每种常量都是表类型的数据项。这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位,代表当前常量属于哪种常量类型。这14种常量类型代表的具体含义见下表:

26267e0458b6c083b263680c1405af56.png

让我们再回到二进制文件中,紧接在版本号的字节是:0A 00 04 00 0F,将0A转换成十进制的值为10,查常量表,得知这个0A对应的表正是CONSTANT_Methodref_info,即描述类中方法的符号引用,我们查询JVM规范,得知CONSTANT_Methodref_info的结构如下:

1f703605d41eee46f53382b31627d5aa.png

和字节码对应如下:

74199dd0368648eb9bd8e66eac5f9248.png

所以这段字节码对应了指令代码文件中的常量池的第一句话:

   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V

这句话的含义是变量Methodref的值是由指令代码文件中的第4行索引和第15行索引的值拼接而成的,第4行和第15行的指令如下:

   #4 = Class              #18            // java/lang/Object
    ……
  #15 = NameAndType        #7:#8          // "<init>":()V

其中第4行对应的二进制字节码如下:

727025ad212786a0e93c14ac139c36b7.png

同理,也能够得知第15行的索引关系。由此我们可以得到整个Methodref的值的逻辑关系,是经过如下图那样,经过多次索引之后拼接起来的:

563ae6d716cdf2b180dd1a19951778d1.png

常量池中其他项也是按照这个规律和字节码进行一一对应的。JVM规范之所以这么设计,是因为通过索引的方式,能够最大化复用常量值,减少冗余的字节码存储,从而使整个字节码文件占用字节尽可能的少。

字段信息

检索完常量表之后,Java的字节码就开始表示字段信息了,我们知道字段信息包括权限修饰符(private/public/void/default) 、静态/非静态修饰符、final修饰符等内容。JVM规范设计了一种Field_info表来存储这些信息,如下图所示:

580de2aa005320ae912e3ad762e24751.png

在我们的示例中,它的二进制内容的解析如下:

5d1dfabbaa4593658894e448ed4d5845.png

观察绿线的部分和内容,我们可以看出,这部分字段表的含义,恰好描述了源代码中:int a = 1;这句话的全部内涵,也对应了指令代码中的:

  int a;
    descriptor: I
    flags:

的内容

方法表和属性表信息

方法表和属性表的解析逻辑和上述内容一致,无非是参考的表定义不同而已。在这里留下方法表的解析由读者自己完成,我附上属性表的结构,以及实例中的对应关系。属性表的JVM规范定义如下:

e8b59b1ed1528b48d72e4e9c7bde9da9.png

示例图对应的具体内容:

3099a1372c00486a25eb8d73fe5be5ed.png

再后面就是init()方法其他属性表,然后是increase()的方法表,以及属性表,周而复始,直到最后一个字符,整个.class文件就解析完了。最后附上整个.class全解析的详细文件链接:

.class详解 | ProcessOn免费在线作图,在线流程图,在线思维导图 |​www.processon.com
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值