Java字节码介绍

news2024/11/19 17:43:10

Java字节码

概述

学习 Java 的都知道,我们所编写的 .java 代码文件通过编译将会生成 .class 文件,最初的方式就是通过 JDK 的 javac 指令来编译,再通过 java 命令执行 main 方法所在的类,从而执行我们的 Java 程序。而在这中间所生成的 .class 文件中的内容,就是 JVM 可以处理运行的字节码(Byte Code),它由 JVM 解释为对应系统可运行的机器指令,这也是我们的 Java 程序能够做到一处编译处处执行的原理。

什么是字节码

Java之所以可以“一次编译,到处运行”。

  • 一是因为JVM针对各种操作系统、平台都进行了定制。
  • 二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。

因此,也可以看出字节码对于Java生态的重要性。之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如下:

Java字节码介绍 - Java技术债务

对于 Java 开发人员来说,平时需要阅读 Byte Code 的场景比较少,但和阅读框架源码能够了解到框架的设计思路一样,阅读 Java Byte Code 也有利于我们理解 Java 一些深层的东西,提高我们解决问题的能力。能够阅读 Byte Code 也有利于我们去理解 Kotlin 或其它运行在 JVM 上的语言,是如何扩展 Java 所没有的特性或语法。

字节码文件结构

首先我们先编写一个简单的 Java 代码作为演示例子,然后编译这个 Hello.java 文件得到 Hello.class 文件。我们知道 .class 是二进制文件,它无法被直接查看,当然我们可以通过一些二进制文件查看工具来阅读里面的内容。

Java字节码介绍 - Java技术债务

编译后生成.class文件,打开后是一堆十六进制数,按字节为单位进行分割后展示如上图右侧部分所示。上文提及过,JVM对于字节码是有规范要求的,那么看似杂乱的十六进制符合什么结构呢?JVM规范要求每一个字节码文件都要由十部分按照固定的顺序组成,整体结构如下图:

Java字节码介绍 - Java技术债务

魔数

一个符合标准的 .class 文件是以 CA FE BA BE 开头,这个四个字节均为魔数,JVM 根据这个开头来判断一个文件是否可能为 .class 文件,如果是才会继续执行。

有趣的是,魔数的固定值是Java之父James Gosling制定的,为CafeBabe(咖啡宝贝),而Java的图标为一杯咖啡。

版本号

魔数后面四个字节 00 00 00 34 是版本号,前两个字节为次版本号,后两个字节为主版本号,在对主版本号进行转换可以得到 52,该序号对应的Java 版本为1.8。

常量池(Constant Pool)

在版本号后面则是常量池(Constant Pool),它包含常量池计数器和常量池数据区两个部分。

Java字节码介绍 - Java技术债务

  • 常量池计数器(constant_pool_count):由于常量的数量不固定,所以需要先放置两个字节来表示常量池容量计数值。图2中示例代码的字节码前10个字节如下图所示,将十六进制的24转化为十进制值为36,排除掉下标“0”,也就是说,这个类文件中共有35个常量。

Java字节码介绍 - Java技术债务

  • 常量池数据区:数据区是由(constant_pool_count-1)个cp_info结构组成,一个cp_info结构对应一个常量。在字节码中共有14种类型的cp_info,每种类型的结构都是固定的。

    Java字节码介绍 - Java技术债务

访问标志

常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被publicabstractfinal等修饰符修饰。JVM规范规定了如下图的访问标志(Access_Flag)。

需要注意的是,JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为public final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。

Java字节码介绍 - Java技术债务

当前类名

访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。

父类名称

当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。

接口信息

父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值。

字段表

字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。

字段表也分为两部分,

  • 第一部分为两个字节,描述字段个数;
  • 第二部分是每个字段的详细信息fields_info。

字段表结构如下图

Java字节码介绍 - Java技术债务

方法表

字段表结束后为方法表,方法表也是由两部分组成,第一部分为两个字节描述方法的个数;第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性,如下图所示:
Java字节码介绍 - Java技术债务

方法的权限修饰符依然可以通过访问标志查询得到,方法名和方法的描述符都是常量池中的索引值,可以通过索引值在常量池中找到。

当我们拥有一个 .class 文件时,我们可以通过 javap 来将字节码指令转换为助记符,这个命令有一些参数,你可以通过 javap -help 来查看所有参数的说明,这里为了显示尽量详细的内容,使用 javap -verbose,其效果如下,但由于内容太长,我们不一次性展示所有内容,而是分区域来进行阅读。

而“方法的属性”这一部分较为复杂,直接借助javap -verbose将其反编译为人可以读懂的信息进行解读,如下图所示。可以看到属性中包括以下三个部分:

  • Code区:源代码对应的JVM指令操作码,在进行字节码增强时重点操作的就是“Code区”这一部分。

    args_size 是参数数量,在主函数中,因为有 args 这个参数,所以在这里 args_size 为 1;

    locals 是该方法中的本地变量有多少个,在我们的主函数里面有定义了 3 个变量,加上一个参数,因此有 4 个变量;

    stack 是方法在执行过程中,操作数栈中最大深度,这个在之后讲解指令执行过程时可以看出。

    在这一行信息之后是字节码指令,一条指令包括偏移量以及执行的指令码,PC Register 利用偏移量来判断指令执行位置。

  • LineNumberTable:行号表,将Code区的操作码和源代码中的行号对应,Debug时会起到作用(源代码走一行,需要走多少个JVM指令操作码)。

    LineNumberTable:
      line 3: 0
      line 4: 2
      line 5: 4
      line 6: 10
      line 7: 17 
    

    line 3: 0 代表 Java 源码文件中的第三行代码从偏移量为 0 的位置开始,而继续往下看可以看到第四行代码从偏移量为 2 的位置开始,也就是说第三行代码所对应的字节码指令有 iconst_1 和 istore_1 两条。这也可以让 JVM 执行指令出现错误时,帮助我们定位到对应的源码位置。

  • LocalVariableTable:本地变量表,包含This和局部变量,之所以可以在每一个方法内部都可以调用This,是因为JVM将This作为每一个方法的第一个参数隐式进行传入。当然,这是针对非Static方法而言。

    第一个属性 start 为这个变量可见的起始偏移位置,它的值必须是在Code 中存在的偏移量值。

    第二个属性 length 为该变量的有效长度,在这个例子中,我们的变量直到方法末尾都有效,因此你会发现 start + lenth 的值都是 18 (方法中执行的指令数)。当我们在一个局部的代码块里面声明一个变量,那么它的有效期长度将会更短。

    Slot 为变量在 local variable 中的位置,这可以帮助我们在指令中确定对应的变量,而 Name 则是变量名,Signature 为该变量的类型。

Java字节码介绍 - Java技术债务

附加属性表

字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息。

操作数栈和字节码

JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性(因为寄存器指令集往往和硬件挂钩),但缺点在于,要完成同样的操作,基于栈的实现需要更多指令才能完成(因为栈只是一个FILO结构,需要频繁压栈出栈)。另外,由于栈是在内存实现的,而寄存器是在CPU的高速缓存区,相较而言,基于栈的速度要慢很多,这也是为了跨平台性而做出的牺牲。

我们在上文所说的操作码或者操作集合,其实控制的就是这个JVM的操作数栈。为了更直观地感受操作码是如何控制操作数栈的,以及理解常量池、变量表的作用,将add()方法的对操作数栈的操作制作为GIF,如下图14所示,图中仅截取了常量池中被引用的部分,以指令iconst_2开始到ireturn结束。

Java字节码介绍 - Java技术债务

JVM 内存结构

我们的Java程序在运行时是通过 main() 方法启动,它是程序的入口,我们的进程在启动时会为该方法创建一个主线程来执行代码。当我们使用多线程时,那么程序的进程将会拥有多个线程。每个线程的资源都拥有独自的资源,当然它们也可以共享进程的资源,那么在 JVM 中,根据资源的可用范围,可将内存区域分为线程独占和线程共享两个类别。JVM内存布局

Java字节码介绍 - Java技术债务

对于每一个线程,都可将其拥有的内存空间分为 PC Register、Native Method Stack、JVM Stack 这3个区域,这3个区域对于线程来说都是独占的,其它线程无法进行访问。

  • PC Register 用于记录当前线程指令的执行位置。由于一个进程可能有多个线程,而CPU会在不同线程之间切换,为了能够记录各个线程的当前执行的指令,每个线程都需要有一个 PC Register,来保证各个线程都可以进行独立运算。
  • JVM Stack 用于存放调用方法时压入栈的栈帧。相信学过数据结构的对栈应该不陌生,JVM Stack 压入的单位为栈帧(Frame),用于存储数据、动态链接、方法返回值和调度异常等。每次调用一个方法都会创建一个新的栈帧压入 JVM Stack 来存储该方法的信息,当该方法调用完成时,对应的栈帧也会跟着被销毁。一个栈帧都有自己的局部变量数组操作数栈对当前方法类的运行常量池的引用
  • Native Method Stack 则是用于调用操作系统本地方法时使用的栈空间。

Java字节码介绍 - Java技术债务

每个线程都可用访问的内存空间为线程共享区域,它包含 Head 和 Method Area 两个部分,Head 用于存放实例对象,也是 GC 回收的主要区域,而 Method Area 用于存放类结构与静态变量。

现在我们初步了解了 JVM 内存的布局,那么接下来可以继续看指令的执行过程了。

指令的执行过程

由于 Java 程序从 main() 方法开始,我们也是从这个方法的指令开始进行分析。

假设程序运行 0 号指令前的状态如下,在 mian 方法栈帧里面,有着 operand stack(操作数栈),它的最大长度为 2(与 Code 下的 stack 的值一致),此外还有一个 local variable(本地变量表)来存放变量的值,其中下标为 0 的变量为主方法的参数 args,我们直接用这个字符串填充在那里来做一个标识(实际的值可能是一个空数组)。

Java字节码介绍 - Java技术债务

接下来我们一步步执行方法中的指令,在这里我们先对出现的几个指令做一个简单的介绍:

  • iconst_<i> 放一个 int 常量(-1, 0, 1, 2, 3, 4 or 5) 到 operand stack 中
  • istore_<n> 从 operand stack 中获取一个 int 到 local variable 的 n 中
  • iload_<n> 从 local variable 中读取 int 变量 n 的值到操作数栈中
  • invokestatic 调用一个 class 的 static 方法
  • getstatic 从 class 中获取一个 static 字段
  • invokevirtual 调用一个实例方法,基于类的调度
  • return 从方法中返回一个 void,ireturn 从方法中返回 operand stack 栈顶的 int

更多的指令与详细的说明请查看文章最后参考中的官方指令文档

现在我们开始分析指令的执行,我们在上面知道了,我们的 Java 代码所对应的指令分别是偏移量为 0 和 1 的两个,最开始执行的是 0: iconst_1,该指令会把 int 常量 1 放置到 operand stack 中,之后执行的是 1: istore_1,把 operand stack 栈顶的 int 常量取出放到 local variable 下标为 1 的变量中,该过程图示如下。

Java字节码介绍 - Java技术债务

我们可以通过查看 LocalVariableTable 得知下标为 1 的变量在我们的 Java 程序中是 int 变量 a,因此上面这两条指令常量 1 赋值给变量 a。同样的,后面两条指令则是将常量 1 赋值给变量 b。这里要注意,操作数栈的数是被取出操作,被取出的数将不会继续在 operand stack 里面。

执行完 0~3 这 4 条指令后,就来到了本例中最为关键的方法调用了。在执行 iload_1iload_2 后,operand stack 中将会存放着变量 a 和 b 的值,作为 invokestatic 调用函数时传入的参数。

而执行到 invokestatic #2 这个指令的时候,该指令为调用一个 class 的 static 方法,也就是调用常量池中 #2 的方法,该方法为 Hello.add:(II)I

当执行 invokestatic 时会依次读取 operand stack 的数据作为方法的参数,并创建一个新的栈帧来执行方法,将数据放到 local variable 对应变量位置。

Java字节码介绍 - Java技术债务

之后开始执行 add() 方法中的指令,首先执行的是两个 iload 指令,将 loca variable 对应下标的变量的值放到 operand stack 中,之后执行 iadd 取出 operand stack 中的值并进行加法运算,再把结果放到,最后执行 ireturn 取出 operand stack 顶部的 int 值进行返回。

Java字节码介绍 - Java技术债务

Java字节码介绍 - Java技术债务

当执行完 ireturn 后,add 方法也就执行完成了,对应的栈帧也会跟着销毁。之后回到 main 方法中继续往下执行,到 istore_3 指令,该指令将栈顶的 int 值取出放到了 local variable 中 Solt 为 3 的地方,这样执行完 4~9 这几条指令后就完成了我们代码中的 int c = add(a, b); 这一行代码。那么接下来就是执行 System.out.println(c); 对应的指令将 2 打印到控制台了。

到这里其实我们就已经知道如何去阅读我们代码生成的 Byte Code 了,这里我就不继续往下分析本文例子的代码了,阅读过程中如果遇到了没见过的指令,我们可以在 Oracle 官方指令文档里面查阅对应的说明。

查看字节码工具

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

Java字节码介绍 - Java技术债务


--------------------------------------欢迎叨扰此地址---------------------------------------

本文作者:Java技术债务
原文链接:https://cuizb.top/myblog/article/1671634067
版权声明: 本博客所有文章除特别声明外,均采用 CC BY 3.0 CN协议进行许可。转载请署名作者且注明文章出处。

参考

  1. 字节码增强技术探索:https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
  2. 一文看懂 JVM 内存布局及 GC 原理:https://www.infoq.cn/article/3wyretkqrhivtw4frmr3
  3. Oracle 官方说明文档:https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-4.html#jvms-4.10
  4. Oracle 官方指令文档:https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/106773.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【矩阵论】6. 矩阵理论——算子范数

6.2 算子范数 6.2.1 定义 CnC^nCn 上任一向量范数 ∥X∥V\Vert X\Vert_V∥X∥V​ 都产生一个矩阵范数 ∥A∥max⁡x≠0{∥AX∥V∥X∥V}\Vert A\Vert\max_{x\neq 0}\limits \{\frac{\Vert AX\Vert_V}{\Vert X\Vert_V}\}∥A∥x0max​{∥X∥V​∥AX∥V​​} ,X∈CnX\in C^nX∈Cn…

Linux 管理联网 测试网络连通性 -- Ping 命令详解 tracepath命令详解

测试网络的连通性 # 我们测试网络的连通性&#xff0c;一般就是使用的 PIng 命令 Ping &#xff1a; 一般格式 &#xff1a; ping [ 选项 ] < 目标主机名 或 IP 地址 > 常用选项 &#xff1a; - c 数字 &#xff1a; 用于 设定本命令发出的 ICMP 消息包的…

限量,Alibaba首发“Java成长笔记”,差距不止一点点

前言 本文是为了帮大家快速回顾了Java中知识点&#xff0c;这套面试手册涵盖了诸多Java技术栈的面试题和答案&#xff0c;相信可以帮助大家在最短的时间内用作面试复习&#xff0c;能达到事半功倍效果。 本来想将文件上传到github上&#xff0c;但由于文件太大有的都无法显示…

CentOS7使用yum安装Golang(超详细)

使用yum安装Golang前言一、go语言介绍二、yum安装golang1.安装go版本为1.19.41.1执行yum install go&#xff08;报错&#xff09;1.2配置go的安装源1.3执行yum install golang1.4查看go的安装版本2.安装go版本为 1.11rc2&#xff08;这个参考&#xff0c;不用操作&#xff09;…

Docker镜像的原理

centos7系统 包括2部分&#xff0c; linux内核&#xff0c;作用是提供操作系统的基本功能&#xff0c;和机器硬件交互&#xff0c;如何读取磁盘数据&#xff0c;管理网络&#xff0c;使用C编写的&#xff0c;由linus的开发团队&#xff0c;内核只提供操作系统的基本功能和特性…

修改嵌入式 ARM Linux 内核映像中的文件系统

zImage 是编译内核后在 arch/arm/boot 目录下生成的一个已经压缩过的内核映像。通常我们不会使用编译生成的原始内核映像 vmlinux&#xff0c;因其体积很大。因此&#xff0c;zImage 是我们最常见的内核二进制&#xff0c;可以直接嵌入到固件&#xff0c;也可以直接使用 qemu 进…

C++的OpenCV中cv::minAreaRect的返回角度的数值范围是多少?

版本不一样的时候&#xff0c;返回也不一样。 我使用opencv/4.5.5。 下图是使用minAreaRect判定的角度&#xff0c;可以看到&#xff0c;数值范围是[0,90]&#xff0c;看起来很离谱。 画出这张图使用的程序如下&#xff1a; C int main() {std::string prefix1 "/mn…

SpringMvc+Thymeleaf实现数据渲染

Thymeleaf是spring boot推荐使用的模板语法&#xff0c;它可以完全替代 JSP 。 从代码层次上讲&#xff1a;Thymeleaf是一个java类库&#xff0c;它是一个xml/xhtml/html5的模板引擎&#xff0c;可以作为mvc的web应用的view层。 Thymeleaf 提供spring标准方言和一个与 SpringMV…

Ui自动化概念+Web自动化测试框架介绍

目录 UI 1.UI自动化测试概念:我们先明确什么是UI 2.为什么对UI采用自动化测试? 3.什么项目适合做UI自动化测试? 4.UI自动化测试介入时机 5.UI自动化测试所属分类 Web自动化测试框架介绍 2.Selenium框架介绍及特点: Web自动化测试环境搭建 2.元素定位(一) idclassna…

【数据结构】栈与集合类Stack

目录 一、栈 二、Java中的集合类之Stack 1、介绍 2、构造方法 3、常用方法 1.push 2.pop 3.peek 4.search 5.empty 三、实现Stack 1、准备字段 2、实现判空 3、实现压栈 4、实现出栈 5、实现获取栈尾元素 6、指定元素到栈顶的距离 一、栈 栈(stack)是一种比较…

Redis高可用之哨兵模式(第二部分)

引言 接上一篇&#xff0c;今天我们来聊一聊Redis的高可用的第二个解决方案&#xff1a;哨兵模式。 一、Redis哨兵模式 哨兵模式&#xff08;sentinel&#xff09;是Redis提供的高可用的解决方案之一。由一个或者多个sentinel示例组成的sentinel系统&#xff0c;可以监听任意…

(Java高级教程)第二章Java多线程常见面试题-第二节:JUC(java.util.concurrent)

文章目录一&#xff1a;Callable接口二&#xff1a;ReentrantLock三&#xff1a;原子类四&#xff1a;信号量SemaphoreJUC&#xff1a;JUC是java.util.concurrent包的简称&#xff0c;目的就是为了更好的支持高并发任务。让开发者进行多线程编程时减少竞争条件和死锁的问题 一…

智己LS7发布,预售价格区间35-50万元

12月20日&#xff0c;智己首款中大型大五座SUV 智己LS7开启预售。动力配置&#xff1a; •最大零百加速4.5S&#xff1b; •峰值公里425kw&#xff0c;峰值扭矩725Nm&#xff1b; •提供90度和100度电池选项&#xff1b; •最大CLTC续航660km&#xff1b;空间配置&#xff1a; …

06. http协议基础,带你了解网络访问

06. http协议基础&#xff0c;带你了解网络访问 渗透测试学习路径 计算机基础网络基础WEB漏洞渗透测试 渗透测试和WEB安全漏洞的区别&#xff1f; 渗透测试包含WEB安全漏洞 WEB网站只是单一的网站服务&#xff0c;在渗透测试过程中可能不是攻击网站&#xff0c;而是寻找其他…

ElasticSearch全文检索原理及过程

倒排索引 ElasticSearch的搜索引擎中&#xff0c;每个文档都有一个对应的文档 ID&#xff0c;文档内容被表示为一系列关键词的集合。例如文档 1 经过分词&#xff0c;提取了 20 个关键词&#xff0c;每个关键词都会记录它在文档中出现的次数和出现位置。那么&#xff0c;倒排索…

差分---(小明的彩灯)蓝桥杯真题,差分思想很明确的模板

小明的彩灯题目描述暴力解法差分的思路和模板差分解法题目描述 小明拥有 N个彩灯&#xff0c;第 i个彩灯的初始亮度为 ai​。 小明将进行 Q次操作&#xff0c;每次操作可选择一段区间&#xff0c;并使区间内彩灯的亮度 x&#xff08;x可能为负数&#xff09;。 求 Q次操作后…

自动控制原理笔记-传递函数

目录 拉普拉斯反变换&#xff1a; 用拉普拉斯变换求解常微分方程的步骤&#xff1a; 部分分式展开法&#xff1a; 留数法&#xff1a; 零极点图: 传递函数 定义&#xff1a; 传递函数的标准形式&#xff1a; 传递函数的性质&#xff1a; 传递函数的局限性&#xff1a…

SOT23-6封装 小封装 超精简外围PD Sink端取电协议芯片

PD协议&#xff08;USB-PD&#xff09;的全名是USB Power Delivery&#xff0c;是由 USB-IF 组织制定的一种快速充电规范&#xff0c;是目前主流的快充协议之一&#xff0c;USB-PD 快充协议是以 Type-C 接口输出的&#xff0c;我们经常看到的华为笔记本配的Type-C 65W充电器就是…

【C语言】函数栈帧的创建和销毁

目录 1.函数栈帧的含义 概念 要用到的汇编语言的知识 示例 2.理解栈帧 2.1 main函数栈帧的创建 2.2 局部变量的创建 2.3 函数传参 2.4 调用函数 2.5 函数返回 一个.c文件在调用函数的时候&#xff08;包括main 函数&#xff09;&#xff0c;其内存中的栈区有什么变…

Qt之实现工具箱界面程序

最近终于有点空闲时间了&#xff0c;就写写博客&#xff0c;就把上次给客户实现的一个程序开发过程写出来&#xff1b;客户要求的是在主界面上能有几个很好看的按钮&#xff0c;单击各个按钮能弹出不同的应用窗口&#xff0c;如游戏窗口&#xff0c;显示图像窗口等等&#xff0…