JVM笔记:Java虚拟机的字节码指令详解

本文详细介绍了Java虚拟机(JVM)的字节码指令,包括字节码格式、字节码指令分类及其作用,如加载和存储指令、运算指令、类型转换指令等。通过对字节码的学习,可以深入理解Java代码的底层运行机制,实现字节码插桩等功能。文章通过实例解析了字节码结构,如魔数、版本号、常量池、访问标志、类索引、字段表集合、方法表集合、属性表集合等,并展示了如何通过字节码查看工具快速查看字节码指令。
摘要由CSDN通过智能技术生成

1.字节码

Java能发展到现在,其“一次编译,多处运行”的功能功不可没,这里最主要的功劳就是JVM和字节码了,在不同平台和操作系统上根据JVM规范的定制JVM可以运行相同字节码(.Class文件),并得到相同的结果。之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用javac命令编译源代码为字节码文件,将java文件编译后生成.class文件交由Java虚拟机去执行,在android上,class文件被包装成.dex文件交由DVM执行。

通过学习Java字节码指令可以对代码的底层运行结构有所了解,能更深层次了解代码背后的实现原理,例如字符串的相加的实现原理就是通过StringBuilderappend进行相加。用过字节码的视角看它的执行步骤,对Java代码的也能有更深的了解,知其然,也要知其所以然。

通过学习字节码知识还可以实现字节码插桩功能,例如用ASM 、AspectJ等工具对字节码层面的代码进行操作,实现一些Java代码不好操作的功能。

1. 字节码的格式

下面举个简单的例子,分析其字节码的结构

public class Main {
    public static void main(String[] args) {
        System.out.println("HelloWorld");
    }
}

Main.class的字节码

上图中纯数字字母就是字节码,右边的是具体代码执行的字节码指令。

上面看似一堆乱码,但是JVM对字节码是有规范的,下面一点一点分析其代码结构

1.1魔数(Magic Number)

魔数唯一的作用是确定这个文件是否为一个能被虚拟机接收的Class文件。很多文件存储标准中都使用魔数来进行身份识别,譬如gif和jpeg文件头中都有魔数。魔数的定义可以随意,只要这个魔数还没有被广泛采用同时又不容易引起混淆即可。

这里字节码中的魔数为0xCafeBabe(咖啡宝贝),这个魔数值在Java还被称作Oak语言的时候就已经确定下来了,据原开发成员所说是为了寻找一些好玩的、容易记忆的东西,选择0xCafeBabe是因为它象征着著名咖啡品牌Peet`s Coffee中深受喜欢的Baristas咖啡,咖啡同样也是Java的logo标志。

1.2版本号(Version Number)

紧接着魔数的四个字节(00 00 00 33)存储的是Class文件的版本号。前两个是次版本号(Minor Version),转化为十进制为0;后两个为主版本号(Major Version),转化为十进制为52,序号52对应的主版本号为1.8,所以编译该文件的Java版本号为1.8.0。高版本的JDK能向下兼容以前的版本的Class文件,但不能运行以后版本的Class文件,及时文件格式并未发生变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

1.3常量池(Constant Pool)

这部分内容前面做了一个简要的笔记,感兴趣的可以去看看。

紧接着版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据结构,也是占用Class文件控件最大的数据项目之一,同事也是在Class文件中第一个出现的表类型数据项目。

常量池的前两个字节(00 22)代表的是常量池容量计数器,与Java中语言习惯不一样的是,这个容量计数是从1开始的,这里的22转换成十进制后为34,去除一个下标计数即表示常量池中有33个常量,这一点从字节码中的Constant pool也可以看到,最后一个是#33 = Utf8 (Ljava/lang/String;)V

容量计数器后存储的是常量池的数据。 常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为Final的常量值(例如字符串),符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符,当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或者运行时解析、翻译到内存地址中。如下图。

常量池的每一项常量都是一个表,在JDK71.7之前共有11中结构不同的表结构数据,在JDK1.7之后为了更好底支持动态语言调用,又额外增加了三种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info),总计14中,表结构如下图

常量池数据表

上图中tag是标志位,用于区分常量类型,length表示这个UTF-8编码的字符串长度是多少节,它后面紧更着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。上图的u1,u2,u4,u8表示比特数量,分别为1,2,4,8个byte。

UTF-8缩略编码与普通UTF-8编码的区别是:从\u0001\u007f之间的字符(相当于1-127的ASCII码)的缩略编码使用一个字节表示,从\u0080\u07ff之间的所有字符的缩略编码用两个字节表示,从\u0800\uffff之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示,这么做的主要目的还是为了节省空间。

由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度就是Java中的方法、字段名的最大长度。这里的最大长度就是length的最大值,即u2类型能表达的最大值65535,所以Java程序中如果定义了超过64K英文字符的变量或发放名,将会无法编译。

回到上面那个例子,00 22后面跟着的是 0A 0006 0014,第一个字节0A转化为十进制为10,表示的常量类型为CONSTANT_Methodref_info,这从常量表中可以看到这个类型后面会两个u2来表示index,分别表示CONSTANT_Class_infoCONSTANT_NameAndType_info。所以0006和0014转化为10进制分别是6和20。这里可能不知道这些数字指代什么意思,下面展示的是编译后的字节码指令就可以清楚了。

Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // HelloWorld
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // com/verzqli/snake/Main
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/verzqli/snake/Main;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Main.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               HelloWorld
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/verzqli/snake/Main
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V

从上面可以看到Constant pool中一共有33个常量,第一个常量类型为Methodref,他其实指代的是这个Main类,它是最基础的Object类,然后这里它有两个索引分别指向6和20,分别是Class和NameAndType类型,和上面十六进制字节码描述的一样。

1.4访问标志(Access Flags)

在常量池结束后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型,如果是类的话,是否被声明为final等,具体的标志位以及标志的含义见下表。

标志名称 标志值 含义
ACC_PUBLIC 0x0001 标识是否为public类型
ACC_FINAL 0x0010 标识是否被声明为final,只有类可设置
ACC_SUPER 0x0020 用于兼容早期编译器,新编译器都设置改标志,以在使用invokespecial指令时对子类方法做特殊处理
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生,而是由编译器产生
ACC_INTERFACE 0x0200 标识是否为一个接口,接口默认同事设置ACC_ABSTRACT
ACC_ABSTRACT 0x0400 标识是否为一个抽象类,不可与ACC_FINAL同时设置
ACC_ANNOTATION 0x2000 标识这是否是一个注解类
ACC_ENUM 0x4000 标识这是否是一个枚举

ACCESS_FLAGS中一共有16个标志位可用,当前只定义了其中8个(上面显示了比8个多,是因为ACC_PRIVATE,ACC_PROTECTED,ACC_STATIC,ACC_VOLATILE,ACC_TRANSTENT并不是修饰类的,这里写出来是让大家知道还有这么些标志符),对于没有使用到的标志位要求一律为0。Java不会穷举上面所有标志的组合,而是同|运算来组合表示,至于这些标志位是如何表示各种状态,可以看这篇文章,讲的很清楚。

我们继续回到例子
标志
例子中只是一个简单的Main类,所以他的标志是ACC_PUBLIC和ACC_SUPER,其他标志都不存在,所以它的访问标志为0x0001|0x0020=0x0021。

1.5 类索引、父类索引、接口索引

类索引和父类索引都是一个u2类型的数据,接口索引是一组u2类型的数据的集合,Class文件中由着三项数据来确定这个类的继承关系。这三者按顺序排列在访问标志之后,本文例子中他们分别是:0005,0006,0000,也就是类索引为5,父类索引为6,接口索引集合大小为0 ,查询上面字节码指令的常量池可以一一对应(5对应com/verzqli/snake/Main,6对应java/lang/Object)。

类索引确定这个类的全限定名,父类索引确定这个类的父类全限定 名,因为Java不允许多重继承,所以父类索引只有一个,除了Object外,所有的类都有其父类,也就是其父类索引不为0.接口索引即可用来描述这个类实现了哪些接口,这些被实现的接口按implements(如果这个类本身就是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。

1.6 字段表集合(Field Info)

字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量。但是不包含方法内部声明的局部变量。在Java中描述一个字段可能包含一下信息:

  • 字段的作用域(public,private,protected修饰符)
  • 是实例变量还是类变量(static修饰符)
  • 是否可变(final修饰符)
  • 并发可见 (vlolatile修饰符,是否强制从主内存中读写)
  • 是否可悲序列化(transient修饰符)
  • 字段数据基本类型(基本类型、对象、数组)
  • 字段名称
    上述信息中,每个修饰符都是bool值,要么有要么没有,很适合用和访问标志一样的标志位来表示。而字段名称,字段数据类型只能引用常量池中
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值