【底层原理之旅—带你分析Class字节码的结构(下)】|Java 开发实战

简介: 【底层原理之旅—带你分析Class字节码的结构(下)】|Java 开发实战

字段表集合


字段表集合用于描述接口或者类中声明的变量,这里的数据为:0000。



fields_count 字段计数器


fields_count 的值表示当前 Class 文件 fields[]数组的成员个数。 fields[]数组中每一项都是一个 field_info 结构的数据项,它用于表示该类或接口声明的类字段或者实例字段。



fields[] 字段表


fields[]数组中的每个成员都必须是一个 fields_info 结构的数 据项,用于表示当前类或接口中某个字段的完整描述。fields[]数组描述当前类或接口 声明的所有字段,但不包括从父类或父接口继承的部分。


field_info 结构格式如下:

field_info { 
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}
复制代码





属性集合


attributes_count 属性计数器


attributes_count 的值表示当前 Class 文件 attributes 表的成员个 数。attributes 表中每一项都是一个 attribute_info 结构的数据项。




attributes[]属性表


属性表,attributes 表的每个项的值必须是 attribute_info 结构(§4.7)。


属性(Attributes)在 Class 文件格式中的 ClassFile 结构、field_info 结构,method_info 结构和 Code_attribute 结构都有使用,所有属性的通用格式如下:


image.png

image.png




对于任意属性,attribute_name_index 必须是对当前 Class 文件的常量池的有效16 位无符号索引。常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示当前属性的名字。attribute_length 项的值给出了跟随其后的字节的长度,这个长度不包括 attribute_name_index 和 attribute_name_index 项的 6 个字节。


有些属性因 Class 文件格式规范所需,已被预先定义好。这些属性在表 4.6 中列出,同时,被列出的信息还包括它们首次出现的Class文件版本和 Java SE 版本号。在本规范定义的环境 中,也就是已包含这些预定义属性的 Class 文件中,它们的属性名称被保留,不能再被属性表中其他的自定义属性所使用。 下表是 class 文件的属性:




方法表集合


在字段表后的 2 个字节是一个方法计数器,表示类中总有有几个方法,在字段计数器后,才是具体的方法数据。这里数据为:0002 。



methods_count 方法计数器


methods[] 数组中的每个成员都必须是一个 method_info 结构的 数据项,用于表示当前类或接口中某个方法的完整描述。如果某个 method_info 结构 的 access_flags 项既没有设置 ACC_NATIVE 标志也没有设置 ACC_ABSTRACT 标志, 那么它所对应的方法体就应当可以被 Java 虚拟机直接从当前类加载,而不需要引用其它 类。method_info 结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、 实例初始化方法方法和类或接口初始化方法方法。methods[]数组 只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。



method_info { 
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}
复制代码



Log 类的字节码文件中,方法计数器的值为 00 02,表示一共有 2 个方法。




第一个方法:


0001 000700 0800 0100 0900 0000 1d00 0100 0100 0000 052a b700 01b1 0000 0001 000a 0000 0006 0001 0000 0003

  • 方法计数器后 2 个字节表示方法访问标识,这里是 0001,表示其实 ACC_PUBLIC 标识,对比上面的图表可知其表示 public 访问标识。
  • 紧接着 2 个字节表示方法名称的索引,这里是 00 07 表示指向了常量池第 7 个常量,查阅可知其指向了。
  • 紧接着的 2 个字节表示方法描述符索引项,这里是 00 08 表示指向了常量池第 8 个常量,查阅可知其指向了()V。
  • 紧接着 2 个字节表示属性表计数器,这里是 00 01 表示该方法的属性表一共有 1 个属性。属性表的表结构如下:


attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}
复制代码



前两个字节是名字索引、接着 4 个字节是属性长度、接着是属性的值。这里前两个字节为 0009,指向了常量池第 9 个常量,查询可知其值为 Code,说明此属性是方法的字节码描述。


Code 属性是一个变长属性,位于method_info结构的属性表。一个 Code 属性只为唯一一个方法、实例类初始化方法或类初始化方法保存 Java 虚拟机指令及相关辅助信息。** 所有 Java 虚拟机实现都必须能够识别 Code 属性。如果方法被声明为 native 或者 abstract 类型,那么对应的 method_info 结构不能有明确的 Code 属性,其它情况下, method_info 有必须有明确的 Code 属性。**


Code 属性的格式如下:

Code_attribute {
    u2 attribute_name_index; 
 u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length; { u2 start_pc;
            u2 end_pc;
            u2 handler_pc;
            u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}
复制代码



  • 根据 Code 属性对应表结构知道,前 2 个字节为 0009,即常量池第 9 个常量,查询知道是字符串常量 Code。
  • 接着 4 个字节表示属性长度,这里值为 0000 001d,即 29 的长度。下面我们继续分析 Code 属性的数据内容。
  • 紧接着 2 个字节为 max_stack 属性。这里数据为 00 01,表示操作数栈深度的最大值。
  • 紧接着 2 个字节为 max_locals 属性。这里是数据为 00 01,表示局部变量表所需的存储空间为 1 个 Slot。在这里 max_locals 的单位是 Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位。
  • 接着 4 个字节为 code_length,表示生成字节码这里给的长度。
  • 这里数据为 00 00 00 05,表示生成字节码长度为 5 个字节。

那么紧接着 5 个自己就是对应的数据,这里数据为 2a b7 00 01 b1,这一串数据其实就是字节码指令。通过查询字节码指令表,可知其对应的字节码指令:


读入 2A,查表得 0x2A 对应的指令为 aload_0,这个指令的含义是将第 0 个 Slot 中为 reference 类型的本地变量推送到操作数栈顶。


读入 B7,查表得0xB7对应的指令为 invokespecial,这条指令的作用是以栈顶的 reference 类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private 方法或者它的父类的方法。这个方法有一个 u2 类型的参数说明具体调用哪一个方法,它指向常量池中的一个 CONSTANT_Methodref_info 类型常量,即此方法的方法符号引用。



读入 00 01,这是 invokespecial 的参数,查常量池得 0x0001 对应的常量为实例构造器“”方法的符号引用。



读入 B1,查表得0xB1对应的指令为 return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。


着 2 个字节为异常表长度,这里数据为 00 00,表示没有异常表数据。那么接下来也就不会有异常表的值。


紧接着 2 个字节是属性表的长度,这里数据为 00 01,表示有一个属性。该属性长度为一个 attribute_info 那么长。


首先,前两个字节表示属性名称索引,这里数据为:00 0A。指向了第 10 个常量,查阅可知值为:LineNumberTable。


LineNumberTable 属性是可选变长属性,位于 Code(§4.7.3)结构的属性表。它被调试 器用于确定源文件中行号表示的内容在 Java 虚拟机的 code[]数组中对应的部分。在 Code 属性 的属性表中,LineNumberTable 属性可以按照任意顺序出现,此外,多个 LineNumberTable 属性可以共同表示一个行号在源文件中表示的内容,即 LineNumberTable 属性不需要与源文件 的行一一对应。


LineNumberTable 属性格式如下:

LineNumberTable_attribute {
    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];
}
复制代码


其前两个字节是属性名称索引,就是上面已经分析过的 00 0A。


接着 4 个字节是属性长度,这里数据为 00 00 00 06,表示有 6 个字节的数据。接着 2 个字节是 LineNumberTable 的长度,这里数据是 00 01,表示长度为 1。接着跟着 1 个 line_number_info 类型的数据,下面是 line_number_info 表的结构,其包含了 start_pc 和 line_number 两个 u2 类型的数据项。前者是字节码行号,后者是 Java 源码行号。


那么接下来 2 个字节为 00 00,即 start_pc 表示的字节码行号为第 0 行。接着 00 03,即 line_number 表示 Java 源码行号为第 3 行。



从上面的反编译来看,上面的分析也是对的:

{
  public com.hello.test.Log();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
}
复制代码

第二个方法这里就不详细分析了,大家可以自己对着上面的反编译结果进行分析。 第二个方法开头是 0009,表示的是 ACC_PUBLIC 与 ACC_STATIC 合在一起的结果。


image.png



相关文章
|
5天前
|
Java
轻松上手Java字节码编辑:IDEA插件VisualClassBytes全方位解析
本插件VisualClassBytes可修改class字节码,包括class信息、字段信息、内部类,常量池和方法等。
36 6
|
6天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
6天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
8天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
6天前
|
SQL 安全 Java
安全问题已经成为软件开发中不可忽视的重要议题。对于使用Java语言开发的应用程序来说,安全性更是至关重要
在当今网络环境下,Java应用的安全性至关重要。本文深入探讨了Java安全编程的最佳实践,包括代码审查、输入验证、输出编码、访问控制和加密技术等,帮助开发者构建安全可靠的应用。通过掌握相关技术和工具,开发者可以有效防范安全威胁,确保应用的安全性。
18 4
|
8天前
|
缓存 监控 Java
如何运用JAVA开发API接口?
本文详细介绍了如何使用Java开发API接口,涵盖创建、实现、测试和部署接口的关键步骤。同时,讨论了接口的安全性设计和设计原则,帮助开发者构建高效、安全、易于维护的API接口。
29 4
|
14天前
|
Java 编译器 Maven
Java“class file contains wrong class”解决
当Java程序运行时出现“class file contains wrong class”错误,通常是因为类文件与预期的类名不匹配。解决方法包括:1. 确保类名和文件名一致;2. 清理并重新编译项目;3. 检查包声明是否正确。
|
13天前
|
SQL Java 程序员
倍增 Java 程序员的开发效率
应用计算困境:Java 作为主流开发语言,在数据处理方面存在复杂度高的问题,而 SQL 虽然简洁但受限于数据库架构。SPL(Structured Process Language)是一种纯 Java 开发的数据处理语言,结合了 Java 的架构灵活性和 SQL 的简洁性。SPL 提供简洁的语法、完善的计算能力、高效的 IDE、大数据支持、与 Java 应用无缝集成以及开放性和热切换特性,能够大幅提升开发效率和性能。
|
7天前
|
安全 Java 测试技术
Java开发必读,谈谈对Spring IOC与AOP的理解
Spring的IOC和AOP机制通过依赖注入和横切关注点的分离,大大提高了代码的模块化和可维护性。IOC使得对象的创建和管理变得灵活可控,降低了对象之间的耦合度;AOP则通过动态代理机制实现了横切关注点的集中管理,减少了重复代码。理解和掌握这两个核心概念,是高效使用Spring框架的关键。希望本文对你深入理解Spring的IOC和AOP有所帮助。
14 0
|
8天前
|
Java API Android开发
kotlin和java开发优缺点
kotlin和java开发优缺点
21 0