字节码(.class)文件解读

字节码文件是什么

我们在命令后使用 java 命令,就能将java源文件(.java)编译成对应的字节码文件(.class)。字节码文件是一种八位字节的二进制流文件,各个数据项按照一定顺序从前到后紧密排列。因此,这样的安排会使得字节码文件非常紧凑,可以被jvm快速的加载到内存中,并且占用较少的内存空间。

java源文件在被编译器编译之后,每个类(或者接口)都单独占据一个字节码文件。类中所有信息都在字节码文件中有所描述,由于字节码文件非常灵活,它对类的描述能力甚至强于java源文件。

字节码文件中的信息是一项一项排列的,每一项都有各自固定的长度:有的占一个字节,有的占两个字节,有的占四个或八个字节。数据项不同的长度分别用u1, u2, u4, u8表示,对应数据项在字节码文件中占据1,2,4,8个字节。

字节码文件的结构

一个字节码文件由十部分构成:MagicNumber,Version,Constant_pool,Access_flag,This_class,Super_class,Interfaces,Fields,Methods和Attributes。可以用下面的表来展示他们的排列顺序和每一部分数据项的大小:
《字节码(.class)文件解读》

下面分别对每一项进行展开:

(1)magic:存在于字节码文件的开头四个字节,存放着字节码文件的魔数,他是一个固定值 0xCAFEBABE(英文就是java那个咖啡的图标)。如果是十六进制的CAFEBABE,那么这个文件符合字节码文件的标准;如果不是,则不能被jvm所识别。

(2)minor_version和major_version:紧跟着魔数的就是次版本号和主版本号。例如版本1.8.0,那么主版本号就是1.8,而此版本号对应是0。版本号与某一个整数之间有映射关系,因此只要将major_version的十六进制数转化为十进制,再查表找到映射关系,就可以知道java编译器的版本号。

(3)constant_pool:版本号后面为常量池,常量池是字节码文件中一个非常重要的部分。常量池存放了文字字符串,常量值,当前类的类名,字段名,方法名,各个字段和方法的描述符,对当前类的字段和方法的引用信息,当前类中对其他类的引用信息等等。几乎包含了类中所有信息的描述。

首先常量池开头两个字节描述了常量池中数据项的数目。常量池中的数据也是一项一项紧密排列的,各个数据项通过索引来访问。每一个数据项都有自己的数据类型,由开头的一个字节(标志值tag)决定,映射关系如下表所示:
《字节码(.class)文件解读》
每一种不同的数据类型,都对应不同的结构,例如CONSTANT_Methodref:

               CONSTANT_Methodref_info { 
                         u1 tag;    //u1表示占一个字节
                         u2 class_index;    //u2表示占两个字节
                         u2 name_and_type_index;    //u2表示占两个字节
               }

再比如CONSTANT_Utf8结构:

                    CONSTANT_Utf8_info { 
                                   u1 tag;
                                   u2 length;
                                   u1 bytes[length];
                    }

总而言之,每一个不同的数据类型都对应不同的组织结构。

(4)access_flag:保存了当前类的访问权限,包括了public,private,protected等等。

(5)this_class:保存了当前类的全局限定名在常量池中的索引。

(6)super_class:保存了当前类的父类全局限定名在常量池中的索引。

(7)interfaces:保存了当前类实现的接口列表,包括两个部分:interfaces_count和interfaces[interfaces_count]。前者表示当前类实现的接口数量,两个字节表示(因此理论上一个类最多实现65535个接口);后者表示包含interfaces_count个接口的全局限定名索引的数组。

(8)fields:保存了当前类中的成员列表,也是分为两部分:fields_count和fields[fields_count]。前者表示一个类中的成员个数,后者表示字段详细信息的列表,如下所示:

               field_info { 
                         u2 access_flags;
                         u2 name_index;
                         u2 descriptor_index;
                         u2 attributes_count;
                         attribute_info attributes[attributes_count];
               }

详细信息包括了访问权限,成员名在常量池中的索引,类型描述符在常量池的索引,一些其他的属性(比如volatile之类的,但count如果是0,这个字段也就不存在了)。

(9)methods:保存了当前类的方法列表,包含两部分的内容:methods_count和methods[methods_count]。前者是该类或者接口显示定义的方法的数量,后者包含方法信息的一个详细列表,每一个方法信息如下:

                    method_info { 
                         u2 access_flags;
                         u2 name_index;
                         u2 descriptor_index;
                         u2 attributes_count;
                         attribute_info attributes[attributes_count];
                    }

这里的attribute比较复杂,涉及到操作数栈、局部变量表等等信息,不展开了。感兴趣的话可以看下结尾引用的那篇文章。

(10)attributes:包含了当前类的attributes列表,包含attributes_count和 attributes[attributes_count]。属性不仅仅可以出现在这个字段,如果出现在这个地方,则是对整个字节码文件所对应的类或者接口的描述;如果出现在fields或者methods中,则是对该字段或者该方法的额外信息的描述。

每一个attribute的结构如下:

          SourceFile_attribute { 
               u2 attribute_name_index;
               u4 attribute_length;
               u2 sourcefile_index;
          }

最常见的一个属性就是attribute_name_index指向常量池中的SourceFile,而sourcefile_index指向常量池中的xxx.java(也就是这个字节码文件的源文件)。

总结

字节码文件中的十部分结构简单的分析如上所述,包含了让jvm识别的magic;java的版本号;常量池(各种成员名,方法名,类名,源文件名,方法描述符等等),这些常量信息被后面的field,method等等所引用;访问权限;类名和父类全限定名;实现的接口信息;类中成员和方法;类的额外属性。

如果想了解更具体的,可以参考一下深入理解JVM之Java字节码(.class)文件详解

最后放一个java代码,和编译后字节码文件的一部分信息(用javap反编译相当于对字节码文件进行解密):

源代码:

    public class Hello{ 
      private int test;
      public int test(){ 
            return test;
        }
    }

字节码文件:
《字节码(.class)文件解读》
javap反编译得到每一部分的值:
《字节码(.class)文件解读》

参考

  1. https://blog.csdn.net/weelyy/article/details/78969412
  2. 《深入理解java虚拟机》
    原文作者:汐梦聆海
    原文地址: https://blog.csdn.net/jackzhang11/article/details/121674940
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞