内存中为什么分堆和栈,能否只用一种模型呢?为什么每个线程都有单独的栈

转自《https://mp.weixin.qq.com/s/Yh2prf3U2qbNFyH_1viitQ》
其实我们都知道,计算机内存本来就是一块内存,没有堆栈之分。

在学编程的时候,我们应该都听过一句话 “如果程序结束之后仍然想要访问那一段数据就要用堆(不释放的话,程序修改后的数据仍存在)”,我想这个其实就是本题目的关键了,堆和栈都有其自己的独特性,可能你了解这两个东西,但是我还是解释下,以免别的小伙伴在看答案的时候,不知道。

linux下一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的(私有的)栈(stack)。堆(heap)的分配与栈有所不同,一般是一个进程有一个C运行时堆,这个堆为本进程中所有线程共享。(一个进程下的所有线程有自己独立的栈,但是堆是共享进程的堆)

栈:就像我第一句话说的,本没有什么堆栈之分,但是编程语言的出现,就有了一个概念“函数”,这个函数之间是可以相互调用的(就像我们传递东西,比如:胡小然 将东西传递 胡小然2 将东西传递 胡小然3,之后需要从后面向前面反馈传递结果,这个传递的过程我们就可以理解为调用),那就出现了前后之分,这就是调用队列了,那这个队列有个什么特点呢,那就是先被调用进入队列的要最后出去,就是我们常说的先进后出(FILO),那么这时栈就出现了,而且它还有一个特点那就是线程独有(所以可以存放其运行状态和局部自动变量、临时变量),生命周期是随线程的,线程结束时对应的栈结束(栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。)。当然我所说的是内存栈的意思,其实“栈”就是个数据结构,是一种限定仅在表尾进行插入和删除操作的线性表,这个特性不正好是符合我刚才说的FILO嘛。所以你可以这么理解c++或者java(jvm)中的内存栈的概念,就是编程语言的作者为了管理内存使用了“栈”这种数据结构(说的再细点就是现代CPU体系结构决定了栈是管理函数调用和局部变量的最佳数据结构。因为CPU已经提供了现成的指令)。

图片

堆:可算是一种特殊的数据结构,好像我们经常使用的二叉树。内存堆这个解释起来就更简单了,就是一块能自由分配的内存,是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。它允许程序在运行时动态地申请某个大小的内存空间,比如:程序员向操作系统申请一块内存,当系统收到程序的申请时,会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。其特点就是分配的速度较慢,地址不连续,容易碎片化并且是由程序员申请,同时也必须由程序员负责销毁,否则导致内存泄露。像在java这种高级语言中,我们不比担心内存回收的问题,那是因为jvm已经在帮我们处理了。

图片

不同的线程有不同的stack栈空间,以及共享的heap堆空间。每个线程确实可以new一片新的内存空间在堆里面,这种方法被称为Thread Local Storage (TLS),但是某一个线程建立的heap空间别的线程也是可以访问的。比如某一个线程使用 Object* o = new Object() 新建一个对象,这个对象是在heap中,而指针o是在stack中,只要这个线程把指针o的值发给别的线程,然后别的线程用另一个指针p来接收,那么别的线程依然能够访问这个线程new的对象,这就是共享heap空间的解释。

一般不建议在线程中用new开辟新内存空间,因为heap是共享的,所以一个线程在用new的时候别的所有线程都得停下来等,这样就有很大的同步代价. 如果m个线程每个线程分配n大小的内存到heap,那么就需要m次内存分配的操作,所有的线程需要等m次。记住每一个new操作都是很耗时的。我们完全可以只分配一次m*n大小的内存到heap,然后每个线程访问自己需要访问的部分。这样只需要一次内存分配,而且之后的操作没有同步代价。

上面说了这么多,就是想说明一下内存栈和内存堆出现的意义和作用,所以答案就出来了,那就是不能“只用堆或者全部只用栈”那样我们程序的调用和数据的存储都会出现问题。

最后说一下两者的特点。

(1)栈具有先进后出,后进先出特性,连续存储,操作简单,使用方便,无需管理,大部分芯片都对栈提供芯片级别的硬件支持,只需要移动指针就可以快速实现内存的分配和回收。比如局部变量使用栈内存,减少不必要的内存分配管理。栈创建和删除的时间复杂度是O(1),速度快。但是不利于管理大内存,栈中的数据大小和生存周期都是确定的,缺乏灵活性。

(2)堆内存的管理机制相对复杂,有一套相应的分配策略,防止大量小碎片出现,同时加快查找。堆用于动态创建分配内存,创建和删除节点的时间复杂度是O(logn)。堆的回收机制也复杂很多,根据内存大小不同,数据生命周期不同,采用相应的回收机制,涉及操作系统的堆管理。因为堆内存的管理和申请相对复杂,更消耗系统资源,通常生命周期更长使用范围更广的全局变量使用堆内存。

图片

为什么每个线程都有单独的栈呢?
有四个函数A、B、C、D,地址分别为100、200、300、400;有两个线程同时执行;

1)假如只有一个栈

图片

函数A在线程1中执行的时候,调用了函数B,将函数A中下一条指令的地址入栈(104),然后执行函数B;

在执行函数B过程中,发现要调用Yield()函数(蓝色,Yield()的作用可以理解为切换线程),于是先将B中下一条要执行的指令地址入栈(204),然后执行Yield()切换到地址300处的线程,也就是线程2;

接下来执行函数C,同样道理调用方法D,304入栈;

最后执行函数D,404入栈,而D中的Yield() 会跳到地址204继续执行(切换到线程1的下一条待执行语句对应的地址204);

紧接着,函数B执行完,会返回,返回地址是栈顶的值(404),这里的返回地址本应该是104;
因此,多个线程共用一个栈就会出现问题!

2)每个线程一个栈

图片

再切换线程时,同时也要切换栈,这里就需要一个数据结构TCB(Thread control block)来存储栈的指针;每个线程都有一个TCB。

线程2中的Yield()函数应该改写成如下格式:

void Yield(){

TCB2.esp=esp;
esp=TCB1.esp;
jmp 204;
}
执行过程:

在A函数中,调用B,将地址104入栈(esp=1000);

在函数B中执行Yield(),保存当前栈指针TCB1.esp =esp,同时切换栈指针esp=TCB2.esp,将地址204入栈,跳转到函数C(esp=1000);

在函数C中调用函数D,将地址304入栈(esp=2000);

函数D执行Yield(),保存栈指针,切换栈指针,将地址404入栈,跳转到函数B,继续执行地址204处的代码;

执行完毕,执行 ‘}’ ,弹出线程1栈的栈顶地址204,发现此处重复执行地址204处的指令;
3)最终

线程2的Yield()

void Yield(){
TCB2.esp=esp;
esp=TCB1.esp;
}
这样在2)中第四步执行时,不再使用jmp 204跳转,而是执行 ‘}’ ,将线程1中的栈顶地址出栈。

示例

一个由c/C++编译的程序占用的内存分为以下几个部分:

1、栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

2、堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放。

4、文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放。

5、程序代码区—存放函数体的二进制代码。

//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; 栈
char s[] = “abc”; 栈
char *p2; 栈
char *p3 = “123456”; 123456\0在常量区,p3在栈上。
static int c =0;全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
//分配得来得10和20字节的区域就在堆区。
strcpy(p1, “123456”); 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
stack由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间

heap需要程序员自己申请,并指明大小,在c中malloc函数,如p1 = (char *)malloc(10); 在C++中用new运算符,如p2 = (char *)malloc(10);但是注意p1、p2本身是在栈中的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值