java类的class文件字节码解析

1.java语言

编程语言的分类:

分类方式说明
核心思想面向过程、面向对象、面向函数
类型静态类型、动态类型
执行方式编译执行、解释执行
虚拟机有虚拟机、无虚拟机
GC有GC、无GC

java语言:面向对象、静态类型、编译执行、有VM/GC和运行时、跨平台的高级语言。

2.java字节码

java bytecode 是由单字节(byte)的指令组成,理论上最多支持256个操作码(opcode)。实际上java只使用了200左右的操作码,还有一些操作码保留给调试使用。
java字节码的分类:

  • 1.栈操作指令,包括与局部变量交互的指令。
  • 2.程序流程控制指令。
  • 3.对象操作指令,包括方法调用指令。
  • 4.算术运算以及类型转换指令。

java字节码的执行过程

2.1 如何生成字节码如何生成字节码?

有如下类:

package com.dhb.geektimestudy.kimmking.week1;

public class HelloByteCode {

	public static void main(String[] args) {
		HelloByteCode obj = new HelloByteCode();
	}
}

编译:
现在位于main/java目录,执行javac:

javac com/dhb/geektimestudy/kimmking/week1/HelloByteCode.java

生成了文件 HelloByteCode.class
现在通过javap查看字节码:

javap -c  com.dhb.geektimestudy.kimmking.week1.HelloByteCode

执行结果如下:

Compiled from "HelloByteCode.java"
public class com.dhb.geektimestudy.kimmking.week1.HelloByteCode {
  public com.dhb.geektimestudy.kimmking.week1.HelloByteCode();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/dhb/geektimestudy/kimmking/week1/HelloByteCode
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: return
}

或者通过idea的插件,在view中通过show bytecode进行查看。
show bytecode

更加有用的插件是通过jclasslib进行查看,这个插件会更加形象。
jclasslib查看效果

如果我们要查看最详细的字节码,那么需要在javap命令之后增加-verbose参数,下面我们来分析完全字节码的含义。

字节码分析:
javap -c -verbose com.dhb.geektimestudy.kimmking.week1.HelloByte
Code

Classfile /....../src/main/java/com/dhb/geektimestudy/kimmking/week1/HelloByteCode.class
  Last modified 2021-8-3; size 325 bytes
  MD5 checksum 57cfd837406242d55d4f6050ea9d2d78
  Compiled from "HelloByteCode.java"
public class com.dhb.geektimestudy.kimmking.week1.HelloByteCode
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
   #2 = Class              #14            // com/dhb/geektimestudy/kimmking/week1/HelloByteCode
   #3 = Methodref          #2.#13         // com/dhb/geektimestudy/kimmking/week1/HelloByteCode."<init>":()V
   #4 = Class              #15            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               SourceFile
  #12 = Utf8               HelloByteCode.java
  #13 = NameAndType        #5:#6          // "<init>":()V
  #14 = Utf8               com/dhb/geektimestudy/kimmking/week1/HelloByteCode
  #15 = Utf8               java/lang/Object
{
  public com.dhb.geektimestudy.kimmking.week1.HelloByteCode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/dhb/geektimestudy/kimmking/week1/HelloByteCode
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
}
SourceFile: "HelloByteCode.java"

2.2 字节码的构成

上述class的字节码主要由 魔数及版本信息、常量池、访问标识符、索引(类索引、父类索引、接口索引)、字段表、方法表、属性表。
我们可以查看上述字节码的二进制文件用16进制查看:

HelloByteCode类的二进制形式

2.2.1 魔数及版本信息

  • 魔数(Magic Number):.class 文件的第 1 - 4 个字节,它唯一的作用就是确定这个文件是否是一个能被虚拟机接受的 class 文件,其固定值是:0xCAFEBABE(咖啡宝贝)。如果一个 class 文件的魔术不是 0xCAFEBABE,那么虚拟机将拒绝运行这个文件
  • 次版本号(minor version):.class 文件的第 5 - 6 个字节,即编译生成该 .class 文件的 JDK 次版本号。
  • 主版本号(major version):.class 文件的第 7 - 8个字节,即编译生成该 .class 文件的 JDK 主版本号。

需要注意的是,在java中,高版本的JDK能够向下兼容低版本的jdk。
字节码的二进制文件对应表示为:

CA FE BA BE 00 00 00 34

那么前面的cafebabe就是魔数,而0000 和0034则分别是主版本号和次版本号。转为十进制0x0034即是52,这与我们javap的输出正好对应。

  minor version: 0
  major version: 52

jdk各版本的版本号如下表:

JDK版本主版本号次版本号十进制
JDK1.20000002E46
JDK1.30000002F47
JDK1.40000003048
JDK1.50000003149
JDk1.60000003250
JDK1.70000003351
JDK1.80000003452

2.2.2 常量池

常量池由两部分构成,分别是常量池的constatn_pool_count和常量池集合。constatn_pool_count是一个二字节的无符号数字,这个计数器从1开始。对应到二进制我们可以看到constatn_pool_count部分:

00 10

0x0010,正好是16,那么久说明常量池的大小是16。此处需要注意的是,只有常量池部分的计数是从1开始,其他集合都从0开始。因为常量池的0有特殊意义。因此常量池的总数应该是16-1为15项。
之后就是常量池每一项的内容,每一项内容由各自部分组成,其组成如下表:

常量简介类型描述
CONSTANT_Utf8_infoutf-8缩略编码字符串tagu1取值为1,即0x01
lengthu2utf-8字符串的长度
bytesu1字符串内容,由多个字节构成
CONSTANT_Integer_info整型字面量tagu1值为3,即是0x03
bytesu4 按照高位在前储存的int值,占4个字节
CONSTANT_Float_info浮点型字面量tagu1值为4,即0x04
bytesu4按照高位在前储存的float值
CONSTANT_Long_info长整型字面量tagu1值为5,即0x05
bytesu8按照高位在前储存的long值,占8个字节
CONSTANT_Double_info双精度浮点型字面量tagu1值为6,即0x06
bytesu8按照高位在前储存的double值
CONSTANT_Class_info类或接口的符号引用tagu1值为7,即是0x07
indexu2指向全限定名常量项的索引
CONSTANT_String_info字符串类型字面量tagu1值为8
indexu2指向字符串字面量的索引
CONSTANT_Fieldref_info字段的符号引用tagu1值为9
indexu2指向声明字段的类或接口描述符CONSTANT_Class_info的索引项
indexu2指向字段描述符CONSTANT_NameAndType_info的索引项
CONSTANT_Methodref_info类中方法的符号引用tagu1值为10,0x0A
indexu2指向声明方法的类描述符CONSTANT_Class_info的索引项
indexu2指向名称及类型描述符CONSTANT_NameAndType_info的索引项
CONSTANT_InterfaceMethodref_info接口中方法的符号引用tagu1值为11 ,0x0B
indexu2指向声明方法的接口描述符CONSTANT_Class_info的索引项
indexu2指向名称及类型描述符CONSTANT_NameAndType_info的索引项
CONSTANT_NameAndType_info字段或方法的部分符号引用tagu1值为12,0x0C
indexu2指向该字段或方法名称常量项的索引
indexu2指向该字段或方法描述符常量项的索引

那么对应到上述二进制中,各常量如下:

序号内容类型说明
10A 00 04 00 0DCONSTANT_Methodref_info,类中方法的符号引用指向的index分别为0x0004和0x000D,正好是指向#4,#13
207 00 0ECONSTANT_Class_info ,类或接口的符号引用index部分为0x000E,指向#15
30A 00 02 00 0DCONSTANT_Methodref_info,类中方法的符号引用index分别为0x0002和0x000D,正好为 #2,#13
407 00 0FCONSTANT_Class_info 类和接口的符号引用index为0x000F,即是#15
501 00 06 3C 69 6E 69 74 3ECONSTANT_Utf8_info utf-8字符串长度为6的字符串,
601 00 03 28 29 56CONSTANT_Utf8_info utf-8字符串长度为3的字符串 ()V
701 00 04 43 6F 64 65CONSTANT_Utf8_info utf-8字符串长度为4的字符串 Code
801 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65CONSTANT_Utf8_info utf-8字符串长度为15的字符串:LineNumberTable
901 00 04 6D 61 69 6ECONSTANT_Utf8_info utf-8字符串长度为4的字符串 main
1001 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56CONSTANT_Utf8_info utf-8字符串长度为22的字符串:([Ljava/lang/String;)V
1101 00 0A 53 6F 75 72 63 65 46 69 6C 65CONSTANT_Utf8_info utf-8字符串长度为15的字符串:SourceFile
1201 00 12 48 65 6C 6C 6F 42 79 74 65 43 6F 64 65 2E 6A 61 76 61CONSTANT_Utf8_info utf-8字符串长度为18的字符串:HelloByteCode.java
130C 00 05 00 06CONSTANT_NameAndType_info 字段或方法的部分符号引用指针指向:#5 #6
1401 00 32 63 6F 6D 2F 64 68 62 2F 67 65 65 6B 74 69 6D 65 73 74 75 64 79 2F 6B 69 6D 6D 6B 69 6E 67 2F 77 65 65 6B 31 2F 48 65 6C 6C 6F 42 79 74 65 43 6F 64 65CONSTANT_Utf8_info utf-8字符串长度为50的字符串:com/dhb/geektimestudy/kimmking/week1/HelloByteCode
1501 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74CONSTANT_Utf8_info utf-8字符串长度为16的字符串:java/lang/Object

可以看出,在常量池中,为了尽可能少的占用内存,许多常量都是指针。
常量池也是这个类中字节的大部分内容。

2.2.3 访问标识

在常量池之后,紧接着的两个字节表示访问标识,用以识别类或者接口的层次访问信息。见下表:

标识标识值说明
ACC_PUBLIC0x0001字段为public
ACC_FINAL0x0010字段是否为final
ACC_SUPER0x0020是否允许使用invokespecial字节码指令,JDK1.2以后编译出来的类这个标志为真
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400字段是否为abstract
ACC_SYNTHETIC0x1000表示由synthetic修饰,不在源代码中出现
ACC_ANNOTATION0x2000表示是annotation类型
ACC_ENUM0x4000表示是枚举类型

需要注意的是,这个访问标识只占两字节,实际上是上述这些标识取|。
我们例子中的值为:“00 21” 即0x0021 = 0x0001 | 0x0020 = =ACC_PUBLIC | ACC_SUPER
因此我们可以看到javap输出中的flag:

 flags: ACC_PUBLIC, ACC_SUPER

2.2.4 类索引、父类索引和接口索引集合

在class文件中,将用这三项来标识类的继承关系。

索引项长度说明
this_class2个字节类索引,用于确定这个类的全限定名
super_class2个字节父类索引,用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了java.lang.Object类之外所有类都有父类,故除了java.lang.Object类之外,所有类该字段值都不为0)
interfaces_count2个字节接口索引计数器,如果该类没有实现任何接口,则该计数器值为0,并且后面的接口的索引集合将不占用任何字节
interfaces2个字节接口索引集合,一组u2类型数据的集合。用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果该类本身为接口,则为extends语句)后的接口顺序从左至右排列在接口的索引集合中

this_class、super_class与interfaces中保存的索引值均指向常量池中一个CONSTANT_Class_info类型的常量,通过这个常量中保存的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
在案例中,上述三个参数为:

00 02 
00 04 
00 00

可以知道,this_class的名称在常量pool中的index为0x0002,super_class的名称在常量池的index为 0x0004。interfaces_counts的值为0x0000,表示本类没有实现任何接口。
HelloByteCode的类索引及父类索引

2.2.5 字段表集合

字段表用于描述接口或者类中声明的变量,包括类级变量和实例级变量(是否是static),但不包括在方法内部声明的局部变量。
此区域用以描述成员变量。

字段名长度说明
fields_count2个字节字段表计数器,即字段表集合中的字段表数据个数。也就是成员变量个数。
fields2个字节字段表集合,成员变量列表,一组字段表类型数据的集合。

在本案例中,由于没有成员变量,因此此处为:

00 00

如果存在,那么将会用如下数据结构构成fileds中的每一个filed:

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

access_flags用两个字节来标识成员变量的访问修饰,这与类的访问标识类似,只是标识会比类要多:

标识项标识值说明
ACC_PUBLIC0x0001字段是否为public
ACC_PRIVATE0x0002字段是否为private
ACC_PROTECTED0x0004字段是否为protected
ACC_STATIC0x0008字段是否为static
ACC_FINAL0x0010字段是否为final
ACC_VOLATILE0x0040字段是否为volatile
ACC_TRANSIENT0x0080字段是否为transient
ACC_SYNTHETIC0x1000字段是否为编译器自动产生
ACC_ENUM0x4000字段是否为enum

2.2.6 方法表集合

方法表集合用以表示类中存在多少个方法,以及方法的访问标识和内容。

方法名长度说明
methods_count2个字节方法表计数器,即方法表集合中的方法表数据个数。
methods数组方法表集合,一组方法表类型数据的集合。

方法表结构和字段表结构一样:

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

数据项的含义非常相似,仅在访问标志位和属性表集合中的可选项上有略微不同,由于ACC_VOLATILE标志和ACC_TRANSIENT标志不能修饰方法,所以access_flags中不包含这两项,同时增加ACC_SYNCHRONIZED标志、ACC_NATIVE标志、ACC_STRICTFP标志和ACC_ABSTRACT标志。

标识项标识值说明
ACC_PUBLIC0x0001字段是否为public
ACC_PRIVATE0x0002字段是否为private
ACC_PROTECTED0x0004字段是否为protected
ACC_STATIC0x0008字段是否为static
ACC_FINAL0x0010字段是否为final
ACC_SYNCHRONIZED0x0020字段是否为synchronized
ACC_BRIDGE0x0040方法是否是由编译器产生的桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NATIVE0x0100字段是否为native
ACC_ABSTRACT0x0400字段是否为abstract
ACC_STRICTFP0x0800字段是否为strictfp
ACC_SYNTHETIC0x1000字段是否为编译器自动产生

我们对字节码进行对照:

00 02

methods的长度为0x0002,即存在2个方法。

2.2.6.1 方法一

方法1如下:

00 01 00 05 00 06 00 01 00 07 

access_flags为0x0001,name_index为0x0005,descriptor_index为0x0006,attributes为0x0001 说明此方法的属性表中存在一个属性,属性名称指向0x0007,即是Code.
结合常量池,我们可以得到该方法为:

public <init> ()V 

在上述方法进入code部分之后,就是真正要执行操作的字节码了。

00 00 00 1D 
00 01 00 01 00 00 00 05 2A B7 00 01 B1 
00 00 
00 01 00 08 00 00 
00 06 00 01 00 00 00 03

接下来4个字节标识code的长度,本案例中是0x0000001D,则说明code要执行的内容为20字节。
在上述执行的代码中:
首先两个字节0x0001表示操作数栈深度为1,接下来的两个字节0x0001表示局部变量所占空间为1。
后续4位为0x000005,表示后续指令将占5个字节,为0x2AB70001B1,这部分内容为虚拟机运行的字节码指令,可以参考虚拟机字节码指令表进行对照。
接下来两个字节 0x0000 说明Code的属性异常表为空。
之后的两个字节,0x0001,说明Code带有1个属性。接下来的0x0008表示这个属性的名称,#8在常量池中为LineNumberTable。
后续4个字节0x00000006表示LineNumberTable属性值的长度为6 rTable属性值的长度为6。。
这6个字节中:0x0001表示行号表中只有一个行信息,start_pc为0x0000,line_number为0x0003,至此,这个方法结束。
javap中该方法如下:

  public com.dhb.geektimestudy.kimmking.week1.HelloByteCode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
2.2.6.1 方法二

方法2中:

00 09 00 09 00 0A 00 01 00 07 

开始两个字节 0x0009 = 0x0001 | 0x0008 = pulic | static
也就是说这个类的访问描述符为 public static
name_index为0x0009 对应到常量池中的main方法
descriptor_index为0x000A即([Ljava/lang/String;)V,attributes为0x0001 说明此方法的属性表中存在一个属性,属性名称指向0x0007,即是Code.

在上述方法进入code部分之后的字节码:

00 00 00 25 
00 02 00 02 00 00 00 09 BB 00 02 59 B7 00 03 4C B1 
00 00 
00 01 00 08 
00 00 00 0A 
00 02 00 00 00 06 00 08 00 07

用4个字节标识属性的长度 0x00000025 长度是37,也就是说后面37个字节都是表示实执行的字节码。
首先两个字节0x0002表示操作数栈深度为2,接下来的两个字节0x0002表示局部变量所占空间为2。
后续4位为0x000009,表示后续指令将占9个字节,为0xBB000259B700034CB1,这部分内容为虚拟机运行的字节码指令,可以参考虚拟机字节码指令表进行对照。
接下来两个字节 0x0000 说明Code的属性异常表为空。
之后的两个字节,0x0001,说明Code带有1个属性。接下来的0x0008表示这个属性的名称,#8在常量池中为LineNumberTable。
后续4个字节0x0000000A表示LineNumberTable属性值的长度为10 LineNumberTable属性值的长度为10。。
这10个字节中:0x0002表示行号表中有两个行信息,start_pc为0x0000,line_number为0x0006,第二个行信息:start_pc为0x0008,line_number为0x0007。
这就是第二个方法的字节码。

通过javap可以查看:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/dhb/geektimestudy/kimmking/week1/HelloByteCode
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8

2.2.7 属性集合表说明

在class文件中,字段表、方法表都可以携带自己的属性集合表。用以描述某些场景的专有信息。
属性集合表格式如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1infoattribute_length

该格式可以解释为,一个2字节构成的index,一个4字节构成的属性长度。另外一个属性的字节数组。

现在案例中的类还剩下的字节部分:

00 01 00 0B 00 00 00 02 00 0C 

0x0001 表示属性集合表的长度,说明有一个类的属性。
0x000B 指向两个字节标识的属性name的index,为#11 即 SourceFile
0x00000002 表示这个属性的长度,长度为2,剩余2字节即为这个属性的内容:
0x000C 即是SourceFile的内容,#12 即常量池中的HelloByteCode.java
这个类属性值表示的内容为:

SourceFile: "HelloByteCode.java"

至此,我们完整解析了class类的字节码。

3.总结

本文详细分析了字节码的构成,现在总结为如下图:

字节码的组成

在图中,u4表示4个字节,u1表示1个字节,u2表示2个字节,而un则表示未知。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值