初探JVM字节码
作者: LemonNan
原文地址: https://juejin.im/post/6885658003811827725
代码地址: https://github.com/LemonLmNan/ByteCode
字节码
概述
本篇要介绍的是能 "一次编译,到处运行的 JVM 字节码"
为什么能到处运行?
是因为在 任意平台下所编译出来的 class文件都遵循相同的字节码规范, 运行期间 不同平台的 JVM 解析相同的 class文件 能解析出特定于该平台的机器码以供使用.
本文大致介绍
1.字节码文件的结构
2.手动解析class文件的字节码, 解析出对应的信息
这里我在解析的时候, 借助到了一个 IDEA 的插件 jclasslib(不得不说, 真香) , 这个插件能极大的加快手动解析的效率, 毕竟可以校验数据的正确性.
当然, java已经提供了对应的命令供查看
javap -verbose xxx.class
class 文件结构简析
以下结构来自 Oracle 官方文档
ClassFile {
u4 magic; // 魔数, class文件的固定开头, cafebabe 开头的文件jvm才会尝试去解析, 4字节
u2 minor_version; // 小版本, 2字节
u2 major_version; // 大版本 比如 1.8, 2字节
u2 constant_pool_count; // 常量池大小
cp_info constant_pool[constant_pool_count-1]; // 常量池数组
u2 access_flags; // 类的描述符
u2 this_class; // 描述类自身的索引
u2 super_class; // 父类的索引
u2 interfaces_count; // 接口数量
u2 interfaces[interfaces_count]; // 接口数组
u2 fields_count; // 字段数量
field_info fields[fields_count]; // 字段数组
u2 methods_count; // 方法数量
method_info methods[methods_count]; // 方法数组
u2 attributes_count; // 基本信息数量
attribute_info attributes[attributes_count]; // 基本信息数组
}
1.魔数(Magic Number)
所有的.class文件的前四个字节都是魔数,魔数的固定值为:0xCAFEBABE
。魔数放在文件开头,JVM可以根据文件的开头来判断这个文件是否可能是一个.class文件,如果是,才会继续进行之后的操作。
2.版本号
魔数后面的4个字节表示将文件 编译成 class文件
的 JVM版本号
, 前两个字节为 次版本号
, 后面两字节为 主版本号
, 比如 1.8, 在这里是 16 进制的值, 比如我电脑上的1.8.0(通过 java -version 查看), 用该 JVM 编译出来的class文件, 在版本号这里就会显示 0000 0034
.
16进制的 0034 转成十进制为 3*16^1 + 4= 52, 正好对应官网上的大版本 1.8, 加上前面的00, 表示 1.8.0 的版本号.
3.常量池(Constant Pool)
常量池整体上分为两部分: 常量池计数器以及常量池数据区, 数据区存储两类常量: 字面量和符号饮用, 字面量就是Java中的直接量, 比如 “12345” 和 12345 这一类, 符号引用比如字段、方法、类的一些描述信息.
- 常量池计数器 (count, 表示有 count 个常量)
- 常量池数据区 (count 个 cp_info 结构, 有14种类型 cp_info 结构)
14种 cp_info 类型
类型 | tag 值 | 字段及大小 | 描述 |
---|---|---|---|
CONSTANT_Utf8_info | 1 | CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; } |
utf8 编码的字符串 |
CONSTANT_Integer_info | 3 | CONSTANT_Integer_info { u1 tag; u4 bytes; } |
整形 |
CONSTANT_Float_info | 4 | CONSTANT_Float_info { u1 tag; u4 bytes; } |
浮点型 |
CONSTANT_Long_info | 5 | CONSTANT_Long_info { u1 tag; u4 high_bytes; u4 low_bytes; } |
长整型 |
CONSTANT_Double_info | 6 | CONSTANT_Double_info { u1 tag; u4 high_bytes; u4 low_bytes; } |
双精度浮点型 |
CONSTANT_Class_info | 7 | CONSTANT_Class_info { u1 tag; u2 name_index; } |
class |
CONSTANT_String_info | 8 | CONSTANT_String_info { u1 tag; u2 string_index; } |
字符串 |
CONSTANT_Fieldref_info | 9 | CONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; } |
字段的类型 |
CONSTANT_Methodref_info | 10 | CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; } |
方法的类型 |
CONSTANT_InterfaceMethodref_info | 11 | CONSTANT_InterfaceMethodref_info { u1 tag; u2 class_index; u2 name_and_type_index; } |
接口方法 |
CONSTANT_NameAndType_info | 12 | CONSTANT_NameAndType_info { u1 tag; u2 name_index; u2 descriptor_index; } |
名称和类型的 |
CONSTANT_MethodHandle_info | 15 | CONSTANT_MethodHandle_info { u1 tag; u1 reference_kind; u2 reference_index; } |
方法句柄 |
CONSTANT_MethodType_info | 16 | CONSTANT_MethodType_info { u1 tag; u2 descriptor_index; } |
方法类型 |
CONSTANT_InvokeDynamic_info | 18 | CONSTANT_InvokeDynamic_info { u1 tag; u2 bootstrap_method_attr_index; u2 name_and_type_index; } |
invokedynamic是1.7后为jvm上的动态类型语言量身定制的, 1.8上还被用到了lambda上. |
4.访问标志
在 class文件的结构里, 访问标志(Access_Flag)表示的是该类的一些访问标志, 访问标志有以下的类型可选, 如果一个类符合多个 Access_Flag 类型, 则该值是多个类型值进行逻辑与.
比如一个 public final 的类, 该类的 Access_Flag 值为 0x0001 | 0x0010 = 0x0011.
Flag Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC |
0x0001 | public类 |
ACC_FINAL |
0x0010 | final类 |
ACC_SUPER |
0x0020 | 如果设置了这个值, 则调用父类方法的时候会搜索类层次, 找到最近的一个父类方法进行调用. |
ACC_INTERFACE |
0x0200 | interface, 接口 |
ACC_ABSTRACT |
0x0400 | abstract 类 |
ACC_SYNTHETIC |
0x1000 | synthetic, 不存在于源代码,由编译器生成 |
ACC_ANNOTATION |
0x2000 | 注解类 |
ACC_ENUM |
0x4000 | 枚举类 |
5.当前类 - this_class
访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。
6.父类 - super_class
当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。
7.接口信息 - interfaces_count & interfaces[interfaces_count]
父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量.
后面是 interfaces_count 个接口, 需要根据class文件的格式进行解析.
8.字段表 - fields_count & fields[fields_count]
字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。字段表结构如下图所示:
// fields_info 结构, 表示每一个字段的结构
field_info {
u2 access_flags; // 字段的访问权限和属性
u2 name_index; // 字段名索引, 名称的常量在 constant_pool 中的索引, 在 constant_pool 中显示该字段的名称
u2 descriptor_index; // 这个表示字段的类型索引, 指向一个在 constant_pool 中的常量
u2 attributes_count; // 字段的基本属性数量
attribute_info attributes[attributes_count]; // 字段的基本属性数组
}
fields_info
里的 access_flags 选项, 表示字段的访问权限和属性
Flag Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC |
0x0001 | public |
ACC_PRIVATE |
0x0002 | private |
ACC_PROTECTED |
0x0004 | protected |
ACC_STATIC |
0x0008 | static |
ACC_FINAL |
0x0010 | final |
ACC_VOLATILE |
0x0040 | volatile |
ACC_TRANSIENT |
0x0080 | transient |
ACC_SYNTHETIC |
0x1000 | synthetic, 不存在于源代码,由编译器生成 |
ACC_ENUM |
0x4000 | enum |
ext
在java中获取字段的类型, 可以通过以下的代码获取
// test1 是类 ByteCodeDemo 中的一个字段,
// 注意使用这个的时候, 要有get 和 set 方法, 否则会抛异常:
// Exception in thread "main" java.beans.IntrospectionException: Method not found: isTest1(read)/ Method not found: setTest1(write), 源码里面默认的话是用 is+字段名首字母大写 和 get+字段名首字母大写 去判断 read 方法
// 这个是 ByteCodeDemo 类里的一个字段 test1
private String test1 = "12345";
// 下面是获取字段类型代码
PropertyDescriptor descriptor = new PropertyDescriptor("test1", ByteCodeDemo.class);
Class clazz = descriptor.getPropertyType();
// 输出 "class java.lang.String"
System.out.println(clazz);
9.方法表 - methods_count & methods[methods_count]
字段表结束后为方法表,方法表也是由两部分组成,第一部分为两个字节描述方法的个数;第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性,如下图所示:
// 描述方法的结构
method_info {
u2 access_flags; // 方法的访问标志
u2 name_index; // 方法名称的索引
u2 descriptor_index; // 方法类型的索引
u2 attributes_count; // attribute 数量
attribute_info attributes[attributes_count]; // attribute 数组
}
方法的访问标志 access_flags
Flag Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC |
0x0001 | public |
ACC_PRIVATE |
0x0002 | private |
ACC_PROTECTED |
0x0004 | protected |
ACC_STATIC |
0x0008 | static |
ACC_FINAL |
0x0010 | final |
ACC_SYNCHRONIZED |
0x0020 | synchronized, 使用 monitor 监视器实现 |
ACC_BRIDGE |
0x0040 | 桥接方法 |
ACC_VARARGS |
0x0080 | 方法是否接受不定参数 |
ACC_NATIVE |
0x0100 | native, 使用其它语言实现的方法 |
ACC_ABSTRACT |
0x0400 | abstract, 抽象方法 |
ACC_STRICT |
0x0800 | 浮点模式, JDk 1.1以及之前版本的编译器是 非FP-strict模式 |
ACC_SYNTHETIC |
0x1000 | 源代码不存在, 由编译器生成 |
10.基本属性表 - attributes_count & attributes[attributes_count]
字节码的最后一部分,存放了在该class文件中类或接口所定义属性的基本信息。
解析
上面说了一大堆, 接下来开始尝试解析一个class文件, 这里解析class 是根据 class 文件下的数据结构一路解下来, 并 没有什么特殊的技巧 .
这里的话是要是字节的读取, 在这里我主要用到了两个类: ByteInfo 和 Utils
(这两个类是后来才放进 common 包里的, 之前只是单纯的放外面)
最终成果图
下面是java代码解析出来的数据.
插件
右边是一个 IDEA 中一个 jclasslib 的插件, 这里就不介绍怎么安装了.
这个插件的很大一个作用是 方便校验自己解析的是否正确.
流程里的一个
下面是代码里大致的流程, 最后的可读字节数是为了查看校验是否解析完了.
从 analy 这里看流程还算是比较清楚的, 一个结构一个结构的解析, 跟 oracle 官方的结构能一一对上.
首先是魔数 magic, 它在class 文件的开头, 占用 4个字节, 而用16进制来表现的话, 它的值就是 "ca fe ba be", 16进制两位表示一个字节, 下面是魔数的一个解析, 用 JDK的Integer转成16进制的字符串. Integer.toHexString(bytes[i] & 0xFF)
/**
* 魔数
* @throws Throwable
*/
void magic()throws Throwable{
String magicString = ByteInfo.readHexString(4);
if(!"cafebabe".equals(magicString)){
throw new ClassFormatError("magic");
}
this.magic = magicString;
}
版本号和常量池里的常量数量, 常量池存放在 ConstantPool 中, 里面用一个map存储, index 作为 key, 下面是解析代码.
/**
* 版本
* @throws Throwable
*/
void version()throws Throwable{
int minorVersion = ByteInfo.readInt(2);
this.minor_version = minorVersion;
int majorVersion = ByteInfo.readInt(2);
this.major_version = majorVersion;
}
/**
* 常量池里常量的数量
* @throws Throwable
*/
void constantPoolCount()throws Throwable{
int cp_count = ByteInfo.readInt(2);
this.constant_pool_count = cp_count;
// 实际上是 n-1 个常量
ConstantPool.init((cp_count - 1) << 1);
}
// 常量池常量, 部分解析见下图
public ConstantPoolEntry(int cp_index){
this.cp_index = cp_index;
map.put("index", cp_index);
}
部分常量的解析
Method
Attributes
所有 Attributes 的通用字段
根据常量池里的值, 解析成不同的Attributes
其中的一个 LineNumberTable_attribute
代码太多不在这里一一列举了, 想看看作者朴实无华的代码可以到下面这个地址去 clone 代码.
注意事项
常量池
这里有一个地方要注意, 常量池里的常量数量如果是 120, 则实际上只有 120 - 1 = 119 个常量.
从 oracle 给的 class 文件的结构也可以看出:
u2 constant_pool_count; // 常量池大小
cp_info constant_pool[constant_pool_count-1]; // 常量池数组
关于数值的解析
分析到这里, 其实大家应该都比较清楚了, 解析class 实际上是对 byte 的一个操作, ByteInfo 里面可以读取3种大小的 byte[] , byte[1], byte[2], byte[4]
, 这三种分别对应 u1, u2, u4, 因为在本次的代码里, u1和u2的使用 int 表示, u4的使用 long来表示(但是由于某些原因, 读取到long值后又强转为了 int 处理).
这里需要注意的是, Double 和 Long 这两个数值的解析, 跟一般的不太一样(其实也差不多), 在解析完数值之后, 因为这两个常量占用常量池里两个常量, 也就是一个 Double/Long 需要占用2个常量, 具体看下面的代码截图.
解析double
解析完后, double和long需要做额外的处理, 这里为了处理方便, 添加了一个空的常量进常量池.
但实际上, 官方文档自己也说了这是一个糟糕的决定.
oracle的官方文档说明:
All 8-byte constants take up two entries in the
constant_pool
table of theclass
file. If aCONSTANT_Long_info
orCONSTANT_Double_info
structure is the item in theconstant_pool
table at index n, then the next usable item in the pool is located at index n+2. Theconstant_pool
index n+1 must be valid but is considered unusable.大概意思就是比如第 n个是double, 读取了数据后, 下一个可用的是第 n+2 个常量, 第 n+1 个默认不可用
最后
本项目目前仅用于class字节码的入门, 如果有什么好的建议或者意见欢迎大家提出来.
今天这篇到这里就结束了, 感谢观看.
参考
oracle官方: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.putstatic
美团点评: https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
做人做事都非常细的老傅: https://bugstack.cn/itstack-demo-jvm/2019/05/03/用Java实现JVM第三章-解析class文件.html
初探JVM字节码的更多相关文章
- JVM 字节码执行实例分析
前言 最近在看<Java 虚拟机规范>和<深入理解JVM虚拟机>,对于字节码的执行有了进一步的了解.字节码就像是汇编语言,是 JVM 的指令集.下面我们先对 JVM 执行引擎做 ...
- Java finally语句到底是在return之前还是之后执行(JVM字节码分析及内部体系结构)?
之前看了一篇关于"Java finally语句到底是在return之前还是之后执行?"这样的博客,看到兴致处,突然博客里的一个测试用例让我产生了疑惑. 测试用例如下: public ...
- 从JVM字节码执行看重载和重写
Java 重写(Override)与重载(Overload) 重写(Override) 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变.即外壳不变,核心重写! 重写的 ...
- JVM 字节码(四)静态方法、构造代码、this 以及 synchronized 关键字
JVM 字节码(四)静态方法.构造代码.this 以及 synchronized 关键字 一.静态代码 public class ByteCodeStatic { private static fin ...
- JVM 字节码(三)异常在字节码中的处理(catch 和 throws)
JVM 字节码(三)异常在字节码中的处理(catch 和 throws) 在 ClassFile 中到底是如何处理异常的呢? 一.代码块异常 catch catch 中的异常代码块在异常是如何处理的呢 ...
- JVM 字节码(二)方法表详解
JVM 字节码(二)方法表和属性表 上一节中对 ClassFile 的整体进行了五个详细的说明, 本节围绕 ClassFile 最重要的一个内容 - 方法表的 Code 属性展开 ,更多 JVM Me ...
- JVM 字节码(一)字节码规范
JVM 字节码(一)字节码规范 JVM 学习资源 Java ClassFile 字节码规范(Oracle) Java 虚拟机规范(Java SE 7 中文版) (周志明等译) Java 反编译工具 - ...
- JVM总结(五):JVM字节码执行引擎
JVM字节码执行引擎 运行时栈帧结构 局部变量表 操作数栈 动态连接 方法返回地址 附加信息 方法调用 解析 分派 –“重载”和“重写”的实现 静态分派 动态分派 单分派和多分派 JVM动态分派的实现 ...
- JVM 字节码指令手册 - 查看 Java 字节码
JVM 字节码指令手册 - 查看 Java 字节码 jdk 进行的编译生成的 .class 是 16 进制数据文件,不利于学习分析.通过下命令 javap -c Demo.class > Dem ...
随机推荐
- pageX的兼容性处理2
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 学习:二维码、QR码、J4L-QRCode、java
开源码 Java 解码器(编码解码)下载:http://sourceforge.jp/projects/qrcode/downloads/28391/qrcode.zip Java QR Code O ...
- rpm与yum安装及管理程序
安装及管理程序 1.Linux应用程序基础 2.RPM软件包管理工具 3.yum源仓库创建 1.应用程序与系统命令的关系如图: 典型应用程序的目录结构如图: 常见的软件包封装类型如图: 2.RPM包 ...
- MySQL中 BETWEEN ... AND ...
MySQL中 BETWEEN ... AND ... 1. 准备测试数据 CREATE TABLE `student` ( `id` int(11) NOT NULL AUTO_INCREMENT, ...
- 添加删除系统右键菜单(就是上下文菜单,也就是Context Menu)中的一些选项
随着电脑安装的东西越来越多,右侧菜单也原来越长,很不方面.所以打算清理一下 我删除的大约以下几个,友好一点的都可以配置.当然也可以通过注册表直接删除. 特:注册表备份,即导入导出,避免一失足成千古恨. ...
- Redis 中 String 类型的内存开销比较大
使用 String 类型内存开销大 1.简单动态字符串 2.RedisObject 3.全局哈希表 使用 Hash 来存储 总结 参考 使用 String 类型内存开销大 如果我们有大量的数据需要来保 ...
- 记录一次dns劫持及其解决办法
发现问题 偶然发现家里的私人云盘不能用了,最开始以为是云盘出现了问题,各种修复重启后发现云盘并没有问题.然后又发现电脑无法使用浏览器访问网页(或者加载异常缓慢),但是各种软件又可以正常使用,win+R ...
- 音视频开发者的福音,快速集成AI配音能力
音视频内容创作者对剪辑应用中的AI配音需求与日俱增.它的优点很多,不光可以解决雇佣人力配音的成本以及创作者本身的口音.语言等限制,还可以大幅提升生产效率.比如,短视频的播放时间短的几十秒,长的vlog ...
- 小甲鱼 python——第一课作业!
0: python是脚本语言把?虽然不是很清楚什么是脚本语言就是了.复制一下: 脚本语言(英语:Scripting language)是为了缩短传统的"编写.编译.链接.运行"( ...
- VUE学习笔记(李天禹老师版本)
目录 VUE 一 脚手架文件结构 二 关于不同版本的Vue 三 vue.config.js配置文件 vue文件的基本结构 components 四 ref属性 TIPS 五 props配置项 Tips ...