0%

JVM操作数栈与局部变量表

序言

知人者智,自知者明。

最近在看Java污点分析,逃不开对象.方法调用形式,做个记录,随时更新。

温故

动态语言与静态语言

对类型的检查,在编译期就是静态语言,在运行期就是动态语言;

静态语言是判断变量自身的类型信息,动态语言是判断变量值的类型信息

动态语言中,变量没有类型信息,变量值才有类型信息。

Java: String info = “atguigu”;//info = atguigu;会报错 类型不匹配

JS:var name = 123;var name = “sun”;都可以 var就是一个变量的泛化。

方法调用

方法的绑定机制:符号引用转换为调用方法

静态链接:

当一个字节码装进JVM内部的时候,如果被调用的方法在编译器可知,且运行时保持不变。静态链接

动态链接:

当被调用的方法无法在编译期确定下来,只有在程序运行起来,才能确定。动态链接

动态链接:将符号引用直接变成直接引用

在.java->.class过程中,所有的变量和方法引用都会作为符号引用(#5)保存在class文件的常量池中

比如描述一个方法调用了另外的其他方法,就是通过常量池中指向方法的符号引用来表示的。

动态链接的用途就是去运行时常量池(在方法区里面)里面把这些符号引用转换为调用方法的直接引用。

重点!!!

  1. JVM是基于栈的计算模型
  2. 在解析过程中,每当为Java方法分配栈帧时
    • 执行每条执行之前,JVM要求该指令的操作数已被压入操作数栈中
    • 在执行指令时,JVM会将该指令所需要的操作数弹出,并将该指令的结果重新压入栈中

栈帧

栈帧是线程私有的,每一个方法对应一个栈帧。

四个主要组成成分:操作数栈,局部变量表,动态链接,方法返回地址。(还有一些附加信息,无所谓了)

重点说两个:局部变量表和操作数栈

局部变量表

当一个方法被调用时,会使用局部变量表来存储参数值和方法内部的方法局部变量。

他是一个数组,用来存放方法参数和定义在方法内部的方法变量。

可以是引用类型(String等),可以是对象引用,可以是返回地址。

线程私有数据。

如果当前方法是对象实例的成员方法(没有被static修饰),那局部变量表[0]存放的是this的引用。

参数分配完毕后,再根据方法体内部定义的局部变量顺序和作用域分配其余的变量槽。

这里基本数据类型(double long除外)+ 引用类型(String) 都是一个slot ;long double 都是两个slot

操作数栈

也常称为操作栈,它是一个后入先出栈(LIFO)。

保存程序执行过程中的临时结果,保存中间变量。

当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中。

小例子

image-20210317174904301

Javap -v Demo:

image-20210317175016279

流程:

操作数栈长度为2,局部变量表长度为4,参数大小为1 pc寄存器地址值:JVM字节码指令

bipush 10从常量池里取出10 放到操作数栈顶

istore_1 将操作数栈中栈顶元素给到局部变量表的1号位置 10(0号位置给了this)

bipush 20从常量池里取出20 放到操作数栈顶

istore_2 将操作数栈中栈顶元素给到局部变量表的2号位置 20

iload1从局部变量表中取出1号元素 10 放到操作数栈栈顶

Iload2从局部变量表中取出2号元素 20 放到操作数栈栈顶

iadd 将当前操作数栈的栈顶两个元素求和 结果放在栈顶

return结束 返回void

再看一个方法调用的:

image-20210317203643349

javap -v Main

image-20210317203803521

先看创建一个对象:

image-20210317205038396

  1. 执行new指令时,JVM将指向一块已分配的但未初始化的内存引用压入操作数栈。此时A类在方法区,堆中创建一个A对象的空间,大小可以确定,一些值会进行默认初始化。
  2. 接下来dup就是将操作数栈栈顶的值(对象堆空间的引用值)复制一份,压入栈顶
  3. invokespecial指令将要以这个引用为调用者,调用其构造器
    • 该指令将消耗掉操作数栈上的元素,作为它的调用者和参数
  4. 因此,在这之前利用dup指令复制一份new指令的结果,并用来调用构造器

这里 invokespecial指令结束之后,返回对象的引用为void,栈中元素只有一个引用(之前复制的那个)

接着aload_1,将局部变量表里1号位置的元素(参数args)取出,放在栈顶

接下来invokevitural,调用虚方法method1(args),这里javap -v A

image-20210317223337010

在method1内部,操作数栈大小为1,局部变量表为2(0号位置本类this)

首先aload_1将参数args放入操作数栈顶

接着areturn,将栈顶元素返回,是引用类型(String)

这时候回到之前main方法的操作数栈,返回的param放在了栈顶。

接着astore_2将返回值param放在了局部变量表的2号位置

此时操作数栈为空

接着继续new出来一个B对象,与上个A对象同理

依旧还是dup之后invokevirtual调用构造方法

之后aload_2将局部变量表里面的2号元素(也就是之前的param拿出来,也就是cmd)放到栈顶

invokevirtual去执行method2(cmd)方法

javap -v B看一下

image-20210317224622996

在method2里面 实例化了C对象 嵌套起来了

在method2里面,操作数栈大小为2,局部变量表大小为2

new-dup-init组合拳生成C对象 接下来调用method3(param)

javap -v C

image-20210317225028439

直接返回param

那么对于method3返回的结果,被B的method2进行了11:areturn返回

返回到哪里 ? 返回到了调用method2的main方法内部23:areturn地方

作为main函数的最终结果返回。

Java创建对象的过程

这篇讲得非常好,来总结一下3种主流方法。

new

1
Object obj = new Object();

最常用的创建方式。

对应字节码:

image-20210322093031418

在Java中,认为创建一个对象就是调用其构造方法,对于new Object()方法其实就是调用了Object类的无参构造方法,但是在字节码中,对象的创建和调用构造方法是分开的

在上图字节码中,

new指令其实就是在堆中创建一个对象,并把对象的引用压入栈(指代操作数栈,后面同理)中。

dup指令会复制栈上最后的一个元素,然后将这个复制压入栈顶。为什么会有两个呢?

因为接下来的invokespecial指令会消耗掉操作数栈顶部的一个对象引用,作为传递给构造器的this参数,我们希望在invokespecial调用之后,在操作数栈顶还维持一个指向新建对象的引用,那么就得先在invokespecial之前先复制一份引用,也就是dup的原因。

newInstance方法来创建

这里,newInstance方法是指Class类中的方法,newInstance方法会调用无参的构造方法创建对象。

两种模板:

1
2
3
User user = (User)Class.forName("com.sec.User").newInstance();

User user = User.class.newInstance();

ldc指令代表将常量池里的引用推入栈顶

forName是静态方法 需要invokestatic

newInstance 是虚方法,需要invokevirtual

使用反射API来创建对象

例子:

1
2
Constructor<User> cons = User.class.getConstructor();
User user = cons.newInstance();

备忘

java中没有被static native修饰的方法就是是虚方法

感觉invokespecial init是消耗一个对象内存 不会返回任何东西(或者说返回void)

image-20210317231200462

注意最后这个pop弹栈操作,很关键。

首先new-dup出现两个ref在栈中 那么invokespecial会消耗一个对象引用 还会剩一个

最后的pop也就是将最后一个(栈顶)弹出来,操作数栈排空。

invokestatic 是不消耗任何对象引用,静态方法是不需要对象.的格式去调用,直接类.方法就好了

image-20210317231143953

invokevirtual是获取通常为this和参数,然后一起进行方法调用。

image-20210317232352641 image-20210317232507679