>  기사  >  백엔드 개발  >  PHP 가비지 수집 메커니즘에 대한 자세한 논의

PHP 가비지 수집 메커니즘에 대한 자세한 논의

不言
不言원래의
2018-04-17 09:10:441097검색

이 문서의 내용은 PHP 가비지 수집 메커니즘에 대한 자세한 내용입니다. 이제 특정 참조 가치가 있습니다. 필요한 친구가 참조할 수 있습니다.

참조 카운팅에 대한 기본 지식

있습니다. "zval"이라는 변수 컨테이너의 각 PHP 변수에 대해 하나씩. zval 변수 컨테이너에는 변수의 유형과 값 외에 2바이트의 추가 정보가 포함되어 있습니다. 첫 번째는 "is_ref"로, 이 변수가 참조 세트에 속하는지 여부를 식별하는 데 사용되는 부울 값입니다. 이 바이트를 통해 PHP 엔진은 일반 변수와 참조 변수를 구별할 수 있습니다. PHP에서는 사용자가 &를 사용하여 사용자 정의 참조를 사용할 수 있으므로 zval 변수 컨테이너에는 메모리 사용을 최적화하는 내부 참조 계산 메커니즘도 있습니다. 두 번째 추가 바이트는 "refcount"이며, 이 zval 변수 컨테이너를 가리키는 변수(기호라고도 함)의 수를 나타내는 데 사용됩니다. 모든 기호는 기호 테이블에 존재하며, 각 기호에는 범위(scope)가 있고, 기본 스크립트(예: 브라우저를 통해 요청된 스크립트)와 각 함수나 메서드에도 범위가 있습니다.

변수에 상수 값이 할당되면 다음 예와 같이 zval 변수 컨테이너가 생성됩니다.

예제 #1 새 zval 컨테이너 생성

<?php
$a = "new string";
?>


위 예에서, new 변수 a가 현재 범위에서 생성됩니다. 그리고 유형이 string이고 값이 new string인 변수 컨테이너가 생성됩니다. 추가 2바이트 정보에서는 사용자 정의 참조가 생성되지 않으므로 "is_ref"가 기본적으로 FALSE로 설정됩니다. 이 변수 컨테이너를 사용하는 변수는 하나만 있기 때문에 "refcount"는 1로 설정됩니다. "refcount" 값이 1인 경우 "is_ref" 값은 » Xdebug를 설치한 경우 항상 FALSE입니다. , xdebug_debug_zval() 함수를 호출하여 "refcount" 및 "is_ref" 값을 표시할 수 있습니다.

예제 #2 zval 정보 표시

<?php
xdebug_debug_zval(&#39;a&#39;);
?>
以上例程会

출력:

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

한 변수를 다른 변수에 할당하면 참조 수가 늘어납니다(refcount ).

예제 #3 zval의 참조 카운트를 늘립니다.

<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( &#39;a&#39; );
?>


위 루틴은 다음을 출력합니다.

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

참조 수는 2입니다. 왜냐하면 동일한 변수 컨테이너가 변수 a 및 변수 b와 연관되어 있기 때문입니다. PHP는 필요하지 않을 때 생성된 변수 컨테이너를 복사하지 않습니다. 변수 컨테이너는 "refcount"가 0이 되면 소멸됩니다. 변수 컨테이너와 연결된 변수가 해당 범위를 벗어나거나(예: 함수 실행 종료) unset() 함수가 변수에서 호출되면 "refcount" 다음 예와 같이 1씩 감소합니다.

예제 #4 참조 횟수 줄이기

<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( &#39;a&#39; );
unset( $b, $c );
xdebug_debug_zval( &#39;a&#39; );
?>


위 루틴은 다음을 출력합니다.

a: (refcount=3, is_ref= 0) ='새 문자열'a: (refcount=1, is_ref=0)='새 문자열'

如果我们现在执行 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; );
?>


以上例程的输出类似于:

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; );
?>


以上例程的输出类似于:

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; );
?>


以上例程的输出类似于:

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; );
?>


以上例程的输出类似于:

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)&array4a1caacb728512ad16d8065cb72b0614 (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;
};


结构体的第一个元素没太大变化,仍然是一个 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;



现在,这个结构体肯定会包含一个存储引用计数的字段。除此之外还有 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 */

注:在 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)


这个过程其实挺简单的。现在整数不再是共享的,变量直接就会分离成两个单独的 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=[])


这种情况下每个变量变量有一个单独的 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


这个列表和 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 进行分离



但是这个设计的一个很大的问题在于它无法在一个 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 的值.



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

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



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

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

struct _zend_reference {
 zend_refcounted gc;
 zval    val;
};


本质上 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])


上面的例子中进行引用传递时会创建一个 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 进行赋值



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

结语

요약하자면, PHP7의 가장 중요한 변화는 zval이 더 이상 힙에서만 메모리를 할당하지 않고 자체 참조 카운트를 저장하지 않는다는 것입니다. zval 포인터가 필요한 복합 유형(예: 문자열, 배열, 객체)은 자체 참조 횟수를 저장합니다. 결과적으로 메모리 할당 작업, 간접 포인터 사용 및 메모리 할당이 줄어듭니다.


정리 문제

이 구조(즉, 변수 컨테이너)를 가리키는 범위에 더 이상 기호가 없지만 배열 요소 "1"은 여전히 ​​배열 자체를 가리키므로 이 컨테이너를 지울 수 없습니다. 그것을 가리키는 다른 기호가 없기 때문에 사용자는 구조를 지울 수 없으며 결과적으로 메모리 누수가 발생합니다. 다행스럽게도 PHP는 스크립트 실행이 끝나면 이 데이터 구조를 지웁니다. 그러나 PHP가 이를 지울 때까지 많은 메모리를 소비하게 됩니다. 이는 구문 분석 알고리즘을 구현하거나 하위 요소가 해당 상위 요소를 가리키도록 하는 등의 다른 작업을 수행하는 경우 자주 발생합니다. 물론, 동일한 상황이 객체에서도 발생할 수 있습니다. 실제로 객체에서 발생할 가능성이 더 높습니다. 객체는 항상 암시적으로 참조되기 때문입니다.

위의 상황이 한두 번만 발생해도 괜찮지만 메모리 누수가 수천 번, 심지어 수십만 번 발생한다면 이는 분명히 큰 문제입니다. 이러한 문제는 요청이 거의 종료되지 않는 데몬이나 단위 테스트의 대규모 세트와 같이 장기 실행 스크립트에서 자주 발생합니다. 후자의 예: 거대한 eZ(잘 알려진 PHP 라이브러리) 구성 요소 라이브러리의 템플릿 구성 요소를 단위 테스트할 때 문제가 발생할 수 있습니다. 경우에 따라 테스트에 2GB의 메모리가 필요할 수 있는데, 테스트 서버에는 그렇게 큰 메모리가 없을 수도 있습니다.


사이클 수집

전통적으로 PHP에서 사용되는 참조 카운팅 메모리 메커니즘은 순환 참조 메모리 누수를 처리할 수 없습니다. 그러나 PHP 5.3.0은 이 메모리 누수 문제를 처리하기 위해 »참조 카운트 시스템의 동시 주기 수집 문서의 동기화 알고리즘을 사용합니다.

알고리즘에 대한 완전한 설명은 이 섹션의 범위를 벗어나므로 기본적인 부분만 소개하겠습니다. 먼저, 몇 가지 기본 규칙을 설정해야 합니다. 참조 횟수가 증가하면 계속 사용되며 더 이상 가비지에는 포함되지 않습니다. 참조 횟수가 0으로 줄어들면 변수 컨테이너가 지워집니다(무료). 즉, 가비지 사이클은 참조 카운트가 0이 아닌 값으로 감소할 때만 발생합니다. 둘째, 가비지 사이클 동안 참조 횟수가 1만큼 감소하는지 확인하고 참조가 0인 변수 컨테이너를 확인하여 어떤 부분이 가비지인지 알아냅니다.

참조 횟수가 줄어들 수 있는 모든 가비지 사이클을 확인해야 하는 것을 피하기 위해 이 알고리즘은 가능한 모든 루트(가능한 루트는 zval 변수 컨테이너임)를 루트 버퍼(보라색으로 표시, 의심되는 가비지라고 함)에 넣습니다. 가능한 가비지 루트는 버퍼에 한 번만 나타납니다. 가비지 수집은 루트 버퍼가 가득 찬 경우에만 버퍼 내의 모든 다른 변수 컨테이너에서 수행됩니다. 위 이미지의 A단계를 살펴보세요.

B단계에서 각 보라색 변수 삭제를 시뮬레이션합니다. 삭제를 시뮬레이션할 때 보라색이 아닌 일반 변수의 참조 카운트가 "1"만큼 줄어들 수 있습니다. 일반 변수의 참조 카운트가 0이 되면 다시 일반 변수의 삭제를 시뮬레이션합니다. 각 변수는 한 번만 시뮬레이션 삭제될 수 있으며 시뮬레이션 삭제 후에는 회색으로 표시됩니다(원본 기사에서는 동일한 변수 컨테이너가 "1"만큼 두 번 감소하지 않도록 보장한다고 했는데 이는 잘못된 것입니다).

C단계에서 시뮬레이션은 각 보라색 변수를 복원합니다. 복구는 조건부입니다. 변수의 참조 횟수가 0보다 크면 시뮬레이션된 복구가 수행됩니다. 마찬가지로 각 변수는 한 번만 복원할 수 있습니다. 복원 후에는 기본적으로 B단계의 역동작입니다. 이러한 방식으로 복구할 수 없는 나머지 파란색 노드 더미는 D 단계에서 이를 탐색하여 삭제해야 하는 파란색 노드입니다.

알고리즘은 모두 단순 순회(가장 일반적인 심층 검색 순회)를 사용하는 시뮬레이션 삭제, 시뮬레이션 복구 및 실제 삭제에 관한 것입니다. 복잡성은 보라색으로 의심되는 가비지 변수뿐만 아니라 시뮬레이션 작업을 수행하는 노드 수와 긍정적인 관련이 있습니다.

이제 이 알고리즘에 대한 기본적인 이해를 마쳤으므로 다시 돌아가 이것이 PHP와 어떻게 통합되는지 살펴보겠습니다. 기본적으로 PHP의 가비지 수집 메커니즘은 켜져 있으며 이를 수정할 수 있는 php.ini 설정이 있습니다: zend.enable_gc.

가비지 수집 메커니즘이 켜져 있으면 루트 버퍼가 가득 찰 때마다 위에서 설명한 루프 검색 알고리즘이 실행됩니다. 루트 캐시 영역은 고정된 크기를 가지며 10,000개의 가능한 루트를 저장할 수 있습니다. 물론 PHP 소스 코드 파일 Zend/zend_gc.c에서 상수 GC_ROOT_BUFFER_MAX_ENTRIES를 수정한 다음 PHP를 다시 컴파일하여 이 10,000개의 값을 수정할 수 있습니다. 가비지 수집이 꺼지면 루프 검색 알고리즘이 실행되지 않습니다. 그러나 구성에서 가비지 수집이 활성화되었는지 여부에 관계없이 루트가 항상 루트 버퍼에 존재할 수 있습니다.

가비지 수집 메커니즘이 꺼지면 루트 버퍼가 가능한 루트로 가득 차면 더 많은 가능한 루트가 기록되지 않습니다. 기록되지 않은 가능한 루트는 이 알고리즘으로 분석 및 처리되지 않습니다. 순환 참조 순환의 일부인 경우에는 지워지지 않으며 메모리 누수가 발생합니다.

가비지 수집이 불가능할 때에도 가능한 루트를 기록하는 이유는 가능한 루트를 찾을 때마다 가비지 수집이 켜져 있는지 확인하는 것보다 가능한 루트를 기록하는 것이 더 빠르기 때문입니다. 그러나 가비지 수집 및 분석 메커니즘 자체에는 많은 시간이 걸립니다.

구성 zend.enable_gc을 수정하는 것 외에도 gc_enable()gc_disable() 함수를 각각 호출하여 가비지 수집 메커니즘을 켜고 끌 수도 있습니다. 이러한 함수를 호출하는 것은 가비지 수집 메커니즘을 켜거나 끄기 위해 구성 항목을 수정하는 것과 동일한 효과를 갖습니다. 루트 버퍼가 가득 차지 않은 경우에도 주기적으로 수집을 강제하는 기능. 이 목적을 달성하려면 gc_collect_cycles() 함수를 호출할 수 있습니다. 이 함수는 이 알고리즘을 사용하여 재활용된 주기 수를 반환합니다.

가비지 수집을 켜고 끄고 자율 초기화를 허용하는 이유는 애플리케이션의 일부 부분이 시간에 민감할 수 있기 때문입니다. 이 경우 가비지 수집을 사용하고 싶지 않을 것입니다. 물론 애플리케이션의 특정 부분에 대해 가비지 수집을 끄면 메모리 누수가 발생할 위험이 있습니다. 일부 루트는 제한된 루트 버퍼에 맞지 않을 수 있기 때문입니다. 따라서 gc_disable() 함수를 호출하여 메모리를 해제하기 직전에 gc_collect_cycles() 함수를 먼저 호출하는 것이 현명할 수 있습니다. 이렇게 하면 루트 버퍼에 저장된 가능한 모든 루트가 지워지기 때문에 가비지 수집 메커니즘이 꺼지면 빈 버퍼를 남겨서 가능한 루트를 저장할 더 많은 공간을 확보할 수 있습니다.

관련 권장 사항:

PHP 가비지 수집 메커니즘 마스터하기

PHP 가비지 수집 및 메모리 관리 메커니즘에 대한 자세한 설명


위 내용은 PHP 가비지 수집 메커니즘에 대한 자세한 논의의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
이전 기사:PHP 출력 로그다음 기사:PHP 출력 로그