在群里看到有人问了这样的一个问题:
a+b操作数栈过程,方法返回地址什么时候回收,程序计数器什么时候为空(开始想的很简单,后面仔细思索了一下发现不对)
好吧,其实是三个小问题,我们先来看第一个
a+b操作数栈过程,首先要知道什么是操作数栈
JVM是软件模拟的虚拟机,基于栈运行
虚拟机栈中又有很多栈帧,栈帧又被分成了其他区域。理解虚拟机栈的核心就是理解栈帧中的这几个区域
1、局部变量表
2、操作数栈
3、动态链接
4、返回地址
5、附加信息
操作数栈是一个基于数据结构栈实现的(废话),存储操作数,什么是操作数呢,首先局部变量表是顾名思义是存储变量的,而操作数栈则是将局部变量进行计算的地方,可以认为,包含但不是全部,加减乘除就在这个地方进行(比如字节码指令 bipush istore 等),所以局部变量表 + 操作数栈才能完成这个操作。
那么a+b到底是怎么做的呢,我们可以通过查看一个类的字节码指令来看一下 (插件为jclasslib)
那么这三行指令做了什么,为了方便理解,我用伪代码的形式来展示可能更好说明
case Bytecodes.ILOAD_1: { // 取出局部变量表中的数据 StackValue value = frame.getLocals().get(1); // 压入操作数栈 frame.getStack().push(value); break; }
iload_2同上,
case Bytecodes.ILOAD_2: { // 取出局部变量表中的数据 StackValue value = frame.getLocals().get(2); // 压入操作数栈 frame.getStack().push(value); break; }
iadd则是
case Bytecodes.IADD: { // 取出操作数 StackValue value1 = frame.getStack().pop(); StackValue value2 = frame.getStack().pop(); // 检查操作数类型 ... // 进行int类型的加法运算 int ret = (int)value1.getData() + (int)value2.getData(); // 压入操作数栈 frame.getStack().push(new StackValue(BasicType.T_INT, ret)); break; }
通过上述代码,就会发现其实整个过程就很容易理解,其实整个过程就是从局部变量表中取出第一压入栈,第二个元素压入栈,然后执行iadd是,从栈中取出两个操作数进行相加后再次压入操作数栈,而后的istore则是如下
case Bytecodes.ISTORE: { // 获取操作数 int index = code.getU1(); // 取出数据 StackValue values = frame.getStack().pop(); // 存入局部变量表 frame.getLocals().add(index, values); break; }
帮助大家理解。
第一个问题就算结束了,第二个问题方法返回地址什么时候回收
当时脑子一热,说的「方法返回地址回收这个问法是有问题,可能想说的是栈帧什么时候回收,方法运行是依赖栈帧的,返回地址本质是栈帧的弹出,并将栈帧中记录的下一行程序计数器位置告知线程,栈是线程私有的,所以会在栈帧弹出后进行回收」,但是仔细来说还是有些问题,
方法返回地址是什么?其实想要理解可以这么想,假如我们有个main方法,有一个init方法,我们想要调用init方法,然后init()方法执行完了,应该返回了,这时候该返回到哪,是不是需要一个返回地址,如果你听说过保护现场和恢复现场其实就不难理解了,其实返回地址就是可以认为是保护现场,而恢复现场便是方法返回后,需要继续往下执行之前进行现场的恢复,而现场恢复后,保护现场的地址这时候就没什么用了,就可以抛弃了(被回收),搜一说是栈帧弹出后进行回收好像也没啥问题。那么为了验证这个严谨性,来看下下面的小实验
会发现,执行到1w+的时候出现了栈溢出(这里想到了一个面试问题,jvm中,一个栈帧的大小大概是多少,看到这里会算了么?),好,那么我们控制住在1w,然后它经历多个循环
进行了2w次的方法调用,如果说是在gc时才回收返回地址应该会有gc信息, 最终会发现,没有触发gc,也没有触发栈溢出,那么可以认为其实在栈帧弹出去的时候就被回收了,那么对应的返回地址也是被回收了。
好了,到第三个问题,程序计数器什么时候为空,当时拍脑子一项,程序计数器不是线程私有的么,那么没有线程就没有程序计数器,「没有程序运行的时候」随口一答,但是这样好像也没问题,但是这个时候貌似是程序计数器都没有,23333,本着刨根问底的想法,程序计数器是什么?答案:字节码指令前面的index
那么我们还是去看下
发现,哪怕是一个空的类中还是会有默认的构造方法,而默认的构造方法也会有对应的字节码指令前面的index,好家伙,我想这个问题应该是有问题的时候,徒然想到,native方法很特殊,因为native方法是jni方式调用的,它不需要程序计数器,没有方法体,来试一试
卧槽,这方法真的就是没有程序计数器哦!!!
没有方法体的多了,我一个举一反三,但是实际的情况是,如果接口和抽象类的抽象方法没有被实现的时候,是会报错的,但是native方法是jni远程调用了c++的,而不会出错,所以,认真点说其实还真只有在执行native方法时程序计数器是为空的
ok,本次博客结束。