Java字节码初窥


一、字节码是什么?

“一次编写,到处运行”的口号,是Java拥有目前如此庞大的用户群的一个基石。其中的原因是:

  • JVM针对各种操作系统进行定制,屏蔽了系统差异;
  • Java编译之后生成字节码,部署到各种系统上面,由JVM加载字节码(.class文件),再通过JIT技术,生成机器码,进行运行。
    因此可以看出字节码在Java技术体系的重要地位, 下图是一个.java文件从编写源码到运行的示例图:
    Java运行示意图
    Java之所以被称之为字节码,是因为字节码文件由十六进制值组成,而 JVM 以两个十六进制值为一组,即以字节为单位进行读取。对于Java程序员来说,Java字节码是JVM的指令集,而JVM是Java代码运行必须依赖的环境,另外字节码技术在Spring AOP、各种ORM框架、APM中应用屡见不鲜,因此了解字节码及其工作原理,对于编写高性能代码至关重要,深入分析和排查问题也有一定作用。

二、字节码结构

2.1 示例

.java文件通过javac编译后得到一个.class文件,如下图所示的hello类,和生成的class文件:

public class Hello {

    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int sum = a + b;
    }
}

通过javac编译,一般采用如下的命令:

javac -g  src/main/java/com/johar/jeektime/jvmweek1/topic1/Hello.java -d target/classes

  • -d:指定生成class文件路径,若不指定-d,默认在当前目录
  • -g:由于javac工具默认开启优化功能,生成的字节码中没有局部变量表,使用-g可以关闭优化功能,生成所有调试信息
    下图是使用notepad++打开后的class文件:
    Hello.class
    使用javap反编译class文件,一般命令如下:
javap -c -v src/main/java/com/johar/jeektime/jvmweek1/topic1/Hello.class
  • -c: 对代码进行反汇编
  • -v: 输出附件信息
Classfile /E:/JavaTrain/homework/JAVA-000/Week_01/jvm-week1/src/main/java/com/johar/jeektime/jvmweek1/topic1/hello.class
  Last modified 2021-2-25; size 512 bytes
  MD5 checksum a12d379d9ed8fc58b46e8717610eabe8
  Compiled from "Hello.java"
public class com.johar.jeektime.jvmweek1.topic1.Hello
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#22         // java/lang/Object."<init>":()V
   #2 = Class              #23            // com/johar/jeektime/jvmweek1/topic1/Hello
   #3 = Class              #24            // java/lang/Object
   #4 = Utf8               count
   #5 = Utf8               I
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcom/johar/jeektime/jvmweek1/topic1/Hello;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               a
  #18 = Utf8               b
  #19 = Utf8               sum
  #20 = Utf8               SourceFile
  #21 = Utf8               Hello.java
  #22 = NameAndType        #6:#7          // "<init>":()V
  #23 = Utf8               com/johar/jeektime/jvmweek1/topic1/Hello
  #24 = Utf8               java/lang/Object
{
  public com.johar.jeektime.jvmweek1.topic1.Hello();
    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/johar/jeektime/jvmweek1/topic1/Hello;

  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: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: istore_3
         8: return
      LineNumberTable:
        line 15: 0
        line 16: 2
        line 17: 4
        line 35: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
            2       7     1     a   I
            4       5     2     b   I
            8       1     3   sum   I
}
SourceFile: "Hello.java"

2.2 class整体结构

JVM规范要求每个字节码文件都要按照如下图格式组成,后面会通过一个实例进行讲解。
字节码整体结构
classfile结构

2.2.1 魔数

Magic魔数的唯一作用就是确定这个文件是否为一个能被虚拟机所接受的class文件。魔数值固定为0xcafebabe,不回改变,可以对照示例的class文件看一下。

2.2.2 JDK版本号

版本号为魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。示例class文件中的版本号为“00 00 00 34”,转换成10进制为52.0,通过查询序号,可以看到jdk的版本为1.8.0.
jdk版本
对应javap反编译的bytecode如下,可以方便看到jdk的版本:

minor version: 0
major version: 52

2.2.3 常量池

jdk主版本号之后的字节为常量池,常量池中存储两类常量:字面量与符号引用。字面量为代码中申明为final 的常量值,符号引用如类和接口的全局限定名、字段名称和描述符、方法的名称和描述符。常量池的代码结构如下:
常量池
如上图所示,常量池分为量部分,常量池计数器,和常量数据区。

  • 常量池计数器
    常量池计数器的值等于常量池表中成员数+1。
  • 常量池数数据区
    常量池可以理解为一个常量的字典,使用编号的方式管理程序中的各种常量。常量池表的所有项格式如下:
cp_info {
	u1 tag;
	u1 info[];
}

在常量池表中,每个cp_info项都必须以一个表示cp_info类型的单字节"tag"项开头。后面info[]数组的内容由tag的值所决定。tag对应的类型如下表所示:
cp_info
通过javap反编译之后的bytecode显示如下:

Constant pool:
   #1 = Methodref          #3.#12         // java/lang/Object."<init>":()V
   #2 = Class              #13            // com/johar/jeektime/jvmweek1/topic1/Hello
   #3 = Class              #14            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               main
   #9 = Utf8               ([Ljava/lang/String;)V
  #10 = Utf8               SourceFile
  #11 = Utf8               Hello.java
  #12 = NameAndType        #4:#5          // "<init>":()V
  #13 = Utf8               com/johar/jeektime/jvmweek1/topic1/Hello
  #14 = Utf8               java/lang/Object

以第一行为例:

  • #1 常量编号,其他使用的地方直接引用常量编号
  • = 分隔符
  • Methodref 表明这个常量指向一个方法,具体的就是指向 #3 (Object)的 #12 方法(""😦)V),具体的方法以及在双斜杠后面显示

2.2.4 访问标记

常量池之后的两个字节是访问标记,描述该类或者接口的访问权限及属性,具体如下所示:
访问标记
实例中的ACC_PUBLIC表示是public, ACC_SUPER则是历史原因,JDK1.0引入ACC_SUPER标记修正invokespecial指令调用super类的方法的问题。

2.2.5 当前类名

访问标记的两个字节是当前类的全限定名,引用的是常量池中的索引值。

2.2.6 父类名称

当前类名后的两个字节是父类名称,引用的是常量池中的索引值。

2.2.7 接口

父类名称之后的两个字节是接口计数器,表示该类实现的接口数量。其后的n个字节是所有接口名称的字符串常量的引用值。

2.2.8 字段表

字段表用于描述类和接口中申明的变量,包含类级别的变量和实例变量,由字段个数和每个字段详细信息组成。
字段表

2.2.9 方法表

方法表描述的是方法的访问标记、方法名、方法描述符以及方法的属性,如下图所示:
方法表
javap反编译后的实例为:

public com.johar.jeektime.jvmweek1.topic1.Hello();  // 默认构造方法
    descriptor: ()V   // 方法描述,没有入参,返回值为void
    flags: ACC_PUBLIC  // 访问标记为public
    Code:
      stack=1, locals=1, args_size=1  // 栈深度为1,局部变量表为1,方法入参个数为1,默认的this
         0: aload_0   // 将this从局部变量表压入操作数栈
         1: invokespecial #1      // 调用hello父类的初始化方法            // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/johar/jeektime/jvmweek1/topic1/Hello;

  public static void main(java.lang.String[]);  // main方法
    descriptor: ([Ljava/lang/String;)V // 方法描述符,[表示数组, L表示对象,java/lang/String表示入参来下,V表示返回值为void
    flags: ACC_PUBLIC, ACC_STATIC  // 访问标记为public static
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1 // 将常量1加入到栈顶
         1: istore_1 // 将栈顶常量1存储到LocalVariableTable Slot1,并栈顶删除
         2: iconst_2 // 将常量2加入到栈
         3: istore_2 // 将栈顶常量2存储到LocalVariableTable Slot2,并从栈顶删除
         4: iload_1 // 将LocalVariableTable Slot1 压入到栈
         5: iload_2 // LocalVariableTable Slot2 压入到栈
         6: iadd // 栈顶两个数相加,结果放在栈顶
         7: istore_3 // 将栈顶的3存储到LocalVariableTable Slot3,并从栈顶删除
         8: return
      LineNumberTable:
        line 15: 0
        line 16: 2
        line 17: 4
        line 35: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
            2       7     1     a   I
            4       5     2     b   I
            8       1     3   sum   I

主要包括:

  • code区:源代码
  • LineNumberTable:将code区的操作码和源码中行号对应表
  • LocalVariableTable:this和局部变量表
    具体方法区的操作,接注释

2.2.10 附加属性

附加属性主要描述class文件中类或接口所定义属性的基本信息

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值