참조 카운팅 기본 사항

PHP 변수는 "zval"이라는 컨테이너에 저장됩니다. zval 컨테이너에는 변수의 유형과 값 외에 두 가지 추가 정보 비트가 포함됩니다. 첫 번째는 "is_ref"라고 하며 변수가 "참조 세트"의 일부인지 여부를 나타내는 부울 값입니다. 이 비트를 통해 PHP 엔진은 일반 변수와 참조를 구별하는 방법을 알고 있습니다. & 연산자에 의해 생성된 것처럼 PHP는 사용자 영역 참조를 허용하므로 zval 컨테이너에는 메모리 사용을 최적화하는 내부 참조 계산 메커니즘도 있습니다. "refcount"라고 하는 이 두 번째 추가 정보에는 이 하나의 zval 컨테이너를 가리키는 변수 이름(기호라고도 함)이 몇 개나 포함되어 있습니다. 모든 기호는 범위당 하나씩 있는 기호 테이블에 저장됩니다. 기본 스크립트(즉, 브라우저를 통해 요청된 스크립트)에 대한 범위와 모든 함수 또는 메서드에 대한 범위가 있습니다.

zval 컨테이너는 다음과 같이 상수 값으로 새 변수가 생성될 때 생성됩니다.

예제 #1 새 zval 컨테이너 만들기

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

이 경우 현재 범위에 새 기호 이름인 a가 생성되고 유형 string 및 값 new string을 사용하여 새 변수 컨테이너가 생성됩니다. "is_ref" 비트는 사용자 영역 참조가 생성되지 않았기 때문에 기본적으로 false로 설정됩니다. "refcount"는 이 변수 ​​컨테이너를 사용하는 기호가 하나만 있으므로 1로 설정됩니다. "refcount"가 1인 참조(즉, "is_ref"가 true임)는 참조가 아닌 것처럼 처리됩니다(즉, "is_ref"가 false인 것처럼). » Xdebug가 설치된 경우 xdebug_debug_zval()을 호출하여 이 정보를 표시할 수 있습니다.

예제 #2 zval 정보 표시

                  
<?php
$a = "new string";
xdebug_debug_zval('a');
?>
                  
                

위의 예는 다음을 출력합니다.

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

이 변수를 다른 변수 이름에 할당하면 참조 수가 증가합니다.

예제 #3 zval의 refcount 증가

                  
<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
?>
                  
                

위의 예는 다음을 출력합니다.

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

refcount는 2입니다. 동일한 변수 컨테이너가 ab 모두에 연결되어 있기 때문입니다. PHP는 필요하지 않을 때 실제 변수 컨테이너를 복사하지 않을 만큼 충분히 똑똑합니다. "refcount"가 0에 도달하면 변수 컨테이너가 파괴됩니다. "refcount"는 변수 컨테이너에 연결된 기호가 범위를 벗어날 때(예: 함수가 종료될 때) 또는 기호가 할당되지 않을 때(예: unset() 호출하여) 하나 감소합니다. 다음 예에서는 이를 보여줍니다.

예제 #4 zval 참조 횟수 감소

                  
<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
$b = 42;
xdebug_debug_zval( 'a' );
unset( $c );
xdebug_debug_zval( 'a' );
?>
                  
                

위의 예는 다음을 출력합니다.

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

이제 unset($a);를 호출하면 유형과 값을 포함하는 변수 컨테이너가 메모리에서 제거됩니다.


Compound Types

배열 및 객체와 같은 복합 유형을 사용하면 상황이 조금 더 복잡해집니다. 스칼라 값과 달리 배열과 객체는 고유한 기호 테이블에 속성을 저장합니다. 즉, 다음 예제에서는 세 개의 zval 컨테이너를 만듭니다.

예제 #5 zval 배열 만들기

                  
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
?>
                  
                

위의 예는 다음과 유사한 결과를 출력합니다.

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

또는 그래픽으로

Zvals for a simple array

세 개의 zval 컨테이너는 a, meaningnumber입니다. 유사한 규칙이 "refcount"를 늘리거나 줄이는 데 적용됩니다. 아래에서 배열에 다른 요소를 추가하고 해당 값을 이미 존재하는 요소의 내용으로 설정합니다.

예제 #6 배열에 이미 존재하는 요소 추가하기

                  
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
xdebug_debug_zval( 'a' );
?>
                  
                

위의 예는 다음과 유사한 결과를 출력합니다.

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'
)
                

또는 그래픽으로

Zvals for a simple array with a reference

위의 Xdebug 출력에서 ​​이전 배열 요소와 새 배열 요소가 모두 "refcount"가 2인 zval 컨테이너를 가리키는 것을 볼 수 있습니다. Xdebug의 출력은 값이 'life'인 두 개의 zval 컨테이너를 보여주지만 동일한 것입니다. xdebug_debug_zval() 함수는 이것을 표시하지 않지만 메모리 포인터도 표시하여 볼 수 있습니다.

배열에서 요소를 제거하는 것은 범위에서 기호를 제거하는 것과 같습니다. 이렇게 하면 배열 요소가 가리키는 컨테이너의 "refcount"가 줄어듭니다. 다시 "refcount"가 0에 도달하면 변수 컨테이너가 메모리에서 제거됩니다. 다시 한 번, 이것을 보여주는 예:

예제 #7 배열에서 요소 제거

                  
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );
?>
                  
                

위의 예는 다음과 유사한 결과를 출력합니다.

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

이제 배열 자체를 배열의 요소로 추가하면 상황이 흥미로워집니다. 다음 예제에서 수행합니다. 여기에서 참조 연산자도 몰래 들어가게 됩니다. 그렇지 않으면 PHP가 복사본을 생성하기 때문입니다.

예제 #8 배열 자체를 자체 요소로 추가

                  
<?php
$a = array( 'one' );
$a[] =& $a;
xdebug_debug_zval( 'a' );
?>
                  
                

위의 예는 다음과 유사한 결과를 출력합니다.

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

또는 그래픽으로

Zvals for an array with a circular reference

배열 변수(a)와 두 번째 요소(1)가 이제 "refcount"가 2인 변수 컨테이너를 가리키는 것을 볼 수 있습니다. 위의 디스플레이에서 "..."는 재귀가 관련되어 있음을 보여줍니다. 물론 이 경우 "..."는 원래 배열을 다시 가리킵니다.

이전과 마찬가지로 변수 설정을 해제하면 기호가 제거되고 변수가 가리키는 변수 컨테이너의 참조 횟수가 1 감소합니다. 따라서 위의 코드를 실행한 후 $a 변수의 설정을 해제하면 $a와 요소 "1"이 가리키는 변수 컨테이너의 참조 횟수가 "2"에서 "1"로 1씩 감소합니다. 이것은 다음과 같이 나타낼 수 있습니다.

예제 #9 $a 설정 해제

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

또는 그래픽으로

Zvals after removal of array with a circular reference demonstrating the memory leak


Cleanup Problems

이 구조를 가리키는 범위에 더 이상 기호가 없지만 배열 요소 "1"이 여전히 동일한 배열을 가리키기 때문에 정리할 수 없습니다. 이를 가리키는 외부 기호가 없기 때문에 사용자가 이 구조를 정리할 방법이 없습니다. 따라서 메모리 누수가 발생합니다. 다행히 PHP는 요청이 끝날 때 이 데이터 구조를 정리하지만 그 전에는 메모리에서 귀중한 공간을 차지합니다. 이 상황은 구문 분석 알고리즘이나 자식이 "부모" 요소를 다시 가리키는 다른 작업을 구현하는 경우 자주 발생합니다. 물론 객체가 참조에 의해 항상 암시적으로 사용되기 때문에 실제로 발생할 가능성이 더 높은 객체에서도 동일한 상황이 발생할 수 있습니다.

이러한 일이 한두 번만 발생하면 문제가 되지 않을 수 있지만 이러한 메모리 손실이 수천 또는 수백만이면 분명히 문제가 되기 시작합니다. 이것은 요청이 기본적으로 끝나지 않는 데몬과 같은 장기 실행 스크립트 또는 대규모 단위 테스트 세트에서 특히 문제가 됩니다. 후자는 eZ 구성 요소 라이브러리의 템플릿 구성 요소에 대한 단위 테스트를 실행하는 동안 문제를 일으켰습니다. 어떤 경우에는 테스트 서버에 충분하지 않은 2GB 이상의 메모리가 필요합니다.