Java字节码技术

news2024/11/25 12:42:06

Java 字节码简介

Java 中的字节码,英文名为 bytecode, 是 Java 代码编译后的中间代码格式。JVM 需要读取并解析字节码才能执行相应的任务。

从技术人员的角度看Java 字节码是 JVM 的指令集。JVM 加载字节码格式的 class 文件,校验之后通过 JIT 编译器转换为本地机器代码执行。 Java 字节码就是 JVM 执行的指令格式

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

操作码, 下面称为 指令, 主要由类型前缀操作名称两部分组成。

例如,’i’ 前缀代表 ‘integer’,所以,’iadd’ 很容易理解, 表示对整数执行加法运算。

根据指令的性质,主要分为四个大类:

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

此外还有一些执行专门任务的指令,比如同步(synchronization)指令,以及抛出异常相关的指令等等。

获取字节码清单

可以用 javap 工具来获取 class 文件中的指令清单。 javap 是标准 JDK 内置的一款工具, 专门用于反编译 class 文件。

先创建一个简单的类仅创建一个对象,后续慢慢扩充。

package com.lkl.jvmDemo;

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

编译这个类:

javac HelloByteCode.java

使用 javac 编译 ,直接在当前目录执行上述指令,得到对应的 class 即可。

javac 不指定 -d 参数编译后生成的 .class 文件默认和源代码在同一个目录。

注意: javac 工具默认开启了优化功能, 生成的字节码中没有局部变量表(LocalVariableTable),相当于局部变量名称被擦除。如果需要这些调试信息, 在编译时请加上 -g 选项。

JDK 自带工具的详细用法, 请使用: javac -help 或者 javap -help 来查看; 其他类似。

使用 javap 工具来执行反编译, 获取字节码清单:

还是在上述目录,执行下面指令:

javap -c HelloByteCode

或者

javap -c HelloByteCode.class

编译结果如下:

Compiled from "HelloByteCode.java"
public class com.lkl.jvmDemo.HelloByteCode {
  public com.lkl.jvmDemo.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           #7                  // class com/lkl/jvmDemo/HelloByteCode
       3: dup
       4: invokespecial #9                  // Method "<init>":()V
       7: astore_1
       8: return
}

解读字节码清单

反编译后的代码清单中, 有一个默认的构造函数 public com.lkl.jvmDemo.HelloByteCode(), 以及 main 方法。

Java基础中,创建一个类如果不定义任何构造函数,就会有一个默认的无参构造函数,这里再次验证了这个知识点。查看编译后的 class 文件证实了其中存在默认构造函数,所以这是 Java 编译器生成的, 而不是运行时JVM自动生成的。

默认构造函数一些指令

回顾 Java 知识, 每个构造函数中都会先调用 super 类的构造函数,但这不是 JVM 自动执行的, 而是由程序指令控制,所以默认构造函数中也就有一些字节码指令来干这个事情。

基本上,这几条指令就是执行 super() 调用:

public com.lkl.jvmDemo.HelloByteCode();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

其中解析的 java/lang/Object 不用说, 默认继承了 Object 类。这里再次验证了这个知识点,而且这是在编译期间就确定了的。

main函数指令

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

main 方法中创建了该类的一个实例, 然后就 return了,关于里面的几个指令, 稍后讲解。

查看 class 文件中的常量池信息

常量池 英文是 Constant pool。这里做一个强调:大多数时候指的是 运行时常量池。但运行时常量池里面的常量是从哪里来的呢? 主要就是由 class 文件中的 常量池结构体 组成的。

要查看常量池信息,需要加一点魔法参数:

javap -c -verbose HelloByteCode

在反编译 class 时,指定 -verbose 选项, 则会 输出附加信息

Classfile /XXX/com/lkl/jvmDemo/HelloByteCode.class
  Last modified 2023-10-29; size 304 bytes
  MD5 checksum 565e4ca34e83f69df37c1f35c971375f
  Compiled from "HelloByteCode.java"
public class com.lkl.jvmDemo.HelloByteCode
  minor version: 0
  major version: 65
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // com/lkl/jvmDemo/HelloByteCode
   #8 = Utf8               com/lkl/jvmDemo/HelloByteCode
   #9 = Methodref          #7.#3          // com/lkl/jvmDemo/HelloByteCode."<init>":()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               SourceFile
  #15 = Utf8               HelloByteCode.java
{
  public com.lkl.jvmDemo.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           #7                  // class com/lkl/jvmDemo/HelloByteCode
         3: dup
         4: invokespecial #9                  // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
}

显示了很多关于 class 文件信息: 编译时间, MD5 校验和, 从哪个 .java 源文件编译得来,符合哪个版本的 Java 语言规范等等。

还可以看到 ACC_PUBLICACC_SUPER 访问标志符。 ACC_PUBLIC 标志很容易理解:这个类是 public 类,因此用这个标志来表示。

ACC_SUPER 标志是怎么回事呢? 这就是历史原因, JDK 1.0 的 BUG 修正中引入 ACC_SUPER 标志来修正 invokespecial 指令调用 super 类方法的问题,从 Java 1.1 开始, 编译器一般都会自动生成ACC_SUPER 标志。

摘取一部分内容,可以看到常量池中的常量定义。还可以进行组合,一个常量的定义中可以引用其他常量。

Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>

第一行: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V, 解读如下:

  • #1 常量编号, 该文件中其他地方可以引用。
  • = 等号就是分隔符.
  • Methodref 表明这个常量指向的是一个方法;具体是哪个类的哪个方法呢? 类指向的 #2, 方法签名指向的 #3; 当然双斜线注释后面已经解析出来可读性比较好的说明了。

总结一下,常量池就是一个常量的大字典,使用编号的方式把程序里用到的各类常量统一管理起来,这样在字节码操作里,只需要引用编号即可。

查看方法信息

javap 命令中使用 -verbose 选项时, 还显示了其他的一些信息。 例如, 关于 main 方法的更多信息被打印出来:

  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

可以看到方法描述: ([Ljava/lang/String;)V

  • 其中小括号内是入参信息/形参信息;
  • 左方括号表述数组;
  • L 表示对象;
  • 后面的java/lang/String就是类名称;
  • 小括号后面的 V 则表示这个方法的返回值是 void
  • 方法的访问标志也很容易理解 flags: ACC_PUBLIC, ACC_STATIC,表示 public 和 static。

可以看到执行该方法时需要的栈(stack)深度是多少,需要在局部变量表中保留多少个槽位, 还有方法的参数个数: stack=2, locals=2, args_size=1。把上面这些整合起来其实就是一个方法:

public static void main(java.lang.String[]);

注:实际上一般把一个方法的修饰符+名称+参数类型清单+返回值类型,合在一起叫“方法签名”,即这些信息可以完整的表示一个方法。

编译器自动生成的无参构造函数字节码:

 public com.lkl.jvmDemo.HelloByteCode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1

会发现一个奇怪的地方, 无参构造函数的参数个数居然不是 0: stack=1, locals=1, args_size=1。 这是因为在 Java 中, 如果是静态方法则没有 this 引用。 对于非静态方法, this 将被分配到局部变量表的第 0 号槽位中

线程栈与字节码执行模型

JVM 是一台基于栈的计算机器。每个线程都有一个独属于自己的线程栈(JVM stack),用于存储栈帧(Frame)。每一次方法调用,JVM都会自动创建一个栈帧。栈帧操作数栈局部变量数组 以及一个class 引用组成。class 引用 指向当前方法在运行时常量池中对应的 class)。

image-20231029155316743

局部变量数组就是局部变量表(LocalVariableTable), 其中包含了方法的参数,以及局部变量。 局部变量数组的大小在编译时就已经确定: 和局部变量+形参的个数有关,还要看每个变量/参数占用多少个字节。操作数栈是一个 LIFO 结构的栈, 用于压入和弹出值。 它的大小也在编译时确定。

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

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

相关文章

B. Qingshan Loves Strings(贪心规律)

Problem - B - Codeforces 解析&#xff1a; 首先判断 t 字符串是不是相邻不同并且两端不同。 然后遍历 s 并且判断每一个相邻的相同字符&#xff0c;必须 t 字符符合并且两侧不同。 #include<bits/stdc.h> using namespace std; #define int long long const int N2e55…

私有云:【15】Composer安装无法使用cloudadmin进行下去

私有云&#xff1a;【15】Composer安装无法使用cloudadmin进行下去 1、Composer安装提示不支持windows授权2、这时候别退出3、稍微等待一会儿即可安装完成 1、Composer安装提示不支持windows授权 2、这时候别退出 上一步确定完之后&#xff0c;下一步让进行安装&#xff0c;不…

【软件测试02】测试方法

测试方法 学习目标&#xff1a; 1、能对穷举场景设计测试点---等价类划分法 2、能对限定边界规则设计测试点---边界值分析法 3、能对多条件依赖关系进行设计测试点---判定表法 4、能对项目业务进行设计测试点 一、等价类划分法 1、说明&#xff1a;在所有的测试数据中&am…

Spring更加简单的读取和存储对象

前言&#xff1a;在上篇文章中&#xff0c;小编写了一个Spring的创建和使用的相关博客&#xff1a;Spring的创建和使用-CSDN博客&#xff0c;但是&#xff0c;操作/思路比较麻烦&#xff0c;那么本文主要带领大家走进&#xff1a;Spring更加简单的读取和存储对象&#xff01; 本…

【数据结构】数组和字符串(十):稀疏矩阵的链接存储:十字链表的矩阵操作(加法、乘法、转置)

文章目录 4.2.1 矩阵的数组表示4.2.2 特殊矩阵的压缩存储a. 对角矩阵的压缩存储b~c. 三角、对称矩阵的压缩存储d. 稀疏矩阵的压缩存储——三元组表4.2.3三元组表的转置、加法、乘法、操作4.2.4十字链表0. 十字链表的基本操作1. 矩阵加法2. 矩阵乘法3. 矩阵转置4. 主函数 5. 代码…

linux 系统编程复习07-信号

1 复习目标 了解信号中的基本概念熟练使用信号相关的函数参考文档使用信号集操作相关函数熟练使用信号捕捉函数signal熟练使用信号捕捉函数sigaction熟练掌握使用信号完成子进程的回收 信号介绍 信号的概念 信号是信息的载体&#xff0c;Linux/UNIX 环境下&#xff0c;古老…

【C语言】优化通讯录管理系统

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家优化上一篇的通讯录&#xff0c;如果你觉得我写的还不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 目录 一. 前言二. 动态通讯录2.1 通讯录结构体2.2 初始化通讯录2.3 增加联系人2.4 销毁通讯…

【需要理解】80 单词搜索

单词搜索 题解1 回溯&#xff08;需要改变起点&#xff09; 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 单词必须按照字母顺序&#xff0c;通过相邻的单元格内…

数据结构——线性表①(顺序表)

一、线性表定义 线性表是一种数据结构&#xff0c;它是由n个具有相同数据类型的数据元素a1,a2,…,an组成的有限序列。 其中&#xff0c;除第一个元素a1外&#xff0c;每一个元素有且只有一个直接前驱元素&#xff0c;除了最后一个元素an外&#xff0c;每一个元素有且只有一个…

IDEA 如何运行 SpringBoot 项目

步骤一&#xff1a;配置 Maven 第一步&#xff1a;用 IDEA 打开项目&#xff0c;准备配置 maven 环境 &#xff0c;当然如果本地没有提前配置好 maven&#xff0c;就用 IDEA 默认的配置即可 配置 maven 步骤 情况 1&#xff1a;如果本地没有配置过 maven&#xff0c;可以保持如…

【Spring】IOC容器与Bean的常用属性配置

文章目录 1.前言2.IOC容器2.1 BeanFactory 容器2.2 ApplicationContext 容器 3.Bean的常用属性配置4. 总结 1.前言 在之前的文章-IOC的快速入门中讲过Bean这个概念. 本来就来介绍容器与Bean的常用属性配置 在Spring框架中&#xff0c;Bean指的是被Spring加载生成出来的对象。 …

ubuntu 18.04 编译安装flexpart 10.4(2023年) —— 筑梦之路

2023年10月29日 环境说明 操作系统版本&#xff1a;ubuntu 18.04 python版本&#xff1a;3.6.9 gcc版本&#xff1a;7.5.0 编译安装路径&#xff1a;/usr/local cmake: 3.10.2 所需要的源码包我已经打包放到我的资源。 2021年1月份已经写过一篇Ubuntu 编译安装的帖子F…

Android OpenGL ES 2.0入门实践

本文既然是入门实践&#xff0c;就先从简单的2D图形开始&#xff0c;首先&#xff0c;参考两篇官方文档搭建个框架&#xff0c;便于写OpenGL ES相关的代码&#xff1a;构建 OpenGL ES 环境、OpenGL ES 2.0 及更高版本中的投影和相机视图。 先上代码&#xff0c;代码效果如下图…

MQ——进阶

文章目录 消息可靠性生产者消息确认消息持久化消费者确认演示none模式演示auto模式 失败重试机制本地重试失败策略 死信交换机初始死信交换机TTL延迟队列安装DelayExchange插件使用DelayExchange 惰性队列消息堆积问题惰性队列 MQ集群集群分类普通集群镜像模式镜像模式的配置 仲…

私有云:【6】VCenter安装SqlServer

私有云&#xff1a;【6】VCenter安装SqlServer 1、VCenter安装SqlServer1.1、通过模板创建虚拟机1.2、安装sqlserver服务 2、搭建sqlserver群集2.1、安装群集功能2.2、在ad域服务器创建共享文件夹&#xff0c;供集群选举使用 3、创建故障转移群集【只需安装一台即可】3.1、创建…

操作系统 --- 存储器管理

一、简答题 1.存储器管理的基本任务&#xff0c;是为多道程序的并发执行提供良好的存储器环境。请问好的存储器环境”应包含哪几个方面&#xff1f; 答&#xff1a; 2.内存保护是否可以完全由软件实现&#xff1f;为什么&#xff1f; 答&#xff1a;内存保护的主要任务是确保每…

LeetCode热题100——双指针

双指针 1.移动零2.盛最多水的容器3.三数之和 1.移动零 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 // 题解&#xff1a;使用双指针&#xff0c;其中快指针指向非零元素&#xff0c;慢指针指向首个零元素下…

msvcp120.dll怎么修复,五种方法教你如何修复msvcp120.dll文件

在运行软件时&#xff0c;我们常常会遇到一些错误提示&#xff0c;其中之一就是“由于找不到msvcp120.dll无法继续执行代码”。这个错误通常发生在使用Microsoft Visual C 2013编译的程序运行时。本文将介绍5种修复这个问题的方法&#xff0c;帮助各位解决这个困扰。 方法一、使…

如何使用批量重命名的方法替换重复文件名内容

在文件管理过程中&#xff0c;我们有时会遇到文件名中包含相同部分内容的情况&#xff0c;这不仅会使文件显得混乱&#xff0c;而且还会给文件检索和使用带来不便。为了解决这个问题&#xff0c;我们可以使用云炫文件管理器批量重命名进行批量替换。下面是如何使用这种方法进行…

RT-Thread入门

1、初识RT-Thread RT-Thread&#xff0c;全称是Real Time-Thread&#xff0c;即嵌入式实时多线程操作系统。其基本属性之一是支持多任务&#xff0c;但是允许多任务同时运行&#xff0c;但是并不是意味着处理器在同一时刻真的执行了多个任务。实际上&#xff0c;一个处理器核心…