이 글에서는 PHP 개발 과정에서의 가비지 수집 및 메모리 관리 관련 내용을 설명합니다.
참조 계산
PHP 5.2 및 이전 버전에서 PHP의 가비지 수집은 참조 계산 알고리즘을 사용합니다.
참조 계산에 대한 기본 지식
PHP 변수는 "zval" 변수 컨테이너(데이터 구조)에 저장됩니다. "zval" 속성에는 다음 정보가 포함됩니다.
- 데이터 유형
- 현재 변수의 값
- 변수가 참조로 전달되었는지 식별하는 데 사용되는 is_ref 부울 유형 식별자
- "zval" 변수 컨테이너의 변수 수를 가리키는 refcount 식별자 (즉, 이 zval이 참조된 횟수를 여기서 주의하십시오. 참조는 값 전달을 의미하지 않으므로 구별에 주의하십시오.)
변수에 값이 할당되면 해당 "zavl" 변수 컨테이너가 생성됩니다. [추천 학습: PHP 동영상 튜토리얼]
변수 zval 컨테이너 정보 보기
변수의 "zval" 컨테이너 정보를 보려면(즉, 변수의 is_ref 및 refcount를 보려면) XDebug 디버깅 도구의 xdebug_debug_zval() 함수 .
XDebug 확장 플러그인 설치 방법은 이 튜토리얼(https://github.com/huliuqing/phpnotes/issues/58)을 참조하세요. XDebug 사용 방법은 공식 문서(https: //xdebug.org/docs/) .
XDebug 도구를 성공적으로 설치했으며 이제 변수를 디버깅할 수 있다고 가정합니다.
- 일반 변수의 zval 정보 보기
PHP 문이 단순히 변수를 할당하는 경우 is_ref 식별자 값은 0이고 이 변수가 다른 변수에 값으로 할당되면 refcount 값은 1입니다. zval 변수 컨테이너의 refcount 수; 마찬가지로 변수가 삭제(설정 해제)되면 그에 따라 "refcount"가 1을 뺍니다.
다음 예를 참조하세요.
<?php // 变量赋值时,refcount 值等于 1 $name = 'liugongzi'; xdebug_debug_zval('name'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9) // $name 作为值赋值给另一个变量, refcount 值增加 1 $copy = $name; xdebug_debug_zval('name'); // (refcount=2, is_ref=0)string 'liugongzi' (length=9) // 销毁变量,refcount 值减掉 1 unset($copy); xdebug_debug_zval('name'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9)
- Copy on Write
Copy On Write(COW)는 간단히 다음과 같이 설명됩니다. 할당을 통해 변수에 값이 할당되면 새 메모리는 새 메모리를 할당하지 않습니다. 변수에 의해 저장된 값은 단순히 변수에 대한 참조 중 하나가 가리키는 값이 변경되는 경우에만 메모리 사용량을 줄이기 위해 값 내용을 저장하기 위해 새로운 공간을 할당합니다. - TPIP copy-on-write
이전 단순 변수의 zval 정보를 통해 $copy와 $name이 zval 변수 컨테이너(메모리)를 공유한다는 것을 알 수 있으며, refcount를 사용하여 이를 나타냅니다. 현재 이 zval 사용에 사용되는 변수 수입니다.
예제를 보세요:
<?php $name = 'liugongzi'; xdebug_debug_zval('name'); // name: (refcount=1, is_ref=0)string 'liugongzi' (length=9) $copy = $name; xdebug_debug_zval('name'); // name: (refcount=2, is_ref=0)string 'liugongzi' (length=9) // 将新的值赋值给变量 $copy $copy = 'liugongzi handsome'; xdebug_debug_zval('name'); // name: (refcount=1, is_ref=0)string 'liugongzi' (length=9) xdebug_debug_zval('copy'); // copy: (refcount=1, is_ref=0)='liugongzi handsome'
liugongzi handsome 값이 $copy에 할당되면 name과 copy의 refcount 값이 1이 된다는 것을 알고 계셨나요? 이 과정에서 다음은 다음과 같습니다.
- $name의 zval에서 $copy를 분리합니다(즉, 복사).
- $name의 refcount를 1로 뺍니다.
- $copy의 zval을 수정(재할당)하고
여기에 "글을 쓰면서 복사하기"에 대한 간략한 소개가 있습니다. 관심 있는 친구들은 기사 마지막 부분에 있는 참고 자료를 읽고 더 깊이 있는 연구를 할 수 있습니다.
- 참조로 전달된 변수의 zval 정보 보기
참조별 전달(&)의 "참조 계산" 규칙은 is_ref 값이 1 변수가 참조에 의한 전달 유형임을 나타냅니다.
이제 참조로 전달하는 예를 살펴보겠습니다.
<?php $age = 'liugongzi'; xdebug_debug_zval('age'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9) $copy = &$age; xdebug_debug_zval('age'); // (refcount=2, is_ref=1)string 'liugongzi' (length=9) unset($copy); xdebug_debug_zval('age'); // (refcount=1, is_ref=1)string 'liugongzi' (length=9)
- 복합 유형의 참조 계산
스칼라 유형(정수, 부동 소수점, 부울 등), 배열 및 객체와 다릅니다.) 유형 규칙은- 참조 카운팅을 준수하는 것은 좀 더 복잡합니다.
더 나은 설명을 위해 먼저 배열의 참조 계산 예를 살펴보겠습니다.
$a = array( 'meaning' => 'life', 'number' => 42 ); xdebug_debug_zval( 'a' ); // a: // (refcount=1, is_ref=0) // array (size=2) // 'meaning' => (refcount=1, is_ref=0)string 'life' (length=4) // 'number' => (refcount=1, is_ref=0)int 42
위의 참조 계산 다이어그램은 다음과 같습니다.
그림에서 복합 유형의 참조 계산 규칙은 다음과 같습니다. 기본적으로 스칼라 계산과 동일합니다. 규칙은 동일합니다. 주어진 예에서 PHP는 3개의 zval 변수 컨테이너를 생성합니다. 하나는 배열 자체를 저장하고 두 개는 배열의 요소를 저장합니다.
기존 요소를 배열에 추가하면 참조 카운터 참조 횟수가 1씩 증가합니다.
$a = array( 'meaning' => 'life', 'number' => 42 ); xdebug_debug_zval( 'a' ); $a['life'] = $a['meaning']; xdebug_debug_zval( 'a' ); // a: // (refcount=1, is_ref=0) // array (size=3) // 'meaning' => (refcount=2, is_ref=0)string 'life' (length=4) // 'number' => (refcount=0, is_ref=0)int 42 // 'life' => (refcount=2, is_ref=0)string 'life' (length=4)
대략적인 도표는 다음과 같습니다.
- 内存泄露
虽然,复合类型的引用计数规则同标量类型大致相同,但是如果引用的值为变量自身(即循环应用),在处理不当时,就有可能会造成内存泄露的问题。
让我们来看看下面这个对数组进行引用传值的示例:
<?php // @link http://php.net/manual/zh/function.memory-get-usage.php#96280 function convert($size) { $unit=array('b','kb','mb','gb','tb','pb'); return @round($size/pow(1024,($i=floor(log($size,1024)))),2).' '.$unit[$i]; } // 注意:有用的地方从这里开始 $memory = memory_get_usage(); $a = array( 'one' ); // 引用自身(循环引用) $a[] =&$a; xdebug_debug_zval( 'a' ); var_dump(convert(memory_get_usage() - $memory)); // 296 b unset($a); // 删除变量 $a,由于 $a 中的元素引用了自身(循环引用)最终导致 $a 所使用的内存无法被回收 var_dump(convert(memory_get_usage() - $memory)); // 568 b
从内存占用结果上看,虽然我们执行了 unset($a) 方法来销毁 $a 数组,但内存并没有被回收,整个处理过程的示意图如下:
可以看到对于这块内存,再也没有符合表(变量)指向了,所以 PHP 无法完成内存回收,官方给出的解释如下:
尽管不再有某个作用域中的任何符号指向这个结构 (就是变量容器),由于数组元素 “1” 仍然指向数组本身,所以这个容器不能被清除 。因为没有另外的符号指向它,用户没有办法清除这个结构,结果就会导致内存泄漏。庆幸的是,php 将在脚本执行结束时清除这个数据结构,但是在 php 清除之前,将耗费不少内存。如果你要实现分析算法,或者要做其他像一个子元素指向它的父元素这样的事情,这种情况就会经常发生。当然,同样的情况也会发生在对象上,实际上对象更有可能出现这种情况,因为对象总是隐式的被引用。 - 摘自 官方文档 Cleanup Problems
简单来说就是「引用计数」算法无法检测并释放循环引用所使用的内存,最终导致内存泄露。
引用计数系统的同步周期回收
由于引用计数算法存在无法回收循环应用导致的内存泄露问题,在 PHP 5.3 之后对内存回收的实现做了优化,通过采用 引用计数系统的同步周期回收 算法实现内存管理。引用计数系统的同步周期回收算法是一个改良版本的引用计数算法,它在引用基础上做出了如下几个方面的增强:
- 引入了可能根(possible root)的概念:通过引用计数相关学习,我们知道如果一个变量(zval)被引用,要么是被全局符号表中的符号引用(即变量),要么被复杂类型(如数组)的 zval 中的符号(数组的元素)引用,那么这个 zval 变量容器就是「可能根」。
- 引入根缓冲区(root buffer)的概念:根缓冲区用于存放所有「可能根」,它是固定大小的,默认可存 10000 个可能根,如需修改可以通过修改 PHP 源码文件 Zend/zend_gc.c 中的常量 GC_ROOT_BUFFER_MAX_ENTRIES,再重新编译。
- 回收周期:当缓冲区满时,对缓冲区中的所有可能根进行垃圾回收处理。
下图(来自 PHP 手册),展示了新的回收算法执行过程:
引用计数系统的同步周期回收过程
- 缓冲区(紫色框部分,称为疑似垃圾),存储所有可能根(步骤 A);
- 采用深度优先算法遍历「根缓冲区」中所有的「可能根(即 zval 遍历容器)」,并对每个 zval 的 refcount 减 1,为了避免遍历时对同一个 zval 多次减 1(因为不同的根可能遍历到同一个 zval)将这个 zvel 标记为「已减」(步骤 B);
- 再次采用深度优先遍历算法遍历「可能根 zval」。当 zval 的 refcount 值不为 0 时,对其加 1,否则保持为 0。并请已遍历的 zval 变量容器标记为「已恢复」(即步骤 B 的逆运算)。那些 zval 的 refcount 值为 0 (蓝色框标记)的就是应该被回收的变量(步骤 C);
- 删除所有 refcount 为 0 的可能根(步骤 D)。
整个过程为:
采用深度优先算法执行:默认删除 > 模拟恢复 > 执行删除 达到内存回收的目的。
优化后的引用计数算法优势
- 将内存泄露控制在阀值内,这个由缓存区实现,达到缓冲区大小执行新一轮垃圾回收;
- 提升了垃圾回收性能,不是每次 refcount 减 1 都执行回收处理,而是等到根缓冲区满时才开始执行垃圾回收。
你可以从 PHP 手册 的回收周期 了解更多,也可以阅读文末给出的参考资料。
PHP 7 的内存管理
PHP 5 中 zval 实现上的主要问题:
- zval 항상 힙에서만 메모리를 할당합니다 ;
- zval 항상 참조 계산 및 재활용 정보를 저장합니다. 이러한 정보가 필요하지 않은 정수(bool/null) 데이터에 대해서도 ;
- 객체나 리소스를 사용할 때 직접 참조하면 이중 계산이 발생합니다.
- 일부 간접 액세스에는 더 나은 처리가 필요합니다. 예를 들어, 변수에 저장된 개체에 액세스하면 이제 4개의 포인터를 간접적으로 사용합니다(포인터 체인의 길이는 4개입니다).
- 직접 계산은 값이 zval 간에만 공유될 수 있음을 의미합니다. zval과 해시테이블 키 사이에 문자열을 공유하려는 경우에는 작동하지 않습니다(해시테이블 키도 zval이 아닌 경우).
PHP 7의 zval 데이터 구조 구현 조정:
가장 기본적인 변경 사항은 zval에 필요한 메모리가 더 이상 힙과 별도로 할당되지 않으며 zval이 더 이상 참조 카운트를 저장하지 않는다는 것입니다. 복잡한 데이터 유형(문자열, 배열, 객체 등)의 참조 횟수는 자체적으로 저장됩니다. - PHP 7의 내부 값 표현에서 발췌 - 1부【번역】이 구현의 장점:
- 간단한 데이터 유형은 메모리를 별도로 할당할 필요가 없고 계산할 필요도 없습니다.
- 더 이상은 없습니다. 이중 계산 . 개체에서는 개체 자체에 저장된 개수만 유효합니다.
- 이제 개수는 값 자체에 의해 저장되므로(PHP에는 zval 변수 컨테이너 저장소가 있음) 다음과 같은 zval이 아닌 구조의 데이터와 공유할 수 있습니다. zval과 해시테이블 키 시간 사이
- 간접 액세스에 필요한 포인터 수가 줄어듭니다.
PHP 7 zval 구현 및 메모리 최적화에 대한 자세한 내용은 PHP7 커널 zval에 대한 심층적 이해 및 PHP 7의 내부 값 표현 - 1부 번역을 참조하세요. (https://www.npopov.com/2015/05/05/Internal-value-representation-in-PHP-7-part-1.html)