>  기사  >  백엔드 개발  >  PHP의 가비지 수집 및 메모리 관리에 대한 자세한 분석

PHP의 가비지 수집 및 메모리 관리에 대한 자세한 분석

不言
不言원래의
2018-08-15 09:40:551302검색

이 기사는 PHP의 가비지 수집 및 메모리 관리에 대한 자세한 분석을 제공합니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

참조 계산

PHP 5.2 및 이전 버전에서 PHP의 가비지 수집은 참조 계산 알고리즘을 사용합니다.

참조 계산에 대한 기본 지식

php 변수는 "zval" 변수 컨테이너(데이터 구조)에 저장됩니다. "zval" 속성에는 다음 정보가 포함됩니다.

  • 현재 변수의 데이터 유형;

  • 현재 변수의 값 여기서 참고하는 것은 값을 전달하는 것이 아니므로 구별에 주의하시기 바랍니다.

  • 변수에 값이 할당되면 해당 "zavl" 변수 컨테이너가 생성됩니다.

  • 변수 zval 컨테이너 정보 보기
  • 변수의 "zval" 컨테이너 정보를 보려면(즉, 변수의 is_ref 및 refcount를 보려면) XDebug 디버깅의
  • xdebug_debug_zval()
함수를 사용할 수 있습니다. 도구.

XDebug 확장 플러그인 설치 방법은 이 튜토리얼에서 볼 수 있습니다. XDebug 사용 방법은 공식 문서를 읽어보세요.

XDebug 도구를 성공적으로 설치했으며 이제 변수를 디버깅할 수 있다고 가정합니다.

일반 변수의 zval 정보 보기

PHP 문이 단순한 변수 할당인 경우 is_ref 식별자 값은 0이고 이 변수가 값으로 할당되면 refcount 값은 1입니다. 다른 변수를 추가한 다음 zval 변수 컨테이너의 참조 횟수를 늘립니다. 마찬가지로 변수가 삭제(설정 해제)되면 그에 따라 "refcount"가 1씩 뺍니다.

아래 예를 참조하세요.
    <?php // 变量赋值时,refcount 值等于 1
    $name = &#39;liugongzi&#39;;
    xdebug_debug_zval(&#39;name&#39;); // (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)
    
    // $name 作为值赋值给另一个变量, refcount 值增加 1
    $copy = $name;
    xdebug_debug_zval(&#39;name&#39;); // (refcount=2, is_ref=0)string &#39;liugongzi&#39; (length=9)
    
    // 销毁变量,refcount 值减掉 1
    unset($copy);
    xdebug_debug_zval(&#39;name&#39;); // (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)
  • 쓰기 시 복사

  • 쓰기 시 복사(복사 ~에 쓰기: COW), 간단한 설명은 다음과 같습니다: 할당에 의해 변수에 값이 할당되면 새 변수에 의해 저장된 값을 저장하기 위해 새 메모리가 할당되지 않지만 메모리는 단순히 카운터를 통해 공유됩니다. 변수의 값이 변경되면 메모리 사용량을 줄이기 위해 값 내용을 저장하기 위해 새로운 공간이 할당됩니다. - TPIP copy-on-write

    이전 단순 변수의 zval 정보를 통해 $copy와 $name이 zval 변수 컨테이너(메모리)를 공유한다는 것을 알고 있으며, refcount를 사용하여 현재 이를 사용하고 있는 변수가 몇 개인지 나타냅니다. zval.

    예제를 보세요:

    <?php $name = &#39;liugongzi&#39;;
    xdebug_debug_zval(&#39;name&#39;); // name: (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)
    
    $copy = $name;
    xdebug_debug_zval(&#39;name&#39;); // name: (refcount=2, is_ref=0)string &#39;liugongzi&#39; (length=9)
    
    // 将新的值赋值给变量 $copy
    $copy = &#39;liugongzi handsome&#39;;
    xdebug_debug_zval(&#39;name&#39;); // name: (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)
    xdebug_debug_zval(&#39;copy&#39;); // copy: (refcount=1, is_ref=0)=&#39;liugongzi handsome&#39;
    liugongzi handsome 값이 $copy 변수에 할당되면 name과 copy의 refcount 값이 1이 된다는 것을 알고 계셨나요? 이 과정에서 다음 작업이 발생합니다. :

    $name의 zval에서 $copy를 분리합니다(예: 사본).

    $name의 refcount에서 1을 뺍니다.

    • $name의 zval을 수정합니다(refcount 재할당 및 수정).

    • 다음은 "기록 중 복사"에 대한 간략한 소개입니다. 관심 있는 친구는 기사 마지막에 제공된 참고 자료를 읽고 더 깊이 있는 연구를 수행할 수 있습니다.

    • 참조 전달 변수의 zval 정보 보기

    참조 전달 값(&)의 "참조 계산" 규칙은

    is_ref 값을 제외하고 일반 할당 문과 동일합니다.

      1
    • 입니다. 이는 변수가 참조 값 유형으로 전달됨을 의미합니다.

      이제 참조로 전달하는 예를 살펴보겠습니다.
    <?php $age = &#39;liugongzi&#39;;
    xdebug_debug_zval(&#39;age&#39;); // (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)
    
    $copy = &$age;
    xdebug_debug_zval(&#39;age&#39;); // (refcount=2, is_ref=1)string &#39;liugongzi&#39; (length=9)
    
    unset($copy);
    xdebug_debug_zval(&#39;age&#39;); // (refcount=1, is_ref=1)string &#39;liugongzi&#39; (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의 가비지 수집 및 메모리 관리에 대한 자세한 분석

      Memory Leak

      복합 유형의 참조 계산 규칙은 스칼라 유형과 거의 동일하지만 참조 값이 변수인 경우 자체(즉, 순환 응용 프로그램)를 잘못 처리하면 메모리 누수가 발생할 수 있습니다. PHP의 가비지 수집 및 메모리 관리에 대한 자세한 분석

      참조로 배열을 전달하는 다음 예를 살펴보겠습니다.
        <?php // @link http://php.net/manual/zh/function.memory-get-usage.php#96280
        function convert($size)
        {
            $unit=array(&#39;b&#39;,&#39;kb&#39;,&#39;mb&#39;,&#39;gb&#39;,&#39;tb&#39;,&#39;pb&#39;);
            return @round($size/pow(1024,($i=floor(log($size,1024)))),2).&#39; &#39;.$unit[$i];
        }
        
        // 注意:有用的地方从这里开始
        $memory = memory_get_usage();
        
        $a = array( &#39;one&#39; );
        
        // 引用自身(循环引用)
        $a[] =&$a;
        
        xdebug_debug_zval( &#39;a&#39; );
        
        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의 가비지 수집 및 메모리 관리에 대한 자세한 분석

        可以看到对于这块内存,再也没有符合表(变量)指向了,所以 PHP 无法完成内存回收,官方给出的解释如下:

        尽管不再有某个作用域中的任何符号指向这个结构 (就是变量容器),由于数组元素 “1” 仍然指向数组本身,所以这个容器不能被清除 。因为没有另外的符号指向它,用户没有办法清除这个结构,结果就会导致内存泄漏。庆幸的是,php 将在脚本执行结束时清除这个数据结构,但是在 php 清除之前,将耗费不少内存。如果你要实现分析算法,或者要做其他像一个子元素指向它的父元素这样的事情,这种情况就会经常发生。当然,同样的情况也会发生在对象上,实际上对象更有可能出现这种情况,因为对象总是隐式的被引用。

        简单来说就是「引用计数」算法无法检测并释放循环引用所使用的内存,最终导致内存泄露。

        引用计数系统的同步周期回收

        由于引用计数算法存在无法回收循环应用导致的内存泄露问题,在 PHP 5.3 之后对内存回收的实现做了优化,通过采用 引用计数系统的同步周期回收 算法实现内存管理。引用计数系统的同步周期回收算法是一个改良版本的引用计数算法,它在引用基础上做出了如下几个方面的增强:

        • 引入了可能根(possible root)的概念:通过引用计数相关学习,我们知道如果一个变量(zval)被引用,要么是被全局符号表中的符号引用(即变量),要么被复杂类型(如数组)的 zval 中的符号(数组的元素)引用,那么这个 zval 变量容器就是「可能根」。

        • 引入根缓冲区(root buffer)的概念:根缓冲区用于存放所有「可能根」,它是固定大小的,默认可存 10000 个可能根,如需修改可以通过修改 PHP 源码文件 Zend/zend_gc.c 中的常量 GC_ROOT_BUFFER_MAX_ENTRIES,再重新编译。

        • 回收周期:当缓冲区满时,对缓冲区中的所有可能根进行垃圾回收处理。

        下图(来自 PHP 手册),展示了新的回收算法执行过程:

        PHP의 가비지 수집 및 메모리 관리에 대한 자세한 분석

        引用计数系统的同步周期回收过程

        1. 缓冲区(紫色框部分,称为疑似垃圾),存储所有可能根(步骤 A);

        2. 采用深度优先算法遍历「根缓冲区」中所有的「可能根(即 zval 遍历容器)」,并对每个 zval 的 refcount 减 1,为了避免遍历时对同一个 zval 多次减 1(因为不同的根可能遍历到同一个 zval)将这个 zvel 标记为「已减」(步骤 B);

        3. 再次采用深度优先遍历算法遍历「可能根 zval」。当 zval 的 refcount 值不为 0 时,对其加 1,否则保持为 0。并请已遍历的 zval 变量容器标记为「已恢复」(即步骤 B 的逆运算)。那些 zval 的 refcount 值为 0 (蓝色框标记)的就是应该被回收的变量(步骤 C);

        4. 删除所有 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에는 zval 변수 컨테이너 저장소가 있음) zval이 아닌 구조 데이터와도 공유할 수 있습니다. 키 사이의 zval 및 해시 테이블과 같은

        • 간접 액세스에 필요한 포인터 수가 줄어듭니다.

        추천 관련 기사:

        PHP 스크립트의 메모리 관리 및 가비지 수집 - 개인 기사 Sifu

        저와 함께 javascript의 가비지 수집 메커니즘 및 메모리 관리_javascript 기술을 배워보세요

        Garbage Recycling에 대한 간략한 설명 PHP 가비지 수집 메커니즘

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

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