查看原文
其他

在 JVM 眼中 .class 文件是什么样的?值得收藏!

点击上方 "程序员小乐"关注公众号, 星标或置顶一起成长

每天凌晨00点00分, 第一时间与你相约

每日英文

When you have something you really love but it causes you pain, God is just testing you to see if you are strong enough to hold it.

当你真正喜欢一样东西,但它又给你带来伤害的时候,其实这是老天在考验你是否足够坚持。


每日掏心话

“你走的路越多,就知道世界有多大,你看的书越多,就知道自己有多浅,一岁又一岁,评判很多事情的标准都与从前不再一样。人啊,这无解的心理和奇怪的人生,就连自己都是捉摸不透的。”  


来自:MxsQ | 责编:乐乐

链接jianshu.com/u/9cf1f31e1d09

程序员小乐(ID:study_tech)第 657 次推文   图片来自网络


往日回顾:12 个酷炫的 IntelliJ IDEA 插件,超实用!看了都说好!



   00 前言   


Java程序具有 " Write Once , Run Anywhere ." 的跨平台特性。实现这样的目的,Java的方案是:半编译 + 半解释,即 .Class + JVM 。


1、源程序内容会被编译为.Class文件,.Class文件具有严格规定如何从中提取信息,可以理解为 “中间码”,约定使用者如何理解文件内容
2、理解了程序内容,各个平台根据自身特色不同,实现各自的JVM用来解释(翻译).Class文件,变成真正的本地可执行指令。

如此实现了Java跨平台的特性。因此,跨平台的基础为.Class,实现为JVM。
本文的目的为:读懂.Class,悉知编写的程序代码在JVM眼中是什么样子。而在理解了.Class之后,对于理解JVM、理解字节码插桩等有进一步帮助。



   01 基础知识   


字节码


字节码是一种包含执行程序,由数据对组成的二进制文件,是一种中间码。一般来说,一字节占用8位,即包含八位的二进制。
文章所指的.Class文件为字节码文件,每字节用16进制表示,数值范围 00 ~ FF (0 ~ 255).

无符号数基本类型


无符号数可以用来描述数字、索引引用、数值量或按照 UTF-8 编码构成字符串。u1、u2、u4、u8分别代表 1个字节、2个字节、4个字节和8个字节的无符号数。

字面量


字面量是一种固定值的表示法,本身没有含义,需要场景来为它赋予含义,如何理解?比如 007 没有含义,但是用来表示詹姆斯·邦德,你就知道007代表一个很厉害的特工。在程序中,int x = 10、String s = "10" 让字面量 10 具有了不同的意义。


全限定名


将一个类的全限定名是将类全名的.全部替换为/,如java.lang.String替换为java/lang/String


描述符


描述符用来描述字段的数据类型、方法的参数类表和返回值,每种符号对应不同数据类型



   02 Class文件   


Java文件包含了一个类的所有信息,以下是一个Java类:
import java.io.Serializable;

public class TestClass implements Serializable{

    private int m = 123;
    private static int x = 10;
    private static final int y = 20;

    public int increace(){
        return m+1;
    }

    public void m() throws Exception{
        // 具体逻辑不写
    }

    public static String hello(){
        return "hello word";
    }
}


此Java文件中,所包含的信息有:
  1. 类明为TestClass并可被外部访问,实现了Serializable接口
  2. 拥有类变量 x和y,拥有成员变量 m
  3. 拥有可被外部访问的类函数 hello(),拥有可被外部访问的成员函数increace() 和 m()


note: 如无特殊说明,文章所说.Class文件均由此Java文件编译得来这些信息在被编译后将在.Class文件中进行表达。通过命令


Javac fileName.java


可将Java文件编译成对应的.Class文件。.Class文件为字节码文件,可借助对应编辑器阅读。


本文使用的编辑器为 “010” ,Windows 和 Mac 都有, 自行下载。
Class 字节码实例文件


.Class文件使用字节码表达信息,各数据间紧凑,不包含任何分隔符,因此整个.Class文件中存储的内容几乎是全部程序运行时的必要数据。如何解析字节码数据,就需要制定规则来解读,严格遵守。
.Class 文件风格采用类似于C语言结构的伪结构来存储数据。可以将.Class文件看成多张表的集合,通过表索引,能找到对应的数据。可以理解为,数据存在的相对位置,决定了它被赋予的涵义。
.Class文件格式如下表


有些数据信息是定长的,有些视具体情况而定,但都会有相应的约束告知具体长度。各信息对应已在.Class实例文件图标出,剩下的是逐层去解析类信息。


   03 常量池   


常量池中主要存放两大类:字面量和符号引用。符号引用包括:


  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符


与C和C++不同,Java代码编译后没有“连接”的步骤,在JVM加载Class文件的时候进行动态连接。.Class文件不会保存各方法、字段的最终内存布局信息,因为不能经过运行期转换无法得到真正的内存入库地址,无法被JVM使用。在JVM运行时,从常量池中拿到对应的符号引用,解析、翻译到具体的内存地址中再进行使用,这些信息也就存于JVM的方法区中。


常量池所占用长度不定,需要 0x0008 ~ 0x0009 提供常量数量统计,再根据常量池里的具体常量类型推算出具体总占用的长度。
但这比较繁琐,每一种常量类型对应一份表,需要根据表的不同查阅具体的表结构来获取信息。常量类型的第一位 u1 表明来对应常量的表结构,对应信息如下


除必要外,本文不打算列出各个对应的表结构,具体结构可参考


Class类文件结构之常量表
https://blog.csdn.net/u014296316/article/details/83020087


这里抛砖引玉。
常量池第一个常量


第一个常量类型由 0x000A 处标出,值为0A,十进制为10,查表,类型为CONSTANT_Methodref_info(不得不说现在的编辑器很强大,没有对应功能的话就只能慢慢查了)。
表中数据类型为u1、u2、u2 共占 5 个字节(具体表信息和内容含义后文再续)。如果是CONSTANT_Utf8_info类型,还会有 length 属性表明字面量占用字节长度,需要加上此长度。则第二个常量类型由 0x000F 处标出,值为0F,十进制为09,表类型为CONSTANT_Fieldref_info。以此类推...
这样一步一步查找对应常量也是比较麻烦的,好在Java内置类工具——javap可对.Class文件字节码进行分析,通过命令


javap -verbose fileName


能得到下图信息 :(仅展示了常量池部分)
javap分析常量池


常量池数量值在 0x0008 ~ 0x0009 为 23,转换十进制为35,表示常量池索引范围为 1~35。观察上两张图,前者索引从0开始,后者索引从1开始。
若摸不着门路,常量池的分析着实让人头大,个人看来,常量池里的信息是在 “搭积木” 。
本例子中,常量池涉及到的常量类型为:


  • CONSTANT_Methodref_info
  • CONSTANT_Fieldref_info
  • CONSTANT_String_info
  • CONSTANT_Class_info
  • CONSTANT_Interger
  • CONSTANT_NameAndType
  • CONSTANT_Utf8


暂时抛开具体表结构,以上表类型结构关系如示:
常量池常量类型结构


上面仅画出了当前例子涉及到的常量类型的组成关系,任意类型的常量,不断拆分,最后都会指向基本类型的常量CONSTANT_Utf8,或自身就为基本类型如CONSTANT_Interger。
可以理解为,基本常量类型CONSTANT_Utf8本身没有过多意义,其它的类型为场景,为CONSTANT_Utf8赋予了意义。
CONSTANT_Utf8_info可以算是最基本的类型,结构为
// 伪代码
{
    // 常量类型
    u1 tag;
    // 字节长度
    u2 length;
    // UTF-8缩略编码
    bytes[length];
}


在遇到CONSTANT_Utf8_info类型的常量时,将bytes逐个按照UTF-8缩略编码即可得到对应的字面量


   04 类级信息   


定义的类为
public class TestClass implements Serializable


其中包含的信息为:
  • 类本身:TestClass
  • 访问标志:public
  • 实现接口Serializable
  • 父类为:Object


从.Class文件格式表中,在常量池后紧接着的数据,就是类级数据



从 0x0143 ~ 0x014C :


  • access_flags(u2): 十六进制值为 0x0021
  • this_class(u2): 十进制值为5,指向常量池第5个常量, 类型为CONSTANT_Class_info,类为 TestClass
  • super_class(u2): 十进制值为5,指向第6个常量,类型为CONSTANT_Class_info,类为 java/lang/Object
  • interface_count(u2): 实现接口数量 1 个
  • interface[0] :指向常量池第7个常量,类型为CONSTANT_Class_info,接口名诶 java/io/Serializable


CONSTANT_Class_info 常量表结构如下
// 伪代码
{
    // 常量类型
    u1 tag ;
    // 指向常量池偏移量为name_index,类型为CONSTANT_Utf8_info类型的索引, 
    //代表类或接口的权限定名
    u2 name_index; 
}


与之前所说常量池里在搭积木的说法一致,后面涉及到的常量池里的类型依然如此。
访问标志使用标志位来表示,各个标志含义如表


当前情况为 0x0001 | 0x0020 = 0x0021


   05 attribute(属性表)   


属性表比较特殊,.Class文件、字段表、方法表等都可以携带自己的属性表集合,用来描述专有的场景,也因此将此表做前置说明。
属性表的特点为:
  1. 规则较宽松,不要求严格的顺序、长度、内容
  2. 只要不与已有属性表重复,任何编译器都可以向属性表中写入自定义的属性信息,JVM会忽略掉不认识的属性。


属性表结构为
// 伪代码
{
    // 指向常量池类型为CONSTANT_Utf8_info的常量,代表属性名
    u2 attribute_name_index ;
    // 属性表info占用长度
    u4 attribute_length; 
    // 这需要具体实现的结构,长度为attribute_length
    Info info;
}


因此一个属性表的长度为 u2 + u4 + attribute_length。Java与定义来很多属性表,文章检出涉及到的做后续说明,其它在实际需要时自行查阅


字段表与方法表携带的属性表暂未涉及,当前节点涉及到类型为SourceFile的.Class携带的属性表。
class携带属性表示例


范围为 0x025A ~ 0x0262 共占 u2 + u4 + attibute_length = 8 字节,SourceFile属性结构如下
伪代码
{
    // 指向常量池类型为CONSTANT_Utf8_info的常量,代表属性名
    u2 attribute_name_index ;
    // 属性表内容占用长度
    u4 attribute_length;
    // 指向常量池类型为CONSTANT_Utf8_info的常量,代表源文件名
    u2 sourcefile_index;
}


因此通过此SourceFile属性表,得知源文件名为TestClass.java


   06 字段表   


查阅.Class文件格式表,接口表之后,就是字段数量已经字段数量表
字节码字段表


从 0x014D ~ 0x016E , 其中 0x014D ~ 0x014E 表示字段数, 值为 0x0003,表示字段数为3,随便就是紧挨着的字段表。字段表结构如下
// 伪代码
{
    // 访问标志
    u2 access_flags
    // 指向常量池类型为CONSTANT_Utf8_info的常量,表示字段名
    u2 name_index
    // 指向常量池类型为CONSTANT_Utf8_info的常量,用描述符表示
    // 字段类型
    u2 descriptor_index
    // 属性表数量
    attributes_count
    // 属性表内容
    attribuite_info
}


字段表和.Class一样,能携带自己的属性表处理特殊场景,attribuite_info是非必须的。当attributes_count的值为0时, 说明无需attribuite_info。字段也拥有访问标志来对字段做进一步约束。字段名则用name_index指向常量池的常量来表示,字段类型则用描述符来表示, 比如 Int 表示为 I (忘了往前看基础知识)。
当前例子定义的字段如下:
private int m = 123;
private static int x = 10;
private static final int y = 20;
定义了成员变量 m 和 类变量 x ,y。举例 y 来看


位置为 0x015F ~ 0x016E,其中:
  • 访问标志:0x001A
  • 字段名索引:0x000B,十进制值为11,指向常量池第11个常量,为y
  • 描述符索引为:0x0009,指向常量池第9个常量,为I
  • 属性表数为:0x0001,数量为1
  • 属性表总占用长度为:u2 + u4 + 2字节 共10位,即 0x0167 ~ 0x016E


字段访问标志位含义如表:


当前为 private static final ,即 0x0001 | 0x0008 | 0x0010 , 为 0x001A。
通过访问标志、字段索引、描述符信息,就可以拿到
-> private static final int y 这一信息。

对于使用 final和static修饰的并且时基本数据类型的变量,会使用属性表ConstantVulue来进行赋值。属性表 ConstantVulue 除了约定的基本数据外,还有类型为 u2 的ConstantValue_index 索引来表示指向常量池中的常量用来初始化数据。值位于 0x016D ~ 0x016E,为 0x000D,指向的常量表索引13处的值为整型的20;


而实例中的变量m,类变量x则在成员初始函数、类初始函数中进行赋值,下文做说明。



   07 方法表   


字段表之后,紧挨着的是方法数量与方法表集合


范围为 0x016F ~ 0x0258,方法数值为 0x0005 共 5 个方法。除了示例自定义的 increace() ,m() 和 hello() 外,还有实例构造方法<init>()v,类构造器<clinit>()方法。
方法表结构为:
伪代码
{
    // 访问标志
    u2 access_flags;
    // 方法名索引,指向常量池类型为CONSTANT_Utf8_info的常量
    u2 name_index;
    // 方法返回值描述符索引,指向常量池类型为CONSTANT_Utf8_info的常量
    u2 descriptor_index;
    // 属性表数量
    u2 attributes_count;
    // 属性表内容
    Info info;
}
方法也可以携带属性表来描述专有场景。通过上述结构以及具体字节码,可以推算出方法所包含的内容。其中,访问标志含义如表:



取构造实例方法方法<init>()做示例来看:


信息为:
  • access_flags: 0x0001 ,为public
  • name_index: 0x000E,为14,对应常量池得到 <init>
  • descriptor_index: 0x000F,为15,对应常量池得到 ()V
  • attributes_count: 0x0001,为1,属性表数量为1


可以根据信息反推得到函数信息,实例化函数被表达为 public <init>()V,依次为访问标志位、函数名、返回值。
一个函数方法,更重要的如何表述它所提供的功能。本质上来说,函数体里所有的代码段都是在进行运算操作,因此,只要将函数体里的代码段转换为字节码指令即可,函数执行时根据执行即可,再次之上再几率一下关键信息,就能得出函数执行、栈深度、局部变量数、字节码占用文件大小。
在attributes_count之后,从 0x0179 ~ 0x01A5 是属性表包含的内容。根据之前所说的属性表约定的格式 0x0179 ~ 0x0180 位置值为 0x0010,十进制为16,查找啊常量池知属性表为Code类型。Code类型属性表结构如下:


// 伪代码
{   
    // 属性表名称索引,指向常量表类型为CONSTANT_Utf8_info的常量
    u2 attribute_name_index;
    // 属性表长度
    u4 attribute_length;
    // 栈深
    u2 max_stack;
    // 局部变量数
    u2 max_locals;
    // 字节码指令长度
    u4 code_length;
    // 字节码指令
    u1 code code_length;
    // 异常表数量
    u2 exception_table_length;
    // 异常表
    exception_info exception_table;
    // 属性表数量
    u2 attributes_count;
    // 属性表
    attribute_info attributes;
}


获取方法信息不仅能直接通过阅读字节码文件,通过
javap -verbose className
也可以拿到,一起贴了


红圈为字节码指令集,黄圈为每一条字节码指令,绿圈为Code属性表的基本信息,蓝圈为javap工具解析出的实例函数信息。
首先,最大栈深为2,在函数执行的任意时刻都不会超过这个操作数栈深度的最大值;然后局部变量数为1,表示局部变量表所需的存储空间,单位为Slot,此单位是JVM为局部变量分配内存所使用的最小单位。蓝圈里还有args_sige表示方法接受的参数数,这里为1,也就是 this。最后就是函数代码块里转成的字节码指令里。
字节码指令不在文章的讨论范围内,不妨简单了解。
字节码指令代表着某种特定操作,由一个字节长度代表其操作含义,后面可以跟随0到多个所需操作数。
本例子中的初始化函数被翻译成了:
2A B7 00 01 2A 10 7B B5 00 02 B1

也就是上图蓝圈处的:


{
   // 将 this 入栈
   0: aload_0
   // 唤醒父类实例化函数
   1: invokespecial #1
   4: aload_0
   // 将 123 入栈
   5: bitpush  123
   // 访问字段 m,将123存入
   7: putfield #2
   // 方法返回
   10: return
}


这里只说明 putfield #2,对应的字节码为 B5 00 02,其中 B5 代表操作执行 00 02 为操作所需参数,值为 0x0002,表示指向常量池类型为 CONSTANT_Fieldref_info 的常量。CONSTANT_Fieldref_info 结构为

// 伪代码
CONSTANT_Fieldref_info
{
  u1 tag;
  // 指向常量池类型为CONSTANT_ClassInfo_info的常量
  // 代表字段所属类
  u2 class_index;
  // 指向常量池类型为CONSTANT_NameAndType_info的常量
  // 代表字段名和类型
  u2 name_and_type_index;
}

CONSTANT_ClassInfo_info
{
  u1 tag;
  // 指向常量池尾CONSTANT_Utf8_info的常量
  // 代表类名
  u2 name_index;
}

CONSTANT_NameAndType_info
{
  u1 tag;
  // 指向常量池尾CONSTANT_Utf8_info的常量
  // 代表 名称 
  u2 name_index;
  // 指向常量池尾CONSTANT_Utf8_info的常量
  // 代表 所属类型 
  u2 descriptor_index;
}

当前为指向第二个常量,对照信息:
m初始化


能知道字节码操作 B5 00 02 是将栈中的 123 给 TestClass.m 进行赋值 ,也就是例子中定义的 private int m = 123 的赋值操作。
而 x 的赋值则在类构造器<cinit>中进行,inicrea()和m()函数也可以用相同的方式进行分析。不再陈述,点到为止。



   08 总结   


至此,了解.Class文件的如何,通过.Class文件格式表可以解析出文件内容的基本信息。.Class文件可以看成多张表的集合,根据表制定的规则,顺藤摸瓜,自然能找出对应信息。
.Class文件在如何表达信息上不难理解,难的是有耐心去缕清这些琐碎的索引关系,尤其常量池和属性表部分。属性表部分则提供了足够的发挥空间,根据场景提供更多内容。
文章仅了解了解析.Class文件的基本规则,更进一步的解析规则感兴趣或需要时再了解即可,方法不变。
参考
《深入理解Java虚拟机》 —— 第6章


1、深入理解JVM字节码执行引擎https://blog.csdn.net/suifeng629/article/details/823497842、java中class文件的意义是什么?https://blog.csdn.net/dangbai01_/article/details/800973403、java语言为什么可以跨平台https://blog.csdn.net/banjing_1993/article/details/82349013

欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,学习能力的提升上有新的认识,欢迎转发分享给更多人。

欢迎各位读者加入程序员小乐技术群,在公众号后台回复“加群”或者“学习”即可。

猜你还想看


阿里、腾讯、百度、华为、京东最新面试题汇集

从 Spring Cloud 看一个微服务框架的「 五脏六腑 」

单例模式与垃圾回收,看这篇就对了!

怎么实现单点登录?看完这篇你就知道了!


关注微信公众号「程序员小乐」,收看更多精彩内容
嘿,你在看吗?

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存