目录

  • 字节码基础
    • 一、字节码概述
    • 二、Java虚拟机栈和栈帧
      • 栈帧
        • 1. 局部变量表
        • 2. 操作数栈
    • 三、字节码指令
      • 1. 加载和存储指令
      • 2. 操作数栈指令
      • 3. 对象相关的字节码指令
        • 1. ``方法
        • 2. new、dup、invokespecial对象创建三条指令
        • 3. ``方法

字节码基础

一、字节码概述

Java虚拟机的指令由一个字节长度的操作码(opcode)和紧随其后的可选的操作数(operand)构成。

<opcode> [<operand1>,<operand2>]

比如将整型常量100压到栈顶的指令是bipush 100,其中bipush是操作码,100是操作数。字节码(bytecode)名字的由来是操作码的长度用一个字节表示因为操作码长度只有一个字节长度,这使得编译后的字节码文件非常小巧紧凑,但也直接限制整个JVM操作码指令集的数量最多只能有256个,目前已经使用了200个。
根据字节码的不同用途,字节码指令可以大概分为以下几类:

  • 加载和存储指令,比如iload将一个整型值从局部变量表加载到操作数栈;
  • 控制转移指令,比如条件分支ifeq
  • 对象操作,比如创建类实例的指令new;
  • 方法调用,比如invokevirtual指令用于调用对象的实例方法;
  • 运算指令和类型转换,比如加法指令iadd
  • 线程同步,比如monitorentermonitorexit这两条指令用于支持synchronized关键字的语义;
  • 异常处理,比如athrow显示抛出异常。

二、Java虚拟机栈和栈帧

虚拟机常见的实现方式有两种:基于栈(Stack bases)和基于寄存器(Register based)。典型的基于栈的虚拟机有Hotspot JVM、.net CLR,而典型的基于寄存器的虚拟机有Lua语言虚拟机LuaVM和Google开发的Android虚拟机DalvikVM。
基于栈和基于寄存器的指令集架构各有优缺点,具体如下:

  • 基于栈的指令集架构的优点是移植性更好、指令更短、实现简单,但是不能随机访问堆栈中的元素,完成相同的功能所需的指令数一般比寄存器架构多,需要频繁地入栈出栈,不利于代码优化。
  • 基于寄存器的指令集架构的优点是速度快,可以充分利用寄存器,有利于程序做运行速度的优化,但操作数需要显示指定,指令较长。

栈帧

Hotspot JVM是一个基于栈的虚拟机,每个线程都有一个虚拟机栈用来存储栈帧,每次方法调用都伴随着栈帧的创建和销毁。

当线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出StakOverflowError异常,可以用JVM命令行参数 -Xxs来指定线程栈的大小。列:-Xxs:256k
每个线程都拥有自己的Java虚拟机栈,一个多线程的应用会拥有多个Java虚拟机栈,每个栈拥有自己的栈帧。

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,随着方法调用而创建,随着方法结束而销毁。栈帧的存储空间分配在Java虚拟机栈中,每个栈帧拥有自己的局部变量表(Local Variable)、操作数栈(Operand Stack)和指向常量池的引用

1. 局部变量表

每个栈帧内部都包含一组称为局部变量表的变量列表局部变量表的大小在编译期间就以确定,对应class文件中方法Code属性的max_locals字段,Java虚拟机会根据max_locals字段来分配方法执行过程中需要分配的最大的局部变量表容量。实例:

使用javac -g MyJVMTest.java进行编译,然后执行javap -c -v -l MyJVMTest查看字节码,结果如下:

可以看到foo方法只有两个参数,args_size却等于3。当一个实例方法(非静态方法)被调用时,第0个局部变量是调用这个实例方法的对象的引用,也就是我们所说的this。调用方法foo(2022,"hello")实际上是调用foo(this,2022,"hello")

重点:局部变量表的大小并不是方法中所有局部变量的数量之和,它与变量的类型和变量作用域有关。当一个局部作用域结束,它内部的局部变量占用的位置就可以被接下来的局部变量复用了。例:

foo方法对应的局部变量表的大小等于1因为是静态方法,局部变量表不用自动添加this为局部变量表的第一个元素a和b共用同一个slot等于0的局部变量表位置

2. 操作数栈

每个栈帧内部都包含一个称为操作数栈的后进先出(LIFO)栈,栈的大小同样是在编译期间确定。Java虚拟机提供的很多字节码指令用于从局部变量表或者对象实例的字段中复制常量或变量到操作数栈,也有一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用时,操作数栈也用于准备调用方法的参数和接收方法返回的结果
比如iadd指令用于将两个int型的数值相加,它要求执行之前操作数栈已经存在两个int型数值,在iadd指令执行时,两个int型数值从操作数栈中出栈,相加求和,然后将求和的结果重新入栈。
整个JVM指令执行的过程就是局部变量表与操作数栈之间不断加载、存储的过程。
操作数栈容量最大值对应方法Code属性的max_stack,表示当前方法的操作数栈在执行过程中任何时间点的最大深度。调用一个成员方法会将this和所有参数入栈,调用完毕this和参数都会出栈。

三、字节码指令

附上查询地址:常用字节码指令查询

1. 加载和存储指令

加载(load)和存储(store)相关的指令是使用得最频繁的指令,分为load类、store类、常量加载这三种。
1)load类指令是将局部变量表中的变量加载到操作数栈,比如iload_0将局部变量表中下标为0的int型变量加载到操作数栈上,根据不同的数据变量类型还有lloadfloaddloadaload这些指令,分别表示加载局部变量表中的long、float、double、引用型的变量。
2)store类指令是将栈顶的数据存储到局部变量表中,比如istore_0将操作数栈顶的元素存储到局部变量表中下标为0的位置,这个位置的元素类型为int,根据不同的数据变量类型还有Istorefstoredstoreastore这些指令。
3)常量加载相关的指令,常见的有const类、push类、ldc类。const、push类指令是将常量值直接加载到操作数栈顶,比如iconst_0是将整数0加载到操作数栈上,bipush 100是将int型常量100加载到操作数栈上。ldc指令是从常量池加载对应的常量到操作数栈顶,比如ldc #10是将常量池中下标为10的常量数据加载到操作数栈上。

为什么同是int型常量,加载需要分这么多类型呢?这是为了使字节码更加紧凑,int型常量值根据n的范围,使用的指令按照如下的规则。

❏ 若n在[-1, 5] 范围内,使用iconst_n的方式,操作数和操作码加一起只占一个字节。比如iconst_2对应的十六进制为0 x05。-1比较特殊,对应的指令是iconst_m1(0x02)
❏ 若n在[-128, 127] 范围内,使用bipush n的方式,操作数和操作码一起只占两个字节。比如 n 值为100(0x64)时,bipush 100对应十六进制为0 x1064。
❏ 若n在[-32768, 32767] 范围内,使用sipush n的方式,操作数和操作码一起只占三个字节,比如 n 值为1024(0x0400)时,对应的字节码为sipush 1024(0x110400)。
❏ 若n在其他范围内,则使用ldc的方式,这个范围的整数值被放在常量池中,比如n值为40000时,40000被存储到常量池中,加载的指令为ldc #i, i为常量池的索引值。

2. 操作数栈指令

常见的操作数栈指令有popdupswappop用于将栈顶的值出栈,一个常见的场景是调用了有返回值的方法,但是没有使用这个返回值,比如:

对应字节码如下

第4行有一个pop指令用于弹出调用bar方法的返回值。
dup指令用来复制栈顶元素并压入栈顶,swap用于交换栈顶的两个元素。
还有几个稍微复杂的指令:dup_x1dup2_x1dup2_x2dup_x1是复制操作数栈顶的值,并插入栈顶以下第二个值之后。

一个用到dup_x1指令的例子

incAndGetId的方法对应的字节码

假如id的初始值为42,调用incAndGetId方法执行过程中操作数栈的变化

第八条指令:putfield #2将栈顶的两个元素this43出栈,完成this.id=43的隐式操作,现在栈中元素只剩下栈顶的[43],最后的ireturn指令将栈顶的43出栈返回。

3. 对象相关的字节码指令

1. <init>方法

<init>方法是对象初始化方法,类的构造方法、非静态变量的初始化、对象初始化代码快都会被编译进这个方法中。比如:

对应的字节码为:

javap输出的字节码中Initializer()方法对应<init> 对象初始化方法,其中5~7行将成员变量a赋值为10,10~12行将b赋值为10,13~15行将c赋值为30。可以看到,虽然Java语法上允许我们把成员变量初始化和初始化语句块写在构造器方法之外,最终在编译以后都会统一编译进<init>方法。

2. new、dup、invokespecial对象创建三条指令

在Java中new是一个关键字,在字节码中也有一个叫new的指令,但两者不是一回事。当创建一个对象时,会发生那些事呢?例:

对应的字节码为

一个对象创建需要三条指令,newdup<init> 方法的invokespecial调用。在JVM中,类的实例初始化方法是<init>,调用new指令时,只是创建了一个类实例引用,将这个引用压入操作数栈顶,此时还没有调用初始化方法。使用invokespecial调用<init> 方法后才真正调用了构造器方法,那中间的dup指令的作用是什么?invokespecial消耗操作数栈顶的类实例引用如果想要在invokespecial调用以后栈顶还有指向新建类对象实例的引用,就需要在调用invokespecial之前复制一份类对象实例的引用,否则调用完<init> 方法以后,类实例引用出栈以后,就再也找不回刚刚创建的对象引用了。有了栈顶的新建对象的引用,就可以使用astore指令将对象引用存储到局部变量表

从本质上来理解导致必须要有dup指令的原因是<init> 方法没有返回值,如果<init>方法把新建的引用对象作为返回值,也不会存在这个问题。

3. <clinit>方法

<clinit> 是类的静态初始化方法,类静态初始化块、静态变量初始化都会被编译进这个方法中。例

对应字节码

javap输出字节码中的static{}表示<clinit>方法,<clinit>不会直接被调用,它在四个指令触发时被调用(newgetstaticputstaticinvokestatic),比如下面的场景:
❏ 创建类对象的实例,比如new、反射、反序列化等;
❏ 访问类的静态变量或者静态方法;
❏ 访问类的静态字段或者对静态字段赋值(final的字段除外);
❏ 初始化某个类的子类。

== END ==

更多JVM字节码相关的进阶内容见深入理解JVM字节码

深入理解JVM字节码(二)相关推荐

  1. 深入理解JVM字节码(一)

    目录 深入剖析Class文件结构 一.初探class文件 二.class文件结构剖析 (一) 魔数 (二) 版本号 (三) 常量池 1. CONSTANT_Integer_info和CONSTANT_ ...

  2. 深入理解JVM——字节码

    字节码 意义 字节码存在的意义就是解决Java跨平台问题,一次编写,到处执行.在不同的操作系统.不同硬件平台上,均可以不同修改代码即可顺畅地执行.作为Java与操作系统的中间码,成功解耦了语言对平台的 ...

  3. JVM字节码指令集大全及其介绍

    Java是怎么跨平台的 我们上计算机课的时候老师讲过:"计算机只能识别0和1,所以我们写的程序要经过编译器翻译成0和1组成的二进制格式计算机才能执行".我们编译后产生的.class ...

  4. 【JVM · 字节码】指令集 解析说明

    1. 概述 Java字节码指令对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令. Java虚拟机的指令由 一个字节长度 的.代表着某种特定操作含义的数字(称为 操作码/Opcode)以及跟随其后 ...

  5. 从一个class文件深入理解Java字节码结构

    前言 我们都知道,Java程序最终是转换成class文件执行在虚拟机上的,那么class文件是个怎样的结构,虚拟机又是如何处理去执行class文件里面的内容呢,这篇文章带你深入理解Java字节码中的结 ...

  6. Java指令全集_Java的JVM字节码指令集详解

    本文详细介绍了如何使用javap查看java方法中的字节码.以及各种字节码的含义,并且配以完善的案例,一步步,从头到尾带领大家翻译javap的输出.在文末还附有JVM字节码指令集表. 本文不适合没有J ...

  7. Android自动化埋点(一) - JVM字节码

    JVM字节码 开头 这一系列文章,主要是讲自动化埋点又叫无痕埋点,或者字节码插桩技术,写这个系列文章的目的是 偶然间发现,网上关于这方面的博客很少,所以我根据自己的一些实战经验,整理了这个系列的文章. ...

  8. java开发C语言编译器:把C实现的快速排序算法编译成jvm字节码

    有了前面一系列的铺垫和准备后,我们终于能走到至关重要的一刻.在本节,我们将用C语言开发快速排序算法,然后利用我们的编译器把它编译成java字节码,让C语言编写的快速排序算法能在java虚拟机上顺利执行 ...

  9. Java生产环境下性能监控与调优详解 第8章 JVM字节码与Java代码层调优

    第8章 JVM字节码与Java代码层调优 8-1 jvm字节码指令-1 8-2 jvm字节码指令-2 8-3 i++与++i 8-4 字符串+拼接 8-5 Try-Finally字节码 8-6 Str ...

最新文章

  1. 一口一个,超灵活的Python迷你项目
  2. Windows Home Server 2011 RC 安装体验
  3. JSON对象和字符串之间的相互转换
  4. Freescale 基于IMX536处理器的Dialog DA9053电源管理参考设计
  5. Mac 10.12安装Office 2011
  6. 字符流中第一个不重复的字符 python实现
  7. win7系统下配置openCV python环境附加 numpy +scipy安装
  8. 不装客户端连接mysql_C#不安装oracle客户端,如何连接到oracle数据库
  9. ultraedit 运行的是试用模式_Wings面向企业级的单元测试用例自动编码引擎
  10. 复现经典:《统计学习方法》第 4 章 朴素贝叶斯
  11. linux7yum安装mysql,CentOS7 使用yum安装mysql
  12. Linux下如何让普通用户具备sudo执行权限(普通用户提权)
  13. 搜狗输入法电脑版_搜狗输入法上线墨水屏定制版
  14. 多线程专题之线程同步(1)
  15. js通过codeURL画二维码
  16. 12306排队是什么意思_12306抢票显示排队中怎么办
  17. IBM推出新功能 加速AI应用
  18. 微观交通仿真软件分析比较
  19. java使用java.lang.Math类,生成100个0-99之间的随机整数,并找出它们中间的最大值和最小值,并统计大于50的整数的个数。打印3次运行结果,看是否相同。
  20. 「学IT一定要看」一些学习的建议

热门文章

  1. Linux useradd命令详解
  2. 开贴梳理沉淀多年来的测试知识,帮助自己总结成长
  3. 播放视频有沙沙的杂音怎么处理?
  4. 快速批量替换文档中的回车符与换行符,包括分节符、分页符的替换
  5. java SE(一)——入门基础
  6. 感动你我,感动中国,历年感动中国人物评选体育类获奖人物盘点
  7. js获取当前时间的年月日时分秒
  8. 对键盘输入的小写字母用大写字母显示出来 asm汇编语言程序设计
  9. Android带有边框的裁剪算法,简单易用的 Android 智能图片裁剪框架
  10. PS太贵,用不起,那我只能用这个了