java虚拟机规范(se8)——java虚拟机的编译(三)
3.6 接受参数
如果n个参数传给一个实例的方法,按照约定,它们被接受并放在这个新方法创建的栈帧中的局部变量表里,在局部变量表中的序号从1到n。这些参数按照它们传递过来的顺序存放。例如:
int addTwo(int i, int j) {
return i + j;
}
编译为
Method int addTwo(int,int)
iload_1 // Push value of local variable (i)
iload_2 // Push value of local variable (j)
iadd // Add; leave int result on operand stack
ireturn // Return int result
按照约定,一个实例的引用要传递到当前实例方法的局部变量0。在java编程语言中这个实例可以通过关键词this来访问。
类(static)方法不拥有实例,所以对他们来说没有必要使用局部变量0。类方法的参数是的从局部这些变量0开始的。如果addTwo是一个类方法,它的参数传递和前面类似:
static int addTwoStatic(int i, int j) {
return i + j;
}
编译为:
Method int addTwoStatic(int,int)
iload_0
iload_1
iadd
ireturn
与前面的唯一不同是参数从局部变量0开始而不是1。
3.7 调用方法
对普通实例方法调用是在运行时根据对象类型进行分派的(相当于在 C++中所说的“虚方法”)。这样的调用是通过invokevitual指令来实现的,该指令的参数是一个指向运行时常量池条码的索引,索引指向的内容给出了对象的类类型的二进制名称的内部形式、要调用的方法名称以及该方法的描述符(4.3.3)。
为了调用之前定义的实例方法addTwo,我们可以这么写:
int add12and13() {
return addTwo(12, 13);
}
编译为:
Method int add12and13()
aload_0 // Push local variable (this)
bipush // Push int constant
bipush // Push int constant
invokevirtual # // Method Example.addtwo(II)I
ireturn // Return int on top of operand stack;
// it is the int result of addTwo()
方法调用首先需要将当前实例的引用(this)压入操作数栈。这个方法的调用参数,int值12和13,紧接着也被压入操作数栈。当addTwo方法的栈帧被创建时,这些传递到这个方法的参数成为新的栈帧中局部变量中的初始值。也就是说,被调用者压入操作数栈中的this引用和两个参数,将成为被调用方法中局部变量0,1,2的初始值。
最终,addTwo方法被调用。当它返回时,它的int类型的返回值被压入调用者(add12and13方法)的栈帧的操作数栈中。然后它的返回值会被立即返回给add12and13的调用者。
add12and13的返回是由ireturn指令来实现的。ireturn指令在当前栈帧的操作数栈中拿到addTwo的int类型返回值,然后压入调用者的栈帧的操作数栈中。ireturn然后将控制权返回给调用者,将调用者的栈帧变成当前帧。对于许多的数值类型和引用类型,java虚拟机提供了对应的返回指令,同样也包括没有返回值的return指令。所有的方法调用的所有变量都使用这一组返回指令。
invokevirtual指令的操作数(在例子中是运行时常量池索引 #4)不是类实例中方法的偏移量。编译器不知道类实例的内部布局。作为替代,它产生符号引用来指向实例的方法,这些符号引用存储在运行时常量池中。那些运行时常量池中的元素在运行时被解析,从而确定实际的方法位置。其他java虚拟机指令访问类实例也采用相同的方式。
调用addTwoStatic(一个类中的addTwo的静态变体),也是类似的:
int add12and13() {
return addTwoStatic(12, 13);
}
尽管使用了一个不同的java虚拟机方法调用指令:
Method int add12and13()
bipush
bipush
invokestatic # // Method Example.addTwoStatic(II)I
ireturn
编译一个类静态方法的调用和编译一个实例方法的调用是非常相似,除了this没有被调用者传递。因此将要接收到的方法参数从局部变量0开始。invokestatic指令总是用于调用类方法。
invokespecial指令必须被用于调用实例的初始化方法。它同样用于调用父类的方法(super),以及调用private方法。例如,对于给定的类Near和Far定义如下:
class Near {
int it;
public int getItNear() {
return getIt();
}
private int getIt() {
return it;
}
} class Far extends Near {
int getItFar() {
return super.getItNear();
}
}
Near.getItNear方法(调用了一个private方法),编译为:
Method int getItNear()
aload_0
invokespecial # // Method Near.getIt()I
ireturn
Far.getItFar(调用了一个父类的方法),编译为:
Method int getItFar()
aload_0
invokespecial # // Method Near.getItNear()I
ireturn
注意,使用invokespecial指令调用方法总是传递this到被调用的方法作为它的第一个参数。照例使用局部变量0来接收。
为了调用一个方法,编译器必须产生一个方法描述符,这个方法描述符中记录了实际的参数和返回类型。编译器在方法调用时不会处理参数的类型转换问题,只是简单的将参数压入操作数栈,且不改变其类型。通常,编译器会把方法所在的对象的引用压操作数栈,接着按顺序压入方法参数。编译器生成一个invokevitual指令,它引用了一个描述符,这个描述符描述了参数和返回类型。作为方法解析时的特殊处理过程,一个用于调用java.lang.invoke.MethodHandle的invoke或者invokeExact方法的invokevirtual指令会提供一个方法描述符,这个方法描述符符合语法规则,并且在描述符中确定的类型将会被解析。
3.8 使用类实例
java虚拟机通过new指令来创建java虚拟机类实例。回想一下,在Java虚拟机级别,构造函数作为方法出现,它使用所编译器提供的名称<init>。这个特殊名字的方法被称为实例初始化方法。多个实例初始化方法,对应于多个构造器,可能存在已一个给定的类中。一旦创建了类实例并且其实例变量(包括该类及其所有超类的实例变量)已初始化为其默认值,就会调用新类实例的实例初始化方法。 例如:
Object create() {
return new Object();
}
编译为:
Method java.lang.Object create()
new # // Class java.lang.Object
dup
invokespecial # // Method java.lang.Object.<init>()V
areturn
类实例的传递和返回(通过引用类型)和数值类型非常相似,尽管引用类型有其完备的指令,例如:
int i; // An instance variable
MyObj example() {
MyObj o = new MyObj();
return silly(o);
}
MyObj silly(MyObj o) {
if (o != null) {
return o;
} else {
return o;
}
}
编译为:
Method MyObj example()
new # // Class MyObj
dup
invokespecial # // Method MyObj.<init>()V
astore_1
aload_0
aload_1
invokevirtual # // Method Example.silly(LMyObj;)LMyObj;
areturn Method MyObj silly(MyObj)
aload_1
ifnull
aload_1
areturn
aload_1
areturn
类实例的字段(实例变量)通过getfield和putfield指令来访问。假设i是一个int类型的实例变量,它的setIt
和getIt定义如下:
void setIt(int value) {
i = value;
}
int getIt() {
return i;
}
编译为:
Method void setIt(int)
aload_0
iload_1
putfield # // Field Example.i I
return Method int getIt()
aload_0
getfield # // Field Example.i I
ireturn
和方法调用指令的操作数一样,putfield和getfield指令的操作数(运行时常量池索引#4)不是类实例字段的偏移量。编译器产生这个实例字段的符号引用,存储在运行时常量池。这些运行时常量池中的元素在运行时被解析从而决定引用对象的字段的位置。
3.9 数组
java虚拟机的数组同样是对象。数组的创建和操作使用不同的指令集。newarray指令用于创建一个数值类型的数组。代码如下:
void createBuffer() {
int buffer[];
int bufsz = 100;
int value = 12;
buffer = new int[bufsz];
buffer[10] = value;
value = buffer[11];
}
可能编译成:
Method void createBuffer()
bipush // Push int constant (bufsz)
istore_2 // Store bufsz in local variable
bipush // Push int constant (value)
istore_3 // Store value in local variable
iload_2 // Push bufsz...
newarray int // ...and create new int array of that length
astore_1 // Store new array in buffer
aload_1 // Push buffer
bipush // Push int constant
iload_3 // Push value
iastore // Store value at buffer[]
aload_1 // Push buffer
bipush // Push int constant
iaload // Push value at buffer[]...
istore_3 // ...and store it in value
return
anewarray指令用于创建一维对象引用数组,例如:
void createThreadArray() {
Thread threads[];
int count = 10;
threads = new Thread[count];
threads[0] = new Thread();
}
编译为:
Method void createThreadArray()
bipush // Push int constant
istore_2 // Initialize count to that
iload_2 // Push count, used by anewarray
anewarray class # // Create new array of class Thread
astore_1 // Store new array in threads
aload_1 // Push value of threads
iconst_0 // Push int constant
new # // Create instance of class Thread
dup // Make duplicate reference...
invokespecial # // ...for Thread's constructor
// Method java.lang.Thread.<init>()V
17 aastore // Store new Thread in array at 0
18 return
anewarray指令同样可以用于创建多维数组的第一维。multianewarray指令可以用于一次创建多维数组。例如,三维数组:
int[][][] create3DArray() {
int grid[][][];
grid = new int[10][5][];
return grid;
}
这样被创建:
Method int create3DArray()[][][]
bipush // Push int (dimension one)
iconst_5 // Push int (dimension two)
multianewarray # dim # // Class [[[I, a three-dimensional
// int array; only create the
// first two dimensions
astore_1 // Store new array...
aload_1 // ...then prepare to return it
areturn
multianewarray指令的第一个操作数是运行时常量池中的索引,这个索引指向被创建的数组类型。第二个操作数表示实际创建出来的数据具有的维数。multianewarray指令可用于创建该类型的所有维度,例如create3DArray代码中的那样。注意多维数组也仅仅是一个对象,所以加载和返回使用了aload_1指令和areturn指令。更多关数组类名的信息,参考4.4.1.
所有的数组都有长度,可以通过arraylength指令来过去。
3.10 编译switch
switch语句的编译使用tableswitch和lookupswitch指令。当switch的case可以有效地表示为目标偏移表中的索引时,使用tableswitch指令。如果switch表达式的值超出了有效索引的返回,则使用default的值。例如:
int chooseNear(int i) {
switch (i) {
case 0: return 0;
case 1: return 1;
case 2: return 2;
default: return -1;
}
}
编译为:
Method int chooseNear(int)
iload_1 // Push local variable (argument i)
tableswitch to : // Valid indices are through
: // If i is , continue at
: // If i is , continue at
: // If i is , continue at
default: // Otherwise, continue at
iconst_0 // i was ; push int constant 0...
ireturn // ...and return it
iconst_1 // i was ; push int constant 1...
ireturn // ...and return it
iconst_2 // i was ; push int constant 2...
ireturn // ...and return it
iconst_m1 // otherwise push int constant -...
ireturn // ...and return it
java虚拟机的tableswtich和lookupswitch指令只操作int类型的数据。因为对于byte,char或者short的操作会内部提升到int,switch表达式中使用了这些类型将会编译为int类型。如果chooseNear方法使用short类型来写,将会使用int类型来生成相同的java虚拟机指令。使用switch时,其他的数值类型必须窄化为int类型。
如果switch的case比较分散,使用tableswitch指令来表示将会在空间上低效。可以使用lookupswitch指令来代替。lookupswitch指令将int键(case标签的值)与表中的目标偏移量配对。当lookupswitch指令运行时,switch表达式的值将会和表中的key进行比较。如果其中一个key和switch表达式的值匹配上,将从其关联的目标便宜位置继续执行。如果没有匹配上,将从default处继续执行。例如,下面的代码:
int chooseFar(int i) {
switch (i) {
case -100: return -1;
case 0: return 0;
case 100: return 1;
default: return -1;
}
}
看起来和chooseNear很像,除了lookupswitch指令:
Method int chooseFar(int)
iload_1
lookupswitch :
-:
:
:
default:
iconst_m1
ireturn
iconst_0
ireturn
iconst_1
ireturn
iconst_m1
ireturn
Java虚拟机指定lookupswitch指令的表必须按键排序,以便实现可以使用比线性扫描更高效的搜索。即便如此,lookupswitch指令必须搜索其键以进行匹配,而不是简单地执行边界检查并索引到像tableswitch这样的表。因此,当空间条件允许时,tableswitch指令可能比lookupswitch指令更高效。
3.11 操作数栈上的操作
java虚拟机拥有大量的指令可以将操作数栈上的内容当做无类型数据进行操作。这是非常有用的,因为java虚拟机依赖于其对操作数栈的灵活操作。例如:
public long nextIndex() {
return index++;
} private long index = 0;
编译为:
Method long nextIndex()
aload_0 // Push this
dup // Make a copy of it
getfield # // One of the copies of this is consumed
// pushing long field index,
// above the original this
dup2_x1 // The long on top of the operand stack is
// inserted into the operand stack below the
// original this
lconst_1 // Push long constant
ladd // The index value is incremented...
putfield # // ...and the result stored in the field
lreturn // The original value of index is on top of
// the operand stack, ready to be returned
注意,java虚拟机决不允许操作数栈的操作指令去修改或者破坏操作数栈上周一个独立的值。
java虚拟机规范(se8)——java虚拟机的编译(三)的更多相关文章
- java虚拟机规范(se8)——java虚拟机的编译(四)
3.12 抛出和处理异常 在程序中使用throw关键字来抛出异常.编译结果很简单. void cantBeZero(int i) throws TestExc { if (i == 0) { thro ...
- java虚拟机规范(se8)——java虚拟机的编译(一)
本文翻译自:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html 第三章 java虚拟机的编译 java虚拟机是设计用来支持ja ...
- java虚拟机规范(se8)——java虚拟机结构(一)
本文翻译自:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html 第二章 虚拟机结构 本文档描述了一个抽象的虚拟机规范,并不描述 ...
- java虚拟机规范(se8)——java虚拟机结构(二)
2.5 运行时数据区域 java虚拟机定义了多个用于程序执行期间的运行时数据区域.这些数据区域中一些随着java虚拟机的启动而创建,随着虚拟机的退出而销毁.其他的数据区域时和线程相关的.线程相关数据区 ...
- java虚拟机规范(se8)——java虚拟机结构(六)
2.11 指令集简介 java虚拟机指令由一个字节的操作码,接着时0个或多个操作数组成,操作码描述了执行的操作,操作数提供了操作所需的参数或者数据.许多指令没有操作数只包含一个操作码. 如果忽略异常处 ...
- java虚拟机规范(se8)——java虚拟机的编译(二)
3.3 算术运算 java虚拟机通常在操作数栈上进行算术运算(例外情况是iinc指令,它直接增加一个局部变量的值).例如下面的align2grain()方法,它的作用是将int值对齐到2的指定次幂: ...
- java虚拟机规范(se8)——java虚拟机结构(四)
2.7 对象的表示 java虚拟机并不要求对象满足任何特定的内部结构. 在Oracle的一些Java虚拟机实现中,对类实例的引用是指向句柄的指针,该句柄本身是一对指针:一个指向包含对象方法的表和指向表 ...
- java虚拟机规范(se8)——java虚拟机结构(三)
2.6. 栈帧 栈帧用于存储数据和部分结果,同样也用于执行动态链接,返回方法的值和分派异常. 当方法被调用的时候会创建一个新的栈帧.当一个方法调用结束时,它对应的栈帧就被销毁了,不管是正常调用结束还是 ...
- java虚拟机规范(se8)——java虚拟机结构(五)
2.10 异常 java虚拟机中的异常用Throwable类或者它的子类的实例来表示.抛出一个异常会导致立即非本地(an inmediate nolocal)的控制转移,从发生异常的地方跳到处理异常的 ...
随机推荐
- quotacheck - 扫描文件系统,创建,检测并修补配额文件
总览(SYNOPSIS) quotacheck [ -agucfinvdFR ] filesystem 描述(DESCRIPTION) quotacheck 察看每一个文件系统,建立当前磁盘使用情况表 ...
- 解决Minikube start卡住的方法
安装与问题 在mac上安装minikube对k8s进行学习,根据官方Quick Start brew cask install minikube 就可以完成minikube的安装 在安装前需要安装vi ...
- ARC062F AtCoDeerくんとグラフ色塗り / Painting Graphs with AtCoDeer Burnside 引理
题目传送门 https://atcoder.jp/contests/arc062/tasks/arc062_d 题解 首先对整张图做 Tarjan 点双. 对于一个点双,如果是由一条边构成的,那么很显 ...
- BZOJ3625 CF438E 小朋友与二叉树
心态崩了 不放传送门了 辣鸡bz 还是正经一点写一下题解= = 就是显然我们可以把权值写成生成函数形式g(0/1序列)来表示权值是否出现 然后f来表示总的方案数 可以列出 分别枚举左右子树和空树的情况 ...
- CentOS7.5常用命令
常用命令: 关机shutdown -h now 参数:重启-r定时-r 23:59 分-r 10 查源软件yum list |grep telnet参数:安装install 服务启动systemctl ...
- @PostMapping
@PostMapping映射一个POST请求 Spring MVC新特性 提供了对Restful风格的支持 @GetMapping,处理get请求 @PostMapping,处理post请求 @Put ...
- Vue.js----date与时间戳的转换(unixTime)Moment.js让日期处理变得更简单
当前日期格式化 let curTime = moment().format('YYYY-MM-DD HH:mm:ss') console.log('当前日期时间curTime:' + curTime) ...
- Python 中内建属性 __getattribute__
参考自:https://blog.csdn.net/yitiaodashu/article/details/78974596 __getattribute__是属性访问拦截器,就是当这个类的属性被访问 ...
- temp = yield i 这句话的意思?
def test(): i = 0 while i < 5: temp = yield i # print(temp) i+=1 t = test() print(t.__next__()) p ...
- Java Web学习总结(8)JSP(二)
一,JSP中的九个内置对象 名称 类型 描述 out javax.servlet.jsp.JspWriter 用于页面输出 request javax.servlet.http.HttpServlet ...