Gitlib Gitlib
首页
  • 分类
  • 标签
  • 归档
  • Golang开发实践万字总结
  • MySQL核心知识汇总
  • Redis实践总结
  • MQ实践万字总结
  • Docker数据持久化总结
  • Docker网络模式深度解读
  • 常用游戏反外挂技术总结
  • 读书笔记
  • 心情杂货
  • 行业杂谈
  • 友情链接
关于我
GitHub (opens new window)

Ravior

以梦为马,莫负韶华
首页
  • 分类
  • 标签
  • 归档
  • Golang开发实践万字总结
  • MySQL核心知识汇总
  • Redis实践总结
  • MQ实践万字总结
  • Docker数据持久化总结
  • Docker网络模式深度解读
  • 常用游戏反外挂技术总结
  • 读书笔记
  • 心情杂货
  • 行业杂谈
  • 友情链接
关于我
GitHub (opens new window)
  • PHP

    • PHP-FPM使用指南
    • PHP7新特性总结
    • PHP安全编程
    • PHP安全配置总结
    • PHP变量的值类型和引用类型
    • PHP标准规范PSR
    • PHP操作Zookeeper实践
    • PHP错误和异常处理机制详解
    • PHP的Session运行机制
    • PHP底层运行机制和原理
    • PHP反射模拟实现注解路由
    • PHP高级用法总结
    • PHP开发常用文档总结
    • PHP开发入门:Memcached扩展安装
    • PHP开发入门:PHP7安装部署
    • PHP开发入门:Redis扩展安装
    • PHP开发SPL总结
    • PHP框架常见URL模式
    • PHP扩展开发入门
    • PHP垃圾回收机制
      • 变量存储结构
      • 引用计数原理
      • 内存泄漏
      • 根缓冲机制
      • 为什么内存没有全部收回来
      • 垃圾回收相关的配置
      • 小知识点
      • 参考文档
    • PHP类的自动加载
    • PHP输入输出流
    • PHP微服务开发指南
    • PHP协程
    • PHP写时拷贝技术
    • PHP性能优化之Opcache
    • PHP依赖注入和控制反转
    • PHP运行模式(SAPI)
    • PHP中file_get_contents与curl区别
    • RPC的简单实现
    • Protobuf:高效数据结构化工具
    • P3P协议详解
    • Laravel之集合(Collection)总结
    • Laravel实践总结
    • Laravel之ORM总结
    • 中高级PHP实践总结
    • PHP Socket编程实战
  • Golang

  • Python

  • Javascript

  • 其他语言

  • 编程语言
  • PHP
Ravior
2018-08-31
目录

PHP垃圾回收机制

PHP垃圾回收机制是基于PHP的变量引用计数机制实现的,在了解引用计数机制之前,我们先了解一下PHP变量存储结构。

# 变量存储结构

在之前的文章PHP写时拷贝技术中,提到过PHP的变量容器通过refcont进行引用计数。在PHP7之后,zval结构发生了变化,以下面代码运行结果为例:

<?php

$startMemory = memory_get_usage();

$i = 4;
xdebug_debug_zval('i');
echo 'memory: ', memory_get_usage() - $startMemory, " bytes\n";

$j = $i;
echo 'share memory: ', memory_get_usage() - $startMemory, " bytes\n";
xdebug_debug_zval('i');

$j = 10;

echo 'no share memory: ', memory_get_usage() - $startMemory, " bytes\n";
xdebug_debug_zval('i');
?>
 
# 运行结果
i: (refcount=0, is_ref=0)=4
memory: 32 bytes
share memory: 32 bytes
i: (refcount=0, is_ref=0)=4
no share memory: 32 bytes
i: (refcount=0, is_ref=0)=4
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

在运行$j=$i之后,变量$i的refcount并没有发生变化,而且是0(在PHP7之前版本测试中,应该是2), 这主要是由于PHP7的相关的数据结构发生了变化。

在PHP7中,变量分为变量名和变量值两部分,分别对应zval和zend_value。

zval

// php 变量对应的c结构体
struct _zval_struct {
    zend_value value;
    union {
       ……
    } u1;
    union {
        ……
    } u2;
};
1
2
3
4
5
6
7
8
9
10

zend_value

typedef union _zend_value {
    zend_long         lval;//整形
    double            dval;//浮点型
    zend_refcounted  *counted;//获取不同类型的gc头部
    zend_string      *str;//string字符串
    zend_array       *arr;//数组
    zend_object      *obj;//对象
    zend_resource    *res;//资源
    zend_reference   *ref;//是否是引用类型
  
    // 忽略下面的结构,与我们讨论无关
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        ZEND_ENDIAN_LOHI(
            uint32_t w1,
            uint32_t w2)
    } ww;
} zend_value;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在 zval的 value中就记录了引用计数zend_refcounted *counted这个类型,我们的垃圾回收机制也是基于此的。

zend_refcounted

typedef struct _zend_refcounted_h {
    uint32_t         refcount;          /* reference counter 32-bit */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,    /* used for strings & objects */
                uint16_t      gc_info)  /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;
1
2
3
4
5
6
7
8
9
10
11
12

新的结构中,引用 (REFERENCE)变为了一种数据结构而不再只是一个标记位了。

# 引用计数原理

了解了php变量的内部存储结构之后,再了解下php变量赋值相关的原理和早期垃圾回收机制。

PHP5.2中使用的内存回收算法是大名鼎鼎的Reference Counting,这个算法中文翻译叫做“引用计数”,其思想非常直观和简洁:为每个内存对象分配一个计数器,当一个内存对象建立时计数器初始化为1(因此此时总是有一个变量引用此对象),以后每有一个新变量引用此内存对象,则计数器加1,而每当减少一个引用此内存对象的变量则计数器减1,当垃圾回收机制运作的时候,将所有计数器为0的内存对象销毁并回收其占用的内存。

# 内存泄漏

但是php5.3版本之前的垃圾回收机制存在一个漏洞,即当数组或对象内部子元素引用其父元素,而此时如果发生了删除其父元素的情况,此变量容器并不会被删除,因为其子元素还在指向该变量容器,但是由于所有作用域内都没有指向该变量容器的符号,所以无法被清除,因此会发生内存泄漏,直到该脚本执行结束

如果你已经安装了Xdebug (opens new window),你能通过调用函数 **xdebug_debug_zval()**显示"refcount"和"is_ref"的值。

举例:

$a = array( 'one' );
$a[] = &$a;
xdebug_debug_zval( 'a' );
1
2
3

由于该示例不好输出结果,用图表示,如图:

php底层原理之垃圾回收机制

举例:

unset($a);
xdebug_debug_zval('a');
1
2

如图:

php底层原理之垃圾回收机制

# 根缓冲机制

php5.3版本之后引入根缓冲机制,即php启动时默认设置指定zval数量的根缓冲区(默认是10000),当php发现有存在循环引用的zval时,就会把其投入到根缓冲区,当根缓冲区达到配置文件中的指定数量(默认是10000)后,就会进行垃圾回收,以此解决循环引用导致的内存泄漏问题

# 为什么内存没有全部收回来

因为php的核心结构Hashtable,在定义的时候不可能一次性分配足够多的内存块,所以初始化的时候只会分配一小块,等不够的时候在进行扩容,而Hashtable只扩容不减少,所以当存入100个变量的时候符号表不够用了就进行一次扩容,当unset()时只是放了为变量值分配的内存,但是为变量名分配的内存还是在符号表中的,符号表并没有缩小,所以没收回来的内存是被符号表占去了。

php并不是只要内存不够就去向OS申请内存,而是先申请一大块内存,然后将其中一部分分给申请者,这样再有逻辑需要申请内存的时候,就不需要再向OS申请内存了,避免了重复申请,只有当一大块内存不够用的时候再去申请。而当释放内存时,php并非把内存还给了OS,而是把内存轨道自己维护的空闲内存列表,以便重复利用。

# 垃圾回收相关的配置

  • zend.enable_gc,默认值为on,如果想关闭垃圾回收机制,可以设置为off

# 小知识点

  • unset():unset()只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数减1,内存是否回收主要还是看refcount是否到0了。
  • null:将null赋值给一个变量是直接将该变量指向的数据结构置空,同时将其引用计数归0。
  • 脚本执行结束:该脚本中所有内存都会被释放,无论是否有环引用。

# 参考文档

  • 引用计数基础知识 (opens new window)
  • 深入理解 PHP7 中全新的 zval 容器和引用计数机制 (opens new window)
#PHP
上次更新: 2022/12/01, 11:09:34
PHP扩展开发入门
PHP类的自动加载

← PHP扩展开发入门 PHP类的自动加载→

最近更新
01
常用游戏反外挂技术总结
11-27
02
Golang开发实践万字总结
11-11
03
Redis万字总结
10-30
更多文章>
Theme by Vdoing | Copyright © 2011-2022 Ravior | 粤ICP备17060229号-3 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式