初探JVM字节码

1 篇文章 0 订阅

作者: 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_info1CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
utf8 编码的字符串
CONSTANT_Integer_info3CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
整形
CONSTANT_Float_info4CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
浮点型
CONSTANT_Long_info5CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
长整型
CONSTANT_Double_info6CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
双精度浮点型
CONSTANT_Class_info7CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
class
CONSTANT_String_info8CONSTANT_String_info {
u1 tag;
u2 string_index;
}
字符串
CONSTANT_Fieldref_info9CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
字段的类型
CONSTANT_Methodref_info10CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
方法的类型
CONSTANT_InterfaceMethodref_info11CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
接口方法
CONSTANT_NameAndType_info12CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
名称和类型的
CONSTANT_MethodHandle_info15CONSTANT_MethodHandle_info {
u1 tag;
u1 reference_kind;
u2 reference_index;
}
方法句柄
CONSTANT_MethodType_info16CONSTANT_MethodType_info {
u1 tag;
u2 descriptor_index;
}
方法类型
CONSTANT_InvokeDynamic_info18CONSTANT_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 NameValueInterpretation
ACC_PUBLIC0x0001public类
ACC_FINAL0x0010final类
ACC_SUPER0x0020如果设置了这个值, 则调用父类方法的时候会搜索类层次, 找到最近的一个父类方法进行调用.
ACC_INTERFACE0x0200interface, 接口
ACC_ABSTRACT0x0400abstract 类
ACC_SYNTHETIC0x1000synthetic, 不存在于源代码,由编译器生成
ACC_ANNOTATION0x2000注解类
ACC_ENUM0x4000枚举类

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 NameValueInterpretation
ACC_PUBLIC0x0001public
ACC_PRIVATE0x0002private
ACC_PROTECTED0x0004protected
ACC_STATIC0x0008static
ACC_FINAL0x0010final
ACC_VOLATILE0x0040volatile
ACC_TRANSIENT0x0080transient
ACC_SYNTHETIC0x1000synthetic, 不存在于源代码,由编译器生成
ACC_ENUM0x4000enum

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 NameValueInterpretation
ACC_PUBLIC0x0001public
ACC_PRIVATE0x0002private
ACC_PROTECTED0x0004protected
ACC_STATIC0x0008static
ACC_FINAL0x0010final
ACC_SYNCHRONIZED0x0020synchronized, 使用 monitor 监视器实现
ACC_BRIDGE0x0040桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NATIVE0x0100native, 使用其它语言实现的方法
ACC_ABSTRACT0x0400abstract, 抽象方法
ACC_STRICT0x0800浮点模式, JDk 1.1以及之前版本的编译器是 非FP-strict模式
ACC_SYNTHETIC0x1000源代码不存在, 由编译器生成

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 代码.

代码地址: https://github.com/LemonLmNan/ByteCode

注意事项

常量池

这里有一个地方要注意, 常量池里的常量数量如果是 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需要做额外的处理, 这里为了处理方便, 添加了一个空的常量进常量池.


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ArCHqoJa-1603192998073)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7c4a6e1fa38344ffb4a4f78fe83f7151~tplv-k3u1fbpfcp-watermark.image)]
但实际上, 官方文档自己也说了这是一个糟糕的决定.

oracle的官方文档说明:

All 8-byte constants take up two entries in the constant_pool table of the class file. If a CONSTANT_Long_info or CONSTANT_Double_info structure is the item in the constant_pool table at index n, then the next usable item in the pool is located at index n+2. The constant_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/%E7%94%A8Java%E5%AE%9E%E7%8E%B0JVM%E7%AC%AC%E4%B8%89%E7%AB%A0-%E8%A7%A3%E6%9E%90class%E6%96%87%E4%BB%B6.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
为什么要学JVM1、一切JAVA代码都运行在JVM之上,只有深入理解虚拟机才能写出更强大的代码,解决更深层次的问题。2、JVM是迈向高级工程师、架构师的必备技能,也是高薪、高职位的不二选择。3、同时,JVM又是各大软件公司笔试、面试的重中之重,据统计,头部的30家互利网公司,均将JVM作为笔试面试的内容之一。4、JVM内容庞大、并且复杂难学,通过视频学习是最快速的学习手段。课程介绍本课程包含11个大章节,总计102课时,无论是笔试、面试,还是日常工作,可以让您游刃有余。第1章 基础入门,从JVM是什么开始讲起,理解JDK、JRE、JVM的关系,java的编译流程和执行流程,让您轻松入门。第2章 字节码文件,深入剖析字节码文件的全部组成结构,以及javap和jbe可视化反解析工具的使用。第3章 类的加载、解释、编译,本章节带你深入理解类加载器的分类、范围、双亲委托策略,自己手写类加载器,理解字节码解释器、即时编译器、混合模式、热点代码检测、分层编译等核心知识。第4章 内存模型,本章节涵盖JVM内存模型的全部内容,程序计数器、虚拟机栈、本地方法栈、方法区、永久代、元空间等全部内容。第5章 对象模型,本章节带你深入理解对象的创建过程、内存分配的方法、让你不再稀里糊涂。第6章 GC基础,本章节是垃圾回收的入门章节,带你了解GC回收的标准是什么,什么是可达性分析、安全点、安全区,四种引用类型的使用和区别等等。第7章 GC算法与收集器,本章节是垃圾回收的重点,掌握各种垃圾回收算法,分代收集策略,7种垃圾回收器的原理和使用,垃圾回收器的组合及分代收集等。第8章 GC日志详解,各种垃圾回收器的日志都是不同的,怎么样读懂各种垃圾回收日志就是本章节的内容。第9章 性能监控与故障排除,本章节实战学习jcmd、jmx、jconsul、jvisualvm、JMC、jps、jstatd、jmap、jstack、jinfo、jprofile、jhat总计12种性能监控和故障排查工具的使用。第10章 阿里巴巴Arthas在线诊断工具,这是一个特别小惊喜,教您怎样使用当前最火热的arthas调优工具,在线诊断各种JVM问题。第11章 故障排除,本章会使用实际案例讲解单点故障、高并发和垃圾回收导致的CPU过高的问题,怎样排查和解决它们。课程资料课程附带配套项目源码2个159页高清PDF理论篇课件1份89页高清PDF实战篇课件1份Unsafe源码PDF课件1份class_stats字段说明PDF文件1份jcmd Thread.print解析说明文件1份JProfiler内存工具说明文件1份字节码可视化解析工具1份GC日志可视化工具1份命令行工具cmder 1份学习方法理论篇部分推荐每天学习2课时,可以在公交地铁上用手机进行学习。实战篇部分推荐对照视频,使用配套源码,一边练习一遍学习。课程内容较多,不要一次性学太多,而是要循序渐进,坚持学习。      

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬楠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值