收藏问题iOS精品文章A原理/底层

栈与堆 (一)

2016-11-04  本文已影响598人  双门

喜欢的话记得点赞

一、内存管理:移动设备的内存及其有限,每一个APP所能占用的内存是有限制的
二、什么行为会增加APP的内存占用?
1、创建一个oc对象 2、定义一个变量 3、调用一个函数或者方法
三、内存管理范围:
1、任何继承了NSObject的对象
2、对其它非对象类型无效
3、简单来说:只有OC对象需要进行内存管理,非OC对象类型比如基本数据类型不需要进行内存管理
四、引入堆和栈的概念:
所以问题就来了,为什么OC对象需要进行内存管理?,而其它非对象类型(比如基本数据类型)就不需要进行内存管理呢?内存管理的本质原因是什么?
因为:Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,就是release,OC对象存放于堆里面(堆内存要程序员手动回收),非OC对象一般放在栈里面(栈内存会被系统自动回收)堆里面的内存是动态分配的,所以也就需要程序员手动的去添加内存、回收内存.

后边高能,重点来了

五、栈和堆的介绍:
我们先来看看一个由C/C++/OBJC编译的程序占用内存分布的结构:


图二.jpg

1.栈区(操作系统之stack): 栈由编译器管理自动创建/分配/释放的,在方法中/函数体中定义的变量(局部变量)以及在函数中的参数值,通常是在栈内,它的内存分配是连续分配的,(如果还不清楚,那么就把它想成数组) 即所分配的内存是在一块连续的内存区域内,当我们声明变量时,那么编译器会自动接着当前栈区的结尾来分配内存,其操作方式类似于数据结构中的栈。即后进先出、先进后出的原则。栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即释放,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。如果你的变量要跨函数的话就需要将其定义为成员变量。
1.1 栈:是个线程独有的,保存其运行状态和局部自动变量的。
栈在线程开始的时候初始化,每个线程的栈互相独立,因此 ,栈是 thread safe的。
每个c++对象的数据成员也存在在栈中,每个函数都有自己的栈,栈被用来在函数之间传递参数。
操作系统在切换线程的时候会自动的切换栈,就是切换ss/esp寄存器。(后面有讲解)
栈空间不需要在高级语言里面显式的分配 和释放。

 > 2.堆区(操作系统之heap): Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存一般由程序员申请并指明大小/分配以及释放,即release,若程序员不释放,则可能会引起内存泄漏。程序结束时也可能由OS回收,在程序中,使用堆来动态分配和释放对象。分配方式倒是类似于链表。对于堆区的管理是采用链表式管理的,操作系统有一个记录空闲内存地址的链表,当接收到程序分配内存的申请时,操作系统就会遍历该链表,遍历到一个记录的内存地址大于申请内存的链表节点,并将该节点从该链表中删除,然后将该节点记录的内存地址分配给程序。堆在内存中的分布是不连续的,它们是不同区域的内存块通过指针链接起来的.一旦某一节点从链中断开,我们要人为的把所断开的节点从内存中释放.
     例如:在C中malloc函数
          char p1;
          p1 = (char )malloc(10);
          但是p1本身是在栈中的。

2.1堆区:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。
2.2 堆(数据结构): 堆可以被看成是一棵树,如:堆排序

2.3 堆:是大家共有的空间,分全局堆和局部堆。
全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。
堆在操作系统对进程初始化的时候分配,
运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。
堆里面一般放的是 静态数据,比如static的数据和字符串常量等,资源加载后一般也放在堆里面。
一个进程的所有线程共有这些堆 ,所以对堆的操作要考虑同步和互斥的问题。
程序里面编译后的数据段都是堆的一部分。

2.4 在下列情况下,调用堆操作?
事先不知道程序所需对象的数量和大小。
对象太大而不适合堆栈分配程序。
堆使用了在运行时分配给代码和堆栈的内存之外的部分内存。eg:
C/C++ 运行时 (CRT) 分配程序:提供了 malloc() 和 free() 以及 new 和 delete 操作符。

2.5在看一个例子.
char c; //栈上分配 1
char *p = new char[3]; //堆上分配,将地址赋给了p; 2️
在 编译器遇到第一条指令时,计算其大小,然后去查找当前栈的空间是大于所需分配的空间大小,如果这时栈内空间大于所申请的空间,那么就为其分配内存空间,注 意:在这里,内在空间的分配是连续的,是接着上次分配结束后进行分配的.如果栈内空间小于所申请的空间大小,那么这时系统将揭示栈溢出,并给出相应的异常 信息.

编译器遇到第二条指令时,由于p是在栈上分配的,所以在为p分配内在空间时和上面的方法一样,但当遇到new关 键字,那么编译器都知道,这是用户申请的动态内存空间,所以就会转到堆上去为其寻找空间分配.大家注意:堆上的内存空间不是连续的,它是由相应的链表将其 空间区时的内在区块连接的,所以在接到分配内存空间的指定后,它不会马上为其分配相应的空间,而是先要计算所需空间,然后再到遍列整个堆(即遍列整个链的 节点),将第一次遇到的内存块分配给它.最后再把在堆上分配的字符数组的首地址赋给p.,这个时候,大家已经清楚了,p中现在存放的是在堆中申请的字符数组的首地址,也就是在堆中申请的数组的地址现在被赋给了在栈上申请的指针变量p.为了更加形象的说明问题,请看下图:


图四.jpg
 > 从图可以看出,我们在堆上动态分配的数组的首地址存入了指针p所指向的内容.
 请 注意:在栈上所申请的内存空间,当我们出了变量所在的作用域后,系统会自动我们回收这些空间,而在堆上申请的空间,当出了相应的作用域以后,我们需要显式 的调用delete来释放所申请的内存空间,如果我们不及时得对这些空间进行释放,那么内存中的内存碎片就越来越多,从而我们的实际内存空间也就会变的越 来越少,即,孤立的内存块越来越多.在这里,我们知道,堆中的内存区域不是连续的,还是将有效的内存区域经过链表指针连接起来的,如果我们申请到了某一块 内存,那么这一块内存区将会从连续的(通过链表连接起来的)内存块上断开,如果我们在使用完后,不及时的对它进行释放,那么它就会孤立的开来,由于没有任 何指针指向它,所以这个区域将成为内存碎片,所以在使用完动态分配的内存(通过NEW申请)后,一定要显式的对它进行DELETE删除.对于这一点,一定 要切记...

> 2.6堆实现的注意事项
 传统上,操作系统和运行时库是与堆的实现共存的。在一个进程的开始,操作系统创建一个默认堆,叫做“进程堆”。如果没有其他堆可使用,则块的分配使用“进程堆”。语言运行时也能在进程内创建单独的堆。(例如,C 运行时创建它自己的堆。)除这些专用的堆外,应用程序或许多已载入的动态链接库 (DLL) 之一可以创建和使用单独的堆。当应用程序或 DLL 创建私有堆时,这些堆存在于进程空间,并且在进程内是可访问的。从给定堆分配的数据将在同一个堆上释放。(不能从一个堆分配而在另一个堆释放。)
 链表:是一种常见的基础数据结构,一般分为单向链表、双向链表、循环链表。以下为单向链表的结构图:
图三.jpg
 >单向链表是链表中最简单的一种,它包含两个区域,一个信息域和一个指针域。
 信息域保存或显示关于节点的信息,指针域储存下一个节点的地址。
 上述的空闲内存地址链表的信息域保存的就是空闲内存的地址。
 
> (注:堆和数据结构中的堆栈不一样) 堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些
(额外补充:3 4 5)

3.静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据和常量。
也称作全局区/静态区:
顾名思义,全局变量和静态变量存储在这个区域。只不过初始化的全局变量和静态变量存储在一块,未初始化的全局变量和静态变量存储在一块。程序结束后由系统释放。

> 4.文字常量区:
 这个区域主要存储字符串常量。程序结束后由系统释放。
 
 >5.程序代码区:
 这个区域主要存放函数体的二进制代码。

//如果您还是不明白请看我总结的 栈与堆 (二)

6.在数据结构中:
栈是一种线性表,而且是只可在表的一端进行插入和删除运算的线性表;
而堆是一种树形结构,其满中树中任一非叶结点的关键字均不大于或不小于其左右子树的结点的关键字。
延伸一点,不同的编程语言在内存分配中就存在堆,栈之分 如:Java中对象创建方式 堆中创建 而C++在堆中或栈中均可创建

 >7.堆和栈的对比:
 7.0管理方式/内存中的堆区和栈区的差别:
 对于栈来讲 [栈区(stack stæk)],是由编译器自动管理/分配释放,无需我们手工控制;存放方法(函数)的参数值,局部变量的值
 对于堆来说[堆区(heap hiːp],释放工作由程序员控制,若程序员不释放,则内存泄漏。容易产生memory leak。
 
 >7.1.申请方式
 stack:由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
 heap:需要程序员自己申请,并指明大小,
 如在c中malloc函数  --> 如p1 = (char *)malloc(10);
 在C++中用new运算符  -->如p2 = (char *)malloc(10);
 但是注意p1、p2本身是在栈中的。

Stack的空间由操作系统自动分配/释放,Heap上的空间手动分配/释放。
Stack空间有限,
Heap是很大的自由存储区:C中的malloc函数分配的内存空间即在堆上,C++中对应的是new操作符。
程序在编译期对变量和函数分配内存都在栈上进行,且程序运行过程中函数调用时参数的传递也在栈上进行

7.2.申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

> 7.3.申请大小的限制
 栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也可能是1M,它是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
 堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
 
 >7.4.申请效率的比较:
 栈由系统自动分配,速度较快。但程序员是无法控制的。
 堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
 另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。
 
 > 7.5.堆和栈中的存储内容
 栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
 形象来说,栈就是一条流水线,而流水线中加工的就是方法的主要程序,在分配栈时,由于程序是自上而下顺序执行,就将程序指令一条一条压入栈中,就像流水线一样。而堆上站着的就是工作人员,他们加工流水线中的商品,由程序员分配:何时加工,如何加工。而我们通常使用new运算符为对象在堆上分配内存(C#),堆上寻找对象的任务交给句柄,而栈中由栈指针管理
 堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排

7.6.碎片问题:
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出

7.7分配方式:
堆都是动态分配的,没有静态分配的堆。动态分配由alloca函数进行分配
栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,栈的动态分配是由编译器进行释放,无需我们手工实现。

7.8分配效率:
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的。

7.9其它:
在多线程环境下,每个线程拥有一个栈和一个程序计数器。
栈和程序计数器用来保存线程的执行历史和线程的执行状态,是线程私有的资源。
其他的资源(比如堆、地址空间、全局变量)是由同一个进程内的多个线程共享。
一般来说栈是私有的,堆是公有的。
但是在多线程中,可以为特定的线程创建私有的堆。《APUE》

上一篇下一篇

猜你喜欢

热点阅读