05-字节码解读分析与实践

本文介绍了Java字节码的基础概念,包括其在JVM中的作用和学习意义。通过实例演示如何阅读和分析字节码,以及字节码结构的详细剖析,旨在帮助开发者理解代码在底层的执行机制。课程练习涵盖了常量池、类型引用和方法表等内容,适合提升对Java虚拟机的理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

字节码简介

什么是字节码

Java的源代码中编译后会生成一个class文件,文件内容为一些JAVA虚拟机指令,这些指令的内容,由多个十六进制值组成,两个十六进制值为一组,例如:
在这里插入图片描述
Java虚拟机的指令由一个字节长度、代表某种特定操作含义的操作码(opcode)以及跟随其后的零个或多个的操作数(operand)构成。
Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制。二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。

为什么要学习字节码

对于开发人员,了解字节码可以更准确、直观地理解Java语言中更深层次的东西,比如通过字节码,可以很直观地看到Volatile关键字如何在字节码上生效。另外,字节码增强技术在Spring AOP、各种ORM框架、热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益。除此之外,由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行,因此这就给了各种运行在JVM上的语言(如Scala、Groovy、Kotlin)一种契机,可以扩展Java所没有的特性或者实现各种语法糖。理解字节码后再学习这些语言,可以“逆流而上”,从字节码视角看它的设计思路,学习起来也“易如反掌”。

如何解读字节码?

准备工作

编写如下代码,例如:

package com.java.jvm.bytecode;

public class IntTests {
    public static void main(String[] args) {
        int a=10;
        int b=20;
        int c=a+b;
        System.out.println(c);
    }
}

直接解读

IntTests.java的源代码编译后,可以通过notepad++(需要安装一下HEX-Editor插件)打开IntTests.class文件,文件内容默认是一种16进制的格式,例如:
在这里插入图片描述
JVM对于字节码是有规范要求的,看似杂乱的十六进制是符合一定结构规范的。JVM规范要求每一个字节码文件都要按照固定的顺序组成。

javap指令应用

在IntTests.class目录使用如下代码对类进行反编译,例如:

javap -verbose IntTests.class  //可以使用javap –help查看帮助

编译后的内容如下:

Classfile /E:/TCGBIV/DEVCODES/JAVACODES/01-java/target/classes/com/java/jvm/bytecode/IntT
ests.class
  Last modified 2022-4-30; size 604 bytes
  MD5 checksum fa079575306a9cf10a1a61cfc4722e88
  Compiled from "IntTests.java"
public class com.java.jvm.bytecode.IntTests
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#23         // java/lang/Object."<init>":()V
   #2 = Fieldref           #24.#25        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #26.#27        // java/io/PrintStream.println:(I)V
   #4 = Class              #28            // com/java/jvm/bytecode/IntTests
   #5 = Class              #29            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcom/java/jvm/bytecode/IntTests;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               c
  #21 = Utf8               SourceFile
  #22 = Utf8               IntTests.java
  #23 = NameAndType        #6:#7          // "<init>":()V
  #24 = Class              #30            // java/lang/System
  #25 = NameAndType        #31:#32       // out:Ljava/io/PrintStream;
  #26 = Class              #33            // java/io/PrintStream
  #27 = NameAndType        #34:#35        // println:(I)V
  #28 = Utf8               com/java/jvm/bytecode/IntTests
  #29 = Utf8               java/lang/Object
  #30 = Utf8               java/lang/System
  #31 = Utf8               out
  #32 = Utf8               Ljava/io/PrintStream;
  #33 = Utf8               java/io/PrintStream
  #34 = Utf8               println
  #35 = Utf8               (I)V
{
  public com.java.jvm.bytecode.IntTests();
    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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/java/jvm/bytecode/IntTests;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: bipush        20
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintSt
ream;
        13: iload_3
        14: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 5: 0
        line 6: 3
        line 7: 6
        line 8: 10
        line 9: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     a   I
            6      12     2     b   I
           10       8     3     c   I
}
SourceFile: "IntTests.java"

为了帮助人们理解,反编译后看到的是十六进制操作码所对应的助记符,十六进制值操作码与助记符的对应关系,以及每一个操作码的用处可以查看Oracle官方文档进行了解,在需要用到时进行查阅即可。

jclasslib插件应用

如果每次查看反编译后的字节码都使用javap命令的话,会非常繁琐。这里推荐一个Idea插件,这个插件的名字为jclasslib。代码在编译后,我们可以在菜单栏”View”中选择”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息。
在这里插入图片描述
在这里插入图片描述

字节码解读课堂练习

课堂练习:编写如下代码,并查看分析其字节码指令。

package com.java.jvm.bytecode;

public class IntCompareTests {
    public static void main(String[] args) {
        Integer a=20;
        int b=20;
        System.out.println(a==b);
    }
}

基于idea的jclasslib插件对IntCompareTests类的自己码进行查看,如图所示:
在这里插入图片描述

课堂练习:编写如下代码,基于jclasslib查看字节码,并进行分析字符串是否相等。

package com.java.jvm.bytecode;

public class StringCompareTests {
    public static void main(String[] args) {
        String s1="hello"+"world";
        String s2="helloworld";
        String s3="hello";
        String s4="world";
        String s5=s3+s4;
        System.out.println(s1==s2);
        System.out.println(s1==s5);
    }
}

课堂练习:编写如下代码,分析输出结果

package com.java.jvm.bytecode;

class Rectangle{//矩形
    int x=10;
    public Rectangle(){
        doPrint();
        this.x=20;
    }
    public void doPrint(){
        System.out.println("Rectangle.x="+this.x);
    }
}
class Square extends Rectangle{//正方形
    int x=30;
    public Square(){
        doPrint();
        this.x=40;
    }
    @Override
    public void doPrint() {
        System.out.println("Square.x="+this.x);
    }
}

public class ExtendsTests {
    public static void main(String[] args) {
        Rectangle r=new Square();
        System.out.println(r.x);
    }
}

字节码结构分析(了解)

整体结构

一个class类文件的结构组成如下(u代表一个字节无符号int,其余info类型是复合结构):

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    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];
}

其中:
 magic(魔数)
 minor_version(次版本号)
 major_version(主版本号)
 constant_pool_count(常量池计数器)
 constant_pool[constant_pool_count-1](常量池)
 access_flags(类的访问标志)
 this_class(当前类名索引值)
 super_class(父类名索引值)
 interfaces_count(接口计数)
 interfaces[interfaces_count](接口数组)
 fields_count(成员变量计数)
 fields[fields_count](成员变量数组)
 methods_count(方法计数)
 methods[methods_count](方法数组)
 attributes_count(属性计数)
 attributes[attributes_count](属性数组)

魔数

 所有.class文件的前四个字节都是魔数(Magic Number),是class文件的标识。
 魔数的固定值为:0xCAFEBABE。
 魔数放在文件开头,JVM可以根据文件魔数判断.class文件的合法性。

版本号

 版本号为魔数之后的4个字节。例如,版本号“00 00 00 34”。
 前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。例如,版本号“00 00 00 34”的次版本号转化为十进制为0,主版本号转化为十进制为52,在Oracle官网中查询序号52对应的主版本号为1.8,所以编译该文件的Java版本号为1.8.0。
 版本号和JAVA编译器的对应关系如图所示:

在这里插入图片描述

说明,不同版本的Java编译器编译的class文件版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器编译生成的class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的class文件。否则JVM可能会抛出如下异常:

java.lang.UnsupportedClassVersionError

常量池

常量池(Constant Pool)整体上分为两部分,常量池计数器以及常量池数据区。

常量池计数器

 版本号后面是常量池数量,用两个字节表示。如图所示:
在这里插入图片描述

 常量池容量计数值从1开始,表示常量池有多少个常量,假如constant_pool_count=1表示有0个常量。这里的0024的值为36,其实常量数为35个。
 索引值0用于表达不引用任何常量池。

常量池数据区

 数据区是由constant_pool_count-1个cp_info结构组成,一个cp_info结构对应一个常量。在字节码中共有14种类型的cp_info,每种类型的结构都是固定的。
 常量池中存储两类常量:字面量(Literal)与符号引用(Symbolic References)。字面量为代码中声明为Final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。
 常量池中的每个常量都是一个表,用于描述不同结构数据,例如:

在这里插入图片描述
其中,cp_info整体结构大同小异,都是先通过Tag来标识类型,然后后续n个字节来描述长度和(或)数据。先知其所以然,以后可以通过javap -verbose 类名命令,查看JVM反编译后的完整常量池,就可以看到反编译结果,将每一个cp_info结构的类型和值都很明确地呈现了出来。

访问标识

 常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被Public、Abstract、Final等修饰符修饰。
 JVM规范规定了多个访问标志(Access_Flag)。需要注意的是,JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。

类型引用

在访问标记后,会指定该类的类别、父类类别以及实现的接口。
 类索引:访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。
 父类索引:当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。
 接口索引:父类名称后的两字节,描述了该类或父类实现了哪些接口,接口的数量以及所有接口名称的字符串常量的索引值。

字段表集合

 字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。
 字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_in。

方法表集合

 字段表结束后为方法表,用于描述每个方法的信息。
 方法表也是由两部分组成,第一部分为两个字节描述方法的个数;第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性。

属性表集合

方法表集合之后是属性表集合,用于描述的是class文件所携带的辅助信息,比如class文件对应的源文件信息。了解即可。

总结(Summary)

本章节主要描述了字节码是什么,如何解读字节码以及字节码主要由哪及部分构成。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值