[TOC]
前置知识
阅读本文需要对以下知识有所了解:
* 栈 * 汇编 * Java 基础 * 逆波兰表达式(有学过的同学阅读本文毫无障碍)
引子
基于寄存器的设计模式
就我们所熟知的x86或arm指令集来说,其对数据的操作都是基于寄存器。例如,要对两个数执行加法操作则需要将这两个数分别送入两个寄存器再执行加法操作,这也符合我们对于编程语言认知,更加易于理解。
基于栈的设计模式
基于栈的设计模式则是将数据存放在栈中,在需要使用的时候将栈顶的数据出栈,并执行相应的操作。
举例来说,在JVM中 执行 a = b + c 的字节码执行过程中操作数栈以及局部变量表的变化如下图所示。
局部变量表中存储着a、b、c 三个局部变量,首先将b和c分别入栈
将栈顶的两个数出栈执行加法操作,并将结果保存至栈顶,之后将栈顶的数出栈赋值给a
一个简单的例子
在上一节中我们了解了栈与局部变量表是如何配合完成一次加法操作的,这一节我们将对局部变量表进行深入的研究。
如何查看局部变量表?
我们可以通过反编译class文件的方式查看局部变量表,不过在这里更加推荐使用IDEA的jclasslib插件(直接搜就有)查看字节码,因为其设计更加人性化,更加友好。
实例方法中的局部变量表
我们知道在实例方法中我们可以直接访问实例的成员变量或函数,而不需要通过this来引用,这是如何实现的? 像这种问题直接动手写个测试类,反编译一下结果自然就清晰了。 首先来一个简单的类
public class T {
private int a = 0;
public void add(int b,int c){
a = b + c;
}
}
其次,将该类选中,然后在view中选中showbytecode with jclasslib
效果如下:
选择我们关注的Methods中add方法,注意观察图中高亮的部分
原来,JVM在编译代码的时候,偷偷在局部变量表中添加了一个this引用(很明显this保存的实例的引用),这也是我们为什么可以在方法中访问实例中的成员变量的原因,证明如下
图中节码的简要解释如下: 0)aload_0 将this的引用入栈 (aload_0即将局部变量表中索引为0的引用压到操作数栈中) 1)iload_1 将参数b入栈 (将局部变量中的索引为1的整数压到操作数栈中) 2)iload_2 将参数c入栈
此时栈的内容有(0为栈顶) 0.c 1.b 2.this
3)iadd 将栈顶的两个数相加,并将结果保存至栈顶,此时栈的内容为 0.b+c 1.this
4). putfield 将栈顶的两个值出栈,第一个值(b+c)赋值给第二个值(this)的对应的成员变量(是的,没错即使是赋值也要执行两次出栈操作) putfield的说明如下(注意图中的高亮部分): 地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.putfield
结论
基于栈的指令集系统可以很方便的做到平台无关性(x86、arm),但也降低了性能,这也是为啥Java性能比C低原因。(操作寄存器快,还是操作栈快?哈哈哈哈哈哈哈哈哈哈哈)