本篇为《JVM指令分析实例》的第四篇,相关实例均使用Oracle JDK 1.8编译,并使用javap生成字节码指令清单。
前几篇传送门:
数组
一维原始类型数组
void createBuffer() {
int buffer[];
int bufsz = 100;
int value = 12;
buffer = new int[bufsz];
buffer[10] = value;
value = buffer[11];
}
字节码指令序列
void createBuffer():
0: bipush 100 // 将单字节int常量值100压入栈顶
2: istore_2 // 将栈顶int类型数值100存入第3个局部变量. bufsz = 100
3: bipush 12 // 将单字节int常量值12压入栈顶
5: istore_3 // 将栈顶int类型数值12存入第4个局部变量. value = 12
6: iload_2 // 将第3个int类型局部变量压入栈顶
7: newarray int // 创建int类型数组,并将数组引用值压入栈顶. new int[bufsz]
9: astore_1 // 将栈顶引用类型值存入第2个局部变量. buffer = new int[bufsz]
10: aload_1 // 将第2个引用类型局部变量压入栈顶
11: bipush 10 // 将单字节int常量10压入栈顶
13: iload_3 // 将第4个int类型局部变量压入栈顶
14: iastore // 将栈顶int类型数值存入数组的指定索引位置. buffer[10] = value
15: aload_1 // 将第2个引用类型值压入栈顶
16: bipush 11 // 将单字节int常量值11压入栈顶
18: iaload // 将int类型数组的指定元素压入栈顶
19: istore_3 // 将栈顶int类型数值存入第4个局部变量
20: return
newarray指令
创建一个指定原始类型(如int、float、char等)的数组,并将其引用值压入栈顶。
执行该指令后,将从操作数栈出栈1个参数count,类型为int,表示要创建数组的大小。
iastore指令
从操作数栈读取一个int类型数据并存入指定数组中。
执行该指令后,将从操作数栈出栈3个参数arrayref、index和value,在本例中分别对应于第10、11和13索引位置压入的值。
其中,arrayref是一个引用类型值,指向一个int类型的数组。index和value为int类型,index表示待存入数组位置的索引号,value表示待存入index索引位置的值。
iaload指令
从数组中加载一个int类型数据到操作数栈。
执行该指令后,将从操作数栈出栈2个参数arrayref和index,在本例中分别对应于第15和16索引位置压入的值。
其中,arrayref是一个引用类型值,指向一个int类型的数组。index为int类型,表示待加载数组数据的索引号。
一维引用类型数组
void createThreadArray() {
Thread threads[];
int count = 10;
threads = new Thread[count];
threads[0] = new Thread();
}
字节码指令序列
void createThreadArray():
0: bipush 10 // 将单字节int类型值10压入栈顶
2: istore_2 // 将栈顶int类型值存入第3个局部变量. count = 10
3: iload_2 // 将第3个int类型局部变量压入栈顶
4: anewarray #15 // class java/lang/Thread. 创建Thread类型数组,并将数组引用值压入栈顶. new Thread[count]
7: astore_1 // 将栈顶引用类型值存入第2个局部变量
8: aload_1 // 将第2个引用类型局部变量压入栈顶
9: iconst_0 // 将int类型常量0压入栈顶
10: new #15 // class java/lang/Thread. 创建Thread对象,并将引用值压入栈顶
13: dup // 复制栈顶值并压入栈顶
14: invokespecial #17 // Method java/lang/Thread."<init>":()V. 调用实例初始化方法
17: aastore // 将栈顶引用类型值存入数组的指定索引位置. threads[0] = new Thread()
18: return
anewarray指令
创建一个引用类型(如类、接口、数组)数组,并将其引用值压入栈顶。可用于创建一维引用数组,或者用于创建多维数组的一部分。
执行该指令后,将从操作数栈出栈1个参数count,类型为int,表示要创建数组的大小。
aastore指令
(aastore指令与iastore指令作用类似)
从操作数栈读取一个引用类型数据并存入指定数组中。
执行该指令后,将从操作数栈出栈3个参数arrayref、index和value,在本例中分别对应于第8、9和10索引位置压入的值。
其中,arrayref是一个引用类型值,指向一个引用类型的数组。index为int类型,index表示待存入数组位置的索引号。value为引用类型,表示待存入index索引位置的值。
在运行时,value的实际类型必须与arrayref所代表的数组的组件类型相匹配。
多维数组
int[][][] create3DArray() {
int grid[][][];
grid = new int[10][5][];
return grid;
}
字节码指令序列
int[][][] create3DArray():
0: bipush 10 // 将单字节int类型值10压入栈顶. 第1维
2: iconst_5 // 将int类型常量5压入栈顶. 第2维
3: multianewarray #16, 2 // class "[[[I". 创建int[][][]类型数组,并将引用值压入栈顶
7: astore_1 // 将栈顶引用类型值存入第2个局部变量
8: aload_1 // 将第2个引用类型局部变量压入栈顶
9: areturn // 从当前方法返回栈顶引用类型值
multianewarray指令
创建指定类型和指定维度的多维数组(执行该指令时,操作数栈中必须包含各维度的长度值),并将其引用值压入栈顶。可以用于创建所有类型的多维数组。
对于本实例,数组类型为[[[I,即#16对应的常量池中的符号引用。数组维度为2,两个维度的长度值分别为10和5。虽然int[][][]为3维数组,但由于仅指定了前2个维度的长度值,因此指令对应的维度值为2。
如果指定了第3个维度的长度值,那么在iconst_5之后还需要再将1个int类型长度值压入栈。
所有的数组都有一个与之关联的长度属性,可通过arraylength指令访问。
switch语句
编译器会使用tableswitch和lookupswitch指令来生成switch语句的编译代码。
Java虚拟机的tableswitch和lookupswitch指令都只能支持int类型的条件值。
tableswitch指令可以高效地从索引表中确定case语句块的分支偏移量。
当switch语句中的case分支条件值比较稀疏时,tableswitch指令的空间使用率偏低。这种情况下,可以使用lookupswitch指令来代替。
tableswitch指令
int chooseNear(int i) {
switch(i) {
case 0: return 0;
case 1: return 1;
case 2: return 2;
default: return -1;
}
}
字节码指令序列
int chooseNear(int):
0: iload_1 // 将第2个int类型局部变量压入栈顶
1: tableswitch { // 0 to 2
0: 28 // 如果case条件值为0,则跳转到索引号为28的指令继续执行
1: 30 // 如果case条件值为1,则跳转到索引号为30的指令继续执行
2: 32 // 如果case条件值为2,则跳转到索引号为32的指令继续执行
default: 34 // 否则,则跳转到索引号为34的指令继续执行
}
28: iconst_0 // 将int类型常量0压入栈顶
29: ireturn // 从当前方法返回栈顶int类型数值
30: iconst_1 // 将int类型常量1压入栈顶
31: ireturn // 从当前方法返回栈顶int类型数值
32: iconst_2 // 将int类型常量2压入栈顶
33: ireturn // 从当前方法返回栈顶int类型数值
34: iconst_m1 // 将int类型常量-1压入栈顶
35: ireturn // 从当前方法返回栈顶int类型数值
tableswitch指令
用于switch条件跳转,case值连续(变长指令)。
根据索引值在跳转表中寻找配对的分支并进行跳转。
指令格式:tableswitch padbytes defaultbytes lowbytes highbytes jumptablebytes
- padbytes:0~3个填充字节,以使得defaultbytes与方法起始地址(方法内第一条指令的操作码所在的地址)之间的距离是4的位数。
- defaultbytes:32位默认跳转地址
- lowbytes:32位低值low
- highbytes:32位高值high
- jumptablebytes:(high-low+1)个32位有符号数值形成的一张零基址跳转表(0-based jump table)
由于采用了索引值定位的方式(可理解为数组随机访问),因此只需要检查索引是否越界,非常高效。
下面结合实例分析一下:
第1条指令的索引号为0,tableswitch指令索引号为1,为了使defaultbytes与方法起始地址之间的距离是4的位数,所以defaultbytes的开始索引号为4。
defaultbytes、lowbytes和highbytes分别占4个字节,总共12个字节。
case高低值分别为2和0,因此jumptablebytes占用(2-0+1)*4=12个字节。
由于defaultbytes的开始索引号为4,defaultbytes~jumptablebytes共占用24个字节,因此紧跟在tableswitch后面的下一条指令的索引号为4+24=28,对应于实例中的指令"28: iconst_0"。
这里顺便提一下,一般情况下,普通的操作数占1个字节,指向常量池的索引值占2个字节(ldc的常量池索引占1个字节,ldc_w、ldc2_w的常量池索引占2个字节)。所以,方法的指令索引号之间有时不是连续的。
lookupswitch指令
int chooseFar(int i) {
switch(i) {
case -100: return -1;
case 0: return 0;
case 100: return 1;
default: return -1;
}
}
字节码指令序列
int chooseFar(int):
0: iload_1
1: lookupswitch { // 3
-100: 36
0: 38
100: 40
default: 42
}
36: iconst_m1
37: ireturn
38: iconst_0
39: ireturn
40: iconst_1
41: ireturn
42: iconst_m1
43: ireturn
lookupswitch指令
用于switch条件跳转,case值不连续(变长指令)。
根据键值(非索引)在跳转表中寻找配对的分支并进行跳转。
指令格式:lookupswitch padbytes defaultbytes npairsbytes matchoffsetbytes
- padbytes:0~3个填充字节,以使得defaultbytes与方法起始地址(方法内第一条指令的操作码所在的地址)之间的距离是4的位数。
- defaultbytes:32位默认跳转地址
- npairsbytes:32位匹配键值对的数量npairs
- matchoffsetbytes:npairs个键值对,每一组键值对都包含了一个int类型值match以及一个有符号32位偏移量offset。
由于case条件值是非连续的,因此无法采用像tableswitch直接定位的方式,必须对每个键值进行比较。然而,JVM规定,lookupswitch的跳转表必须根据键值排序,这样(如采用二分查找)会比线性扫描更有效率。
下面结合实例分析一下:
第1条指令的索引号为0,lookupswitch指令索引号为1,为了使defaultbytes与方法起始地址之间的距离是4的位数,所以defaultbytes的开始索引号为4。
defaultbytes、npairsbytes分别占4个字节,总共8个字节。
case有3个条件,共3个键值对(npairs为3)。由于每个键值对占8个字节(4字节match+4字节offset),因此matchoffsetbytes共占24个字节。
所以,紧跟在lookupswitch后面的下一条指令的索引号为4+8+24=36,对应于实例中的指令"36: iconst_m1"。
参考
《The Java Virtual Machine Specification, Java SE 8 Edition》
《Java虚拟机规范》(Java SE 8版)
转载请注明来源:http://zhanjia.iteye.com/blog/2431768
个人公众号
二进制之路
相关推荐
8. Java指令集:JVM指令集定义了Java虚拟机的一系列指令,每条指令都是对操作数栈进行某种操作,如加载、存储、运算、控制流等。 9. 局限性:JVM在设计上有一些限制,如方法区大小限制、支持的数据类型数量限制等。...
* 堆(Heap-线程共享):用于存储JAVA程序的数据和对象,包括实例对象、数组对象等。 * 方法区/永久代(线程共享):用于存储JAVA程序的代码和常量池。 JVM运行时内存 JVM运行时内存是JVM中最重要的组件之一,负责...
* 程序计数器:程序计数器是一块较小的内存空间,用于存储当前线程执行的字节码指令地址。每个线程都有自己的程序计数器。 * 虚拟机栈:虚拟机栈是线程私有的,用于存储 Java 方法的执行信息,包括局部变量、操作数...
其优势在于它的“一次编写,到处运行”特性,这得益于JVM的存在,JVM能够将编译后的字节码转换为特定操作系统下的机器指令。JDK(Java Development Kit)是开发Java程序的核心工具集,包含了编译器(javac)、解释器...
- **数组**:涵盖了数组的基本概念、初始化方式、多维数组的使用技巧以及一些实用的数组操作函数。 #### 三、面向对象基础 - **面向对象的概念**:比较了面向过程与面向对象两种编程范式的差异,强调了面向对象的...
Java支持多种流程控制结构,包括if-else语句、switch语句、for循环、while循环等。 ##### 17. 三大循环结构 - **for循环**:适用于已知迭代次数的情况。 - **while循环**:先判断条件,再执行循环体。 - **do-...
15. **数组实例化**:数组实例化有两种方式,静态实例化指定元素,动态实例化只指定数组长度。 16. **默认值**:Java中,各类型都有默认值,如整型默认为0,布尔型为false,对象类型为null。 17. **常用包**:Java...
- **跨平台性**:Java 最大的特点是“一次编写,到处运行”(Write Once, Run Anywhere),这得益于Java虚拟机(JVM)的存在。 - **面向对象**:Java 是一种纯面向对象的语言,支持封装、继承和多态等面向对象的特性。...
这种平台独立性是通过JVM的解释器来实现的,它读取字节码并将其转换为机器语言指令,在特定的操作系统上执行。 - **字节码**: Java编译器将源代码编译成中间格式——字节码,这些字节码可以在任何支持JVM的平台上...
1. 编程是指使用特定的语言编写指令,让计算机执行特定任务的过程。 2. Java语言由Sun Microsystems的James Gosling于1995年推出,其设计目标是“简单、面向对象、健壮、安全、高性能”。Java的特点包括:一次编写,...
以上只是Java 7 API中的一部分重要特性,实际上,它还包括许多其他改进和优化,如改进的垃圾回收机制、更好的数组操作以及对IPv6的更好支持等。这些特性共同提升了Java开发的效率和代码质量,使得Java 7成为了一个...
- 编程是通过特定的计算机语言来编写指令,让计算机执行一系列任务的过程。 2. **Java语言概述,历史、特点** - **概述**:Java是一种广泛使用的面向对象的编程语言,由Sun Microsystems在1995年发布。 - **历史...
Java虚拟机(JVM)是Java程序运行的核心,它将编译后的字节码转换为特定硬件平台的机器指令。了解JVM的工作原理,包括类加载、垃圾收集和内存管理,对于优化代码性能至关重要。 课程的第三章深入探讨了变量,这是...
除了基本类型外,所有都是引用类型,包括类实例、数组和接口引用。 #### 5. 运算符 Java支持算术运算符(+、-、*、/、%)、关系运算符(==、!=、<、>、、>=)、逻辑运算符(&&、||、!)等,还有赋值运算符、位...
- 编程是通过特定的计算机语言来编写指令,让计算机能够执行一系列任务的过程。 2. **Java语言概述,历史、特点** - Java是由Sun Microsystems公司在1995年发布的面向对象的编程语言。 - 特点包括:简单性、面向...
6. **动态语言支持**:通过invokedynamic指令,JVM现在可以更好地支持脚本语言和其他动态类型语言的运行。 7. **改进的数组初始化**:允许在数组声明时直接赋值,类似于集合初始化。 在下载的压缩包"jdk-7u80-...
创建数组时,需要明确指定数组的长度,此时JVM会为数组分配相应的内存空间。 **11. 二维数组** 二维数组可以看作是一维数组的数组。 **12. 操作数组的工具类-Arrays** `java.util.Arrays`类提供了许多静态方法,...
Java源代码编译成字节码后,可以在任何支持JVM的平台上运行,JVM负责将字节码转换为对应平台的机器指令。 3. **String是否为基本类型**:String不是基本类型,而是引用类型,它继承自Object类,不可被继承。 4. **...