Une discussion détaillée du mécanisme de récupération de place PHP

不言
Libérer: 2023-03-24 07:16:01
original
1060 Les gens l'ont consulté

Le contenu de cet article concerne une discussion détaillée du mécanisme de récupération de place PHP. Il a une certaine valeur de référence. Maintenant, je le partage avec tout le monde. Les amis dans le besoin peuvent s'y référer

Connaissances de base. de comptage de références

Chaque variable php est stockée dans un conteneur de variables appelé "zval". Un conteneur de variable zval contient, en plus du type et de la valeur de la variable, deux octets d'informations supplémentaires. Le premier est "is_ref", qui est une valeur booléenne utilisée pour identifier si cette variable appartient à l'ensemble de référence. Grâce à cet octet, le moteur PHP peut distinguer les variables ordinaires des variables de référence. Puisque PHP permet aux utilisateurs d'utiliser des références personnalisées en utilisant &, il existe également un mécanisme de comptage de références interne dans le conteneur de variables zval pour optimiser l'utilisation de la mémoire. Le deuxième octet supplémentaire est "refcount", qui est utilisé pour indiquer le nombre de variables (également appelées symboles) pointant vers ce conteneur de variables zval. Tous les symboles existent dans une table de symboles, où chaque symbole a une portée (scope), le script principal (par exemple : le script demandé via le navigateur) et chaque fonction ou méthode ont également une portée.

Lorsqu'une variable reçoit une valeur constante, un conteneur de variable zval sera généré, comme dans l'exemple suivant :

Exemple #1 Générer un nouveau conteneur zval

<?php
$a = "new string";
?>
Copier après la connexion


Dans l'exemple ci-dessus, la nouvelle variable a est générée dans la portée actuelle. Et un conteneur de variables avec le type string et la valeur new string est généré. Dans les deux octets d'informations supplémentaires, "is_ref" est défini sur FALSE par défaut puisqu'aucune référence personnalisée n'est générée. "refcount" est défini sur 1 car il n'y a qu'une seule variable utilisant ce conteneur de variables. Notez que lorsque la valeur de "refcount" est 1, la valeur de "is_ref" est toujours. FAUX Si vous avez installé » Exemple n°2 Afficher les informations zval

Sortie :

<?php
xdebug_debug_zval(&#39;a&#39;);
?>
以上例程会
Copier après la connexion

a : (refcount=1, is_ref=0)= 'nouvelle chaîne'

L'attribution d'une variable à une autre variable augmentera le nombre de références (refcount).

Exemple n°3 Augmenter le nombre de références d'un zval

<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( &#39;a&#39; );
?>
Copier après la connexion

La routine ci-dessus affichera :

a : (refcount=2, is_ref=0)='new string'

A ce moment, le nombre de références est

2

, car le même conteneur de variable est associé à la variable

a et à la variable b. Lorsque cela n’est pas nécessaire, les conteneurs de variables générés par php ne seront pas copiés. Le conteneur de variables est détruit lorsque "refcount" devient 0. Lorsqu'une variable associée à un conteneur de variables quitte sa portée (par exemple : l'exécution de la fonction se termine), ou que la fonction unset()< est appelée sur la variable 🎜> , "refcount" sera réduit de 1, comme indiqué dans l'exemple suivant : Exemple n°4 Diminuer le nombre de références

<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( &#39;a&#39; );
unset( $b, $c );
xdebug_debug_zval( &#39;a&#39; );
?>
Copier après la connexion

La routine ci-dessus affichera :
a : (refcount=3, is_ref=0)='new string'a: (refcount=1, is_ref=0) =' nouvelle chaîne'

如果我们现在执行 unset($a);,包含类型和值的这个变量容器就会从内存中删除。

复合类型(Compound Types)

当考虑像 arrayobject这样的复合类型时,事情就稍微有点复杂. 与 标量(scalar)类型的值不同,arrayobject类型的变量把它们的成员或属性存在自己的符号表中。这意味着下面的例子将生成三个zval变量容器。

Example #5 Creating aarray zval

<?php
$a = array( &#39;meaning&#39; => &#39;life&#39;, &#39;number&#39; => 42 );
xdebug_debug_zval( &#39;a&#39; );
?>
Copier après la connexion


以上例程的输出类似于:

a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=1, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42)

图示:

上面的结果如果在PHP5中是没有问题的,但是当我在PHP7中进行试验验证是发现输出的结果和上面并不一致,如下:

a:

(refcount=1, is_ref=0)array(size=2) 'meaning'=> (refcount=2, is_ref=0)string'life' (length=4) 'number'=> (refcount=0, is_ref=0)int42


这三个zval变量容器是:ameaningnumber。增加和减少”refcount”的规则和上面提到的一样. 下面, 我们在数组中再添加一个元素,并且把它的值设为数组中已存在元素的值:

Example #6 添加一个已经存在的元素到数组中

<?php
$a = array( &#39;meaning&#39; => &#39;life&#39;, &#39;number&#39; => 42 );
$a[&#39;life&#39;] = $a[&#39;meaning&#39;];
xdebug_debug_zval( &#39;a&#39; );
?>
Copier après la connexion


以上例程的输出类似于:

a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=2, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42, 'life' => (refcount=2, is_ref=0)='life')

PHP7中的运行结果

a:

(refcount=1, is_ref=0)array(size=3) 'meaning'=> (refcount=3, is_ref=0)string'life' (length=4) 'number'=> (refcount=0, is_ref=0)int42 'life' =>(refcount=3, is_ref=0)string 'life' (length=4)

图示:

从以上的xdebug输出信息,我们看到原有的数组元素和新添加的数组元素关联到同一个"refcount"2的zval变量容器. 尽管 Xdebug的输出显示两个值为'life'的 zval 变量容器,其实是同一个。 函数xdebug_debug_zval()不显示这个信息,但是你能通过显示内存指针信息来看到。

删除数组中的一个元素,就是类似于从作用域中删除一个变量. 删除后,数组中的这个元素所在的容器的“refcount”值减少,同样,当“refcount”为0时,这个变量容器就从内存中被删除,下面又一个例子可以说明:

Example #7 从数组中删除一个元素

<?php
$a = array( &#39;meaning&#39; => &#39;life&#39;, &#39;number&#39; => 42 );
$a[&#39;life&#39;] = $a[&#39;meaning&#39;];
unset( $a[&#39;meaning&#39;], $a[&#39;number&#39;] );
xdebug_debug_zval( &#39;a&#39; );
?>
Copier après la connexion


以上例程的输出类似于:

a: (refcount=1, is_ref=0)=array ( 'life' => (refcount=1, is_ref=0)='life')

PHP7中运行的结果

a:

(refcount=1, is_ref=0)array(size=1) 'life'=> (refcount=2, is_ref=0)string'life' (length=4)

现在,当我们添加一个数组本身作为这个数组的元素时,事情就变得有趣,下个例子将说明这个。例中我们加入了引用操作符,否则php将生成一个复制。

Example #8 把数组作为一个元素添加到自己

<?php
$a = array( &#39;one&#39; );
$a[] =& $a;
xdebug_debug_zval( &#39;a&#39; );
?>
Copier après la connexion


以上例程的输出类似于:

a: (refcount=2, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=2, is_ref=1)=...)

PHP中运行的结果

a:

(refcount=2, is_ref=1)array(size=2) 0=>(refcount=2, is_ref=0)string'one' (length=3) 1=> (refcount=2, is_ref=1)&array<

图示:

能看到数组变量 (a) 同时也是这个数组的第二个元素(1) 指向的变量容器中“refcount”为2。上面的输出结果中的"..."说明发生了递归操作, 显然在这种情况下意味着"..."指向原始数组。

跟刚刚一样,对一个变量调用unset,将删除这个符号,且它指向的变量容器中的引用次数也减1。所以,如果我们在执行完上面的代码后,对变量$a调用unset, 那么变量$a 和数组元素 "1" 所指向的变量容器的引用次数减1, 从"2"变成"1". 下例可以说明:

Example #9 Unsetting$a

(refcount=1, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=...)

图示:


通过PHP5和PHP7环境中的运行结果对比可以看出,PHP7中的内存回收机制有了改变,那么为什么会有这种改变呢?我查阅了一些资料供大家参考。

PHP7 中的 zval

在 PHP7 中 zval 有了新的实现方式。最基础的变化就是 zval 需要的内存不再是单独从堆上分配,不再自己存储引用计数。复杂数据类型(比如字符串、数组和对象)的引用计数由其自身来存储。这种实现方式有以下好处:

简单数据类型不需要单独分配内存,也不需要计数;

不会再有两次计数的情况。在对象中,只有对象自身存储的计数是有效的;

由于现在计数由数值自身存储,所以也就可以和非 zval 结构的数据共享,比如 zval 和 hashtable key 之间;

间接访问需要的指针数减少了。

我们看看现在 zval 结构体的定义(现在在 zend_types.h 文件中):

struct _zval_struct {
 zend_value  value;   /* value */
 union {
  struct {
   ZEND_ENDIAN_LOHI_4(
    zend_uchar type,   /* active type */
    zend_uchar type_flags,
    zend_uchar const_flags,
    zend_uchar reserved)  /* call info for EX(This) */
  } v;
  uint32_t type_info;
 } u1;
 union {
  uint32_t  var_flags;
  uint32_t  next;     /* hash collision chain */
  uint32_t  cache_slot;   /* literal cache slot */
  uint32_t  lineno;    /* line number (for ast nodes) */
  uint32_t  num_args;    /* arguments number for EX(This) */
  uint32_t  fe_pos;    /* foreach position */
  uint32_t  fe_iter_idx;   /* foreach iterator index */
 } u2;
};
Copier après la connexion


结构体的第一个元素没太大变化,仍然是一个 value 联合体。第二个成员是由一个表示类型信息的整型和一个包含四个字符变量的结构体组成的联合体(可以忽略 ZEND_ENDIAN_LOHI_4 宏,它只是用来解决跨平台大小端问题的)。这个子结构中比较重要的部分是 type(和以前类似)和 type_flags,这个接下来会解释。

上面这个地方也有一点小问题:value 本来应该占 8 个字节,但是由于内存对齐,哪怕只增加一个字节,实际上也是占用 16 个字节(使用一个字节就意味着需要额外的 8 个字节)。但是显然我们并不需要 8 个字节来存储一个 type 字段,所以我们在 u1 的后面增加了了一个名为 u2 的联合体。默认情况下是用不到的,需要使用的时候可以用来存储 4 个字节的数据。这个联合体可以满足不同场景下的需求。

PHP7 中 value 的结构定义如下:

typedef union _zend_value {
 zend_long   lval;    /* long value */
 double   dval;    /* double value */
 zend_refcounted *counted;
 zend_string  *str;
 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 {
  uint32_t w1;
  uint32_t w2;
 } ww;
} zend_value;
首先需要注意的是现在 value 联合体需要的内存是 8 个字节而不是 16。它只会直接存储整型(lval)或者浮点型(dval)数据,其他情况下都是指针(上面提到过,指针占用 8 个字节,最下面的结构体由两个 4 字节的无符号整型组成)。上面所有的指针类型(除了特殊标记的)都有一个同样的头(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;
Copier après la connexion



现在,这个结构体肯定会包含一个存储引用计数的字段。除此之外还有 type、flags 和 gc_info。type 存储的和 zval 中的 type 相同的内容,这样 GC 在不存储 zval 的情况下单独使用引用计数。flags 在不同的数据类型中有不同的用途,这个放到下一部分讲。

gc_info 和 PHP5 中的 buffered 作用相同,不过不再是位于根缓冲区的指针,而是一个索引数字。因为以前根缓冲区的大小是固定的(10000 个元素),所以使用一个 16 位(2 字节)的数字代替 64 位(8 字节)的指针足够了。gc_info 中同样包含一个『颜色』位用于回收时标记结点。

zval 内存管理

上文提到过 zval 需要的内存不再单独从堆上分配。但是显然总要有地方来存储它,所以会存在哪里呢?实际上大多时候它还是位于堆中(所以前文中提到的地方重点不是堆,而是单独分配),只不过是嵌入到其他的数据结构中的,比如 hashtable 和 bucket 现在就会直接有一个 zval 字段而不是指针。所以函数表编译变量和对象属性在存储时会是一个 zval 数组并得到一整块内存而不是散落在各处的 zval 指针。之前的 zval * 现在都变成了 zval。

之前当 zval 在一个新的地方使用时会复制一份 zval * 并增加一次引用计数。现在就直接复制 zval 的值(忽略 u2),某些情况下可能会增加其结构指针指向的引用计数(如果在进行计数)。

那么 PHP 怎么知道 zval 是否正在计数呢?不是所有的数据类型都能知道,因为有些类型(比如字符串或数组)并不是总需要进行引用计数。所以 type_info 字段就是用来记录 zval 是否在进行计数的,这个字段的值有以下几种情况:

#define IS_TYPE_CONSTANT   (1<<0) /* special */
#define IS_TYPE_IMMUTABLE   (1<<1) /* special */
#define IS_TYPE_REFCOUNTED   (1<<2)
#define IS_TYPE_COLLECTABLE   (1<<3)
#define IS_TYPE_COPYABLE   (1<<4)
#define IS_TYPE_SYMBOLTABLE   (1<<5) /* special */
Copier après la connexion

注:在 7.0.0 的正式版本中,上面这一段宏定义的注释这几个宏是供 zval.u1.v.type_flags 使用的。这应该是注释的错误,因为这个上述字段是 zend_uchar 类型。

type_info 的三个主要的属性就是『可计数』(refcounted)、『可回收』(collectable)和『可复制』(copyable)。计数的问题上面已经提过了。『可回收』用于标记 zval 是否参与循环,不如字符串通常是可计数的,但是你却没办法给字符串制造一个循环引用的情况。

是否可复制用于表示在复制时是否需要在复制时制造(原文用的 "duplication" 来表述,用中文表达出来可能不是很好理解)一份一模一样的实体。"duplication" 属于深度复制,比如在复制数组时,不仅仅是简单增加数组的引用计数,而是制造一份全新值一样的数组。但是某些类型(比如对象和资源)即使 "duplication" 也只能是增加引用计数,这种就属于不可复制的类型。这也和对象和资源现有的语义匹配(现有,PHP7 也是这样,不单是 PHP5)。

下面的表格上标明了不同的类型会使用哪些标记(x 标记的都是有的特性)。『简单类型』(simple types)指的是整型或布尔类型这些不使用指针指向一个结构体的类型。下表中也有『不可变』(immutable)的标记,它用来标记不可变数组的,这个在下一部分再详述。

interned string(保留字符)在这之前没有提过,其实就是函数名、变量名等无需计数、不可重复的字符串。

| refcounted | collectable | copyable | immutable

----------------+------------+-------------+----------+----------

simple types | | | |

string | x | | x |

interned string | | | |

array | x | x | x |

immutable array | | | | x

object | x | x | |

resource | x | | |

reference | x | | |

要理解这一点,我们可以来看几个例子,这样可以更好的认识 zval 内存管理是怎么工作的。

下面是整数行为模式,在上文中 PHP5 的例子的基础上进行了一些简化 :

<?php
$a= 42; // $a = zval_1(type=IS_LONG, value=42)
$b= $a; // $a = zval_1(type=IS_LONG, value=42)
   // $b = zval_2(type=IS_LONG, value=42)
$a+= 1; // $a = zval_1(type=IS_LONG, value=43)
   // $b = zval_2(type=IS_LONG, value=42)
unset($a); // $a = zval_1(type=IS_UNDEF)
   // $b = zval_2(type=IS_LONG, value=42)
Copier après la connexion


这个过程其实挺简单的。现在整数不再是共享的,变量直接就会分离成两个单独的 zval,由于现在 zval 是内嵌的所以也不需要单独分配内存,所以这里的注释中使用 = 来表示的而不是指针符号 ->,unset 时变量会被标记为 IS_UNDEF。下面看一下更复杂的情况:

<?php
$a= []; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])
$b= $a; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=2, value=[])
   // $b = zval_2(type=IS_ARRAY) ---^
// zval 分离在这里进行
$a[] = 1 // $a = zval_1(type=IS_ARRAY) -> zend_array_2(refcount=1, value=[1])
   // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])
unset($a); // $a = zval_1(type=IS_UNDEF), zend_array_2 被销毁
   // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])
Copier après la connexion


这种情况下每个变量变量有一个单独的 zval,但是是指向同一个(有引用计数) zend_array 的结构体。修改其中一个数组的值时才会进行复制。这点和 PHP5 的情况类似。

类型(Types)

我们大概看一下 PHP7 支持哪些类型(zval 使用的类型标记):

/* regular data types */
#define IS_UNDEF     0
#define IS_NULL     1
#define IS_FALSE     2
#define IS_TRUE      3
#define IS_LONG     4
#define IS_DOUBLE    5
#define IS_STRING    6
#define IS_ARRAY    7
#define IS_OBJECT    8
#define IS_RESOURCE    9
#define IS_REFERENCE    10
/* constant expressions */
#define IS_CONSTANT     11
#define IS_CONSTANT_AST    12
/* internal types */
#define IS_INDIRECT     15
#define IS_PTR      17
Copier après la connexion


这个列表和 PHP5 使用的类似,不过增加了几项:

IS_UNDEF 用来标记之前为 NULL 的 zval 指针(和 IS_NULL 并不冲突)。比如在上面的例子中使用 unset 注销变量;

IS_BOOL 现在分割成了 IS_FALSE 和 IS_TRUE 两项。现在布尔类型的标记是直接记录到 type 中,这么做可以优化类型检查。不过这个变化对用户是透明的,还是只有一个『布尔』类型的数据(PHP 脚本中)。

PHP 引用不再使用 is_ref 来标记,而是使用 IS_REFERENCE 类型。这个也要放到下一部分讲;

IS_INDIRECT 和 IS_PTR 是特殊的内部标记。

实际上上面的列表中应该还存在两个 fake types,这里忽略了。

IS_LONG 类型表示的是一个 zend_long 的值,而不是原生的 C 语言的 long 类型。原因是 Windows 的 64 位系统(LLP64)上的 long 类型只有 32 位的位深度。所以 PHP5 在 Windows 上只能使用 32 位的数字。PHP7 允许你在 64 位的操作系统上使用 64 位的数字,即使是在 Windows 上面也可以。

zend_refcounted 的内容会在下一部分讲。下面看看 PHP 引用的实现。

引用

PHP7 使用了和 PHP5 中完全不同的方法来处理 PHP & 符号引用的问题(这个改动也是 PHP7 开发过程中大量 bug 的根源)。我们先从 PHP5 中 PHP 引用的实现方式说起。

通常情况下, 写时复制原则意味着当你修改一个 zval 之前需要对其进行分离来保证始终修改的只是某一个 PHP 变量的值。这就是传值调用的含义。

但是使用 PHP 引用时这条规则就不适用了。如果一个 PHP 变量是 PHP 引用,就意味着你想要在将多个 PHP 变量指向同一个值。PHP5 中的 is_ref 标记就是用来注明一个 PHP 变量是不是 PHP 引用,在修改时需不需要进行分离的。比如:

<?php
$a= []; // $a  -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[])
$b=& $a; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[])
 
$b[] = 1; // $a = $b = zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[1])
   // 因为 is_ref 的值是 1, 所以 PHP 不会对 zval 进行分离
Copier après la connexion



但是这个设计的一个很大的问题在于它无法在一个 PHP 引用变量和 PHP 非引用变量之间共享同一个值。比如下面这种情况:

<?php
$a= []; // $a   -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[])
$b= $a; // $a, $b  -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
$c= $b// $a, $b, $c -> zval_1(type=IS_ARRAY, refcount=3, is_ref=0) -> HashTable_1(value=[])
$d=& $c; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
   // $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[])
   // $d 是 $c 的引用, 但却不是 $a 的 $b, 所以这里 zval 还是需要进行复制
   // 这样我们就有了两个 zval, 一个 is_ref 的值是 0, 一个 is_ref 的值是 1.
$d[] = 1; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
   // $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[1])
   // 因为有两个分离了的 zval, $d[] = 1 的语句就不会修改 $a 和 $b 的值.
Copier après la connexion



这种行为方式也导致在 PHP 中使用引用比普通的值要慢。比如下面这个例子:

<?php
$array= range(0, 1000000);
$ref=& $array;
var_dump(count($array)); // <-- 这里会进行分离
Copier après la connexion



因为 count() 只接受传值调用,但是 $array 是一个 PHP 引用,所以 count() 在执行之前实际上会有一个对数组进行完整的复制的过程。如果 $array 不是引用,这种情况就不会发生了。

现在我们来看看 PHP7 中 PHP 引用的实现。因为 zval 不再单独分配内存,也就没办法再使用和 PHP5 中相同的实现了。所以增加了一个 IS_REFERENCE 类型,并且专门使用 zend_reference 来存储引用值:

struct _zend_reference {
 zend_refcounted gc;
 zval    val;
};
Copier après la connexion


本质上 zend_reference 只是增加了引用计数的 zval。所有引用变量都会存储一个 zval 指针并且被标记为 IS_REFERENCE。val 和其他的 zval 的行为一样,尤其是它也可以在共享其所存储的复杂变量的指针,比如数组可以在引用变量和值变量之间共享。

我们还是看例子,这次是 PHP7 中的语义。为了简洁明了这里不再单独写出 zval,只展示它们指向的结构体:

<?php
$a= []; // $a          -> zend_array_1(refcount=1, value=[])
$b=& $a; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[])
$b[] = 1; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[1])
Copier après la connexion


上面的例子中进行引用传递时会创建一个 zend_reference,注意它的引用计数是 2(因为有两个变量在使用这个 PHP 引用)。但是值本身的引用计数是 1(因为 zend_reference 只是有一个指针指向它)。下面看看引用和非引用混合的情况:

<?php
$a= []; // $a   -> zend_array_1(refcount=1, value=[])
$b= $a; // $a, $b, -> zend_array_1(refcount=2, value=[])
$c= $b// $a, $b, $c -> zend_array_1(refcount=3, value=[])
$d=& $c; // $a, $b         -> zend_array_1(refcount=3, value=[])
   // $c, $d -> zend_reference_1(refcount=2) ---^
   // 注意所有变量共享同一个 zend_array, 即使有的是 PHP 引用有的不是
$d[] = 1; // $a, $b         -> zend_array_1(refcount=2, value=[])
   // $c, $d -> zend_reference_1(refcount=2) -> zend_array_2(refcount=1, value=[1])
   // 只有在这时进行赋值的时候才会对 zend_array 进行赋值
Copier après la connexion



这里和 PHP5 最大的不同就是所有的变量都可以共享同一个数组,即使有的是 PHP 引用有的不是。只有当其中某一部分被修改的时候才会对数组进行分离。这也意味着使用 count() 时即使给其传递一个很大的引用数组也是安全的,不会再进行复制。不过引用仍然会比普通的数值慢,因为存在需要为 zend_reference 结构体分配内存(间接)并且引擎本身处理这一块儿也不快的的原因。

结语

Pour résumer, le changement le plus important dans PHP7 est que zval n'alloue plus de mémoire à partir du tas seul et ne stocke pas son propre nombre de références. Les types complexes qui nécessitent des pointeurs zval (tels que des chaînes, des tableaux et des objets) stockent leurs propres décomptes de références. Cela entraîne moins d’opérations d’allocation de mémoire, moins d’utilisation de pointeurs indirects et moins d’allocations de mémoire.


Problèmes de nettoyage

Bien qu'il n'y ait plus de symboles dans une portée pointant vers cette structure (c'est-à-dire un conteneur de variables ), comme l'élément de tableau "1" pointe toujours vers le tableau lui-même, ce conteneur ne peut pas être effacé. Comme aucun autre symbole ne pointe vers cette structure, l'utilisateur n'a aucun moyen d'effacer la structure, ce qui entraîne une fuite de mémoire. Heureusement, PHP effacera cette structure de données à la fin de l'exécution du script, mais jusqu'à ce que PHP l'efface, cela consommera beaucoup de mémoire. Cela arrive souvent si vous implémentez un algorithme d'analyse ou si vous faites d'autres choses comme faire pointer un élément enfant vers son parent. Bien sûr, la même situation peut se produire avec des objets, en fait, cela est plus susceptible de se produire avec des objets, car les objets sont toujours implicitement référencés.

Ce n'est pas grave si la situation ci-dessus ne se produit qu'une ou deux fois, mais si les fuites de mémoire se produisent des milliers, voire des centaines de milliers de fois, c'est évidemment un gros problème. De tels problèmes surviennent souvent dans les scripts à exécution longue, tels que les démons dont les requêtes se terminent rarement, ou les grands ensembles de tests unitaires. Un exemple de ce dernier : des problèmes peuvent survenir lors des tests unitaires des composants de modèles de l'énorme bibliothèque de composants eZ (une bibliothèque PHP bien connue). Parfois, le test peut nécessiter 2 Go de mémoire et le serveur de test peut ne pas disposer d'une mémoire aussi grande.


Cycles de collecte

Traditionnellement, le mécanisme de mémoire de comptage de références utilisé en PHP ne peut pas être traité. Fuite de mémoire de référence circulaire. Cependant, PHP 5.3.0 utilise l'algorithme de synchronisation de l'article » Concurrent Cycle Collection in Reference Counted Systems pour résoudre ce problème de fuite de mémoire.

Une explication complète de l'algorithme dépasse quelque peu la portée de cette section, et seules les bases seront présentées. Tout d’abord, nous devons établir quelques règles de base. Si un nombre de références augmente, il continuera à être utilisé et bien sûr plus à la poubelle. Si le nombre de références est réduit à zéro, le conteneur de variables sera effacé (libre). Autrement dit, un cycle de récupération se produit uniquement lorsque le nombre de références est réduit à une valeur non nulle. Deuxièmement, pendant un cycle de déchets, découvrez quelles parties sont des déchets en vérifiant si le nombre de références est réduit de 1 et en vérifiant quels conteneurs de variables ont zéro référence.

Pour éviter d'avoir à vérifier tous les cycles de déchets où le nombre de références peut être réduit, cet algorithme place toutes les racines possibles (les racines possibles sont des conteneurs de variables zval) dans la marque du tampon racine (de couleur violette), appelée poubelle suspectée), ce qui garantit également que chaque racine de déchets possible n'apparaît qu'une seule fois dans le tampon. Le garbage collection est effectué sur tous les différents conteneurs de variables dans le tampon uniquement lorsque le tampon racine est plein. Regardez l'étape A dans l'image ci-dessus.

À l'étape B, simulez la suppression de chaque variable violette. Lors de la simulation de suppression, le nombre de références des variables ordinaires qui ne sont pas violettes peut être réduit de "1". Si le nombre de références d'une variable ordinaire devient 0, simulez à nouveau la suppression de cette variable ordinaire. Chaque variable ne peut être supprimée qu'une seule fois et sera marquée en gris après une suppression simulée (l'article original disait de garantir que le même conteneur de variable n'est pas décrémenté de "1" deux fois, ce qui est faux).

A l'étape C, la simulation restaure chaque variable violette. La récupération est conditionnelle. Lorsque le nombre de références de la variable est supérieur à 0, sa restauration est simulée. De même, chaque variable ne peut être restaurée qu'une seule fois. Après restauration, elle est marquée en noir. Il s'agit essentiellement de l'opération inverse de l'étape B. De cette façon, la pile restante de nœuds bleus irrécupérables sont les nœuds bleus qui doivent être supprimés à l'étape D et supprimez-les.

L'algorithme est entièrement composé de suppression simulée, de récupération simulée et de suppression réelle, le tout utilisant une traversée simple (la traversée de recherche approfondie la plus typique). La complexité est positivement liée au nombre de nœuds effectuant des opérations de simulation, et pas seulement aux variables suspectes en violet.

Maintenant que vous avez une compréhension de base de cet algorithme, revenons en arrière et voyons comment il est intégré à PHP. Par défaut, le mécanisme de récupération de place de PHP est activé et il existe un paramètre php.ini qui vous permet de le modifier : zend.enable_gc.

Lorsque le mécanisme de récupération de place est activé, l'algorithme de recherche de boucle décrit ci-dessus sera exécuté chaque fois que le tampon racine est plein. La zone du cache racine a une taille fixe et peut stocker 10 000 racines possibles. Bien entendu, vous pouvez modifier la constante GC_ROOT_BUFFER_MAX_ENTRIES dans le fichier source PHP Zend/zend_gc.c puis recompiler PHP. . pour modifier cette valeur de 10 000. Lorsque le garbage collection est désactivé, l'algorithme de recherche de boucle ne s'exécute jamais. Cependant, il est possible que la racine existe toujours dans le tampon racine, que le garbage collection soit activé ou non dans la configuration.

Lorsque le mécanisme de récupération de place est désactivé, si le tampon racine est plein de racines possibles, davantage de racines possibles ne seront évidemment pas enregistrées. Les racines possibles qui ne sont pas enregistrées ne seront pas analysées et traitées par cet algorithme. S'ils font partie d'un cycle de référence cyclique, ils ne seront jamais effacés et provoqueront une fuite de mémoire.

La raison pour laquelle les racines possibles sont enregistrées même lorsque le garbage collection n'est pas disponible est que l'enregistrement des racines possibles est plus rapide que de vérifier si le garbage collection est activé à chaque fois qu'une racine possible est trouvée. Cependant, le mécanisme de collecte et d’analyse des déchets lui-même prend beaucoup de temps.

En plus de modifier la configuration zend.enable_gc, vous pouvez également activer et désactiver le garbage collection en appelant gc_enable() et gc_disable() fonctionne respectivement. L’appel de ces fonctions a le même effet que la modification des éléments de configuration pour activer ou désactiver le mécanisme de récupération de place. Possibilité de forcer une collecte périodique même lorsque le tampon racine n'est pas plein. Vous pouvez appeler la fonction gc_collect_cycles() pour atteindre cet objectif. Cette fonction renverra le nombre de cycles recyclés à l'aide de cet algorithme.

La raison pour laquelle vous autorisez l'activation et la désactivation du garbage collection et l'initialisation autonome est que certaines parties de votre application peuvent être sensibles au facteur temps. Dans ce cas, vous ne souhaiterez probablement pas utiliser le garbage collection. Bien entendu, la désactivation du garbage collection pour certaines parties de votre application entraîne un risque de fuites de mémoire, car certaines racines possibles peuvent ne pas rentrer dans le tampon racine limité. Par conséquent, juste avant d'appeler la fonction gc_disable() pour libérer de la mémoire, il peut être judicieux d'appeler d'abord la fonction gc_collect_cycles(). Parce que cela effacera toutes les racines possibles qui ont été stockées dans le tampon racine, lorsque le mécanisme de récupération de place est désactivé, un tampon vide peut être laissé pour avoir plus d'espace pour stocker les racines possibles.

Recommandations associées :

Maîtriser le mécanisme de récupération de place PHP

Explication détaillée du mécanisme de récupération de place PHP et de gestion de la mémoire


Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal
À propos de nous Clause de non-responsabilité Sitemap
Site Web PHP chinois:Formation PHP en ligne sur le bien-être public,Aidez les apprenants PHP à grandir rapidement!