???? 本篇我们了解一下Java的栈内存空间。
???? 1.我们首先从Intel80386架构下的Linux汇编开始,看看会把什么东西存放在栈中。在开始之前,需要注意一点,Intel80386架构下的linux系统的堆是从高位地址往低位地址增长的。
???? 我们看一个简单的例子,计算从1加到100,文件存储为test.c
int sum(int max); int test() { sum(100); } int sum(int max) { int result = 0; int i; for (i=1; i<=max; i++) { result += i; } return result; }
???? 编译一下,生成test.o
???? 反汇编一下test.o
???? 得到如下的汇编代码
00000000 <test>:
0: push %ebp
1: mov %esp,%ebp ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ;1)
3: sub $0x8,%esp
6: movl $0x64,(%esp)
d: call e <test+0xe>?????????????????????????????????????? ;2)
12: leave
13: ret??????????????????????????????????????????????????????? ;3)
00000014 <sum>:
14: push %ebp
15: mov %esp,%ebp
17: sub $0x10,%esp
1a: movl $0x0,0xfffffff8(%ebp)
21: movl $0x1,0xfffffffc(%ebp)????????????????????????? ;4)
28: jmp 34 <sum+0x20>
2a: mov 0xfffffffc(%ebp),%eax
2d: add %eax,0xfffffff8(%ebp)
30: addl $0x1,0xfffffffc(%ebp)
34: mov 0xfffffffc(%ebp),%eax ;
37: cmp 0x8(%ebp),%eax?????????????????????????????? ;5)
3a: jle 2a <sum+0x16>
3c: mov 0xfffffff8(%ebp),%eax
3f: leave
40: ret
??? 1)push?? %ebp
???????? mov??? %esp,%ebp
????? 标准的函数进入后处理方式,将上一个方法的基址寄存器(ebp)入栈,以备在返回时恢复上一个方法的现场。并将当前的栈顶esp(即当前方法栈的开始地址)设置给ebp(即ebp总是指向当前方法栈的开始位置)
??? 2)sub??? $0x8,%esp
??????? movl?? $0x64,(%esp)
??????? call?? e <test+0xe>
???? 需要再次提醒的是,栈是从高位地址往低位地址增长的,所以此处是减,从栈中分配8个字节,其中前4个字节预留给方法入参(movl?? $0x64,(%esp)),后4个字节预留给返回地址(在call?? e <test+0xe>,call指令自动会将下一条指令的地址塞入这个位置),需要注意的是,这里call的目标地址是e(当前指令位置d+1),是因为这是一个目标文件(.o),在连接(link)阶段,会自动解析正确的地址,有兴趣可以参考我的另一篇BLOG《
》。
???? 3)leave
????????? ret
?? 标准的离开方法时的指令组合,这两条指令会进行返回到上一个方法的现场回复,譬如,恢复上一个方法的ebp/esp,并转向上一个方法的下一条指令的位置。
????? 4)现在进入到方法sum,同样的,经过标准参数进入处理后,ebp指向当前方法栈的开始位置,而指令(sub??? $0x10,%esp)预留了16个字节给当前的局部变量(发现在这里预留给局部变量的空间总是16个字节的倍数,不知道为什么一定要这样子),经过这番折腾后,我们可以看到栈数据的分布大概是如下,有了这个图,后面的程序的理解就比较轻松了。
?
?????? 5)mov??? 0xfffffffc(%ebp),%eax ;
????????? cmp??? 0x8(%ebp),%eax
?????? 这里非常好理解,就是比较i(ebp-4)和入参max(ebp+8)
????? 2.通过如上程序的分析,我们大概可以知道,Intel80386架构下Linux汇编中,入参、返回方法地址、局部变量会存储到栈中,使用ebp+偏移量可以访问到特定的某个局部变量。JVM汇编与之类似,但有点不同的是,Intel80386的汇编中,计算是通过EAX/EBX等寄存器来进行的,而JVM中则没有这两个寄存器的概念,计算是通过堆来进行的。
????? 我们看看JVM汇编是如何出来的,首先是Java程序,如上的例子一样
package ray.test; public class Test { public int test() { return sum(100); } public int sum(int max) { int result = 0; int i; for (i = 1; i <= max; i++) { result += i; } return result; } }
????? 我们看看JVM汇编结果
public ray.test.Test();
Code:
0: aload_0
1: invokespecial java/lang/Object.<init>:()V
4: return
public int test();
Code:
0: aload_0
1: bipush 100 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ;1)
3: invokevirtual sum:(I)I????????????????????????? ;2)
6: ireturn
public int sum(int);
Code:
0: iconst_0
1: istore_2
2: iconst_1
3: istore_3?????????????????????????????????????????? ;3)
4: goto 14
7: iload_2
8: iload_3
9: iadd
10: istore_2
11: iinc 3, 1??????????????????????????????????????? ;4)
14: iload_3
15: iload_1
16: if_icmple 7????????????????????????????????????? ;5)
19: iload_2
20: ireturn ;6)
}
????? 1)? aload_0?????????????????? :将{dy}个方法参数入栈,{dy}个参数就是this的地址
?????????? bipush? 100????????????? :将参数值100入栈
????? 2)invokevirtual?? sum:(I)I :调用sum方法,JVM是基于栈的运算,此处传入的参数就是栈顶部分的数据,即100,(实际上会传递2个参数,一个是this,一个是100,大部分面向对象的语言在转换成面向过程的汇编的过程当中,大体都会是类似的处理方法)
????? 3)iconst_0?????????????????? :将值0入栈,此指令类似于bipush 0,只是会xxx。JVM对于最常见的0-5,都有一条iconst_x指令。
????????? istore_2??????????????????? :把第3个局部变量(即result)的值设为当前栈顶值(0),第2个参数可以理解为ebp偏移4个字节。第1个局部变量是this,第2个局部变量是入参数,
????????? iconst_1????????
????????? istore_3??????????????????? :与上类似,第4个局部变量为i,设置值为1
???? 4) iload_2???????????????????? :将第3个局部变量(result)入栈
????????? iload_3????????????????????? :将第4个局部变量(i)入堆栈
????????? iadd???????????????????????? :将两个栈顶元素相加,并将两个栈顶元素移除,将结果再入栈
????????? istore_2???????????????????? :将栈顶元素(即相加结果)设到第3个局部变量(即result)
????????? iinc??? 3, 1???????????????? :递增第4个局部变量(i)的值
???? 5)iload_3????????????????????? :将第4个局部变量(i)的值入栈
????????? iload_1????????????????????? :将第2个局部变量(max)的值入栈
????????? if_icmple???? 7???????????? :比较栈顶两个值,如果i<=max,则重新进入指令7的位置(对应for循环)
???? 从如上的指令说明,我们可以看到JVM的指令都是基于栈来进行运算的。为加深运算,我们再举一个数学运算的例子说明。一般,进行数学公式描述的时候,我们比较喜欢使用中缀表达式,譬如:18 * 12 + 17 * 13,而对于JVM这种基于栈运算的汇编来说,采用后缀表达式(逆波兰表达式)描述会更方便:18 12 * 17 13 * +。如下我们采用JVM汇编来进行这个运算
???? 我们可以从栈的变迁来理解这个过程,从JVM汇编我们看不出栈是由上自下增长(Intel80386下的linux系统是采用这种方式)还是自下往上增长,我们这里假设是由下往上增长。
????? 3.从如上的分析过程,我们可以了解到JVM的栈中会存储一些什么东西,以及如何利用堆来进行运算的。除此之外,我们需要知道的是,在JVM中,栈是针对线程的,在线程构造函数中,我们可以看到可以传入栈的大小,需要注意的是,该值对JVM而言只是一个建议,JVM有权选择更合适的值
public Thread(ThreadGroup group, Runnable target, String name, long stackSize) { init(group, target, name, stackSize); }
???? 当然也可以通过JVM启动参数来指定
??? 一般情况下采用默认的值即可
- (2010年04月10日)
- (2010年04月15日)
- (2010年04月09日)
- (2010年04月09日)
- (2010年04月12日)
- (2010年04月03日)
- (2010年04月13日)
- (2010年04月03日)
- (2010年04月14日)
- (2010年04月20日)
- (2010年04月05日)
- (2010年04月03日)
- (2010年04月10日)
- (2010年04月10日)
- (2010年04月12日)
- (2010年04月03日)
- (2010年04月10日)