JVM字节码学习

101 阅读10分钟

JVM是什么

JVM是Java Virtual Machine,引入Java虚拟机,编译Java Code生成对应的字节码文件,JVM运行时,把字节码解释成为具体平台上的指令,所以跨平台的时候Java语言就不需要重新编译了,编译一次即可,JVM运行的是字节码文件,这也就是”一次编译,到处运行“。

字节码

如何查看Class File

先编写一个简单的A类,类里面没有任何信息,然后编译,生成class文件

public class A {

}

它的class文件如下

package com.example.ndkstudy;
public class A {
    public A() {
    }
}

使用BinEd查看,IDEA和AndroidStudio插件里面可以找到,运行Java语言,生成对应的字节码文件class,查找到对应的class文件,使用Open In 打开里面有查看的Binary Editor ,这里有二进制、八进制、十进制,十六进制的格式,我们一般查看十六进制格式的字节码

截屏2023-11-11 01.55.28.png

class文件结构

- Magic number  magic(CA FE BA BE)表示字节
- Minor Version jdk版本号(小的版本号)
- Major Verison jdk版本号(00 00 00 34) 如这里是jdk是8 java版本号也就是52
- constant_pool_count(00 10) 
- constant_pool
- access_flags 标注的是类是否为public、final、abstract、enum、annotation等类型
               ACC_PUBLIC    0x0001是否为public
               ACC_INTERFACE 0x0200 接口
               ACC_ABSTRACT  0x0400 抽象类
               ACC_FINAL     0x0010 是否为final
               ACC_SUPER     0x0020 jdk1.0为真,指明invokespectial指令使用新语义
- this_clas
- super_class
- interface_count
- interfaces
- fileds_count
- fileds
- methods_count
- methods
- attribute_count
- attributes

查看bytecode

bytecode更能直观的表示class字节码里面的结构,翻译了机器语言十六进制code,我这里使用工具 jclassLib 插件,查看,还有其他的如JBE(还能修改) 整个class的内容如图

截屏2023-11-11 16.52.22.png

一般信息内容分析

  1. 前面几项已经对应上图分析的版本号和常量池大小,
  2. 访问标志是0x0021,由于类是public A,所以这个0x0021 是ACC_PUBLIC & ACC_SUPER = 0x0021形成的,2个字节就能代表这部分内容
  3. 本类索引 cp_info#7 <com/example/ndkstudy/A> 这就是类所在常量池的位置
  4. 父类索引 cp_info#2 <java/lang/Object> A的父类是Object所在常量池的位置
  5. 接口数为0,方法计数有一个默认的构造方法,所以为1

常量池信息

截屏2023-11-11 21.51.34.png
常量类型
| CONSTANT_Utf8_info               | 1  | UTF-8 编码的字符串 1个字节
| CONSTANT_Integer_info            | 3  | 整型字面量        4个字节
| CONSTANT_Float_info              | 4  | 浮点型字面量       4个字节
| CONSTANT_Long_info               | 5  | 长整型字面量       8个字节
| CONSTANT_Double_info             | 6  | 双精度浮点型字面量    8个字节
| CONSTANT_Class_info              | 7  | 类或接口的符号引用    
| CONSTANT_String_info             | 8  | 字符串类型字面量     
| CONSTANT_Fieldref_info           | 9  | 字段的符号引用      
| CONSTANT_Methodref_info          | 10 | 类中方法的符号引用 (CONSTANT_class_info的索引项   2个字节 )
| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用   
| CONSTANT_NameAndType_info        | 12 | 字段或方法的部分符号引用 
| CONSTANT_MethodHandle_info       | 15 | 标识方法句柄       
| CONSTANT_MethodType_info         | 16 | 标识方法类型       
| CONSTANT_InvokeDynamic_info      | 18 | 表示一个方法的调用点 

CONSTANT_Methodref_info的

常量池01位置内容具体分析

1.cp_info#2

引用了cp_info#2位置信息,查看常量池的02位置就是CONSTANT_Class_info,也就是object类名,类名又引用了常量池cp_info#4位置的内容,继续点击查看04位置是CONSTANT_Utf8_info,也就是Object的具体内容字符串java/lang/Object

截屏2023-11-11 22.15.27.png 截屏2023-11-11 22.17.15.png 截屏2023-11-12 12.26.22.png

2.cp_info#3

引用的是常量池03的位置,点击查看03位置,内容是初始化构造方法,引用的是常量池cp_info#5位置的表示init方法,如05位置的截图,是CONSTANT_utf8_info内容,表示的是字符串内容

截屏2023-11-11 22.21.27.png 截屏2023-11-12 12.41.16.png

cp_info#6 <()V>描述符位置括号表示无参构造,V表示返回值是void,查看常量池06位置CONSTANT_utf8_info是具体的无参数无返回值内容

截屏2023-11-12 12.38.46.png

总结:常量池里面各个位置上存储的内容都是引用指向到常量池其他位置上去的;

对应class的十六进制码查看

截屏2023-11-11 22.46.05.png

0A 00 02 00 03

前面几个十六进制码我们已知晓,所以01位置从0A开始,0A = 10用上面的常量类型表查询表示CONSTANT_Methodref_info的类型描述,00 02 指向的是常量池里面的位置2的内容,也就是CONSTANT_Class_info,00 03 对应的是常量池03位置的内容CONSTANT_NameAndType_info,常量池01里面的内容,占用了5个字节;

截屏2023-11-12 12.51.45.png

07 00 04

07对应上面CONSTANT_Class_info,00 04 对应里面的具体信息也完全对应使用jclassLib查看的翻译十六进制之后的结构信息,后续也就是常量池02位置上内容,完全一一对应。这就是十六进制码对应的bytecode结构信息。

截屏2023-11-11 22.17.15.png

常量池其他内容

1.utf-8 字符串 object类名

截屏2023-11-11 23.17.12.png

2.init方法描述字符

截屏2023-11-11 23.20.14.png

3.方法类型描述符,()V 表示无参,返回Void

截屏2023-11-11 23.22.07.png

4. 类名存储

截屏2023-11-11 23.44.18.png

5.字面量是code,属性表的名字

截屏2023-11-11 23.34.30.png 截屏2023-11-11 23.36.11.png

其中LineNumberTable 和LocalVariableTable方法里面用到的属性名字 LineNumberTable 属性是用来描述 Java 源码行号与字节码行号之间的对应关系,这个属性可以用来在调试的时候定位代码执行的行数

LocalVariableTable 是可选变长属性,它被调试器用于确定方法在执行过程中局部变量的信息。 在 Code 属性的属性中,LocalVariableTable 属性可以按照任意顺序出现。Code 属性中的每个局部变量最多只能有一个 LocalVariableTable 属性。

6.this

我们在类里面用到的this关键字

截屏2023-11-11 23.42.55.png

其他都是存储class类里面的信息描述符,不再分析

methods描述符

methods
access_flags u2访问标志
name_index u2方法名索引
descriptor_index u2描述符索引
attributes_count u2属性计数器
attributes属性集合(如上面的code属性)

u2 表示2字节

u1 表示1字节

accessflags

访问标志有

标记名说明
ACC_PUBLIC0x0001public
ACC_PRIVATE0x0002private
ACC_PROTECTED0x0004protected
ACC_STATIC0x0008static
ACC_FINAL0x0010final
ACC_SYNCHRONIZED0x0020synchronized
ACC_BRIDGE0x0040编译器产生的桥接方法
ACC_VARARGS0x0080方法是否是不定参数
ACC_NATIVE0x0100方法是否为native
ACC_ABSTRACT0x0400方法是否为抽象
ACC_STRICTFP0x0800方法是否为strictfp
ACC_SYNTHETIC0x1000方法是否由编译器自动产生的

属性

截屏2023-11-12 10.43.58.png

这里指的是class文件对应的属性信息,这里有源文件名称和索引,还有其他的RetentionPolicy.CLASS 或者 RetentionPolicy.RUNTIME 的注解,如下图定义一个接口,查看其class文件,属性里面就能看属性的名称还有其他的RetentionVisibileAnotation,也可以看到定义接口,其类信息里面是没有方法描述信息的。

截屏2023-11-12 11.15.52.png

这里是class的属性内容,还有其他的方法表、字段表都可以有自己的属性表等,java里面定义了23种属性; 如:

属性名所在位置描述
ConstantValuefield_info通过 final 关键字修饰的常量池
Codemethod_info方法对应字节码指令
Exceptionsmethod_info方法抛出的异常列表
SourceFileClassFile记录源码文件名
InnerClassesClassFile内部类的列表
EnclosingMethodClassFile当且仅当类为局部类或者匿名类时,才有这个属性,标记这个类所在的外部方法
SourceDebugExtensionClassFile用于储存额外的调式信息;比如JSP文件调试时,无法通过java堆栈来定位JSP文件行号。
BootstrapMethodsClassFile引导方法列表,invokedynamic 指令就是和对应引导方法绑定
RuntimeVisibleParameterAnnotationsmethod_info可见的方法参数注解
RuntimeInvisibleParameterAnnotationsmethod_info不可见的方法参数注解
AnnotationDefaultmethod_info记录注解类元素默认值
MethodParametersmethod_info将方法参数参数名编译到字节码文件中(编译时加上 -parameters 参数)
SyntheticClassFile, field_info, method_info标志字段,方法和类由编译器自动生成
DeprecatedClassFile, field_info, method_info声明字段,方法和类将弃用
SignatureClassFile, field_info, method_info记录字段,方法和类的泛型信息
RuntimeVisibleAnnotationsClassFile, field_info, method_info可见的字段,方法和类注解
RuntimeInvisibleAnnotationsClassFile, field_info, method_info不可见的字段,方法和类注解
LineNumberTableCodejava 源码中方法中字节指令和行号对应关系
LocalVariableTableCode方法中局部变量描述
LocalVariableTypeTableCode方法中泛型局部变量描述
StackMapTableCode记录数据供类型检查验证器检查,来验证方法局部变量表和操作数栈所需类型是否匹配
RuntimeVisibleTypeAnnotationsClassFile, field_info, method_info, Code可见的类型注解
RuntimeInvisibleTypeAnnotationsClassFile, field_info, method_info, Code不可见的类型注解

Code信息

截屏2023-11-14 13.08.57.png

aload_0指令,可以查询JVM规范了解这个指令的意思,jclasslib可以直接跳转到对应的指令说明地址,下一篇再分析这些指令的意思。

总结

简单的认识了字节码里面的包含信息,包属性、方法、常量池等信息,也分析了字节码class文件的十六进制码对照上翻译过后的bytecode指令,还有方法描述符的定义。最复杂的就是常量池里面的存储信息,和方法里面的code信息(是如何执行指令的)。

这里简单介绍字节码方法描述符的定义;

void m() ->()V
String toString() ->()Ljava/lang/String;
long pos(int[] arr,int arr2 ,long length) -> ([iiJ)J

1. 全限定名和简单名称把类名中的.替换成/,连续多个全限定名时,为了不产生混淆,在使用时最后一般都会加入一个;表示全限定名结束。

2. 方法、字段索引描述方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(bytechardoublefloatintlongshortboolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。
基本数据类型
B----->byte
C----->char
D----->double
F----->float
I----->int
J----->long
S----->short
Z----->boolean
V----->void
对象类型String------>Ljava/lang/String;

3. 数组类型
[来表示int[] ------>[I
String [][]------>[[Ljava.lang.String;
[[表示二维数组[[C