深入理解JVM字节码(二)
目录
- 字节码基础
- 一、字节码概述
- 二、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
; - 线程同步,比如
monitorenter
和monitorexit
这两条指令用于支持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型变量加载到操作数栈上,根据不同的数据变量类型还有lload
、fload
、dload
、aload
这些指令,分别表示加载局部变量表中的long、float、double、引用型的变量。
2)store类指令是将栈顶的数据存储到局部变量表中,比如istore_0
将操作数栈顶的元素存储到局部变量表中下标为0的位置,这个位置的元素类型为int,根据不同的数据变量类型还有Istore
、fstore
、dstore
、astore
这些指令。
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. 操作数栈指令
常见的操作数栈指令有pop
、dup
和swap
。pop
用于将栈顶的值出栈,一个常见的场景是调用了有返回值的方法,但是没有使用这个返回值,比如:
对应字节码如下
第4行有一个pop
指令用于弹出调用bar
方法的返回值。
dup
指令用来复制栈顶元素并压入栈顶,swap
用于交换栈顶的两个元素。
还有几个稍微复杂的指令:dup_x1
、dup2_x1
和dup2_x2
。dup_x1
是复制操作数栈顶的值,并插入栈顶以下第二个值之后。
一个用到dup_x1
指令的例子
incAndGetId的方法对应的字节码
假如id的初始值为42,调用incAndGetId方法执行过程中操作数栈的变化
第八条指令:putfield #2
将栈顶的两个元素this
和43
出栈,完成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
的指令,但两者不是一回事。当创建一个对象时,会发生那些事呢?例:
对应的字节码为
一个对象创建需要三条指令,new
、dup
、<init>
方法的invokespecial
调用。在JVM中,类的实例初始化方法是<init>
,调用new
指令时,只是创建了一个类实例引用,将这个引用压入操作数栈顶,此时还没有调用初始化方法。使用invokespecial
调用<init>
方法后才真正调用了构造器方法,那中间的dup指令的作用是什么?invokespecial
会消耗操作数栈顶的类实例引用,如果想要在invokespecial
调用以后栈顶还有指向新建类对象实例的引用,就需要在调用invokespecial
之前复制一份类对象实例的引用,否则调用完<init>
方法以后,类实例引用出栈以后,就再也找不回刚刚创建的对象引用了。有了栈顶的新建对象的引用,就可以使用astore
指令将对象引用存储到局部变量表。
从本质上来理解导致必须要有dup指令的原因是<init>
方法没有返回值,如果<init>
方法把新建的引用对象作为返回值,也不会存在这个问题。
3. <clinit>
方法
<clinit>
是类的静态初始化方法,类静态初始化块、静态变量初始化都会被编译进这个方法中。例
对应字节码
javap输出字节码中的static{}
表示<clinit>
方法,<clinit>
不会直接被调用,它在四个指令触发时被调用(new
、getstatic
、putstatic
和invokestatic
),比如下面的场景:
❏ 创建类对象的实例,比如new、反射、反序列化等;
❏ 访问类的静态变量或者静态方法;
❏ 访问类的静态字段或者对静态字段赋值(final的字段除外);
❏ 初始化某个类的子类。
== END ==
更多JVM字节码相关的进阶内容见深入理解JVM字节码
深入理解JVM字节码(二)相关推荐
- 深入理解JVM字节码(一)
目录 深入剖析Class文件结构 一.初探class文件 二.class文件结构剖析 (一) 魔数 (二) 版本号 (三) 常量池 1. CONSTANT_Integer_info和CONSTANT_ ...
- 深入理解JVM——字节码
字节码 意义 字节码存在的意义就是解决Java跨平台问题,一次编写,到处执行.在不同的操作系统.不同硬件平台上,均可以不同修改代码即可顺畅地执行.作为Java与操作系统的中间码,成功解耦了语言对平台的 ...
- JVM字节码指令集大全及其介绍
Java是怎么跨平台的 我们上计算机课的时候老师讲过:"计算机只能识别0和1,所以我们写的程序要经过编译器翻译成0和1组成的二进制格式计算机才能执行".我们编译后产生的.class ...
- 【JVM · 字节码】指令集 解析说明
1. 概述 Java字节码指令对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令. Java虚拟机的指令由 一个字节长度 的.代表着某种特定操作含义的数字(称为 操作码/Opcode)以及跟随其后 ...
- 从一个class文件深入理解Java字节码结构
前言 我们都知道,Java程序最终是转换成class文件执行在虚拟机上的,那么class文件是个怎样的结构,虚拟机又是如何处理去执行class文件里面的内容呢,这篇文章带你深入理解Java字节码中的结 ...
- Java指令全集_Java的JVM字节码指令集详解
本文详细介绍了如何使用javap查看java方法中的字节码.以及各种字节码的含义,并且配以完善的案例,一步步,从头到尾带领大家翻译javap的输出.在文末还附有JVM字节码指令集表. 本文不适合没有J ...
- Android自动化埋点(一) - JVM字节码
JVM字节码 开头 这一系列文章,主要是讲自动化埋点又叫无痕埋点,或者字节码插桩技术,写这个系列文章的目的是 偶然间发现,网上关于这方面的博客很少,所以我根据自己的一些实战经验,整理了这个系列的文章. ...
- java开发C语言编译器:把C实现的快速排序算法编译成jvm字节码
有了前面一系列的铺垫和准备后,我们终于能走到至关重要的一刻.在本节,我们将用C语言开发快速排序算法,然后利用我们的编译器把它编译成java字节码,让C语言编写的快速排序算法能在java虚拟机上顺利执行 ...
- 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 ...
最新文章
- 一口一个,超灵活的Python迷你项目
- Windows Home Server 2011 RC 安装体验
- JSON对象和字符串之间的相互转换
- Freescale 基于IMX536处理器的Dialog DA9053电源管理参考设计
- Mac 10.12安装Office 2011
- 字符流中第一个不重复的字符 python实现
- win7系统下配置openCV python环境附加 numpy +scipy安装
- 不装客户端连接mysql_C#不安装oracle客户端,如何连接到oracle数据库
- ultraedit 运行的是试用模式_Wings面向企业级的单元测试用例自动编码引擎
- 复现经典:《统计学习方法》第 4 章 朴素贝叶斯
- linux7yum安装mysql,CentOS7 使用yum安装mysql
- Linux下如何让普通用户具备sudo执行权限(普通用户提权)
- 搜狗输入法电脑版_搜狗输入法上线墨水屏定制版
- 多线程专题之线程同步(1)
- js通过codeURL画二维码
- 12306排队是什么意思_12306抢票显示排队中怎么办
- IBM推出新功能 加速AI应用
- 微观交通仿真软件分析比较
- java使用java.lang.Math类,生成100个0-99之间的随机整数,并找出它们中间的最大值和最小值,并统计大于50的整数的个数。打印3次运行结果,看是否相同。
- 「学IT一定要看」一些学习的建议