Java教程

jvm知识小课堂(一)

本文主要是介绍jvm知识小课堂(一),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

在群里看到有人问了这样的一个问题:

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,本次博客结束。

 

 

这篇关于jvm知识小课堂(一)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!