JAVA运行栈

栈每个线程私有的内存空间由多个栈帧组成,每个栈帧代表一个方法无法满足内存,需求的时候,会报栈溢出异常:StackOverflowError
它分为四个部份:局部变量表,操作栈,动态连接,返回地址。

局部变量表
局部变量表用于存储方法参数和方法内部定义,
最小单位是Slot(虚拟机中没有明确规范该内存空间大小)
64位的数据会存放两个Slot
对象方法的局部变量表 第0位Slot通常为this
为节约栈帧空间,Slot可重用,不在同一个作用域的局部变量可以公用一个Slot
 优点:解决栈帧空间
 缺点:会对垃圾回收造成影响
局部变量必须要初始化,未初始化的变量只能存,无法取

上右图:
Stack:操作栈最大大小
Locals是本地变量的slot个数,但是并不代表是stack宽度一致,本地变量是在这个方法生命周期内,局部变量最多的时候,需要多大的宽度来存放数据(double、long会占用两个slot)。
Args_size代表的是入参的个数,不再是slot的个数,也就是传入一个long,也只会记录1。

Localvariableable:
Start,length代码作用域,slot代码变量占用slot的下标最大是4,也就是5个,name是变量名字。

作为GC ROOT的局部变量表,并不是变量超过作用域而马上关联的对象就可以进行GC 回收的,而是当此变量在局部变量表中失效,或者被另外的变量覆盖。

操作栈:

操作栈是一个后出先入的LIFO结构
32位数据占栈容量为1,64位数据占栈容量为2
操作栈的元素类型必须和字节码中指令的序列严格匹配,在类加载的数据流分析校验阶段会对此进行校验
比如栈顶的元素为long类型,字节码字节码指令用的是iadd,这肯定就是不符合字节码语义

基于寄存器的指令集
    优点:效率高
    缺点:可移植性不高,遇到不同的操作系统,用户代码可能不兼

基于栈的指令集
    优点:可移植性高,用户可以忽略各种操作系统的硬件限制,避免了直接操作寄存器
    缺点:由于是对内存的操作(就算将常用的数据映射到寄存器),而且还要入栈出栈,所以效率较低


概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数复制传递,Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。
例:主方法调用子方法要向子方法传参数(形参)。而这形参就是存在主方法的局部变量表中,如果 子方法中没对此部份参数进行修改操作,则可共享主方法空间,如果改变,则在子方法自己的局部变量表中创建
 

动态链接:

每个栈帧都包含一个指向运行时常量池中该方法的直接引用,持有这个引用是为了支持方法调用过程中的动态连接。
字节码在进行方法调用的时候,会将该符号引用作为参数(如图)。

常见的动态链接命令:
1.invokestatic:调用静态方法
2.invokespecial:调用实例构造器<init>方法,私有方法,父类方法
       被invokestatic和invokespecial调用的方法,都是在类加载的时候,虚拟机将方法的符号引用解析成了直接引用
3.invokevritual:调用所有的虚方法及少部份非虚方法

      虚方法大致概括:可调用对象中的成员函数 
      非虚方法概括:编译器可知,运行期不可变(被final修饰的成员函数,因为这种方法,无法被重写)
4.invokeinterface:调用接口方法,会在运行时确定此接口的方法
5.invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后在执行该方法,该条指令的分派逻辑是由用户引导的方法决定。例如,JAVA反射方法

静态分派与动态分派

      Java对象具备面向对象的三大基本特征:继承,封装,多态.而触成三大特征的核心就是 静态分派(重载)及动态分派(重写).

左图是典型的重载的例子。从Main函数的字节CODE可以看出来,重载是编译器利用参数的静态类型而不是实际类型作为判定依据的。这种依赖静态类型来定位方法执行的版本的分派动作称为静态分派。

右图,运行MAIN函数他会调用哪个方法呢?’男’ 可以属于char,也可以属于 Charecter,Object当然也可以传成int,long。。。。所以,静态分派是在编译期确定方法的调用版本,但很多时候,方法调用的版本并不是唯一性的,这种时候就会选择一个最为合适的版本进行调用。

动态分派的典型应用就是方法的重写
invokevirtual指令的运行时解析过程:
     1.找到操作栈栈顶的第一个元素所指对象的实际类型,然后记做类型C
     2.如果在类型C中找到与常量中描述符和简单名称都相符的方法,则进行权限验证,如果通过,则返回该方法的直接引
    3.找不到的话,则按照继承关系依次找下去

由于第一步就是查找对象的实际类型,所以invokevirtual指令可以将常量池中的方法的符号引用根据对象的实际类型解析到不同的直接引用上,这种根据运行期对象实际类型来确定方法的执行版本的分派过程称为动态分派

 

方法返回地址:

当一个方法开始执行后,只有两种方式可以退出这个方法。

第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。调用者的PC计数器的值可以作为返回地址,栈帧中方法返回地址会保存这个计数器值。

另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

 

本地方法栈

上图所示,该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。图中的本地方法栈显示为 一个连续的内存空间。假设这是一个C语言栈,期间有两个C函数,他们都以包围在虚线中的灰色块表示。第一个C函数被第二个Java方法当做本地方法调用, 而这个C函数又调用了第二个C函数。之后第二个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过 本地方法接口回调了一个Java方法(第三个Java方法)。最终这个Java方法又调用了一个Java方法。

 

当某个线程调用一个本地方法时,他就进入了一个全新的并且不再受虚拟机限制的世界 ,本地方法可以通过本地方法接口 来访问虚拟机得运行时数据区,但不止于此,他还可以做任何他想做的事情。比如,他甚至可以直接使用本地处理器中的寄存器,或者直接从本地内存的堆中分配任意数量的内存等等。总之,他和虚拟机拥有同样的权限(或者说能力)

       如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那个他的本地方法栈就是C栈。我们知道,当C程序调用一个C函数时,其栈操作都是确定的。传递 给该函数的参数已某个确定的顺序压入栈,他的返回值也以确定的方式传回调用者。同样,这就是改虚拟机实现中本地方法栈的行为。

     很可能本地方法接口需要回调Java虚拟机中的Java方法(这也是由设计者决定的,设计者会提供方法),在这种情形下,该线程会保存本地方法栈的状态并进入到Java栈。

     就像其他运行时内存区一样,本地方法栈占用的内存区也不必是固定大小的,他可以根据需要动态扩展或者收缩。某些是实现也允许用户或者程序员指定该内存区的初始大小以及最大,最小值。(和JAVA栈一样都在栈区可随时申请内存空间 ,受限于XSS参数

程序计数器

       内存模型中,唯一一个不会内存溢出的一块内存,可以看做当前线程所执行的字节码的行号指令器

       如果线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的位置

       如果线程执行的是一个Native方法,这个计数器记录则为空,在本地方法栈(例C栈)会使用他们自己的计数器。

评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值