堆,栈,常量池(转载)


转载于:https://www.iteye.com/topic/634530


1.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制.
2. 栈:存放基本类型的变量数据(指变量和值)和对象的引用(对象变量),但对象本身(指new 出来的对象)不存放在栈中,而是存放在堆或者常量池中(字符串常量对象存放在常量池中。)
3. 堆:存放所有new出来的对象。
4. 静态域:存放静态成员(static定义的)
5. 常量池:存放字符串常量和基本类型常量(public static final)。
6. 非RAM存储:硬盘等永久存储空间

这里我们主要关心栈,堆和常量池,对于栈和常量池中的对象可以共享,对于堆中的对象不可以共享。栈中的数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会消失。堆中的对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定,具有很大的灵活性。
对于字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。
如以下代码(java):

String s1 = "china";  
String s2 = "china";  
String s3 = "china";  
String ss1 = new String("china");  
String ss2 = new String("china");  
String ss3 = new String("china");  

在这里插入图片描述
这里解释一下黄色这3个箭头,对于通过new产生一个字符串(假设为”china”)时,会先去常量池中查找是否已经有了”china”对象,如果没有则在常量池中创建一个此字符串对象,然后堆中再创建一个常量池中此”china”对象的拷贝对象。

这也就是有道面试题:String s = new String(“xyz”);产生几个对象?
一个或两个,如果常量池中原来没有”xyz”,就是两个。

对于基础类型的变量和常量:变量和引用存储在栈中,常量存储在常量池中。
如以下代码(java):

int i1 = 9;  
int i2 = 9;  
int i3 = 9;   
public static final int INT1 = 9;  
public static final int INT2 = 9;  
public static final int INT3 = 9;  

在这里插入图片描述
对于成员变量和局部变量:成员变量就是方法外部,类的内部定义的变量;局部变量就是方法或语句块内部定义的变量。局部变量必须初始化。
形式参数是局部变量,局部变量的数据存在于栈内存中。栈内存中的局部变量随着方法的消失而消失。成员变量存储在堆中的对象里面,由垃圾回收器负责回收。
如下代码(java):

class BirthDate {  
    private int day;  
    private int month;  
    private int year;      
    public BirthDate(int d, int m, int y) {  
        day = d;   
        month = m;   
        year = y;  
    }  
    省略get,set方法………  
}  
  
public class Test{  
    public static void main(String args[]){  
int date = 9;  
        Test test = new Test();        
           test.change(date);   
        BirthDate d1= new BirthDate(7,7,1970);         
    }    
  
    public void change(int i){  
        i = 1234;  
    } 
} 

在这里插入图片描述
对于以上这段代码,date为局部变量,i,d,m,y都是形参为局部变量,day,month,year为成员变量。下面分析一下代码执行时候的变化:
1, main方法开始执行:int date = 9;
date为局部变量,基础类型,引用和值都存在栈中。

2,Test test = new Test();
test为对象引用,存在栈中,对象(new Test())存在堆中。

3, test.change(date);
i为局部变量,引用和值存在栈中。当方法change执行完成后,i 就会从栈中消失。

4,BirthDate d1= new BirthDate(7,7,1970);
d1为对象引用,存在栈中,对象(new BirthDate())存在堆中,其中d,m,y为局部变量存储在栈中,且它们的类型为基础类型,因此它们的数据也存储在栈中。day,month,year为成员变量,它们存储在堆中(new BirthDate()里面)。当BirthDate构造方法执行完之后,d,m,y将从栈中消失。

5,main方法执行完之后,date变量,test,d1引用将从栈中消失,new Test(),new BirthDate()将等待垃圾回收。

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&


转载于:https://blog.csdn.net/u014608280/article/details/82218079


大多数操作系统会将内存空间分为内核空间和用户空间,而每个进程的内存空间又有如下的“默认”区域。

1. 栈:栈用于维护函数调用的上下文,离开栈函数调用就会无法实现。栈通常在用户空间的最高地址处分配,通常有数兆字节。

2. 堆:堆用来容纳应用程序动态分配的内存区域,我们使用malloc 或者new分配内存时,得到的内存来自堆里。堆通常存于栈的下方(低地址方向),堆一般比栈大很多,可以有几十至数百兆字节的容量。

3. 可执行文件镜像:可执行文件由装载器在装载时将可执行文件读取到内存或者映射到内存。

   3.1 代码段:代码段中存放可执行的指令,在内存中,为了保证不会因为堆栈溢出被覆盖,将其放在了堆栈段下面,通常来讲代码段是共享的,这样多次反复执行的指令只需要在内存中驻留一个副本即可,比如 C 编译器,文本编辑器等。代码段一般是只读的,这样程序执行时不能随意更改指令,也是为了进行隔离保护。

   3.2 初始化数据段:初始化数据段有时就称之为数据段。数据段是一个程序虚拟地址空间的一部分,包括一全局变量和静态变量,这些变量在编程时就已经被初始化。数据段是可以修改的,不然程序运行时变量就无法改变了,这一点和代码段不同。数据段可以细分为初始化只读区和初始化读写区。这一点和编程中的一些特殊变量吻合。比如全局变量 int global n = 1就被放在了初始化读写区,因为 global 是可以修改的。而 const int m = 2 就会被放在只读区,很明显,m 是不能修改的。

4. 保留区:保留区并不是一个单一的内存区域,而是对内存中受保护而禁止访问的内存区域的总称,例如,大多说操作系统中,极小的地址通常都是不允许访问的,如NULL。(顺便提一下,我们编程的时候经常会遇到‘段错误(segment fault)’或者‘非法操作,该内存地址不能read/write’的错误信息,其中一个原因就是我们初始化了一个指针为NULL但是没有给它赋合理的值就开始使用它。)

linux 进程内存空间布局如下图:
在这里插入图片描述
栈是由高地址向低地址增长。
堆是由低地址向高地址增长。
当栈或堆现有的大小不够用的时候,它将按照图中的增长方向扩大自身的尺寸,直到预留的空间被用完为止。

---------------------------------------------栈---------------------------------------------

一个栈的实例:
在这里插入图片描述
在经典的操作系统中,栈总是向下增长的,栈顶由称为esp的寄存器进行定位,压栈的操作使栈顶的地址减小,弹出的操作使栈顶地址增大。栈保存了一个函数调用所需的维护信息,常常被称为堆栈帧或者活动记录。堆栈帧一般包括如下几个方面:

函数的返回地址和参数。
临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
保存的上下文:包括在函数调用前后需要保持不变的寄存器。

ESP(Extended stack pointer)是指针寄存器的一种(另一种为EBP)。用于堆栈指针。
ESP为栈指针,用于指向栈的栈顶(下一个压入栈的活动记录的顶部),而EBP为帧指针,指向当前活动记录的底部。
栈指针与帧指针标识出了当前活动记录的位置。
当函数被调用的时候,执行如下操作:
⒈将帧指针压入栈中:push ebp
⒉用esp保存当前栈指针:mov esp,ebp
⒊使得栈指针自减,自减得到的内存应当能够被用来存储被调用函数的本地状态:sub esp,0CCh

一个函数调用函数的活动记录
在这里插入图片描述
一个函数的活动记录用ebp和esp这两个寄存器划定范围。esp寄存器始终指向栈的顶部,ebp寄存器指向了函数活动记录的一个固定位置,ebp寄存器又被称为帧指针。函数调用时栈的操作如下:

  • 把所有的或者一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递。
  • 把当前指令的下一条指令的地址压入栈中。
  • 跳转到函数体执行。
  • push ebp:把ebp压入栈中(称为old ebp)。为了在函数返回的时候便于恢复以前的ebp值。
  • mov ebp , esp: ebp = esp (这时ebp指向栈顶,而此时栈顶就是old ebp)。
  • 【可选】sub esp,XXX :在栈上分配XXX字节的临时空间。
  • 【可选】push XXX:如果有必要,保存名为XXX的寄存器(可重复多个,由于编译器可能要求某些寄存器在调用前后保持不变,那么函数就在调用开始将这些寄存器的值压入栈中,在结束后取出)。

函数调用结束时

  • 【可选】pop XXX:如果有必要,恢复保存过的寄存器(可重复多个)。
  • mov esp, ebp:恢复ESP同时回收局部变量空间。
  • pop ebp: 从栈中恢复保存的ebp的值。
  • ret :从栈中取得返回地址,并跳转到该位置。

函数返回值传递

函数的返回值是通过eax寄存器返回的,但是eax寄存器只有4字节,如果返回值在5-8字节范围内,几乎所有的调用惯例都是采用eax和edx联合返回的,eax存储返回值的低4字节,其他的字节在edx中存储。但是大于8字节的返回值呢?

先上代码©:

typedef struct big_thing
{
    char buf[128];
}big_thing;
 
big_thing return_test()
{
    big_thing b;
    b.buf[0] = 0;
    return b;
}
 
int main()
{
    big_thing n = return_test(); 
}

在调用return_test函数时,进行了如下操作:

  • 首先main函数在栈上额外开辟了一片空间,并将这块空间的一部分作为传递返回值的临时对象,这里称为temp。
  • 将temp对象的地址作为隐藏参数传递给return_test函数。
  • return_test函数将数据拷贝给temp对象,并将temp对象的地址用eax传出。
  • return_test 返回之后,main函数将eax指向的temp对象的内容拷贝给n。

也就是如下伪代码:

void return_test(void *temp)
{
    big_thing b;
    b.buf[0] = 0;
    memcpy(temp, &b, sizeof(big_thing));
    eax = temp;
}
 
int main()
{
    big_thing n;
    big_thing temp;
    return_test(&temp);
    memcpy(&, eax, sizeof(big_thing)); 
}

所以我们在编程时尽量不要返回大于4字节的数据,避免两次拷贝,减小开销。

---------------------------------------------栈---------------------------------------------

栈上的数据在函数返回时就会被释放掉,所以无法将数据传至函数外部,而全局变量没有办法动态地产生,只能在编译的时候定义,在这种情况下,堆是唯一地选择。malloc是C语言申请堆空间的函数,但是它是怎么实现的呢?

其实可以直接让操作系统的内核来管理进程的内存,但是每次申请内存都要经过系统调用,如果操作频繁会导致效率很低,程序性能降低。比较好的做法是程序向操作系统申请一块适当的堆空间,然后由程序的运行库根据算法管理堆空间的分配,当堆空间不够的时候再向操作系统申请堆空间。linux下提供两种堆空间分配方式:一个是brk()系统调用,另外一个是mmap()。

int brk (void *end_data_segment)

brk()的作用实际上就是设置进程数据段的结束地址,她可以扩大或者缩小数据段。

void mmap(void *start, size_t length, int port, int flags, int fd, off_t offset)

mmap的前两个参数分别指定需要申请的空间的起始地址和长度,如果其实地址设为0,那么操作系统会挑选合适的起始地址。port/flags这两个参数用于设置申请的空间的权限(可读,可写,可执行)以及映射类型(文件类型,匿名空间等),最后两个参数用于文件映射是指定文件的描述符和文件偏移。用mmap实现的malloc函数:

void *malloc(size_t nbytes)
{
    void *ret = mmap(0, nbytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0 , 0);
    if (ret == MAP_FAILED)
        return 0;
    return ret;
}

mmap()的作用是向操作系统申请一段虚拟空间,当这块虚拟空间可以映射到某个文件(也就是这个系统调用的最初的作用),当他不将地址空间映射到某个文件时,我们又称这块空间为匿名空间。

glibc的malloc 函数是这样处理用户的空间请求的:对于小于128KB的请求来说,它会在现有的堆空间里面,按照堆分配算法为它分配一块空间返回,对于大于128KB的请求来说,它会使用mmap()函数为它分配一块匿名空间,然后再这个匿名空间中为用户分配空间。(所以问一个很常见的问题,malloc申请的内存,进程结束以后还会不会存在? 答案是不存在)

堆分配算法

1,空闲链表法
空闲链表的方法是把堆中各个空闲的快按照链表的方式连接起来,当用户请求一块空间时,可以遍历整个列表,直到找到合适大小的快并且将它拆分,当用户释放空间时将它合并到空闲链表中。
在这里插入图片描述
2,位图
核心思想就是将整个堆划分为大量的块,每个块大小相同。当用户请求内存的时候总是分配整个块的空间给用户。第一个块我们称为已分配区域的头,其余的称为已分配区域的主体。而我们可以使用一个整数数组来记录块的使用情况,由于每个块只有头/主体/空闲三种状态,因此仅仅需要两位即可表示一个块,因此称为位图。
在这里插入图片描述
优点:速度快,稳定性好,容易管理。
缺点:容易产生碎片,浪费空间。

3,对象池
如果实际上在一些场合,被分配对象的大小是固定的几个值,我们可以采用对象池的方法。对象池思想就是,如果每一次分配的空间大小都一样,那么就可以按照这个每次请求分配的大小作为一个单位,把整个堆空间划分为大量的小块,每次请求只要找到一个空闲的小块就可以了。

实际上很多应用中,堆的分配算法往往是采取多种算法复合而成的,对于glibc来说,小于64字节的采用对象池的方法,对于大于512字节的采用最佳适配算法,对于64字节和512字节之间的采取最佳折中策略;对于大于128kb的申请,它会直接使用mmap向操作系统申请空间。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值