字节码详解 ---- .class 文件
什么是 .class 文件?
经过 java 编译器(javac)编译之后形成的二进制文件,JVM 可以解析并执行编译后的 java 代码。
.class 文件内容的结构
魔数头和 .class 文件的版本
开头的四字节的内容翻译过来就是 "Coffee Baby",中文就是“咖啡宝贝”,是识别 .class 文件的标识。不仅是 .class 文件,其他类型的文件,比如 .jpg,.jpeg,这些文件都有魔数头的,可以根据这个标识去识别文件的类型。
常量池
就是字节码文件存放常量的一块区域,主要包括两大类常量:字面量和符号引用。
字面量其实很好理解,是面向我们 java 程序员的,比如说,字符串字面量,数字等这些。
符号引用有三种:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
符号引用可以理解为依赖关系,最终组合到一起就可以形成之前说的那几个东西,......
访问标志
字节码文件是以类为单位的,一个类就对应一个字节码文件,访问标志就是表示这个class是接口还是类,是否public的,是否被标记为final。
比如说 ACC_PUBLIC 表示class是 public 类型的,ACC_FINAL 表示这是一个最终类。
类索引、父类索引、接口索引
class文件可以依靠这三个索引去确定继承关系。
字段表属性
用来描述类或者接口中的属性信息的,比如说是否是静态变量、它的作用域、数据类型等等。
方法表属性
用来描述方法信息的,比如说方法的类型、作用域等等。
方法表中有一个比较重要的就是 code 属性了,然后 code 属性也有几个比较重要的属性:
比如说 stack、locals、args_size、LineNumberTable、LocalVariableTable。
stack:最大操作栈深度,JVM 运行时会根据这个属性来分配栈帧中的操作栈深度。
locals:表示局部变量所需要的存储空间大小,单位是 slot。
args_size:方法的参数个数,如果是实例方法的话,会有一个隐藏参数this。
LineNumberTable:描述源代码与字节码文件的行号偏移量的映射关系,我们的debug逻辑就是这个。
LocalVariableTable:描述帧栈中局部变量与源码中定义的变量之间的关系。(作用域,方法签名)
ExceptionTable:异常表,try-catch-finally 会有的。from、to、target、type => from to 代表执行的指令集合,target 代表发生异常时跳转到的行数,type 表示异常类型(any是任何异常)。
属性表属性
这个是用来描述方法表或者字段表中一些特殊的属性的。
字节码增强技术
概念
既然字节码文件是有规则的,那么我们就可以根据规则对字节码文件进行修改:
了解 Spring 的都知道,Spring 的 AOP 是基于动态代理实现的,一个是 jdk 的动态代理,另一个就是 cglib。glib底层依赖于 ASM ,而 ASM 就可以用来修改字节码文件。(具体怎么操作,自己可以百度一下)
ASM 直接操作指令,Javassist 则是在源代码上着手,相较于 ASM 来说,上手难度大大降低,无须了解虚拟机指令,直接使用 java 代码编程即可。
运行时类的重载
默认情况下,JVM 是不允许在运行时动态重载一个类的,也就是说一个类被加载后,就不能改变了。
怎么样才能实现动态改变类的行为能力,并且让类重载呢?
Instrument
Instrument 是 JVM 提供的一个可以修改已加载类的类库。JDK 1.6 之前只能在 JVM 刚启动加载类时生效,1.6 之后支持在运行时对类定义的修改。实现 ClassFileTransformer 接口即可,在 transform() 方法中,可以利用 Javassist 或者 ASM 修改字节码,生成新的字节码数组并返回。当然,还需要借助 Agent 的能力进行注入。
JVMTI & Agent & Attach API
Instrument 功能的实现离不开这三个的支持。但是我们首先要了解一个东西,叫做 JPDA(Java Platform Debugger Architecture)。
JPDA 是一套用于 java 程序调试的标准规范,允许 JVM 卸载旧的类信息,重新加载新版本的类,即类的重载,任何 JDK 都必须实现。
JVMTI:java 虚拟机工具接口
JDWP:java 调试协议
JDI:java 调试接口
JVMTI提供的所谓的接口其实就是去注册一些钩子函数,在特定的事件生命周期中会去触发,比如说 类文件的加载、异常的产生与捕获、线程的启动与结束等等。
Agent 其实就是 JVMTI 的一种实现,有两种启动方式,一种是随着 java 进程启动而启动,另一中就是通过 Attach API ,在运行的时候,将模块(jar包)动态注入到 java 进程内。
这个 Attach API 的作用就是提供了 JVM 进程间的通信能力,比如说一些 java 工具,jstack 或者 jmap 就是利用这个 Attach API 讲 对应 JVM 内部的信息 dump 下来。
总结
字节码文件内容这里,可以使用 javap 进行反编译查看学习,字节码增强技术这里了解一下,背诵即可。
2025/01/15
writeBy kaiven