JVM动态方法栈内存分配

 

目录

运行时动态计算栈大小

Java类型实例访问方式

参数在方法栈中的存在方式

生成机器指令

统计栈空间大小


      上一篇日志最后写了JVM在函数内部调用其它函数时栈空间的分配方式,也就是调用者函数和被调用者函数两者的方法栈模型,是一种对接的方式,即被调用者分配的方法栈空间,会对接在调用者方法栈栈顶处,这样被调用者函数可以通过ebp寄存器很方便找到调用者函数中压栈的参数,这是方法栈分配方式问题,不过还有一个问题是,JVM怎么知道要为函数方法栈分配多大的空间呢?你可能会想,像C那样,编译器在编译程序时期,通过对函数的入参,和局部变量的声明来计算出需要分配的栈空间大小,然后为其分配栈空间,这是一种方法。得益于C语言可以被编译器直接编译成机器码,让机器运行,所以计算机可以知道该程序中有多少个变量,它们都是什么类型,一共需要多大的空间,但Java不同,前面日志说过,Java程序因为跨平台性,运行时需要翻译成一种中间语言-字节码,再有JVM来决定哪些部分是解释执行,哪些部分是编译执行(JVM里的JIT即时编译器)。也就是说Java的变量类型不能直接被编译成机器码,Java面向对象编程语言有些自己独特的类型,例如类,对象,所以无法像C那样方便地直接编译成机器能读懂的机器码,从而在运行前计算出调用函数所需要的栈空间大小。

 

运行时动态计算栈大小

      JVM运行时将Java程序加载进来,但是由于计算机并不能读懂Java代码,无法执行方法(或者说函数),所以,JVM通过call_stub()函数,其返回的CallStub函数指针,来执行Java方法,并将函数传递进去,前面也说过,Java主函数就必须通过call_stub()来执行,call_stub()函数的执行链中最后调用generate_call_stub()函数来初始化,得到需要执行函数的首地址,返回给_call_stub_entry变量。主函数需要的参数String[ ] args就是放在了generate_call_stub()函数的方法栈中。举一个简单的例子来说明,假设main()函数里调用了run()函数,run()函数需要两个参数a和b,那么该两个参数在main()函数中就需要完成压栈,保存在了main()函数的方法栈内,而不是run()函数的方法栈里,其实也很好理解,run()函数需要的参数在其运行前就要被保存好,因为运行前JVM不会为run()函数分配栈空间,只有当函数被调用后,被调用函数才会得到空间分配,然后从调用者函数的压栈中获得需要的参数。

JVM执行最开始的主函数时也是如此,

public static void main(String[ ] args) {

      …………………

}

      参数String[ ] args被保存在了generate_call_stub()函数的方法栈中,不过在保存参数之前,为了能知道函数调用需要多大的栈空间分配,该初始化函数还需要做一些事情,就是运行时动态计算参数的个数,需要的空间大小,在generate_call_stub()函数中对应的代码部分如下:

address generate_call_stub(address& return_address) {
	StubCodeMark mark(this, "StubRoutines", "call_stub");
    address start = __ pc(); // 当前函数的入口地址
	
	assert(frame::entry_frame_call_wrapper_offset == 2, "adjust this code");
	bool sse_save = false;
	const Address rsp_after_call(rbp, -4 * wordSize);
	const int locals_count_in_bytes (4 * wordSize);
	const Address mxcsr_save (rbp, -4 * wordSize);
	
	// 部分源码省略....
	_enter();
	_movptr(rcx, parameter_size);
	_shlptr(rcx, Interpreter::logStackElementSize);
	_addptr(rcx, locals_count_in_bytes);
	_subptr(rsp, rcx);
	_andptr(rsp, -(StackAlignmentInBytes));
	
	// 下面源码省略....
}

      标亮部分就是JVM在对Java程序中函数的入参进行计算,可以看到,只需要传入parameter_size,即入参数量,JVM就可以统计出函数所需要的栈空间大小。虽然不同的数据类型所占的空间大小不同,例如int型占4个字节,char型占1个字节,JVM需要知道每一种类型的空间大小,才能进行累加求和,在Java程序中除了基本数据类型,还有一些类的实例对象这样的数据类型,即使各个参数类型大小不同,JVM还是能做到统计需要的栈空间大小。还记得前面日志有总结到,JVM会为每一个Java类型对象建立内存模型,在CallStub函数指针中需要的八个参数里,其中一个method()参数,它做的事就是为当前调用的Java方法在JVM内部建立函数模型,模型里包含有被调用函数的方法名,入参类型,入参数量和编译后的字节码指令等,使得JVM可以在程序运行时 动态获取类和对象的信息。

      总的来说,JVM知道各种Java基本类型的大小,还为各种类型的实例对象建立了内存模型,这样就可以知道每一种变量所占空间大小,最后根据参数数量统计出总的方法栈空间。

 

Java类型实例访问方式

参数在方法栈中的存在方式

      有一点要注意的是,JVM的栈内存模型中,存放的是变量的引用,也就是指针,而不是数据,这个在前面的日志里也提到了一下,这样做的好处是在被调用者函数中对入参进行寻址时很方便,可以通过寄存器地址+偏移量的方式找到需要的参数的位置,因为方法栈中存放的就是地址。Java类型变量,例如类的实例对象中的成员变量和方法,都是这样的访问方式,通过指针+偏移量的方式访问读取,同样,JVM在函数间传参,传的也是指针。因为JVM这种传递引用(或者说指针)的方式,不管是int*型还是char*型变量,它们的宽度都是一样的,32位系统下指针宽度就是4个字节,64位系统下宽度就是8个字节,即使是结构体类型的指针变量,它们的大小都是统一的,这也是为什么JVM在统计函数需要的栈空间时,只需要知道入参数量就可以的原因。

 

生成机器指令

回到generate_call_stub()函数中,先把图片再贴上来一次:

      parameter_size在函数栈顶往上偏移32位(4个字节)处的地方,对应的代码部分:

_movptr(rcx, parameter_size);

_shlptr(rcx, Interpreter::logStackElementSize);

      第一句的意思是将parameter_size参数的值传到rcx寄存器中(32位系统下的ecx寄存器在64位下扩展为rcx,ecx寄存器用来保存临时变量),如果把第一句翻译成汇编:

movl 0x20(%ebp), %ecx

      意思是将ebp寄存器往上0x20,十六进制换成十进制就是32,往上偏移32个字节处的地方,对应上面的图,即4个字节处,parameter_size参数的值,放到ecx寄存器中,留意parameter_size的值表示参数的个数。第二句代码翻译成汇编如下:

shl $0x2, %ecx

      意思是将ecx寄存器中的值往左移两位,也就是乘以4,在32位系统下每个指针宽度为4个字节(64位系统下位8个字节,那么就是乘以8,ecx寄存器的值是往左移三位),参数个数乘以指针宽度,最后计算出了函数总的需要的栈空间大小。

 

统计栈空间大小

      继续跟着generate_call_stub()函数往下走,计算完需要的栈空间大小后,下一条语句是:

_addptr(rcx, locals_count_in_bytes);

      它的作用是保存调用者函数的现场,即在进入函数之前保存函数的基地址,具体的做法是保存一些寄存器的值,因为这部分我当初没有看透彻,所以不详细展开,要注意的是保存现场,也就是保存一些寄存器的值,这里需要的空间也是算进方法栈空间里的,所以最后总的方法栈空间大小是入参所以需要的空间+保存现场寄存器占用的空间。通常在32位系统下一个寄存器也是4个字节,最后,申请分配方法栈空间:_subptr(rsp, rcx);  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章