引言

现在的编程语言,大多数有自己的内存管理机制,而不像C、C++需要程序员自己malloc、free内存。所谓内存管理,我的理解是,由虚拟机、编译器(编译原理有点忘记了,不太记得应该是属于哪一步了)等自动判断何时需要分配内存、怎样分配、何时需要释放内存。在PHP,这个工作是由Zend引擎来做的。

这样做的原因,一方面是简化了程序员的工作,另一方面,像PHP这种需要长时间运行的进程,一旦有内存泄漏,后果不堪设想。并且,malloc这种函数,是系统函数,会导致用户态与内核态的切换,通过预分配内存的方式,减少切换,也可以提高性能。

在写PHP扩展的时候,推荐使用的是pemalloc函数(其实是个宏),该函数可以申请persistent的内存,或者per-request的内存。前者可以在多个请求间共享,后者是每次请求结束就释放的。一般应用层面的PHP扩展,常用的都是per-request内存。

pmalloc的per-request方式,实际调用的是_emalloc函数。这条线,就涉及到PHP的内存管理了。

在pmalloc这些接口函数的下面,有heap和存储层。heap可以看作是PHP维护的一些分配好的内存,构造为堆、bucket、树等数据结构,等待接口层使用。而存储层,会真正的向操作系统请求和释放内存。

具体关注下heap层,分为小内存指针数组,和大内存指针数组,分别存储着多个不同尺寸的bucket列表(想像成二维的吧)。当需要内存时,首先根据真实size,计算一个合适的bucket大小(可能略大于实际需求),如果这个bucket列表里有可用内存,则直接使用;否则,向存储层请求一大块内存,初始化为buckets,取用其中一块。这里,怎样高效的定位到bucket列表,怎样快速找到可用bucket,freeed的bucket怎样回收,都是有趣的事情。结合到struct heap 中的bitmap字段,猜测应该使用到了位map。具体待看。

当然,heap不可能无限制的扩张,每个php进程都有memory_limit限制,如果使用的内存超过这个限制,php进程就会崩溃。

问题

以上的引言只是泛泛而谈,可能每个大型软件都会考虑这些。具体到PHP,又是怎样做的呢?

  • zend memory manager是如何管理内存的呢?具体而言,怎样分配内存、回收、碎片整理的呢?unset是否立刻触发内存释放?
  • 对PHP的内存使用,如何监控?包括VM、RSS,甚至heap、stack等?
  • 在实现层面,heap、storage层的数据结构如何?底层的malloc、mmap_xxx方法都如何使用?

基础知识与监控方法

一些内存的基础知识,强烈建议先阅读:http://www.slideshare.net/jpauli/understanding-php-memory

注意,使用cli模式启动php时,grep有两个进程:

其中第一个是bash的进程,第二个才是php进程,以下分析时,都是针对php进程而言的。

$ cat /proc/55082/status

Name: php

State: S (sleeping)

Tgid: 55082 // thread id, 必定属于某个进程

Pid: 55082  // 当前进程id

PPid: 40321  // 父进程id

TracerPid: 0 // 跟踪当前进程的进程ID,如果是0,表示没有跟踪。例如可以用strace -p跟踪进程

Uid: 11797 11797 11797 11797

Gid: 11797 11797 11797 11797

FDSize: 256 // 当前分配的文件描述符,按32、64对齐(由于fd号不能复用,所以关闭fd不会使该值降低)

Groups: 11797

VmPeak:   52836 kB // VM峰值

VmSize:   52836 kB // size of VM map

VmLck:       0 kB // 代表进程已经锁住的物理内存的大小.锁住的物理内存不能交换到硬盘

VmHWM:     1032 kB  // 程序分配到物理内存的峰值

VmRSS:     1032 kB // resident set size, 实际物理内存使用

VmAnon:     168 kB

VmFile:     864 kB

VmData:     188 kB // 数据存储空间,进程独占。注意,包含了进程内存段里的data segment和heap,即malloc动态分配的内存也在这儿

VmStk:       88 kB // size of stack segment in VM,进程独占。对应进程内存段的stack segment

VmExe:     688 kB // size of text segment in Vm,机器中运行同一程序的数个进程共享

VmLib:     1296 kB // 进程所使用lib的大小

VmPTE:       56 kB

VmSwap:       0 kB

Threads: 1

SigQ: 1/515319

SigPnd: 0000000000000000

从proc status文件能够看到内存消耗概况,例如总量、各segment用量。但到底有哪些东西在消耗内存呢?这时可以通过pmap -x <pid>命令,或者/proc/<pid>/maps ,  /proc/<pid>/smaps文件查看详情。

$man 5 proc // 可以查看proc下一些文件的说明

注意一下perms字段,其中可以显示出copy on write特性。

通过以上方法,能够看到PHP依赖的so包、PHP内核引擎和用户代码、动态数据、stack等的使用情况。但如果我们想具体看执行到某行代码时,所占用的内存大小,怎么办呢?

这时可以依赖PHP提供的memory_get_usage()方法。注意其唯一的参数,默认为false:

real_usage

Set this to TRUE to get the real size of memory allocated from system. If not set or FALSE only the memory used by emalloc() is reported.

如果设置为false,则仅显示通过emalloc分配的内存,而persistent的pemalloc分配的内存就没有包含在内了。

以一个最简单的php代码为例:

$arr = array();

for ($i = 0; $i < 100000; $i++) {

    $arr[] = str_pad($i, 'a', 10000);

}

var_dump(memory_get_usage(true));

var_dump(memory_get_usage(false));

echo "Sleep...\n";

sleep(1000);

  1. 其ps显示的vsz大小为109M,rss大小为23M,这里仅关注rss内存先,代表整个进程独立消耗的实际内存大小。
  2. cat /proc/<pid>/status,VmRss = 23236kB(与ps结论一致),VmData = 19836kB这里再关注VmData代表已初始化的全局变量、heap内存。
  3. memory_get_usage(true) = 17563648,real size of memory allocated from system
  4. memory_get_usage(false) = 17148992,only the memory used by emalloc()
但为什么memory_get_usage(true)返回的内存用量也小于VmData呢?首先,该方法度量的是通过PHP分配的内存,无论是调用emalloc族还是pemalloc族,虽然用户的php代码里所有的变量(包括“全局变量”),都包括在内。但PHP内核和Zend引擎必定有一些已初始化的全局变量,这些属于data segment,不会被memory_get_usage度量,却属于VmData。另外,PHP依赖的一些更底层的so包,如果需要内存,肯定是自行通过malloc获取的,虽然也属于heap,但也不会被memory_get_usage度量。还有不排除一些不标准的PHP扩展会绕过emalloc/pemalloc,如果自行通过malloc管理内存,那也不会被memory_get_usage度量了。所以,可以认为memory_get_usage统计的内存是VmData的一个子集。

内存管理

zval、refcount和is_ref相关基础知识,请参考,尤其是其中“变量赋值与引用计数”一段。

PHP的内存管理是由Zend Memory Manager实现的。为了整个掌管内存,Zend MM中有一个重要的zend_mm_heap结构体,其中包含了storage(存储层的各种函数指针)、可用内存buckets bitmap、可用内存buckets链表等。相关重要数据结构列举如下,其中注意zend_mm_heap的cache、free_buckets、large_free_buckets、rest_buckets都是指针的数组

heap初始化

heap一个重要属性是storage的实现,根据Zend/README.ZEND_MM说法:

The Zend MM can be tweaked using ZEND_MM_MEM_TYPE and ZEND_MM_SEG_SIZE environment

variables.  Default values are “malloc” and “256K”. Dependent on target system you

can also use “mmap_anon”, “mmap_zero” and “win32″ storage managers.

$ ZEND_MM_MEM_TYPE=mmap_anon ZEND_MM_SEG_SIZE=1M sapi/cli/php ..etc.

即Linux默认情况下使用”malloc”作为storage handler。注意,这里并不是指malloc(),而是zend_mm_mem_handlers.name字段,可选项有”win32″,”malloc”,”mmap_anon”,”mmap_zero”。

heap初始化过程如下流程图所示:

其中,zend_mm_init中对free_buckets的设置比较特殊,利用ZEND_MM_SMALL_FREE_BUCKETS宏操作内存偏移,可参考《PHP的ZEND_MM_SMALL_FREE_BUCKET》。zend_mm_init后,heap->free_buckets 如下图所示,即free_buckets里仅存储了prev和next指针,而非先存储zend_mm_free_block*指针,再开辟新内存,由它指向的区域保持prev和next以及其他信息。

heap->rest_bucket[0]和rest_bucket[1]这时都是指向与上面类似的dummy内存区域。

heap内存的使用

TODO: _zend_mm_alloc_int 

一些常用宏说明如下:

#define ZEND_MM_ALIGNED_HEADER_SIZE ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_block))

// 对齐后的 zend_mm_block 大小,包含了block最基本的信息:当前block的size,前一个block的size等

 

#define ZEND_MM_ALIGNED_FREE_HEADER_SIZE ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_small_free_block))

// 对齐后的 zend_mm_small_free_block 大小,即小块内存头信息的大小

 

#define ZEND_MM_MIN_ALLOC_BLOCK_SIZE ZEND_MM_ALIGNED_SIZE(ZEND_MM_ALIGNED_HEADER_SIZE + END_MAGIC_SIZE)

// 对齐后的 zend_mm_block 以及魔术码的大小,在第一个宏基础上添加了魔术码所需空间

 

#define ZEND_MM_ALIGNED_MIN_HEADER_SIZE (ZEND_MM_MIN_ALLOC_BLOCK_SIZE>ZEND_MM_ALIGNED_FREE_HEADER_SIZE?ZEND_MM_MIN_ALLOC_BLOCK_SIZE:ZEND_MM_ALIGNED_FREE_HEADER_SIZE)

// 对齐后的最小头信息,取第二个和第三个宏里大的那个值

 

#define ZEND_MM_ALIGNED_SEGMENT_SIZE ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_segment))

// 对齐后的zend_mm_segment大小

#define ZEND_MM_MIN_SIZE ((ZEND_MM_ALIGNED_MIN_HEADER_SIZE>(ZEND_MM_ALIGNED_HEADER_SIZE+END_MAGIC_SIZE))?(ZEND_MM_ALIGNE
D_MIN_HEADER_SIZE-(ZEND_MM_ALIGNED_HEADER_SIZE+END_MAGIC_SIZE)):0)

// 需分配的最小内存,即如果MIN_HEADER_SIZE取的是比MIN_ALLOC_BLOCK_SIZE大的值,那就是差值,否则为0

 

#define ZEND_MM_MAX_SMALL_SIZE ((ZEND_MM_NUM_BUCKETS<<ZEND_MM_ALIGNMENT_LOG2)+ZEND_MM_ALIGNED_MIN_HEADER_SIZE)

// 小块内存的上限,如果超过这个值,就得使用large_free_buckets了

#define ZEND_MM_TRUE_SIZE(size) ((size<ZEND_MM_MIN_SIZE)?(ZEND_MM_ALIGNED_MIN_HEADER_SIZE):(ZEND_MM_ALIGNED_SIZE(size+ZEND_MM_A
LIGNED_HEADER_SIZE+END_MAGIC_SIZE)))

// 如果申请内存比MIN_SIZE还小,那就直接申请MIN_HEADER_SIZE即可;否则除了size之外,还需申请头信息和魔术码所需内存

#define ZEND_MM_BUCKET_INDEX(true_size) ((true_size>>ZEND_MM_ALIGNMENT_LOG2)-(ZEND_MM_ALIGNED_MIN_HEADER_SIZE>>ZEND_MM_ALIGNMENT_LOG2))

// 计算真实数据的字节偏移量

#define ZEND_MM_SMALL_SIZE(true_size) (true_size < ZEND_MM_MAX_SMALL_SIZE)

// 是否应该使用小块内存

 

 

需注意的一个细节是heap内存的分配都是8位对齐的,通过ZEND_MM_ALIGNED_SIZE宏实现。

heap内存的回收

 

unset与内存

在启用USE_ZEND_ALLOC(默认启用)时,unset后可以看到RSS内存的明显下降,但仍大于分配这块内存前;而若关闭USE_ZEND_ALLOC,则unset后RSS内存仅略微下降。

TODO

参考资料

http://www.slideshare.net/jpauli/understanding-php-memory

http://www.laruence.com/2011/11/09/2277.html

https://wiki.php.net/internals/zend_mm

http://www.webreference.com/programming/php_mem/

http://www.ibm.com/developerworks/cn/opensource/os-php-v521/

http://blog.csdn.net/zjl410091917/article/details/8075691  proc status 字段含义介绍

http://www.cnblogs.com/jiayy/p/3458076.html proc下文件含义介绍

http://c.biancheng.net/cpp/html/476.html 指针数组和数组指针

 

Leave a Reply