linux的内存回收机制设计得简直是一种艺术,精通c语言的不一定不会把c语言玩得导致内存泄漏,精通java的虽然再也不用玩指针了,那也不能完全相信java的内存回收机制,毕竟一个java程序出事了还有操作系统兜着呢。内存不好管理的罪魁祸首就是--指针。
指针这个概念是直接从汇编中引入的,它主要的目的除了汇编中的间接寻址之外还包括了动态内存管理,如果在语言级别没有指针的情况下,我们想操作某个变量的话,编译系统就必须为我们保存这个变量的标识符,其实这个标识符本质就是指针,只不过它没有导出给我们用罢了,这样的话,程序就会很大很不灵活,如果动态构建一棵二叉树,那么树的每一个节点的标识符都要保存...其实指针是最接近机器的一个语言特性,也可以叫做一个品质,因为冯氏机器上,指令和数据都在内存当中,cpu取数据或者指令就是依靠地址来取的,看看cpu的一个寄存器是不是叫做ip,再看看它的内容,是不是一个指针?既然冯氏机器本质对地址是如此依赖,那么语言上只有提供地址操作才可以将机器性能发挥出来,因此c语言提供了指针,因此c语言成为了最高效的高级语言。但是这种高效是需要付出代价的,我们只要会遇到以下两个问题:1.野指针问题;2.指针引用丢失问题。野指针问题就不必多说了,这个问题会导致程序引用一个不再归该指针所管辖的一块动态内存区域。引用丢失问题更为严重,它虽然不会引起野指针问题,但是会使得内存垃圾堆积,最终内存泄漏,试想如下语句:p = malloc(16);p = malloc(32);这样的话,第一个16字节的动态内存区域就永远的失去了引用,如果最终我们调用delete p,那么也是释放了后面的32个字节的空间,前面的16字节的空间将永远不会被释放。
现在该说说什么是动态内存了,正是因为指针的提供,动态内存才加以提供,我们想想这是为什么。如果没有指针的话,我们几乎没有办法自己管理内存,因为就像刚才说的,在没有间接寻址的情况下我们只能直接寻址,直接寻址是一件很好的事情,毕竟直接呗,直接总是不错的,但是我们访问数据必须访问数据本身,因为没有指针类型的数据,那么我们就必须在编译期得到每一个变量的引用,因为我们没有办法引用一个地址从而引用一个值,那么我们就不可能在运行期间新建立一个变量,然后用它,如果可以那么做的话,我们怎么使用它呢,要知道我们建立的直接引用关系是通过编译器才建立的,运行期编译器就脱手了,我们做不到运行时创建变量从而实现动态内存管理,因此只有语言提供指针这种数据类型,动态内存管理才有可能,不管这种指针是显示的(C/C++)还是隐式的(java)。所谓动态内存管理就是给你一块内存,让你在运行时自己管制,自己管理,这在很多术语中叫做堆,既然是管理,最重要的就是申请和释放了,申请很容易,毕竟我可以保证既然你来申请那么肯定是第一个了,但是你释放的时候,我可不能保证你是最后一个释放的哦,你调用delete了,但是可能还有别人在引用,但是你掉malloc,那我保证为你新分配一块空间,既然释放操作如此不便,那就必须采取措施了,在java中有垃圾回收机制,垃圾回收是一种动态的运行时机制,我们完全可以将内存管理的操作交给程序员,但是那样显得程序员的门槛过于高了,于是动态内存回收基本就是两种机制,一种是积极的方式,一种是懒惰的方式,积极的方式就是引用计数法,懒惰的方式就是标记法,这个可以参见《程序设计语言原理》一书的第六章,就不多说了,值得说一下懒惰法,就是在内存不够用的时候才实施回收,基本分为三个步骤,先将所有的堆内存打上标记,然后将所有还在用的堆内存清除标记,最后将存在标记的内存回收,而积极的方式就是在delete操作的时候递减引用计数,并且进行判断,一旦引用计数为0,那么就实施回收操作,也可以此时不进行回收,而是简单地将此内存置于垃圾链表上,让专门的垃圾回进程进行回收。其实无外乎就是这两种回收方式,接下来,我就要简单看一下linux的内存回收机制了。
linux的内核是c语言写成的,而且它的内存本质上也是由c语言写成的代码回收的,这里不考虑用户空间的标准c库的delete方式回收,毕竟delete释放了内存后最终还是要由sys_brk(linux下)通过内核来回收的,而且,无论是malloc还是delele操作,操作的都是虚拟内存,但是inux管理的是物理内存,虚拟内存是操作系统固有的机制,在32位的系统上就是4G,因此,linux内存管理的精髓就是物理内存的管理,因此,我这里要说的就是linux的物理内存回收管理机制。谈到物理内存回收,主要涉及到两个方面,一个是用户空间的物理内存回收,一个是内核空间的物理内存回收,这里要注意的是用户空间物理内存虽然回收了,但是其虚拟内存可能仍然没有被delete,这里的内核的物理内存管理和用户glibc库的内存管理是两码事。linux的用户内存管理的精髓就是页式管理,也就是说对于用户内存,是一页一页进行管理的,如果用户进程访问的虚拟内存还没有映射到物理内存,那么缺页中断处理会将页面调入,记住,只会调入一个页面,而不会调入更多的页面,不管用户的数据在虚拟内存占用几个页面,它最终只会在访问到一个地址的时候才有可能缺页,而一个地址不会占用超过一个页面的,linux的这种懒惰的方式促使用户页面的管理非常简单,就是以页面为单位的管理,用户进程的所有页面都会加入到lru链表中,内核每隔一段时间就会对lru链表进行扫描,然后回收那些没有可以被回收的物理内存,这里的要点就是,lru是固定为页面大小的内存连接而成的链表,这样的话就不会牵扯变长问题,所有的内存大小都是一样的,因此就比较好管理,内核的用户空间物理内存回收系统只需要统一的进行页面级别的扫描并回收就可以了,其实就是对lru链表进行扫面,而lru链表的对象就是单个页面,这是一种非常统一的方式,效率非常高。操作系统的请求调页机制使得每次申请的就是一个页面而不是一个数据结构,内核对内面管理是很容易的,但是却不易管理复杂的可变的用户数据结构,因此内核的用户空间的物理内存回收机制才可以如此高效和简单,简单说来就是利用基于页面的引用计数机制配合页面老化机制来加以回收,记住这里的回收不会废掉页面上的数据,而只会将数据暂时转移到一个后备空间中,因此回收不必考虑太多用户数据刷新的问题。页面老化机制其实就是一个基于效率的强化机制,它并不涉及可靠性和正确性问题,而仅仅是效率问题。
既然linux对用户空间的内存是如此对待的,那么内核空间呢?linux的内核空间内存是不可换出的,也就是说如果一个内核数据结构占据了一块物理内存,那么在此物理内存释放之前,这块物理内存将一直被它占有,这是linux内核空间物理内存管理的前提,在这个前提下,也就出现了内核独有的内存回收方式,一种和用户空间物理内存回收方式截然相反的回收方式,内核的内存回收不是基于固定大小的页面的,而是基于可变大小的数据结构的,其实正是因为内核数据结构的不可换出从而消除了换入换出导致的混乱性才使得基于数据结构的内存回收成为可能,在内核空间,所有的已分配内存不被加入到lru链表,也就是说它们不受基于页面的内存回收机制的管理,相反的,它们有自己的一套管理方式,其中包括slab的方式,vmalloc的方式,还有直接嫁接于伙伴系统之上的高端内存映射的方式,slab直接从伙伴系统得到内存,然后由slab系统将之奉还,也就是说伙伴系统没有权利主动要回分配给slab的内存,毕竟它们同样服务于内核,地位相同。然而slab本身却知道自己申请了多少内存,每个对象有多大,当slab不再用这些内存的时候,slab系统会自己将整个slab内存归还伙伴系统,在slab归还之前,伙伴系统没有任何权利自己索要内存。slab归还的内存的大小由该slab所代表的数据结构决定。
总结一下,linux内核对于用户空间的内存是按照页面以懒惰方式进行回收的,但是并非内存全部用完才回收,而是基于以下原则:1.定时回收。这样可以保证可用内存数量的稳定性;2.设置一个阀值,在可用内存数量低于这个值得时候进行回收,直到高于一个值,这样可以保证可用内存永远在一个合适的范围内。linux对用户空间内存的这种策略是为了在实现复杂度和效率以及稳定性之间作一个权衡。Linux对内核的内存回收时积极方式进行的,因为内核内存的不可换出性从而导致内核内存一旦回收就是真正的回收,因此懒惰方式回收是最有效的方式。
从这个思想我们看到一个层次问题,用户空间接收内存管理,但是用户空间的内存需求十分不确定,因此内核为了简化也为了效率,就以固定大小的页面作为管理对象,并且进行主动的强制管制措施,相反内核空间的数据结构固定,加上内核的内存需求固定导致的不可换出,那么就决定了内核的内存管理只能实行被动的管理方式,由内核自己决定何时释放。既然指针一定程度上代表的是动态的内存管理,那么这种动态性也完全适用于内核,比如slab的最新实现slub中的freelist的管理就是栈式的管理,十分有趣,可以详细读一下。因此指针的作用是强大的。十分困倦,本来想再多写一些的,实在没有词了!