메모리 관리와 가비지 컬렉션
zval, 참조 횟수 계산과 GC의 작동 방식을 살펴봅니다.
메모리 관리와 가비지 컬렉션은(는) CoddyKit의 무료 PHP Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 PHP Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
PHP의 메모리 관리 방식
PHP 값은 zval에 저장되며, 주로 참조 카운팅으로 관리되고 참조 카운팅으로 처리할 수 없는 경우를 순환 참조 수집기가 찾아냅니다. 이를 이해하면 오래 실행되는 작업자에서 메모리 누수가 발생하는 이유, 복사 비용, gc_collect_cycles()가 중요한 시점을 알 수 있습니다.
zval
zval은 모든 PHP 값을 담는 컨테이너입니다. 형식 태그와 값을 저장하는 유니언으로 구성됩니다. 스칼라 값(정수, 실수, 불리언)은 zval 내부에 값으로 저장되며 참조 카운팅되지 않습니다. 복합 형식(문자열, 배열, 객체)은 힙에 있는 참조 카운팅 구조를 가리킵니다.
<?php
// debug_zval_refcount-style inspection via gc + count
$arr = [1, 2, 3];
$copy = $arr; // refcount of the array bumps, no data copy yet (CoW)
// Modifying triggers copy-on-write separation
$copy[] = 4;
var_dump($arr); // still [1,2,3]
var_dump($copy); // [1,2,3,4]
?>참조 카운팅
참조 카운팅되는 각 값은 자신을 가리키는 zval의 수를 추적합니다. 대입하면 카운트가 증가하고, 설정을 해제하거나 범위를 벗어나면 감소합니다. 카운트가 0이 되면 가비지 컬렉터 실행을 기다리지 않고 메모리가 즉시 해제됩니다. PHP 메모리의 대부분은 이런 방식으로 회수됩니다.
<?php
$a = str_repeat('x', 1000); // refcount 1 on the string
$b = $a; // refcount 2
unset($a); // refcount 1
unset($b); // refcount 0 -> freed instantly
echo "Refcounting frees deterministically\n";
?>쓰기 시 복사
대입할 때 데이터가 복사되는 것이 아니라 데이터를 공유하고 참조 카운트를 증가시킵니다(쓰기 시 복사). 실제 복제는 공유된 값에 처음으로 기록할 때만 발생합니다. 따라서 큰 배열도 수정하기 전까지는 전달 비용이 저렴합니다.
<?php
$big = range(1, 100000);
$shared = $big; // O(1): shared, refcount++
// Reads stay shared and cheap
echo count($shared), PHP_EOL;
// First write separates (copies) — now it costs O(n)
$shared[0] = -1;
echo $big[0], ' vs ', $shared[0], PHP_EOL; // 1 vs -1
?>문제: 순환 참조
참조 카운팅만으로는 순환 참조를 해제할 수 없습니다. 객체 A가 B를 참조하고 B가 다시 A를 참조하면, 모든 외부 변수를 설정 해제한 뒤에도 두 객체의 카운트가 0에 도달하지 않습니다. 따라서 순환 참조 수집기가 실행될 때까지 메모리가 누수됩니다.
<?php
class Node { public ?Node $ref = null; }
$a = new Node();
$b = new Node();
$a->ref = $b; // a -> b
$b->ref = $a; // b -> a (cycle!)
unset($a, $b); // external refs gone, but internal cycle keeps refcount > 0
// The objects are unreachable but NOT yet freed by refcounting alone.
echo gc_collect_cycles() . " cycles collected\n";
?>순환 참조 수집기
PHP의 가비지 컬렉터는 루트를 대상으로 동기식 표시 및 쓸어내기를 수행합니다. 잠재적인 순환 루트(참조 카운트가 감소했지만 0에 도달하지 않은 값)가 버퍼에 저장됩니다. 루트 버퍼가 가득 차면(기본값 10,000) 가비지 컬렉터가 실행되어 감소를 시뮬레이션하고, 실제로 도달할 수 없는 그룹을 찾아 해제합니다.
<?php
// GC status exposes the root buffer and run stats
for ($i = 0; $i < 3; $i++) {
$a = new stdClass();
$b = new stdClass();
$a->b = $b; $b->a = $a; // create a cycle
unset($a, $b); // becomes a GC root
}
print_r(gc_status());
?>가비지 컬렉터 제어
수집기를 제어할 수 있습니다. gc_disable()/gc_enable()로 수집기를 켜고 끌 수 있고, gc_collect_cycles()는 수집을 강제로 실행한 뒤 해제한 개수를 반환합니다. 처리량이 중요한 배치 작업에서는 처리 중인 짧은 단계 동안 가비지 컬렉터를 끈 다음 마지막에 한 번 수집하기도 합니다.
<?php
gc_disable(); // no automatic cycle collection
// ... heavy phase that creates and drops many cycles ...
for ($i = 0; $i < 1000; $i++) {
$x = new stdClass(); $y = new stdClass();
$x->y = $y; $y->x = $x; unset($x, $y);
}
$freed = gc_collect_cycles(); // reclaim all at once
gc_enable();
echo "Freed $freed objects\n";
?>오래 실행되는 작업자
CLI 데몬, 대기열 소비자, Swoole/RoadRunner 작업자에서는 요청 종료 시 메모리가 회수되지 않습니다. 프로세스가 몇 시간 동안 계속 실행되기 때문입니다. 무제한으로 증가하는 정적 캐시, 쌓이는 순환 참조, 닫히지 않은 리소스를 주의하십시오. memory_get_usage(true)로 모니터링하고 작업자를 주기적으로 재시작하십시오.
<?php
$peakStart = memory_get_usage(true);
// Simulate processing a batch of jobs
for ($job = 0; $job < 5; $job++) {
$payload = range(0, 10000);
// ... process ...
unset($payload);
}
printf("start: %d KB, peak: %d KB, now: %d KB\n",
$peakStart >> 10,
memory_get_peak_usage(true) >> 10,
memory_get_usage(true) >> 10
);
?>WeakReference 및 WeakMap
객체를 살아 있게 유지하지 않고 객체에 데이터를 캐시하거나 연결하려면 WeakReference(8.0)와 WeakMap(8.0)을 사용하십시오. WeakMap 항목은 키 객체의 참조 카운트를 증가시키지 않으므로 객체가 수집될 수 있고 해당 항목도 자동으로 제거됩니다. 따라서 누수가 발생해서는 안 되는 메타데이터 보조 테이블에 적합합니다.
<?php
$map = new WeakMap();
$obj = new stdClass();
$map[$obj] = 'metadata'; // does NOT keep $obj alive
echo count($map) . PHP_EOL; // 1
unset($obj); // object collectible -> entry removed
echo count($map) . PHP_EOL; // 0
?>인터닝된 문자열과 불변 값
소스의 문자열 리터럴은 인터닝되어 한 번만 저장되고 공유되며, 요청 중에는 참조 카운팅되거나 해제되지 않습니다. OPcache는 인터닝된 문자열과 불변인 캐시된 연산 코드 배열을 요청 간 공유 메모리로 옮깁니다. 따라서 문자열 리터럴은 복사 비용이 사실상 없습니다.
메모리 제한과 할당자
PHP는 시스템 할당자 위에 자체 emalloc 할당자(Zend 메모리 관리자)를 계층화해 사용하며, 요청별 사용량을 memory_limit과 비교해 추적합니다. 제한에 도달하면 치명적 오류가 발생합니다. memory_get_usage(true)에서 true를 사용하면 현재 스크립트가 보유한 양만이 아니라 운영 체제에서 실제로 할당한 블록을 보고합니다.
<?php
echo 'limit: ' . ini_get('memory_limit') . PHP_EOL;
echo 'emalloc usage: ' . memory_get_usage() . " bytes\n"; // ZendMM tracked
echo 'real usage: ' . memory_get_usage(true) . " bytes\n"; // from OS
?>빠른 확인
참조 카운팅만으로는 왜 일부 객체에서 메모리 누수가 발생할 수 있나요?
복습
PHP는 값을 zval에 저장합니다. 스칼라는 값으로 저장되고, 복합 형식은 힙에 할당되며 참조 카운팅과 쓰기 시 복사로 관리됩니다. 참조 카운팅은 카운트가 0이 되면 결정적으로 메모리를 해제하지만 순환 참조는 회수할 수 없습니다. 따라서 루트 버퍼가 가득 차면(또는 gc_collect_cycles()를 통해) 동기식 표시 및 쓸어내기 수집기가 실행됩니다. 소유하지 않는 연결에는 WeakMap/WeakReference를 사용하고, 오래 실행되는 작업자에서 무제한 증가가 발생하지 않는지 확인하십시오.
자주 묻는 질문
“메모리 관리와 가비지 컬렉션” 강의는 무료인가요?
네 — “메모리 관리와 가비지 컬렉션” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 PHP Academy 강의 전체를 잠금 해제할 수 있습니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“메모리 관리와 가비지 컬렉션”에서 뭘 배우나요?
zval, 참조 횟수 계산과 GC의 작동 방식을 살펴봅니다. 브라우저에서 직접 실행하는 실습 코드로 PHP Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
PHP Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 PHP Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“메모리 관리와 가비지 컬렉션” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 PHP Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 PHP Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- Zend Engine의 작동 원리
- 메모리 관리와 가비지 컬렉션
- OPcache와 JIT 컴파일
- C로 기본 PHP 확장 기능 작성