"栈"和"栈帧"这两个概念到底如何区分

如题所述

1、栈:FILO先进后出的数据结构

栈底是第一个进栈的数据的位置(压箱 底) 

栈顶是最后一个进栈的数据位置

2、根据SP指针指向的位置,栈可分为 满栈和空栈 

满栈:当sp指针总是指向最后压入堆栈 的数据(ARM采用满栈)

空栈:当堆栈指针SP总是指向下一个将 要放入数据的空位置。

3、根据SP指针移动的方向,可分为升 栈和降栈 

升栈:随数据的入栈,SP由低地址--> 高地址 


降栈:随数据的入栈,SP由高地址--> 低地址(ARM采用降栈)

 

4、栈帧:存储在用户栈上的(当然内核栈同样适用)每一次函数调用涉及的相关信息的记录单元 ; 栈帧(stack frame)就是一个函数所使用的那部分栈,所有函数的栈帧串起来就组成了一个完整的栈。

栈帧的两个边界分别有FP(R11)和SP(R13)L来限定。 

栈帧


栈的作用:

1)保存局部变量

分析代码:

[html] view plain copy

    #include <stdio.h>  

    int main()  

    {      

    int a;  

    a++;  

    return a;  

    }</span>  


    反汇编之后的代码;

    [html] view plain copy

    stack: file format elf32-littlearm  

    Disassembly of section .text:  

    00000000 <main>:  

    #include <stdio.h>  

    int main()  

    {  

    0:   e52db004 push   {fp}     ; (str fp, [sp, #-4]!) @将栈帧底部指针FP压入栈中;创建属于main函数的栈帧。  

    4:   e28db000 add    fp, sp, #0  ; 0x0 @fp指针为函数栈帧的底部,  

    8:   e24dd00c sub    sp, sp, #12 ; 0xc   @sp指针为栈帧的顶部,同时为栈的栈顶。  

    int a;  

    a++;  

    c:   e51b3008 ldr    r3, [fp, #-8]   @由此三句可知变量a在栈帧中执行了加法操作,及栈帧具有保存局部变量的作用  

    10:   e2833001 add    r3, r3, #1  ; 0x1  

    14:   e50b3008 str    r3, [fp, #-8]  

    return a;  

    18:   e51b3008 ldr    r3, [fp, #-8]  

    }  

    </span>  



    2)保存函数的参数 

    分析代码:

    [html] view plain copy

    <span style="font-size:18px;">#include <stdio.h>  

    void func1(int a,int b,int c,int d,int e,int f)  

    {  

    int k;  

    k=e+f;  

    }  

    int main()  

    {  

    func1(1,2,3,4,5,6);  

    return 0;  

    }  

    反汇编之后的代码;  

    void func1(int a,int b,int c,int d,int e,int f) @多于4个参数  

    {  

    0:   e52db004 push   {fp}     ; (str fp, [sp, #-4]!)@保存main函数的栈帧底部指针FP  

    4:   e28db000 add    fp, sp, #0  ; 0x0  

    8:   e24dd01c sub    sp, sp, #28 ; 0x1c @由栈帧顶部指针SP创建一片栈帧保存子函数的前四个参数  

    c:   e50b0010 str    r0, [fp, #-16]  @ a  

    10:   e50b1014 str    r1, [fp, #-20]  @ b  

    14:   e50b2018 str    r2, [fp, #-24]  @ c  

    18:   e50b301c str    r3, [fp, #-28]  @ d  

    int k;  

    k=e+f;  

    1c:   e59b3004 ldr    r3, [fp, #4]    @在子函数的栈帧中实现第五个参数与第六个参数的运算  

    20:   e59b2008 ldr    r2, [fp, #8] @由ldr  r2, [fp, #8]知参数保存在main函数的栈帧中,并运算  

    24:   e0833002 add    r3, r3, r2   @以子函数的栈帧底部指针(fp)做参考坐标实现对参数的查找  

    28:   e50b3008 str    r3, [fp, #-8]  

    }  

    2c:   e28bd000 add    sp, fp, #0  ; 0x0  

    30:   e8bd0800 pop    {fp}  

    34:   e12fff1e bx lr  

    00000038 <main>:  

    int main()  

    {  

    38:   e92d4800 push   {fp, lr}    @由于调用子函数,先保存main函数的栈帧底部指针FP和返回地址LR(当前PC指针的下一地址)  

    3c:   e28db004 add    fp, sp, #4  ; 0x4 @可知先压入FP,后压入lr.把此时子函数(被调用者)的栈帧底部指针FP指向保存在子函数栈帧的main函数(调用者)的栈帧底部指针FP  

    40:   e24dd008 sub    sp, sp, #8  ; 0x8   @创建栈  

    func1(1,2,3,4,5,6);  

    44:   e3a03005 mov    r3, #5  ; 0x5  

    48:   e58d3000 str    r3, [sp]  

    4c:   e3a03006 mov    r3, #6  ; 0x6  

    50:   e58d3004 str    r3, [sp, #4]  

    54:   e3a00001 mov    r0, #1  ; 0x1 @用通用寄存器保存前四个参数的值  

    58:   e3a01002 mov    r1, #2  ; 0x2  

    5c:   e3a02003 mov    r2, #3  ; 0x3  

    60:   e3a03004 mov    r3, #4  ; 0x4  

    64:   ebfffffe bl 0 <func1>  

    return 0;  

    68:   e3a03000 mov    r3, #0  ; 0x0  

    }  

    6c:   e1a00003 mov    r0, r3  

    70:   e24bd004 sub    sp, fp, #4  ; 0x4  

    74:   e8bd4800 pop    {fp, lr}  

    78:   e12fff1e bx lr</span>  


    注:C中,若函数的参数小于等于4个,则用通用寄存器保存其参数值,多于4个的参数保存在栈中

    3)保存寄存器的值

    分析代码:

    [html] view plain copy

    <span style="font-size:18px;">include <stdio.h>  

    void func2(int a,int b)  

    {  

    int k;  

    k=a+b;  

    }  

    void func1(int a,int b)  

    {  

    int c;  

    func2(3,4);  

    c=a+b;  

    }  

    int main()  

    {  

    func1(1,2);  

    return 0;  

    }</span>  


    反汇编之后的代码;

    [html] view plain copy

    <span style="font-size:18px;">void func2(int a,int b)  

    {  

    0:   e52db004 push   {fp}     ; (str fp, [sp, #-4]!)  

    4:   e28db000 add    fp, sp, #0  ; 0x0  

    8:   e24dd014 sub    sp, sp, #20 ; 0x14  

    c:   e50b0010 str    r0, [fp, #-16] @保存寄存器的值  

    10:   e50b1014 str    r1, [fp, #-20]  

    int k;  

    k=a+b;  

    14:   e51b3010 ldr    r3, [fp, #-16]  

    18:   e51b2014 ldr    r2, [fp, #-20]  

    1c:   e0833002 add    r3, r3, r2  

    20:   e50b3008 str    r3, [fp, #-8]  

    }  

    24:   e28bd000 add    sp, fp, #0  ; 0x0  

    28:   e8bd0800 pop    {fp}  

    2c:   e12fff1e bx lr  

    00000030 <func1>:  

    void func1(int a,int b)  

    {  

    30:   e92d4800 push   {fp, lr}  

    34:   e28db004 add    fp, sp, #4  ; 0x4  

    38:   e24dd010 sub    sp, sp, #16 ; 0x10  

    3c:   e50b0010 str    r0, [fp, #-16] @代码44行调用func2函数后,又使用r0\r1保存参数,所以此时将r0\r1寄存器的  

    40:   e50b1014 str    r1, [fp, #-20]  @值放入栈中  

    int c;  

    func2(3,4);  

    44:   e3a00003 mov    r0, #3  ; 0x3  

    48:   e3a01004 mov    r1, #4  ; 0x4  

    4c:   ebfffffe bl 0 <func2>  

    c=a+b;  

    50:   e51b3010 ldr    r3, [fp, #-16]  

    54:   e51b2014 ldr    r2, [fp, #-20]  

    58:   e0833002 add    r3, r3, r2  

    5c:   e50b3008 str    r3, [fp, #-8]  

    }  

    60:   e24bd004 sub    sp, fp, #4  ; 0x4  

    64:   e8bd4800 pop    {fp, lr}  

    68:   e12fff1e bx lr  

    0000006c <main>:  

    int main()  

    {  

    6c:   e92d4800 push   {fp, lr}  

    70:   e28db004 add    fp, sp, #4  ; 0x4  

    func1(1,2);  

    74:   e3a00001 mov    r0, #1  ; 0x1  

    78:   e3a01002 mov    r1, #2  ; 0x2  

    7c:   ebfffffe bl 30 <func1>  

    return 0;  

    80:   e3a03000 mov    r3, #0  ; 0x0  

    }  

    84:   e1a00003 mov    r0, r3  

    88:   e24bd004 sub    sp, fp, #4  ; 0x4  

    8c:   e8bd4800 pop    {fp, lr}  

    90:   e12fff1e bx lr</span>  


    初始化栈:即对SP指针赋予一个内存地址(统一标准:2440、6410、210)

    在内存的64MB位置即ldr sp, =0x34000000(2440)

    ldr sp, =0x54000000(6410)

    ldr sp, =0x24000000(210)

    由上可知ARM采用满栈(指向刚入栈的数据)、降栈(由高地址向低地址入栈)

    问题:因为ARM不同工作模式有不同的栈,定义栈的技巧是什么,避免定义相同的地址使用不同栈?

    转自:http://blog.csdn.net/u011467781/article/details/39559737

温馨提示:答案为网友推荐,仅供参考
第1个回答  2019-01-08
GC在什么时候对什么做了什么?
要回答这个问题,先了解下GC的发展史、jvm运行时数据区的划分、jvm内存分配策略、jvm垃圾收集算法等知识。
先说下jvm运行时数据的划分,粗暴的分可以分为堆区(Heap)和栈区(Stack),但jvm的分法实际上比这复杂得多,大概分为下面几块:
1、程序计数器(Program Conuter Register)
程序计数器是一块较小的内存空间,它是当前线程执行字节码的行号指示器,字节码解释工作器就是通过改变这个计数器的值来选取下一条需要执行的指令。它是线程私有的内存,也是唯一一个没有OOM异常的区域。
2、Java虚拟机栈区(Java Virtual Machine Stacks)
也就是通常所说的栈区,它描述的是Java方法执行的内存模型,每个方法被执行的时候都创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法被调用到完成,相当于一个栈帧在虚拟机栈中从入栈到出栈的过程。此区域也是线程私有的内存,可能抛出两种异常:如果线程请求的栈深度大于虚拟机允许的深度将抛出StackOverflowError;如果虚拟机栈可以动态的扩展,扩展到无法动态的申请到足够的内存时会抛出OOM异常。
3、本地方法栈(Native Method Stacks)
本地方法栈与虚拟机栈发挥的作用非常相似,区别就是虚拟机栈为虚拟机执行Java方法,本地方法栈则是为虚拟机使用到的Native方法服务。
4、堆区(Heap)
所有对象实例和数组都在堆区上分配,堆区是GC主要管理的区域。堆区还可以细分为新生代、老年代,新生代还分为一个Eden区和两个Survivor区。此块内存为所有线程共享区域,当堆中没有足够内存完成实例分配时会抛出OOM异常。
5、方法区(Method Area)
方法区也是所有线程共享区,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。GC在这个区域很少出现,这个区域内存回收的目标主要是对常量池的回收和类型的卸载,回收的内存比较少,所以也有称这个区域为永久代(Permanent Generation)的。当方法区无法满足内存分配时抛出OOM异常。
6、运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

垃圾收集(Garbage Collection)并不是Java独有的,最早是出现在Lisp语言中,它做的事就是自动管理内存,也就是下面三个问题:
1、什么时候回收
2、哪些内存需要回收
3、如何回收

1、什么时候回收?
上面说到GC经常发生的区域是堆区,堆区还可以细分为新生代、老年代,新生代还分为一个Eden区和两个Survivor区。
1.1 对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快;
1.2 Full GC,发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC。大对象直接进入老年代,如很长的字符串数组,虚拟机提供一个-XX:PretenureSizeThreadhold参数,令大于这个参数值的对象直接在老年代中分配,避免在Eden区和两个Survivor区发生大量的内存拷贝;
1.3 发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则进行一次Full GC,如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC。

2、哪些内存需要回收
jvm对不可用的对象进行回收,哪些对象是可用的,哪些是不可用的?Java并不是采用引用计数算法来判定对象是否可用,而是采用根搜索算法(GC Root Tracing),当一个对象到GC Roots没有任何引用相连接,用图论的来说就是从GC Roots到这个对象不可达,则证明此对象是不可用的,说明此对象可以被GC。对于这些不可达对象,也不是一下子就被GC,而是至少要经历两次标记过程:如果对象在进行根搜索算法后发现没有与GC Roots相连接的引用链,那它将会第一次标记并且进行一次筛选,筛选条件是此对象有没有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用执行过一次,这两种情况都被视为没有必要执行finalize()方法,对于没有必要执行finalize()方法的将会被GC,对于有必要有必要执行的,对象在finalize()方法中可能会自救,也就是重新与引用链上的任何一个对象建立关联即可。

3、如何回收
选择不同的垃圾收集器,所使用的收集算法也不同。
在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,则使用复制算法,新生代内存被分为一个较大的Eden区和两个较小的Survivor区,每次只使用Eden区和一个Survivor区,当回收时将Eden区和Survivor还存活着的对象一次性的拷贝到另一个Survivor区上,最后清理掉Eden区和刚才使用过的Survivor区,Eden和Survivor的默认比例是8:1,可以使用-XX:SurvivorRatio来设置该比例。
而老年代中对象存活率高,没有额外的空间对它进行分配担保,必须使用“标记-清理”或“标记-整理”算法。本回答被网友采纳