JVM(四)——class字节码文件结构深度剖析

目录

1、源码及class文件: 

1.1、源码

1.2、class文件

2、阅读字节码方式及工具

3、字节码文件结构

3.1、class文件结构图

4、剖析字节码文件

4.1、魔数

4.2、次版本号

4.3、主版本号

4.4、常量池

4.4.1、常量池结构表

4.4.2、常量池项细化分类

4.4.3、数据类型字段

4.4.4、常量池分析

4.5、访问标记符

4.6、当前类名称

4.7、父类名称

4.8、实现接口个数

4.9、字段表信息分析

4.9.1、数据结构表

4.9.2、字段分析

4.10、方法表信息分析

4.10.1、方法表和属性表结构

4.10.2、方法分析

4.11、附加属性

 5、总结


1、源码及class文件: 

1.1、源码

public class ByteCode {

    private String userName;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }
}

1.2、class文件

CAFE BABE 0000 0034 0019 0A00 0400 1509
0003 0016 0700 1707 0018 0100 0875 7365
724E 616D 6501 0012 4C6A 6176 612F 6C61
6E67 2F53 7472 696E 673B 0100 063C 696E
6974 3E01 0003 2829 5601 0004 436F 6465
0100 0F4C 696E 654E 756D 6265 7254 6162
6C65 0100 124C 6F63 616C 5661 7269 6162
6C65 5461 626C 6501 0004 7468 6973 0100
284C 636F 6D2F 6465 6D6F 2F7A 7379 6465
6D6F 2F6A 766D 2F62 7974 6563 6F64 652F
4279 7465 436F 6465 3B01 000B 6765 7455
7365 724E 616D 6501 0014 2829 4C6A 6176
612F 6C61 6E67 2F53 7472 696E 673B 0100
0B73 6574 5573 6572 4E61 6D65 0100 1528
4C6A 6176 612F 6C61 6E67 2F53 7472 696E
673B 2956 0100 104D 6574 686F 6450 6172
616D 6574 6572 7301 000A 536F 7572 6365
4669 6C65 0100 0D42 7974 6543 6F64 652E
6A61 7661 0C00 0700 080C 0005 0006 0100
2663 6F6D 2F64 656D 6F2F 7A73 7964 656D
6F2F 6A76 6D2F 6279 7465 636F 6465 2F42
7974 6543 6F64 6501 0010 6A61 7661 2F6C
616E 672F 4F62 6A65 6374 0021 0003 0004
0000 0001 0002 0005 0006 0000 0003 0001
0007 0008 0001 0009 0000 002F 0001 0001
0000 0005 2AB7 0001 B100 0000 0200 0A00
0000 0600 0100 0000 0A00 0B00 0000 0C00
0100 0000 0500 0C00 0D00 0000 0100 0E00
0F00 0100 0900 0000 2F00 0100 0100 0000
052A B400 02B0 0000 0002 000A 0000 0006
0001 0000 000F 000B 0000 000C 0001 0000
0005 000C 000D 0000 0001 0010 0011 0002
0009 0000 003E 0002 0002 0000 0006 2A2B
B500 02B1 0000 0002 000A 0000 000A 0002
0000 0013 0005 0014 000B 0000 0016 0002
0000 0006 000C 000D 0000 0000 0006 0005
0006 0001 0012 0000 0005 0100 0500 0000
0100 1300 0000 0200 14

2、阅读字节码方式及工具

首先推荐给大家两个IDEA查看字节码的插件:

  • BinEd:可以直接以十六进制的方式打开class文件
  • Jclasslib:对字节码提供了一个可视化的界面

上述两款插件可以让你在阅读字节码文件时基本没有什么阻碍,下图是.class文件、BinEd的二进制文件以及Jclasslib打开的字节码文件:

我们也可以使用原生的javap命令来查看.class文件,通过javap -v可以看到详细的字节码信息,示例如下:

//表示我们通过反编译的来源是哪个字节码文件
Classfile /Users/yyyz/dailywork/daily-study/target/classes/com/demo/zsydemo/jvm/bytecode/ByteCode.class
  //最后的修改日期;文件大小
  Last modified 2023-2-19; size 601 bytes
  //文件MD5值
  MD5 checksum 8c4a7e6ec68a6f213328018bf3c1e324
  //.class文件是通过哪个文件编译过来的
  Compiled from "ByteCode.java"
//字节码的详细信息
public class com.demo.zsydemo.jvm.bytecode.ByteCode
  //次版本信息
  minor version: 0
  //主版本信息
  major version: 52
  //访问权限
  flags: ACC_PUBLIC, ACC_SUPER
//常量池
Constant pool:
   #1 = Methodref          #4.#21         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#22         // com/demo/zsydemo/jvm/bytecode/ByteCode.userName:Ljava/lang/String;
   #3 = Class              #23            // com/demo/zsydemo/jvm/bytecode/ByteCode
   #4 = Class              #24            // java/lang/Object
   #5 = Utf8               userName
   #6 = Utf8               Ljava/lang/String;
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/demo/zsydemo/jvm/bytecode/ByteCode;
  #14 = Utf8               getUserName
  #15 = Utf8               ()Ljava/lang/String;
  #16 = Utf8               setUserName
  #17 = Utf8               (Ljava/lang/String;)V
  #18 = Utf8               MethodParameters
  #19 = Utf8               SourceFile
  #20 = Utf8               ByteCode.java
  #21 = NameAndType        #7:#8          // "<init>":()V
  #22 = NameAndType        #5:#6          // userName:Ljava/lang/String;
  #23 = Utf8               com/demo/zsydemo/jvm/bytecode/ByteCode
  #24 = Utf8               java/lang/Object
{
  //构造方法
  public com.demo.zsydemo.jvm.bytecode.ByteCode();
    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 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/demo/zsydemo/jvm/bytecode/ByteCode;
  //get方法
  public java.lang.String getUserName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field userName:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 15: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/demo/zsydemo/jvm/bytecode/ByteCode;
  //set方法
  public void setUserName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field userName:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 19: 0
        line 20: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/demo/zsydemo/jvm/bytecode/ByteCode;
            0       6     1 userName   Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      userName
}

下面我们来完整的分析一下字节码文件。

3、字节码文件结构

通过上述的BinEd插件打开.class或通过其他文本编辑工具打开.class文件,如下:

3.1、class文件结构

类型名称描述数量
u4magic_number魔数1
u2major_version次版本号1
u2main_version主版本号1
u2constant_pool_count常量池个数1
cp_infoconstant_poolconstant_pool_count - 1
u2access_flag访问标记符号1
u2this_class_name当前类名称1
u2super_class_name父类名称1
u2interfaces_count接口个数1
u2interfaces接口名称interface_count
u2fields_count字段个数1
fields_infofields字段表fields_count
u2method_count方法个数1
methods_infomethods方法表method_count
u2attruibute_count附加属性个数1
attruibute_infoattruibutes附加属性表attruibute_count

4、剖析字节码文件

4.1、魔数

文件开头的四个字节是固定值位 CA FE BA BE

4.2、次版本号

两个字节00 00表示jdk的次版本号

4.3、主版本号

两个字节00 34表示jdk的主版本号,34对应的是十进制的52,52代表的是jdk1.8.x版本,51代表的是1.7.x版本依次类推,现在最新的jdk版本出到了jdk19,他对应的主版本号就是00 3F

4.4、常量池

占用两个字节,表示常量池中的个数0x19,转为十进制为25,通过上述工具查看到常量池的个数为24,为什么少了一个,因为常量池中第0位被jvm占用了表示为null,所以常量池索引都是从1开始的。

4.4.1、常量池结构表

u1、u2、u4、u8分别代表1个字节、2个字节、4个字节和8个字节的无符号数 

常量类型描述
CONSTANT_Utf8_infotagu1值为1,16进制即为0x01
lengthu2UTF-8编码的字符串占用了字节数
bytesu1长度为length的UTF-8编码的字符串
CONSTANT_Integer_infotagu1值为3,16进制即为0x03
bytesu4按照高位在前存储的int值
CONSTANT_Float_infotagu1值为4,16进制即为0x04
bytesu4按照高位在前存储的float值
CONSTANT_Long_infotagu1值为5,16进制即为0x05
bytesu8按照高位在前存储的long值
CONSTANT_Double_infotagu1值为6,16进制即为0x06
bytesu8按照高位在前存储的double值
CONSTANT_Class_infotagu1值为7,16进制即为0x07
indexu2指向 全限定名常量 的索引
CONSTANT_String_infotagu1值为8,16进制即为0x08
indexu2指向 字符串字面量 的索引
CONSTANT_Fieldref_infotagu1值为9,16进制即为0x09
indexu2指向 声明字段 的 类或接口描述符CONSTANT_InterfaceMethodref_info 的索引项
indexu2指向 字段描述符CONSTANT_NameAndType_info 的索引项
CONSTANT_Methodref_infotagu1值为10,16进制即为0x0A
indexu2指向 声明方法 的 类描述符CONSTANT_Class_info 的索引项
indexu2指向 名称及类型描述符CONSTANT_NameAndType_info 的索引项
CONSTANT_IntegerfaceMethodref_infotagu1值为11,16进制即为0x0B
indexu2指向 声明方法 的 类描述符CONSTANT_Class_info 的索引项
indexu2指向 名称及类型描述符CONSTANT_NameAndType_info 的索引项
CONSTANT_NameAndType_infotagu1值为12,16进制即为0x0C
indexu2指向该字段或方法名称 常量项 的索引
indexu2指向该字段或方法 描述符常量项 的索引

4.4.2、常量池项细化分类

常量池可以看做Java class类的资源仓库(比如Java类定的方法和变量信息),我们后面的方法、类的信息的描述信息都是通过索引去常量池中获取的,常量池主要存放两种常量,一种字面量一种是符号引用

4.4.3、数据类型字段

在JVM底层基本参数类型和void类型都是通过大写的字符来表示的,对象类型是通过L加全类名表示的,这样既可以保证JVM能读懂class文件也可以压缩class文件大小

基础数据类型:

基本数据类型
数据类型表示字符
byteB
charC
doubleD
floatF
intI
longJ
shortS
booleanZ
void()V

对象类型:大写L加类全名

对象类型
对象表示字符
ObjectLjava/lang/Object;
StringLjava/lang/String;
......

数组类型:每一个维度都是[”来表示:

int[] i ---->[I
String[][] strArr---->[[Ljava/lang/String;
Long[][][][][] longArr---->[[[[[Ljava/lang/Long;

4.4.4、常量池分析

通过上述常量池的结构和描述,我们可以依次将字节码文件中常量池部分分解出来:

第一个常量 0A 00 04 00 15

0A:代表常量类型为CONSTANT_Methodref_info

00 04:表示方法所在类,指向常量池的索引位置为#4,然后发现#4的位置常量类型为CONSTANT_Class_info,也是符号引用类型,指向常量池#24的位置,而#24的位置的常量池类型为UTF-8字面量型结构体值为:java/lang/Object

00 15:表示方法的描述符,指向常量池索引#21的位置,#21的位置的常量类型为CONSTANT_NameAndType_info类型,属于引用类型,指向常量池中#7和#8的位置,#7是UTF-8字面量类型,值为<init>为构造方法,#8也是UTF-8字面量类型,值为()V

综上所述,第一个常量是:java/lang/Object."<init>":()V

第二个常量 09 00 03 00 16

09:代表常量类型为CONSTANT_Fieldref_info

00 03:表示字段所在类,指向#3的位置,#3位置的类型为CONSTANT_Class_info,指向常量池#23的位置,而#23的位置的常量池类型为UTF-8字面量型结构体值为:com/demo/zsydemo/jvm/bytecode/ByteCode

00 16:表示字段的描述符,指向常量池索引#22的位置,#22位置的类型为CONSTANT_NameAndType_info,指向常量池中#5和#6的位置,都属于UTF-8字面量类型,#5值为userName,#6值为Ljava/lang/String;

综上所述,第二个常量是:com/demo/zsydemo/jvm/bytecode/ByteCode.userName:Ljava/lang/String;

下图是图解两个第一和第二个常量

 简单分析两个,就不一一分析了

4.5、访问标记符

访问修饰符占用两个字节,紧跟在常量池后面,我们可以看到该class文件访问标识字节为0x0021,我们可以通过下面标识符手册查询对应访问权限

标识描述
ACC_PUBLIC0x0001public修饰符
ACC_FINAL0x0010final修饰符
ACC_SUPER0x0020jdk1.2之后通过invokenonspecical调用指令调用父类方法,jdk1.2之前是invokenonsvirtual
ACC_INTERFACE0x0200标识是一个接口
ACC_ABSTRACT0x0400表示是一个抽象类
ACC_SYNTHETIC0x1000表示是动态生成的,没有源文件
ACC_ANNOTATION0x2000表示是一个注解
ACC_ENUM0x4000表示是一个枚举
ACC_PRIVATE0x0002

private修饰符

0x0021我们在手册中没有查到对应的值,为什么呢?因为手册中并未穷举出所有的修饰符类型,而是通过上述这些标识进行位运算中的“或”运算得到的。

计算逻辑:0x0020二进制为100000,0x0001二进制为1,100000|1=100001转换为16进制即为0x0021

所以我们可以得到0x0021对应的即为ACC_PUBLIC, ACC_SUPER

4.6、类名称

接着是由两个字节表示的当前类名称,是一个常量池索引,该class文件索引值为0x0003,指向常量池#3的位置

4.7、父类名称

是由两个字节表示的当前类名称,也是一个常量池索引,该class文件索引值为0x0004,指向常量池#4的位置

4.8、实现接口个数

也是由两个字节表示当前类实现的接口个数,因为该类中没有实现接口,所以该位置值为0x0000

4.9、字段表信息分析

4.9.1、字段表结构

类型名称描述数量
u2access_flag权限修饰符1
u2name_index字段名称索引1
u2descript_index字段类型索引1
u2attributes_count属性表个数1
attribute_infoattruibutes属性表attributes_count

4.9.2、字段分析

字段部分是有两部分组成由两个字节表示字段的个数,后面会使用上面数据结构构成每个字段,该class文件字段部分内容为:

00 01 00 02 00 05 00 06 00 00

下图是字段表的图解:

我们可以看到属性表个数为0,所以后面是没有属性表集合

4.10、方法表信息分析

4.10.1、方法表和属性表结构

类型名称描述数量
u2attribute_name_index属性字段名称索引1
u4attribute_length属性字段内容长度1
u1info[attribute_length]属性字段内容1

方法表结构和字段表结构一样,只是access_flag类型比字段的access_flag多,下面我们来分析方法表,首先是有两个字节表示方法的个数,从源码中可以看到我们再类中写了get和set方法,但是为什么方法个数是0x0003,因为还有一个默认的构造方法。所以方法个数为3。

4.10.2、方法表分析

第一个方法:

//为了方便阅读,先分割好
00 01 00 07 00 08 00 01 

00 09 00 00 00 2F 

00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 02 00 0A 00 00 00 06 00 01 00 00 00 0A 00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00 

前八个字节00 01 00 07 00 08 00 01,由上述结构表,可以分析出:

  • 00 01表示public修饰符
  • 00 07表示字段名称,这个指向常量池#7的位置
  • 00 08表示字段类型,也指向常量池#8的位置
  • 00 01表示方法属性表个数,0x0001表示有一个属性表

接着就是方法表的属性表字段分析

00 09 00 00 00 2F中前两个字节00 09表示属性字段类型,是一个索引指向常量池#9位置,接着由四个字节表示属性内容长度,00 00 00 2F表示接下来有47个字节来表示属性字段内容。

接下来就是属性字段内容的分析

00 01 //最大操作数栈深度为1
00 01 //局部变量表个数为1
00 00 00 05 //表示指令长度
2A B7 00 01 B1 //指令内容 每个字节对应一个操作指令 详情可以查看JVM规范,通过IDEA插件JClasslib查看可以直接跳转到JVM官方文档所对应的指令描述
00 00 //异常信息表个数 0x0000表示方法没有抛出异常
00 02 //表示该属性字段的属性表个数
00 0A 00 00 00 06 00 01 00 00 00 0A //00 0A两个字节是索引,指向常量池中#10的位置,00 00 00 06四个字节表示该属性字段的长度,00 01两个字节表示有几对指令码和源码的映射关系,00 00 00 0A四个字节表示指令码映射的是第10行源码
00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00 //00 0B两个字节是索引,指向常量池中#11的位置,00 00 00 0C四个字节表示该属性字段的长度,00 01表示局部本地变量表个数,00 00表示这个局部变量的生命周期偏移量,00 05表示作用范围覆盖长度,00 0C是索引指向常量池#12的位置,00 0D也是索引指向常量池#13的位置。00 00是指这个局部变量在栈帧的局部变量表中变量槽的位置。

剩余两个方法也可以基于上面的分解步骤进行拆分,在此就不一一赘述了

4.11、附加属性

在字节码文件的最后是

00 01 00 13 00 00 00 02 00 14 

下图是class附加属性的图解: 

 5、总结

Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在下图中的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,全部都不允许改变。

字节码文件结构图如下:

​​​​

 Tips:图中只是展示了部分属性表,自己写的测试类文件比较简单,其他的属性表字段没有展示出来,如果想要了解其他内容,可以参考一下周志明写的那本《深入理解Java虚拟机》,本文中一些内容就是参考这本书的第三版中第6章的6.3小节。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值