java进出栈_JVM函数调用:Java出入栈

本文详细探讨了JVM中的函数调用过程,包括Java栈的局部变量表、索引复用、垃圾回收以及栈数据区的虚拟机栈和本地方法栈。局部变量表在函数调用结束后会随栈帧一同销毁,而索引复用可以节省栈空间。此外,文章还介绍了如何影响垃圾回收,以及栈上分配对性能的影响。
摘要由CSDN通过智能技术生成

JVM函数调用:Java出入栈

JVM函数调用:Java出入栈

目录

局部变量表

索引复用

垃圾回收

栈数据区

栈上分配

线程作为系统运算调度的最小单位,在JVM中线程的行为体现就是函数调用,函数调用中数据的传递就是通过Java栈,Java栈顾名思义有着和数据结构中“栈”相似的属性,后进先出,出栈入栈,栈中保存的是栈帧,当JVM发生函数调用时,就会有一个栈帧被压入Java栈,当函数调用结束后,再从栈中弹出栈帧,当前正在执行的函数其对应的栈帧位于栈顶处,且保存有当前函数的局部变量表和栈数据区(保存一些中间结果等数据)。在函数返回,也就是有栈帧要从Java栈中弹出时,正常的情况是函数通过return返回,此时栈帧正常弹出,如果函数调用出现问题无法正常返回,则抛出异常,举个例子:我们每一次函数调用时都会对Java栈进行入栈操作,栈空间是一定的,随着不断入栈操作,例如递归函数调用,栈空间变得越来越小,最后达到最大可用深度时,就会抛出栈溢出异常,所以有时我们递归函数调用过程中出现的“StackOverflowError”,就是栈空间因为某些原因被占满了导致的。

局部变量表

函数对应的栈帧中有一个局部变量表,里面保存了调用函数的局部变量,参数等,这些参数和变量是跟着函数走的,只在当前函数调用中有效,函数调用结束后,栈帧就会弹出Java栈,局部变量表也就随之被销毁。来看一个简单的例子:

6575dc9ecee636c3444f1238782bd98d.png

draw()方法中有3个入参和3个局部变量,它们都是int数据类型,一个占用24个字节内存空间,在32位操作系统中每4个字节为一个字,所以在局部变量表中,函数draw()的局部变量一共占6个字。

93119077c7ec5226834d664738192327.png

从上图可以看到,Maximum local variables表示最大局部变量表大小为6个字。

索引复用

详细点开draw()函数中的局部变量表,可以看到一些更详细的属性,例如每一个变量的索引index,变量名name和数据类型descriptor等。

8226b99f229feaefa0e11d2859302ad7.png

局部变量表中的索引是可以进行复用的,以次来节省Java栈空间,具体的复用方式是这样,假设我们定义了一个局部变量i,当程序运行到i离开其作用域后,再定义的其他变量可能就会复用i变量的索引,具体看个例子:

package cell;

public class Cell {

private static boolean tag = true;

public void example1() {

int i = 9527;

System.out.println("i = " + i);

int s = 9527;

}

public void example2() {

if (tag) {

int i = 9527;

System.out.println("i = " + i);

}

int s = 9527;

}

}

程序中有两个方法,example1()中定义的变量i和变量s作用域都是一样的,直到example1()方法的结束,所以两者的索引没有办法复用。example2()方法中,局部变量i在第16行后就离开了作用域范围,那么后续定义的局部变量s可以复用它的索引,从局部变量表里看,的确是这样的:

1d07fa27e7294e7d42948ebbc035f0d0.png

9a79b17406a40cc2ddd8390343717c6a.png

可以看到,方法example1()中局部变量i和s的所有不同,分别是1和2,而到了方法example2()中,局部变量i和s的所有都一样是1。

垃圾回收

索引复用有时也会对JVM的垃圾回收产生影响,例如某一个变量i,虽然离开了自己的作用域,但是它之前指向了某一对象,使得变量i仍然存在于局部变量表中,导致变量i指向的对象无法被回收如果变量i指向了某一对象,i离开了作用域后,其索引被后面定义的局部变量所服用了,那么变量i也会被销毁掉,其指向的对象也就能被正常GC,看一个例子:

public void example1() {

byte[] buffer = new byte[2*1024*1024];

System.gc();

}

public void example2() {

byte[] buffer = new byte[2*1024*1024];

buffer = null;

System.gc();

}

public void example3() {

boolean tag = true;

if (tag) {

byte[] buffer = new byte[2*1024*1024];

}

System.gc();

}

public void example4() {

boolean tag = true;

if (tag) {

byte[] buffer = new byte[2*1024*1024];

}

int i = 9527;

System.gc();

}

上面的程序中,4个方法,第一个example1()中,首先定义byte数组,申请2MB大小的空间,之后立刻进行GC,回收该数组对象,但是注意,此时因为局部变量buffer强引用了这块内存空间,所以gc暂时无法对其进行回收。到example2(),在为byte数组申请空间,并用局部变量buffer引用这块区域后,显示将buffer置为null,这样buffer就不会强引用这块内存空间,之后再进行GC就可以成功回收。example3()中,我们让局部变量buffer作用在if语句中,当if语句结束,buffer离开了它的作用域后,我们再进行GC,此时因为变量buffer还存在于局部变量表中,所以它的引用还是有效的,GC无法对其引用的空间进行回收,解决的办法来看example4()方法。在example4()中,buffer离开其作用域后,我们再声明另一个局部变量i,让它来复用变量buffer的索引,,这样buffer变量就真正被替代销毁了,并且没有其他变量引用这片内存区域,之后再进行GC,可以成功进行回收。

栈数据区

Java栈中局部变量表上面简单总结了一下,除了局部便变量表,还有栈数据区,分为虚拟机栈和本地方法栈,虚拟机栈存放就是栈帧,Java方法运行时所需要的数据,本地方法栈存放的是JVM调用的本地方法。

栈上分配

上面说的栈数据区中,虚拟机栈和本地方法栈都是线程独占的,对于一些线程私有的,不能被其他线程访问的对象,JVM可以把它们分配在栈上,这样当函数调用结束后就会自行出栈销毁,不需要GC来进行回收,好处就是提高了性能。

private static People p;

public static void initPeople() {

p = new People();

p.name = "Alex";

p.age = 20;

}

上面代码中People类对象p是一个逃逸的对象,因为它是类的成员变量,可能会被其他线程访问到,所以虚拟机会把它分配到堆上,而不是线程私有的栈数据区中。如果我们把它改成非逃逸的对象:

public static void initPeople() {

People p = new People();

p.name = "Alex";

p.age = 20;

}

把对象p改成局部变量的方式,且initPeople()方法也没有将其返回出去,那么该对象p没有发生逃逸,虚拟机就会将它分配到栈数据区中。

JVM函数调用:Java出入栈相关教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值