PHP内存的一些简单理解

今天在群里看到一个问题 “PHP执行中内存是什么样子的?”,我还真不知道…找了点资料


内存,是什么

1.来自硬件的内存
2.来自软件的内存

内存映像是按 来分配的

1.文本(Text)这个一般就是代码段了 通常用来存储程序执行的代码 比如函数和方法。代码段需要防止在运行时被非法修改,所以只准读取操作,而不允许写入(修改)操作。
2.数据(Data)通常是指用来存放程序中已初始化且不为0的全局变量,如:静态变量和常量。
3.堆(Heap) 用来存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。用于存储数据长度可变或占用内存比较大的数据。例如,字符串、数组和对象就存储在这段内存中。
4.栈(Stack) 他的特点是空间小但被CPU访问的速度快,是用户存放程序中临时创建的变量。由于栈的后进先出特点,所以栈特别方便用来保存和恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个临时数据寄存、交换的内存区。用于存储占用空间长度不变且占用空间小的数据类型的内存段,例如整型1、100、100000等在内存中占用空间是等长的,占用的空间都是32位4个字节。还有double、boolean等都可以存储在栈空间段中。

Zend内存管理

Zend Memory Manager(Zend内存管理) 就是php内部请求绑定堆分配器。

它会一次申请很多的内存放在内存池中,当我们需要内存的时候就会从内存池当取出一块,然后判断有没有时候的内存给调用,如果够的话直接给一块内存 如果不够的话ZendMM就会再次向系统申请内存,另外他里面还有个CopyOnWrite(复制代替写入?)的机制也可以提高内存管理的效率,对了还有垃圾回收机制!

可以通过 phpinfo() 来查看 如果发现 有 Zend Memory Manager = enabled 就表示开启了Zend内存管理

允许通过基本计算 malloc() / free() 调用来监视请求限制的堆使用
允许PHP用户限制堆内存使用量
允许缓存已分配的块以防止内存碎片和系统调用
允许预先分配PHP内部结构的已知大小的块以对齐的方式适应
简化核心和扩展中的内存泄露调试

zend_memory_manager

看一个Zend内存管理的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini_set('memory_limit', -1); /*不设置Zend Memory Manager 的堆内存限制*/
function heap()
{
return shell_exec(sprintf('grep "VmRSS:" /proc/%d/status', getmypid()));
}
echo heap();
$a = range(1, 1024 * 1024); // 分配内存
echo heap();
unset($a); // 或者 $a = null; 释放内存
echo heap();
// 结果分别是
// VmRSS: 87268 kB
// VmRSS: 120036 kB
// VmRSS: 87268 kB

PHP内存函数

在PHP中有些函数可以设置与获取内存

ini_set(‘memory_limit’, xxx); // 这个可以设置内存
memory_get_usage(); // 返回分配给PHP的内存量
memory_get_usage(true); // 返回所有已分配段的大小
memory_get_peak_usage(); // 返回已分配给PHP脚本的内存峰值(以字节为单位)

zend_memory_monitor

说下 memory_get_usage() 函数 它只显示请求绑定的分配,而不是持久分配(通过请求驻留)。PHP扩展可以分配持久内存,如果你不会用到他们的话不要启用。被PHP用到的库也会分配持久内存,使用你的系统来准确的监控你的进程内存消耗

管理PHP内存

在PHP中 所有的变量类型都会消耗内存,每个要被编译的脚本都会吃掉内存, 这部分内存可以使用zned内存管理来分配。请求结束时释放已解析脚本的内存。

当用户变量不在使用的时候 这部分用户变量将会被释放,虽然是这么讲 这里会有个问题 什么时候这些变量才是不被需要 不会在使用的呢?(垃圾回收机制)

编译脚本的时候会占用请求限制的内存。如果你编译了一个类,那将会吃掉更多的内存,所以最好是在运行时使用它(那个类) 并确保使用了 autoloader 自动加载

在PHP中所有的变量在底层都是一个 zval 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _zval_struct {
zvalue_value value;
zend_uint refcount;
zend_uchar type;
zend_uchar is_ref;
} zval;
...
typedef union _zvalue_value {
long lval;
double dval;
struct {
char *val;
int len;
} str;
HashTable *ht;
zend_object_value obj;
} zvalue_value;

吃掉内存的时候 zval 中的东西 而不是 zval 结构本身,比如一个很长的字符串 一个很复杂的数组或者对象,资源类型不会真正的消耗zval中的内存。
尽量避免让PHP复制zval,另外PHP只计算有多少符号指向zval 这是他的引用计数
PHP对zval操作是使用复制代替写入的系统,只有当发生改变的时候内存才会得到分配

memory_runtime

如上图(注意引用计数的变化) 假设我们执行了4行代码

当执行 $a = 'foo'; 的时候首先会在内存申请一块空间 其中存有他的值 和 他的引用计数
当执行 $b = $a; 的时候 $a 和 $b 都是指向了同一块内存地址
当执行 $a = 17; 的时候,$a 发生了类型改变 这个时候就会重新分配内存 所以发生了上面的操作 $b 还是指向一开始申请的内存地址 也就是字符串 foo 的内存,而$a 已经指向了新申请的内存地址 int 17上
当执行 $b = null; 的时候会回收内存 $b 就不存在了 就只剩下 $a了

看下内存消耗情况 代码太渣 直接看结果吧 因为在浏览器输出的 加了 <br/> 为了明显看出内存的变化 我把 $a = 17 换了重新赋值字符串 这样内存变化比较明显

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
ini_set('memory_limit', -1); /*不设置Zend Memory Manager 的堆内存限制*/
function get_mem_status()
{
printf("Memory usage: %.2f Kb <br/>", memory_get_usage() / 1024);
if ($segmentSize = getenv('ZEND_MM_SEG_SIZE')) {
printf("Heap segmentation : %d segments of %d bytes (%d kb used)\n\r", memory_get_usage(1) / $segmentSize, $segmentSize, memory_get_usage(1) / 1024);
}
}
echo "初始化内存:";
get_mem_status();
$a = str_repeat('a', 1024 * 1024);
echo "声明了变量a之后的内存:";
get_mem_status();
$b = $a;
// $a = 17;
$a = str_repeat('b', 1024);
echo "变量b的类型是:".gettype($b)."<br/>";
echo "变量a的类型是:".gettype($a)."<br/>";
echo "此时的内存:";
get_mem_status();
$b = null;
echo "手动释放变量b后的类型是:".gettype($b)."<br/>";
echo "变量a的类型是:".gettype($a)."<br/>";
echo "手动释放变量b最后的内存:";
get_mem_status();
$a = str_repeat('a', 1024 * 1024);
$b = $a;
$a = 17;
$b = null; // or unset($b);
下面是结果 结合上面的图就好理解一点了
1
2
3
4
5
6
7
8
初始化内存:Memory usage: 348.51 Kb
声明了变量a之后的内存:Memory usage: 1376.51 Kb
变量b的类型是:string
变量a的类型是:string
此时的内存:Memory usage: 1377.76 Kb
手动释放变量b后的类型是:NULL
变量a的类型是:string
手动释放变量b最后的内存:Memory usage: 349.76 Kb

可以看出声明了变量 $a 后内存变成了 1376.51 可以计算得出$a占用内存 1028 Kb
当执行$b = $a; 的时候只是把$b指向了同一块内存地址 堆内存中并没有发生改变 内存大小没变
当执行$a = str_repeat(‘b’, 1024); 的时候$a发生了改变需要重新分配内存 所以$a重新开辟了内存出来, 而$b还是指向原来的内存地址(那个地址还是占用1028kb) 这时候的内存因为$a新分配的内存而增加了 变成 1377.76kb 可以计算出新的$a的 内存是 1.25kb
当执行 $b = null; 的时候相当于释放了$b 也就是他指向的那块内存(大小1028kb)被释放,所以后面获取的内存是 1377.76-1028 = 349.76kb

PHP用户空间内存泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function get_mem_status()
{
echo "<br/>";
printf("Memory usage: %.2f Kb <br/>", memory_get_usage() / 1024);
echo "<br/>";
}
class A
{
public $b;
}
class B
{
public $a;
}
get_mem_status();
$a = new A(); // 这时 $a 指向 ObjectA refcount = 1
get_mem_status();
$b = new B(); // 这时 $b 指向 ObjectB refcount = 1
get_mem_status();
$a->b = $b; // 这个时候 $a->b 指向了 ObjectB refcount = 2
$b->a = $a; // 这个时候 $b->a 指向 ObjectA refcount = 2
unset($a, $b); // 这个时候 $a->b 还是指向了 ObjectB 只是 refcount = 1 $b->a 还是指向了 ObjectA 只是 refcount = 1
get_mem_status();
// 结果
// Memory usage: 349.30 Kb
// Memory usage: 349.36 Kb
// Memory usage: 349.41 Kb
// Memory usage: 349.41 Kb

如上代码 最后你会发现即使释放了$a 和 $b 内存也没有减少,因为这种情况下,对象仍然存在内存当中即使没有别的东西引用他但他的引用计数是1 所以还会占用内存 这种就是PHP的用户空间内存泄露

zend_gc

监控内存

Linux下可以使用 top 命令等查看

1
2
3
4
5
6
7
8
9
10
[test@web110 www]$ top
top - 17:38:17 up 41 days, 1:30, 13 users, load average: 1.23, 1.30, 1.44
Tasks: 1579 total, 2 running, 1576 sleeping, 0 stopped, 1 zombie
%Cpu(s): 33.1 us, 3.2 sy, 0.0 ni, 63.7 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
KiB Mem : 8011568 total, 507840 free, 5467360 used, 2036368 buff/cache
KiB Swap: 4325372 total, 1001356 free, 3324016 used. 1433472 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
910 root 20 0 4277076 563328 13820 S 52.6 7.0 3671:10 dockerd
1687 root 20 0 2595720 72488 3532 S 35.5 0.9 3328:52 java

找到上面的 pid 比如上面 910 的 docker 然后执行 cat /proc/910/status

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[test@lcoalhost ~]$ cat /proc/910/status
Name: dockerd
State: S (sleeping)
Pid: 910
...
FDSize: 1024
Groups:
VmPeak: 4277588 kB
VmSize: 4277076 kB
VmHWM: 581216 kB
VmRSS: 564340 kB 这个是常驻内存大小
VmData: 4188136 kB 这个是 Data段的内存大小
VmStk: 136 kB 这个是 Stack段的内存大小
VmExe: 37352 kB 这个是 Text段的内存大小
VmLib: 5896 kB
VmPTE: 2328 kB
VmSwap: 51004 kB
...

还可以通过 cat /proc/910/smaps来查看更具体的内存

1
2
3
4
5
6
7
8
9
[test@localhost ~]# cat /proc/910/smaps
7f8dc9414000-7f8dc9454000 r--s 00000000 fd:00 25365236 /var/lib/docker/containerd/daemon/io.containerd.metadata.v1.bolt/meta.db
Size: 256 kB
Shared_Clean: 0 kB 这个是已经分享的内存
Shared_Dirty: 0 kB 这个同上
Private_Clean: 80 kB 这个是私有内存
Private_Dirty: 0 kB 同上
Referenced: 80 kB
...

PHP他本身也只是像其他进程一样 只是个进程而已,也可以使用上面的系统命令来监控他的内存

1
2
3
4
5
6
function heap()
{
return shell_exec(sprintf('grep "VmRSS:" /proc/%d/status', getmypid()));
}
echo heap();

参考

http://www.laruence.com/2008/08/22/412.html;

http://mars.run/2016/01/Understanding-PHP-memory-management/

https://gywbd.github.io/posts/2015/4/php-variable-in-memory.html

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


-------------The End-------------