函数的调用过程与出入栈

摘要:
函数调用过程线程的基本行为是函数调用,每个函数调用的数据都通过Java堆栈传递。Java堆栈与数据结构上的堆栈具有类似的含义。它是一种先入后出的数据结构。它只支持两种操作:push和push。每个函数调用都会有一个相应的堆栈帧被推入Java堆栈。在每个函数调用结束时,将从Java堆栈中弹出一个堆栈帧。局部变量表中的变量仅在当前函数调用中有效。当函数调用结束时,局部变量表也将随着函数堆栈框架的破坏而被破坏。
函数调用的过程

线程执行的基本行为是函数调用,每次函数调用的数据都是通过Java栈传递的。Java栈与数据结构上的栈有类似的含义,它是一块先进后出的数据结构,只支持入栈和出栈两种操作。Java栈的主要内容是栈帧。每次函数调用都会有一个对应的栈帧被压入Java栈,每次函数调用结束(无论是正常返回或者抛出异常),都会有一个栈帧被弹出Java栈。

如图所示,函数1中调用函数2,函数2中调用函数3,函数3调用函数4。函数1被调用,栈帧1入栈;函数2被调用,栈帧2入栈;函数3被调用,栈帧3入栈;函数4被调用,栈帧4入栈;函数4调用完毕,栈帧4出栈;函数3调用完毕,栈帧3出栈,一直到函数1出栈。

1565679392532

每次函数调用都会生成对应的栈帧,从而占用一定的内存。由于HotSpot虚拟机并不区分本地方法栈和Java栈,栈内存大小由-Xss参数设定。关于本地方法栈和Java栈,在Java虚拟机规范中定义了两种异常。

  • 线程的请求栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
public class StackDeepTest {

    private static int count = 0;

    public static void recursionCall() {
        count ++;
        recursionCall();
    }

    public static void main(String[] args) {
        try {
            recursionCall();		
        }catch(Throwable ex) {
            System.out.println("调用了:"+count);
            ex.printStackTrace();
        }
    }

}

使用-Xss128k参数,结果为“调用了:1089”,当使用-Xss256k参数,结果为“调用了:3546”。说明参数改大之后调用的次数能够明显增加。

  • 虚拟机在扩展栈时无法申请到足够的内存时,将抛出OutOfMemoryError异常
public class StackOOMTest {

    public static void main(String[] args) {
        while(true) {
            Thread th = new Thread(()->{
                while(true) {}

            });

            th.start();
        }	
    }
}

没有演示出来,执行有风险,导致了电脑卡死。在《深入理解Java虚拟机》中,给出了OOM异常。

栈帧的数据内容

方法调用在JVM中转换成的是字节码执行,字节码指令执行的数据结构就是栈帧(stack frame)。也就是在虚拟机栈中的栈元素。虚拟机会为每个方法分配一个栈帧,因为虚拟机栈是LIFO(后进先出)的,所以当前线程正在活动的栈帧,也就是栈顶的栈帧,JVM规范中称之为“CurrentFrame”,这个当前栈帧对应的方法就是“CurrentMethod”。字节码的执行操作,指的就是对当前栈帧数据结构进行的操作。

栈帧的数据结构主要分为四个部分:局部变量表、操作数栈、动态链接以及方法返回地址(包括正常调用和异常调用的完成结果)

局部变量表

局部变量表用于保存函数参数以及局部变量。局部变量表中的变量只在当前的函数调用中有效,当函数调用结束,随着函数栈帧的销毁,局部变量表也会随之销毁。局部变量表和操作数栈的容量在编译期就确定了,并通过相关方法的code属性保存及提供给栈帧使用。

栈帧中的局部变量标准的槽位是可以复用的,如果一个局部变量表过了其作用域,那么在其作用域之后声明的变量就很有可能复用过期的局部变量表的槽位,达到节省资源的目的。局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或者间接引用的对象都不会被垃圾回收。

public class StackVarTest {

	private static final int CAPACITY = 6*1024*1024;
	
	public static void test1() {
		byte[] a = new byte[CAPACITY];
		System.gc();
	}
	
	public static void test2() {
		byte[] a = new byte[CAPACITY];
		a=null;
		System.gc();
	}
	
	public static void test3() {
		{
			byte[] a = new byte[CAPACITY];			
		}
		System.gc();
	}
	
	public static void test4() {
		{
			byte[] a = new byte[CAPACITY];
		}
		int b = 10;
		System.gc();
	}
	
	public static void test5() {
		test1();
		System.gc();
	}
	
	public static void main(String[] args) {
		test3();
		System.err.println("----------");
		test4();
	}
}

使用参数-XX:+PrintGC,结果显示如下。可以看到test3没有GC,test4有GC。test3在进行垃圾回收前,虽然a已经离开了作用域,但是变量a依然存在局部变量表中,并且也指向这块byte的数组,故数组不能被垃圾回收。test4声明的变量b复用了a的槽位,导致a不存在局部变量表,故可以被垃圾回收

[GC (System.gc())  8140K->6784K(125952K), 0.0024535 secs]
[Full GC (System.gc())  6784K->6672K(125952K), 0.0055990 secs]
----------
[GC (System.gc())  13482K->6704K(125952K), 0.0005817 secs]
[Full GC (System.gc())  6704K->528K(125952K), 0.0282544 secs]

操作数栈

和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。不同于程序计数器,Java虚拟机没有寄存器,程序计数器也无法被程序指令直接访问。Java虚拟机的指令是从操作数栈中而不是从寄存器中取得操作数的,因此它的运行方式是基于栈的而不是基于寄存器的。虽然指令也可以从其他地方取得操作数,比如从字节码流中跟随在操作码(代表指令的字节)之后的字节中或从常量池中,但是主要还是从操作数栈中获得操作数。

栈帧在刚创建的时候,操作数栈是空的。Java虚拟机提供一些字节码指令从局部变量表或者对象实例的字段中复制常量或者变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据以及把操作结果重新入栈。在调用方法时,操作数栈也用来准备调用方法的参数以及接受方法的返回结果。

动态链接

每个栈帧的内部都包含一个指向当前方法所在类型的运行时常量池的引用,以便对当前的方法的代码实现动态链接。在class文件里,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用来表示,动态链接的作用就是将这些以符号引用表示的方法转换为实际方法的直接引用。类加载的过程中将要解析尚未被解析的符号引用,并且将对变量的访问转化访问这些变量的存储。加载阶段或第一次使用时转化为直接引用的(将变量的访问转化为访问这些变量的存储结构所在的运行时内存位置)就叫做静态解析。JVM的动态链接还支持运行期转化为直接引用。也可以叫做Late Binding,晚期绑定。

方法返回地址

方法正常返回会把返回值压入调用者的栈帧的操作数栈,PC计数器的值就会调整到方法调用指令后面的一条指令。这样使得当前的栈帧能够和调用者连接起来,并且让调用者的栈帧的操作数栈继续往下执行。

方法的异常调用完成,主要是JVM跑出的异常,如果异常没有被捕获主,或者遇到athrow字节码指令显示抛出,那么就一定不会有返回值返回给调用者。

栈上分配

栈上分配是Java虚拟机提供的一项优化技术,它的基本思想是:对那些线程私有的对象(指不能被其他线程访问到的对象),可以将他们打散分配到栈上,而不是分配在堆上。其好处是可以在函数调用完毕自行销毁,而不需要垃圾回收期介入,从而提高了系统的性能。

栈上分配对象的技术基础是进行逃逸分析。逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。p1是成员变量,该字段可能被任何线程访问,属于逃逸对象;p2是局部变量,并且没有被返回,因此它并未发生逃逸,对这种情况,对象就可能被分配在栈上,而不是堆上。

public class StackObjectTest {

	static class Person{
		public String name;
		public int age;
	}
	
	private Person p1;
	
	/**
	 * 逃逸对象
	 */
	public void alloc1() {
		p1 = new Person();
		p1.age=23;
		p1.name="ss";
	} 
	
	/**
	 * 非逃逸对象
	 */
	public static void alloc2() {
		Person p2 = new Person();
		p2.age=23;
		p2.name="ff";
	} 
		
	public static void main(String[] args) {
		long start = System.currentTimeMillis();
		for(int i=0; i<1000000000; i++) {
			alloc2();
		}
		long end = System.currentTimeMillis();
		System.out.println(end-start);
	}
	
}

对于大量零散的小对象,栈上分配提供了一种很好的对象优化分配策略,栈上分配速度快,并且可以有效避免垃圾回收带来的负面影响,但是和堆空间相比,栈空间小,因此对大对象不适合也不能在栈上分配。

免责声明:文章转载自《函数的调用过程与出入栈》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇C#制作的屏幕取色器GLIBC_2.18 not found 之类的问题解决办法下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

枚举和宏的区别

枚举: 枚举是一种变量类型,枚举基本等效于int类型,占用同样的空间,同样的数值范围,但是枚举通常都是表示常数变量,对枚举变量做一些算术计算通常是编译器不允许的,但是可以加上强制类型转换,本来不在枚举符表里面的值也可以大摇大摆的登堂入室,枚举符表甚至允许数值相等。在没有赋值的引用中,只会是int范围内的垃圾数值,根本就不会是枚举符表中的数值。对于默认的情况...

Idea 软件使用快捷键归纳01

<1>CTRL+P 方法参数提示 <2>ctrl+/ 单行注释 <3>Ctrl+Alt+MIDEA 重复代码快速重构(抽取重复代码快捷键) <4>alt+enter自我修复,出现红色错误代码的解决方案(注意:光标必须定位在红色错误代码处) 可以实现自动导包 <5>ctrl+alt+L自动格式化代码...

罗云彬win32汇编教程笔记 子函数的声明, 定义与调用

在主程序中用call指令来调用子程序。 Win32汇编中的子程序也采用堆栈来传递参数,这样就可以用invoke伪指令来进行调用和语法检查工作。 一. 子程序的定义子程序的定义方式如下所示。子程序名 proc [距离][语言类型][可视区域][USES 寄存器列表][,参数:类型]...[VARARG] local 局部变量列表 指令 子程序名 endp...

iOS--Block的那些事

假设我们熟悉代理递值的话,对代理我们可能又爱有恨!我们先建立模型A页面 push B页面,如果把A页面的值传递到B页面,属性和单例传值可以搞定!但是如果Pop过程中把B页面的值传递到A页面,那就可以用单例或者代理了!说到代理,我们要先声明协议,创建代理,很是麻烦。常常我们传递一个数值需要在两个页面间写很多代码,这些代码改变页面的整体顺序,可读性也打了折扣。...

云计算openstack——虚拟机获取不到ip(13)

一、现象描述:openstack平台中创建虚拟机后,虚拟机在web页面中显示获取到了ip,但是打开虚拟机控制台后查看网络状态,虚拟机没有ip地址,下图为故障截图: 二、分析思路:(1)查看neutron服务状态,确保dchp服务正常运行 root@controller22:15:11~#neutron agent-list neutron CLI is...

WIN10 终止进程弹出拒绝访问的解决办法

现象: 今天打开 VMware Workstation 启动虚拟机时遇到一个问题:由于之前改过虚拟机名称,此次又修改了虚拟机设置,故而导致启动异常;然后我找到虚拟机文件的所在目录打算直接删掉也失败,提示已被其他程序占用;接着我打开任务管理器查看详细进程,发现有个进程无法终止。 解决办法: 1、win+r,输入 msconfig,进入系统配置页面 2、切换...