内存是计算机?常关键的部件之?,是暂时存储程序以及数据的空间,CPU只有有限的寄存器可以?于存储计算数据,??部分的数据都是存储在内存中的,程序运?都是在内存中进?的。和CPU计算能??样, 内存也是决定计算效率的?个关键部分。
计算中的资源中主要包含:CPU计算能?,内存资源以及I/O。现代计算机为了充分利?资源,?出现了多任务操作系统,通过进程调度来共享CPU计算资源,通过虚拟存储来分享内存存储能?。 本章的内存管理中不会介绍操作系统级别的虚拟存储技术,?是关注在应?层?:如何?效的利?有限的内存资源。
?前除了使?C/C++等这类的低层编程语?以外,很多编程语?都将内存管理移到了语?之后, 例如Java,
各种脚本语?:PHP/Python/Ruby等等,程序?动维护内存的成本?常?,
?这些脚本语?或新型语?都专注于特定领域,这样能将程序员从内存管理中解放出来专注于业务的实现。
虽然程序员不需要?动
维护内存,?在程序运?过程中内存的使?还是要进?管理的, 内存管理的?作也就编程语?实现程序员的?作了。
内存管理的主要?作是尽可能?效的利?内存。
内存的使?操作包括申请内存,销毁内存,修改内存的??等。如果申请了内存在使?完后没有及时释放则可能会造成内存泄露,如果这种情况出现在常驻程序中,久?久之,程序会把机器的内存耗光。所以对于类似于PHP这样没有低层内存管理的语?来说, 内存管理是其?关重要的?个模块,它在很?程序上决定了程序的执?效率。
在PHP层?来看,定义的变量、类、函数等等实体在运?过程中都会涉及到内存的申请和释放, 例如变量可能会在超出作?域后会进?销毁,在计算过程中会产?的临时数据等都会有内存操作, 像类对象,函数定义等数据则会在请求结束之后才会被释放。在这过程中合适申请内存合适释放内存就?较关键了。PHP从开始就有?套属于??的内存管理机制,在5.3之前使?的是经典的引?计数技术, 但引?技术存在?定的技术缺陷,在PHP5.3之后,引?了新的垃圾回收机制,?此,PHP的内存管理机制更加完善。
本章将介绍PHP语?实现中的内存管理技术实现。
第?节 内存管理概述
从某个意义上讲,资源总是有限的,计算机资源也是如此,衡量?个计算机处理能?的指标有很多,根据不同的应?需要也会有不同的指标,?如3D游戏对显卡的性能有要求,?Web服务器对吞吐量及响应时间有要求, 通常CPU、内存及硬盘的读取和计算速度具有决定性的作?,在同?时刻这些资源是有限的, 正是因为有限我们才需要合理的利?他们。
操作系统的内存管理
当计算机的电源被打开之后,不管你使?的是什么操作系统,这些软件可能已经在使?内存了。 这是由计算机的结构决定的,操作系统也是?种软件,只不过它是?较特殊的软件, 管理计算机的所有资源,普通应?程序和操作系统的关系有点像?师和学?,?师通常管理?切, ?学?的?为会受到?师或学校规定的限制,例如普通应?程序?法直接访问物理内存或者其他硬件资源。
操作系统直接管理着内存,所以操作系统也需要进?内存管理,内存管理是如此之重要, 计算机中通常都有内存管理单元(MMU) ?于处理CPU对内存的访问。
应?层的内存管理
由于计算机的内存由操作系统进?管理,所以普通应?程序是?法直接对内存进?访问的,
应?程序只能向操作系统申请内存,通常的应?也是这么做的,在需要的时候通过类似malloc之类的库函数向操作系统申请内存,在?些对性能要求较?的应?场景下是需要频繁的使?和释放内存的,
?如Web服务器,
编程语?等,由于向操作系统申请内存空间会引发系统调?,系统调?和普通的应?层函数调?性能差别?常?,因为系统调?会将CPU从?户态切换到内核,
因为涉及到物理内存的操作,只有操作系统才能进?,?这种切换的成本是?常?的,如果频繁的在内核态和?户态之间切换会产?性能问题。
鉴于系统调?的开销,?些对性能有要求的应?通常会??在?户态进?内存管理, 例如第?次申请稍?的内存留着备?,?使?完释放的内存并不是马上归还给操作系统, 可以将内存进?复?,这样可以避免多次的内存申请和释放所带来的性能消耗。
PHP不需要显式的对内存进?管理,这些?作都由Zend引擎进?管理了。PHP内部有?个内存管理体系, 它会?动将不再使?的内存垃圾进?释放,这部分的内容后?的?节会介绍到。
PHP中内存相关的功能特性
可能有很多的读者碰到过类似下?的错误吧:
Fatal error: Allowed memory size of X bytes exhausted (tried to allocate Y bytes)
这个错误的信息很明确,PHP已经达到了允许使?的最?内存了,通常上来说这很有可能是我们的程序编写的有些问题。 ?如:?次性读取超?的?件到内存中,或者出现超?的数组,或者在?循环中的没有及时是放掉不再使?的变量, 这些都有可能会造成内存占?过??被终?。
PHP默认的最?内存使???是32M, 如果你真的需要使?超过32M的内存可以修改php.ini配置?件的如下配置:
memory_limit = 32M
如果你?法修改php配置?件,如果你的PHP环境没有禁?ini_set()函数,也可以动态的修改最?的内存占???:
<?php ini_set("memory_limit", "128M");
既然我们能动态的调整最?的内存占?,那我们是否有办法获取?前的内存占?情况呢?答案是肯定的。
1. memory_get_usage(),这个函数的作?是获取?前PHP脚本所?的内存??。
2.
memory_get_peak_usage(),这个函数的作?返回
当前脚本到?前位置所占?的内存峰值,这样就可能获取到?前的脚本的内存需求情况。
单就PHP?户空间提供的功能来说,我们似乎?法控制内存的使?,只能被动的获取内存的占?情况, 这样的话我们学习内存管理有什么?呢?
前?的章节有介绍到引?计数,函数表,符号表,常量表等。当我们明?这些信息都会占?内存的时候, 我们可以有意的避免不必要的浪费内存,?如我们在项?中通常会使?autoload来避免?次性把不?定会使?的类 包含进来,?这些信息是会占?内存的,如果我们及时把不再使?的变量unset掉之后可能会释放掉它所占?的空间。
前?之所以会说把变量unset掉时候可能会把它释放掉的原因是: 在PHP中为了避免不必要的内存复制,采?了引?计数和写时复制的技术, 所以这?unset只是将引?关系打破,如果还有其他变量指向该内存, 它所占?的内存还是不会被释放的。当然这还有?种情况:出现循环引?,这个就得靠gc来处理了, 内存不会当时就是放,只有在gc环节才会被释放。
后?的章节主要介绍PHP在运?时的内存使?和管理细节。这也能帮助我们写出更为内存友好的PHP代码。
第?节 PHP中的内存管理
在前?的?节中我们介绍了内存管理?般会包括以下内容:
1. 是否有?够的内存供我们的程序使?;
2. 如何从?够可?的内存中获取部分内存;
3.
对于使?后的内存,是否可以将其销毁并将其重新分配给其它程序使?。
与此对应,PHP的内容管理也包含这样的内容,只是这些内容在ZEND内核中是以宏的形式作为接?提供给外部使?。 后?两个操作分别对应emalloc宏、efree宏,?第?个操作可以根据emalloc宏返回结果检测。
PHP的内存管理可以被看作是分层(hierarchical)的。
它分为三层:存储层(storage)、堆层(heap)和接?层(emalloc/efree)。
存储层通过 malloc()、mmap() 等函数向系统真正的申请内存,并通过 free() 函数释放所申请的内存。
存储层通常申请的内存块都?较?,这?申请的内存?并不是指
storage层结构所需要的内存?,
只是堆层通过调?存储层的分配?法时,其以?块?块的?式申请的内存,存储层的作?是将内存分配的?式对堆层透明化。
如图6.1所?,PHP内存管理器。PHP在存储层共有4种内存分配?案: malloc,win32,mmap_anon,mmap_zero,
默认使?malloc分配内存,如果设置了ZEND_WIN32宏,则为windows版本,调?HeapAlloc分配内存,
剩下两种内存?案为匿名内存映射,并且PHP的内存?案可以通过设置环境变量来修改。
图6.1 PHP内存管理器
?先我们看下接?层的实现,接?层是?些宏定义,如下:
/* Standard wrapper macros */ #define emalloc(size) _emalloc((size) ZEND_FILE_LINE_CC,ZEND_FILE_LINE_EMPTY_CC) #define safe_emalloc(nmemb, size, offset) _safe_emalloc((nmemb), (size),(offset) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define efree(ptr) _efree((ptr) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define ecalloc(nmemb, size) _ecalloc((nmemb), (size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define erealloc(ptr, size) _erealloc((ptr), (size), 0 ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define safe_erealloc(ptr, nmemb, size, offset) _safe_erealloc((ptr), (nmemb), (size), (offset) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define erealloc_recoverable(ptr, size) _erealloc((ptr), (size), 1 ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define estrdup(s) _estrdup((s) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define estrndup(s, length) _estrndup((s), (length) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC) #define zend_mem_block_size(ptr) _zend_mem_block_size((ptr) TSRMLS_CC ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
这?为什么没有直接调?函数?因为这些宏相当于?个接?层或中间层,定义了?个?层次的接?,使得调?更加容易。它隔离了外部调?和PHP内存管理的内部实现,实现了?种松耦合关系。虽然PHP不限制这些函数的使?, 但是官??档还是建议使?这些宏。这?的接?层有点门?模式(facade模式)的味道。
在接?层下?是PHP内存管理的核?实现,我们称之为heap层。 这个层控制整个PHP内存管理的过程,?先我们看这个层的结构:
/* mm block type */ typedef struct _zend_mm_block_info { size_t _size; /* block的??*/ size_t _prev; /* 计算前?个块有?到*/ } zend_mm_block_info; typedef struct _zend_mm_block { zend_mm_block_info info; } zend_mm_block; typedef struct _zend_mm_small_free_block { /* 双向链表 */ zend_mm_block_info info; struct _zend_mm_free_block *prev_free_block; /* 前?个块 */ struct _zend_mm_free_block *next_free_block; /* 后?个块 */ } zend_mm_small_free_block; /* ?的空闲块*/ typedef struct _zend_mm_free_block { /* 双向链表 + 树结构 */ zend_mm_block_info info; struct _zend_mm_free_block *prev_free_block; /* 前?个块 */ struct _zend_mm_free_block *next_free_block; /* 后?个块 */ struct _zend_mm_free_block **parent; /* ?结点 */ struct _zend_mm_free_block *child[2]; /* 两个?结点*/ } zend_mm_free_block;
struct _zend_mm_heap { int use_zend_alloc; /* 是否使?zend内存管理器 */ void *(*_malloc)(size_t); /* 内存分配函数*/ void (*_free)(void*); /* 内存释放函数*/ void *(*_realloc)(void*, size_t); size_t free_bitmap; /* ?块空闲内存标识 */ size_t large_free_bitmap; /* ?块空闲内存标识*/ size_t block_size; /* ?次内存分配的段??,即ZEND_MM_SEG_SIZE指定的??,默认为ZEND_MM_SEG_SIZE (256 * 1024)*/ size_t compact_size; /* 压缩操作边界值,为ZEND_MM_COMPACT指定??,默认为 2 * 1024 * 1024*/ zend_mm_segment *segments_list; /* 段指针列表 */ zend_mm_storage *storage; /* 所调?的存储层 */ size_t real_size; /* 堆的真实?? */ size_t real_peak; /* 堆真实??的峰值 */ size_t limit; /* 堆的内存边界 */ size_t size; /* 堆?? */ size_t peak; /* 堆??的峰值*/ size_t reserve_size; /* 备?堆??*/ void *reserve; /* 备?堆 */ int overflow; /* 内存溢出数*/ int internal; #if ZEND_MM_CACHE unsigned int cached; /* 已缓存?? */ zend_mm_free_block *cache[ZEND_MM_NUM_BUCKETS]; /* 缓存数组/ #endif zend_mm_free_block *free_buckets[ZEND_MM_NUM_BUCKETS*2]; /* ?块内存数组,相当索引的?? */ zend_mm_free_block *large_free_buckets[ZEND_MM_NUM_BUCKETS]; /* ?块内存数组,相当索引的?? */ zend_mm_free_block *rest_buckets[2]; /* 剩余内存数组*/ };
当初始化内存管理时,调?函数是zend_mm_startup。它会初始化storage层的分配?案,
初始化段??,压缩边界值,并调?zend_mm_startup_ex()初始化堆层。
这?的分配?案就是图6.1所?的四种?案,它对应的环境变量名为:ZEND_MM_MEM_TYPE。
这?的初始化的段??可以通过
ZEND_MM_SEG_SIZE设置,如果没设置这个环境变量,程序中默认为256 * 1024。
这个值存储在_zend_mm_heap结构的block_size字段中,将来在维护的三个列表中都没有可?的内存中,会参考这个值的??来申请内存的??。
PHP中的内存管理主要?作就是维护三个列表:?块内存列表(free_buckets)、 ?块内存列表(large_free_buckets)和剩余内存列表(rest_buckets)。 看到bucket这个单词是不是很熟悉?在前?我们介绍HashTable时,这就是?个重要的??,它作为HashTable中的?个单元??。 在这?,每个bucket也对应?定??的内存块列表,这样的列表都包含双向链表的实现。
我们可以把维护的前?两个表看作是两个HashTable,那么,每个HashTable都会有??的hash函数。 ?先我们来看free_buckets列表,这个列表?来存储?块的内存分配,其hash函数为:
#define ZEND_MM_BUCKET_INDEX(true_size)
((true_size>>ZEND_MM_ALIGNMENT_LOG2)-(ZEND_MM_ALIGNED_MIN_HEADER_SIZE>>ZEND_MM_ALIGNMENT_LOG2))
假设ZEND_MM_ALIGNMENT为8(如果没有特殊说明,本章的ZEND_MM_ALIGNMENT的值都为8),则ZEND_MM_ALIGNED_MIN_HEADER_SIZE=16,
若此时true_size=256,则((256>>3)-(16>>3))= 30。
当ZEND_MM_BUCKET_INDEX宏出现时,ZEND_MM_SMALL_SIZE宏?般也会同时出现,
ZEND_MM_SMALL_SIZE宏的作?是判断所申请的内存??是否为?块的内存,
在上?的?例中,
?于272Byte的内存为?块内存,则index最多只能为31, 这样就保证了free_buckets不会出现数组溢出的情况。
在内存管理初始化时,PHP内核对初始化free_buckets列表。 从heap的定义我们可知free_buckets是?个数组指针,其存储的本质是指向zend_mm_free_block结构体的指针。 开始时这些指针都没有指向具体的元素,只是?个简单的指针空间。 free_buckets列表在实际使?过程中只存储指针,这些指针以两个为?对(即数组从0开始,两个为?对),分别存储?个个双向链表的头尾指针。 其结构如图6.2所?。
图6.2 free_buckets列表结构
对于free_buckets列表位置的获取,关键在于ZEND_MM_SMALL_FREE_BUCKET宏,宏代码如下:
#define ZEND_MM_SMALL_FREE_BUCKET(heap, index) \ (zend_mm_free_block*) ((char*)&heap->free_buckets[index * 2] + sizeof(zend_mm_free_block*) * 2 -sizeof(zend_mm_small_free_block))
仔细看这个宏实现,发现在它的计算过程是取free_buckets列表的偶数位的内存地址加上 两个指针的内存?? 并减去zend_mm_small_free_block结构所占空间的??。 ?zend_mm_free_block结构和zend_mm_small_free_block结构的差距在于两个指针。 据此计算过程可知,ZEND_MM_SMALL_FREE_BUCKET宏会获取free_buckets列表 index对应双向链表的第?个zend_mm_free_block的prev_free_block指向的位置。 free_buckets的计算仅仅与prev_free_block指针和next_free_block指针相关, 所以free_buckets列表也仅仅需要存储这两个指针。
那么,这个数组在最开始是怎样的呢? 在初始化函数zend_mm_init中free_buckets与large_free_buckts列表?起被初始化。 如下代码:
p = ZEND_MM_SMALL_FREE_BUCKET(heap, 0); for (i = 0; i < ZEND_MM_NUM_BUCKETS; i++) { p->next_free_block = p; p->prev_free_block = p; p = (zend_mm_free_block*)((char*)p + sizeof(zend_mm_free_block*) * 2); heap->large_free_buckets[i] = NULL; }
继续...