《Java 底层原理》Java 字节码详解

前言

我们在开发中会遇到一些Java的执行超出我们的想象,但是又不知道他为什么会这样执行,这个时候我们就需要能够知道他编译后Class文件是什么样子的,并且理解字节码的含义。

Java字节码的原理

进制

class文件就是字节码文件,直接是打不开,打开也是乱码,需要解析才能看明白里面的内容。

现在存在很多语言都是允许在Jvm上,比如Kotlin。 他们其实就是通过编译也编译成Jvm认识的.class 文件即可。

大端和小端

大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:

地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。

小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,

高地址部分权值高,低地址部分权值低。

字节码文件内容组成结构

Java class文件的内容结构

下面解析一下一个不太好理解的结构。

1. 魔数:用于判断这个文件是不是class合格的文件。

2. 次版本号和主板本号:主板本号加次版本号用于判断这个class文件是否能被这个版本的Jvm 解析, 比如jdk8的class就不能被java7版本的Jvm解析。

3. 常量池个数,最小是1,真实的常量池个数是2个字节计算出来的数量 - 1。

下面是常量池的字节码结构(u1 就是一个字节,u2 就是两个字节,类推):

4. 类的访问控制权限

补充 :Acc_static                               0x0008         static 修饰
举个案例:
String[] 数组通过字节码表示是[Ljava/lang/String;
5. 类的成员变量字节码格式:
filed_info: {
    u2 access_flags;    -- 属性的访问类型和修饰符
    u2 name_index;      -- 成员变量的名字,指向常量池的地址
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

6. 方法的字节码格式:

method_info {
    u2 access_flags;    -- 方法的访问属性和修饰符
    u2 name_index;      -- 方法的名字,指向常量池的地址
    u2 descriptor_index;   -- 描述符字符串,指向常量池地址,mian方法的描述符([java/lang/String;)v
    u2 attributes_count;  -- 对应下面的code_attribute
    attribute_info attributes[attributes_count];
}

一般一个类除了你定义的方法外还会存在两个方法,clinit 和 init 处理你的静态代码和默认构造函数。

方法字节码:还包含:

code_attribute { -- Code_attribute包含某个方法、实例初始化方法、类或接口初始化方法的Java虚拟机指令及相关辅助信息
    u2  attribute_name_index; -- 指向常量池,名称
    u4  attribute_length; -- 后面全部的总长度
    u2  max_stack; -- 用来给出当前方法的操作数栈在方法执行的任何时间点的最大深度
    u2  max_locals;  -- 用来给出分配在当前方法引用的局部变量表中的局部变量个数
    u4  code_length; --给出当前方法code[]数组的字节数
    u1  code[code_length]; -- 给出了实现当前方法的Java虚拟机代码的实际字节内容(这些数字代码实际对应一些Java虚拟机的指令)
    u2  exception_table_lentgh;  -- 异常的信息个数
    {
        u2  start_pc; -- 这两项的值表明了异常处理器在code[]中的有效范围,即异常处理器x应满足:start_pc≤x≤end_pc
        u2  end_pc; -- start_pc必须在code[]中取值,end_pc要么在code[]中取值,要么等于code_length的值
        u2  handler_pc; -- 表示一个异常处理器的起点
        u2  catch_type; -- 表示当前异常处理器需要捕捉的异常类型。为0,则都调用该异常处理器,可用来实现finally。
    } exception_table[exception_table_lentgh]; 
    u2  attribute_count; -- 表示该方法的其它附加属性,
    attribute_info  attributes[attributes_count]; -- LineNumberTable、LocalVariableTable
}

Java方法所在行信息:

LineNumberTable_attribute{ --被调试器用来确定源文件中由给定的行号所表示的内容,对应于Java虚拟机code[] 数组的哪部分
    u2 attribute_name_index;
    u4 attribute_length;
    u2 line_number_table_length;
    {
        u2 start_pc;
        u2 line_number; -- 该值必须与源文件中对应的行号相匹配
    } line_number_table[line_number_table_length];
}

局部变量表信息:

LocalVariableTable_attribute{
    u2  attribute_name_index;
    u4  attribute_length;
    u2  local_variable_table_length;
    {
        u2 start_pc;
        u2 length;
        u2 name_index;
        u2 descriptor_index; --用来表示源程序中局部变量类型的字段描述符
        u2 index;
    } local_variable_table[local_variable_table_length];
}

7.类属性字节码格式:

attribute_info: {
    u2 attribute_name_index;
    u1 attribute_length;
    u1 info[attribute_length];
}

字节码文件解析

我们一起看一下Java编译后的class文件:

这个是按照16进制显示的,没有按照任何编码的方式进行解析过的原信息。

我们按照上面字节码文件内容组成结构,来解析一下这个字节码文件

魔数:cafe babe 就表示这个是class 文件,Jvm才识别。

次版本号:0000

主版本号:0034

常量池数量:0021 就是2*16 + 1 33个常量池,但是需要减一,得到32个常量池。

常量池信息:前一个字节是tag 表示常量池的类型: oa 等于10 从常量池结构图可以找到时 constent_Methodref_info 这个类型,后面读取4个字节 00 0600 12;

                      00 06 表示constent_class_info的索引项;00 12 表示constent_nameAndType_info 名称和类型描述符的索引项,一次解析32个常量池。

                      特殊说明: 01类型的常量池,需要根据length 的长度动态解析。 比如 tag : 01  length : 0006   字符串:3c69 6e69 743e。

类的控制访问权限:0021 表示:0020加0001组合,说明是...和public 。

类名:0005 间接引用常量池第5个常量池

父类名:0006 间接引用常量池第6个常量池

接口数量:0001 实现一个接口。

接口数组:0007 指向常量池 第7个常量池,如果接口数量为零则不出现。

成员变量数量:0000 表示没有成员变量。

成员变量数组:如果成员变量数量为零则不占用字节。

方法数量:0002  两个方法,

方法数组:0001:修饰词pubilc;0008:方法名,指向常量池;0009 :描述符,指向常量池 ;0001:code_attributes的数量;

                  开始解析code_attribute 000a:code_attribute名称,指向常量池;0000 002f :attribute的长度47;0001:max_stack 操作数栈;0001:max_locals 局部变量个数;0000 0005 :code的长度; 2ab7 0001 b1 :code的内容就是操作虚拟机的指令信息; 00 00 :异常信息没有;00 02:表是其他附加信息有两个。

                 开始解析LineNumberTable_attribute :00 0b:指向常量池,就是指的lineNumberTable;00 0000 06:指的是这个信息的长度;00 01 :line_number_table_length;

                 00 00:start_pc ;  00 03 :Java这个方法的代码行号。

                 开始解析LocalVariableTable_attribute  00 0c:额外信息的名字,指向常量池;00 0000 0c:该信息长度;00 01:variable_table 信息的长度;00 00:start_pc;

                 00 05:长度;000d:指向常量池,局部变量描述符this;00 0e:指向常量池,类信息描述符;

解析方法字节码的过程中init 方法解析完成后,中间出现0000 无法解析,我猜是clinit 方法的解析,但是因为我们写所以使用0000 表示了。

类属性数量:

类属性数组:0001:第一次属性;  0010:指向常量池;  0000:不知道; 0002:第二个属性; 0011:指向常量池。

到此字节码文件全部解析完毕, 中间有一点瑕疵,后续学习中改进。 

总结

字节码学习,让我们了解Java底层的实现有巨大的帮助。

posted @ 2021-01-29 17:29  加速丨世界  阅读(578)  评论(0编辑  收藏  举报