动态内存管理
动态内存管理
动态内存管理DMM(Dynamic Memory Management)是从Heap中直接分配内存和回收内存。
有两种方法实现动态内存管理。
一是显示内存管理EMM(Explicit Memory Management)。
在EMM方式,内存从Heap中进行分配,用完后手动回收。程序使用malloc()函数分配整数数组,并使用free()函数释放分配的内存。
二是自动内存管理AMM(Automatic Memory Management)。
AMM也可叫垃圾回收器(Garbage Collection)。Java编程语言实现了AMM,与EMM不同,Run-time system关注已分配的内存空间,一旦不再使用,立即回收。
无论是EMM还是AMM,所有的Heap管理计划都面临一些共同的问题和前在的缺陷:
1)内部碎片(Internal Fragmentation)
当内存有浪费时,内部碎片出现。因为内存请求可导致分配的内存块过大。比如请求128字节的存储空间,结果Run-time system分配了512字节。
2)外部碎片(External Fragmentation)
当一系列的内存请求留下了数个有效的内存块,但这些内存块的大小均不能满足新请求服务,此时出现外部碎片。
3)基于定位的延迟(Location-based Latency)
延迟问题出现在两个数据值存储得相隔很远,导致访问时间增加。
EMM往往比AMM更快。
EMM与AMM比较表:
——————————————————————————————————————
EMM AMM
——————————————————————————————————————
Benefits 尺寸更小、速度更快、易控制 stay focused on domain issues
Costs 复杂、记账、内存泄露、指针悬空 不错的性能
——————————————————————————————————————
早期的垃圾回收器非常慢,往往占用50%的执行时间。
垃圾回收器理论产生于1959年,Dan Edwards在Lisp编程语言的开发时实现了第一个垃圾回收器。
垃圾回收器有三种基本的经典算法:
1)Reference counting(引用计数)
基本思想是:当对象创建并赋值时该对象的引用计数器置1,每当对象给任意变量赋值时,引用记数+1;一旦退出作用域则引用记数-1。一旦引用记数变为0,则该对象可以被垃圾回收。
引用记数有其相应的优势:对程序的执行来说,每次操作只需要花费很小块的时间。这对于不能被过长中断的实时系统来说有着天然的优势。
但也有其不足:不能够检测到环(两个对象的互相引用);同时在每次增加或者减少引用记数的时候比较费时间。
在现代的垃圾回收算法中,引用记数已经不再使用。
2)Mark-sweep(标记清理)
基本思想是:每次从根集出发寻找所有的引用(称为活对象),每找到一个,则对其做出标记,当追踪完成之后,所有的未标记对象便是需要回收的垃圾。
也叫追踪算法,基于标记并清除。这个垃圾回收步骤分为两个阶段:在标记阶段,垃圾回收器遍历整棵引用树并标记每一个遇到的对象。在清除阶段,未标记的对象被释放,并使其在内存中可用。
3)Copying collection(复制收集)
基本思想是:将内存划分为两块,一块是当前正在使用;另一块是当前未用。每次分配时使用当前正在使用内存,当无可用内存时,对该区域内存进行标记,并将标记的对象全部拷贝到当前未用内存区,这是反转两区域,即当前可用区域变为当前未用,而当前未用变为当前可用,继续执行该算法。
拷贝算法需要停止所有的程序活动,然后开始冗长而繁忙的copy工作。这点是其不利的地方。
近年来还有两种算法:
1)Generational garbage collection(分代)
其思想依据是:
(1) 被大多数程序创建的大多数对象有着非常短的生存期。
(2) 被大多数程序创建的部分对象有着非常长的生存期。
简单拷贝算法的主要不足是它们花费了更多的时间去拷贝了一些长期生存的对象。
而分代算法的基本思想是:将内存区域分两块(或更多),其中一块代表年轻代,另一块代表老的一代。针对不同的特点,对年轻一代的垃圾收集更为频繁,对老代的收集则较少,每次经过年轻一代的垃圾回收总会有未被收集的活对象,这些活对象经过收集之后会增加成熟度,当成熟度到达一定程度,则将其放进老代内存块中。
分代算法很好的实现了垃圾回收的动态性,同时避免了内存碎片,是目前许多JVM使用的垃圾回收算法。
2)Conservative garbage collection(保守)
哪一种算法最好?答案是没有最好。
EMM作为很常用的垃圾回收算法,有5种基本方法:
1)Table-driven algorithms
表驱动算法把内存分为固定尺寸的块集合。这些块使用抽象数据结构进行索引。比如一个bit对应一个块,用0和1表示是否分配。不利因素:位映射依赖于内存块的尺寸;另外,搜索一系列的空闲内存块可能需要搜索整个bit映射表,这影响性能。
2)Sequential fit
顺序适应算法允许内存分为不同的尺寸。此算法跟踪已分配和空闲的Heap,标记空闲块的起始地址和结束地址。它有三种子分类:
(1) First fit(首次适应)——分配找到的第一个适合内存请求的块
(2) Best fit(最佳适应)——分配最适合内存请求的块
(3) Worst fit(最不适应)——分配最大的块给内存请求
3)Buddy systems
Buddy systems算法的主要目的是加速已分配内存在释放后的合并速度。显示内存管理EMM使用Buddy systems算法可能导致内部碎片。
4)Segregated storage
隔离存储技术涉及到把Heap分成多个区域(zone),并为每个区域采用不同的内存管理计划。这是很有效的方法。
5)Sub-allocators
子配置技术尝试解决在Run-time System下分配大块内存并单独管理的内存分配问题。换句话说,程序完全负责自己的私有存储堆(stockpile)的内存分配和回收,无需run-time System的帮助。它可能带来额外的复杂性,但是你可以显著地提高性能。在1990年的《C Compiler Design》一书中,Allen Holub就极好地利用了Sub-allocators来加速其编译器的实现。
注意,显示内存管理EMM必须是灵活的,能够响应数种不同类型的请求。
最后,使用EMM还是使用AMM?这是一个Religious question,凭个人喜好。EMM在复杂的开销下实现了速度和控制。AMM牺牲了性能,但换来了简单性。