메모리 누수 탐지
동적 메모리의 해제 책임을 잃는 누수 원인을 이해하고 스마트 포인터·분석 도구·정적 분석으로 탐지하고 예방합니다.
C++과 같은 언어에서 동적 메모리 할당을 사용하는 경우, 또 다른 종류의 치명적인 오류가 발생할 수 있습니다.
바로 메모리 누수(Memory Leak)입니다.
메모리 누수는 당장은 눈에 띄지 않지만, 프로그램의 성능 저하와 결국에는 시스템 전체의 안정성을 위협할 수 있는 심각한 문제입니다.
이번 장에서는 메모리 누수가 무엇인지 이해하고, 이를 탐지하고 해결하기 위한 다양한 기법과 도구들을 학습하겠습니다.
먼저 아래 다이어그램으로 힙 블록의 해제 책임을 잃는 순간이 어떻게 누수로 이어지는지 확인합니다.
소유권은 획득에서 해제까지 끊기지 않아야 한다
leaky_function과 safe_unique_ptr_function의 독립 실행에서 할당·범위 종료·객체 잔존/해제를 같은 시점에 나란히 보이면 소유권 차이가 드러난다. 두 함수를 이어 실행한 것으로 표시하지 않는다.
C++ · RESOURCE RELEASE
포인터 변수의 소멸과 힙 객체의 해제를 구분한다
본문의 raw 포인터와 unique_ptr 예제를 같은 함수 종료 시점에서 비교한다. 소유권 이동은 선택 가능한 연산이며 해제에 항상 필요한 단계는 아니다.
raw 포인터 · 누수
본문 leaky_function은 MyClass(1)을 new하고 delete하지 않는다. 함수 종료 때 obj_ptr 변수만 사라지고 힙 객체는 남는다.
unique_ptr · 자동 해제
본문 safe_unique_ptr_function은 MyClass(2)의 해제 책임을 unique_ptr에 연결한다. 함수 종료 때 소유자가 소멸해 객체와 힙 자원을 정리한다.
본문 shared_ptr 예제의 MyClass(3)은 마지막 강한 소유자가 사라질 때 소멸한다. shared_ptr 순환은 weak_ptr로 설계해야 하며, raw 관찰 포인터의 유효 수명은 별도로 보장한다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 서로 다른 두 함수의 비교이다. raw 경로에서는 함수 종료가 포인터 변수만 정리하고 MyClass1의 해제 책임을 잃는다. unique_ptr 경로에서는 소유자 소멸이 기본 삭제자를 실행해 MyClass2를 소멸시키고 힙 자원을 반환한다. 두 경로를 잇는 실행 화살표는 없다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 두 독립 할당 leaky_function과 safe_unique_ptr_function이 각각 자원을 만든다. |
|
|
| 범위 종료 두 함수의 지역 변수가 범위를 나간다. |
|
|
| 자원 결과 명시적 해제가 없는 raw와 RAII 해제를 구분한다. |
|
|
소유권은 획득에서 해제까지 끊기지 않아야 한다
leaky_function과 safe_unique_ptr_function의 독립 실행에서 할당·범위 종료·객체 잔존/해제를 같은 시점에 나란히 보이면 소유권 차이가 드러난다. 두 함수를 이어 실행한 것으로 표시하지 않는다.
C++ · RESOURCE RELEASE
포인터 변수의 소멸과 힙 객체의 해제를 구분한다
본문의 raw 포인터와 unique_ptr 예제를 같은 함수 종료 시점에서 비교한다. 소유권 이동은 선택 가능한 연산이며 해제에 항상 필요한 단계는 아니다.
raw 포인터 · 누수
본문 leaky_function은 MyClass(1)을 new하고 delete하지 않는다. 함수 종료 때 obj_ptr 변수만 사라지고 힙 객체는 남는다.
unique_ptr · 자동 해제
본문 safe_unique_ptr_function은 MyClass(2)의 해제 책임을 unique_ptr에 연결한다. 함수 종료 때 소유자가 소멸해 객체와 힙 자원을 정리한다.
본문 shared_ptr 예제의 MyClass(3)은 마지막 강한 소유자가 사라질 때 소멸한다. shared_ptr 순환은 weak_ptr로 설계해야 하며, raw 관찰 포인터의 유효 수명은 별도로 보장한다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 서로 다른 두 함수의 비교이다. raw 경로에서는 함수 종료가 포인터 변수만 정리하고 MyClass1의 해제 책임을 잃는다. unique_ptr 경로에서는 소유자 소멸이 기본 삭제자를 실행해 MyClass2를 소멸시키고 힙 자원을 반환한다. 두 경로를 잇는 실행 화살표는 없다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 두 독립 할당 leaky_function과 safe_unique_ptr_function이 각각 자원을 만든다. |
|
|
| 범위 종료 두 함수의 지역 변수가 범위를 나간다. |
|
|
| 자원 결과 명시적 해제가 없는 raw와 RAII 해제를 구분한다. |
|
|
메모리 누수란 무엇인가?
메모리 누수는 프로그램이 동적으로 할당한 메모리(예: new 연산자를 통해 힙에 할당된 메모리)를 더 이상 사용하지 않음에도 불구하고, 해당 메모리를 해제해 할당자가 재사용할 수 있게 하지 않아 발생하는 현상입니다.
이렇게 반환되지 않은 메모리는 프로그램이 종료될 때까지 다른 용도로 재사용될 수 없으며, 마치 물이 새는 것처럼 계속 쌓여갑니다.
메모리 누수의 위험성- 성능 저하: 사용 가능한 물리 메모리가 줄어들어 운영체제가 가상 메모리(페이징 파일)를 더 자주 사용하게 되고, 이는 디스크 I/O를 유발하여 프로그램의 속도를 현저히 떨어뜨립니다.
- 시스템 불안정: 결국에는 시스템의 사용 가능한 메모리를 모두 소진시켜 다른 프로그램이나 운영체제 자체의 오작동 또는 강제 종료를 유발할 수 있습니다.
- 찾기 어려움: 당장 프로그램이 크래시되지 않아 발견하기 어렵고, 누수가 서서히 진행되기 때문에 원인을 파악하기가 매우 어렵습니다.
new또는malloc으로 메모리를 할당했지만, 해당 메모리에 대한delete또는free호출을 누락한 경우.- 포인터 변수가 스코프를 벗어나거나 재할당되어 이전에 할당된 메모리 주소를 잃어버린 경우.
- 예외 발생 등으로
delete호출이 건너뛰어진 경우 (RAII를 사용하지 않았을 때).
스마트 포인터를 이용한 예방
소유권 상황에 따라 unique_ptr·shared_ptr·weak_ptr을 구별하고 소유자 소멸이 해제 책임으로 이어지는지 확인합니다.
가장 좋은 메모리 누수 탐지 및 해결책은 바로 예방입니다.
현대 C++에서는 스마트 포인터(Smart Pointer)를 사용하여 동적 메모리 관리를 자동화하고 메모리 누수를 효과적으로 방지할 수 있습니다. (14장 3절 스마트 포인터 참조)
-
std::unique_ptr- 단일 소유권(exclusive ownership)을 가지는 스마트 포인터입니다.
unique_ptr객체가 스코프를 벗어나 소멸될 때, 비어 있지 않다면 설정된 삭제자를 호출합니다. 이 절의 단일 객체·기본 삭제자 예제에서는delete로 객체를 소멸하고 메모리를 해제합니다.- 소유자의 해제 책임을 연결해 메모리 누수를 예방합니다. 하지만
get()으로 얻은 raw 관찰 포인터(dangling pointer가 될 수 있는 포인터)의 수명을 자동으로 연장하지는 않습니다.
-
std::shared_ptr- 공유 소유권(shared ownership)을 가지는 스마트 포인터입니다.
- 참조 카운팅(reference counting) 방식을 사용하여, 같은 소유 그룹의 마지막 강한 소유권이 사라질 때 관리 객체를 설정된 삭제자로 정리합니다. 이 절의 기본 삭제자 예제에서는
delete에 해당하는 객체 정리가 이때 일어납니다. 모든shared_ptr변수 자체의 스코프 종료만이 아니라 reset·대입도 소유권을 줄일 수 있습니다. - 순환 참조(circular reference) 시 메모리 누수가 발생할 수 있으므로 주의해야 합니다 (이 경우
std::weak_ptr사용).
#include <iostream>
#include <memory> // std::unique_ptr, std::shared_ptr을 위해
class MyClass {
public:
int value;
MyClass(int v) : value(v) {
std::cout << "MyClass(" << value << ") created.\n";
}
~MyClass() {
std::cout << "MyClass(" << value << ") destroyed.\n";
}
};
void leaky_function() {
// raw pointer 사용: delete 호출을 잊으면 메모리 누수 발생
MyClass* obj_ptr = new MyClass(1);
// obj_ptr을 delete 하는 것을 잊었다면 누수
// delete obj_ptr; // 이 라인이 없으면 누수
} // obj_ptr 스코프 벗어나도 힙 메모리는 해제되지 않음
void safe_unique_ptr_function() {
// std::unique_ptr 사용: 스코프 벗어날 때 자동 해제
std::unique_ptr<MyClass> obj_uptr = std::make_unique<MyClass>(2);
// 아무것도 하지 않아도 자동으로 delete 호출
} // obj_uptr 소멸, MyClass(2) 자동 해제
void safe_shared_ptr_function() {
// std::shared_ptr 사용: 참조 카운트가 0이 될 때 자동 해제
std::shared_ptr<MyClass> obj_sptr1 = std::make_shared<MyClass>(3);
std::shared_ptr<MyClass> obj_sptr2 = obj_sptr1; // 참조 카운트 증가
} // obj_sptr1, obj_sptr2 모두 소멸될 때 MyClass(3) 자동 해제
int main() {
std::cout << "--- Calling leaky_function ---\n";
leaky_function(); // 메모리 누수 발생 가능성
std::cout << "\n--- Calling safe_unique_ptr_function ---\n";
safe_unique_ptr_function(); // 자동 해제
std::cout << "\n--- Calling safe_shared_ptr_function ---\n";
safe_shared_ptr_function(); // 자동 해제
std::cout << "\n--- Program finished ---\n";
return 0;
}예시
위의 누수 예제는 MyClass(1)의 생성 출력 뒤 소멸 출력이 없고, MyClass(2)·MyClass(3)은 해당 소유자의 정리 때 소멸 출력이 나옵니다. 소멸 출력만으로 모든 메모리 누수를 증명하거나 배제할 수는 없지만 해제 책임을 추적하는 첫 단서입니다. 다음 별도 프로그램은 누수를 만들지 않고 관찰·이동·정리의 경계를 확인합니다. C++14 이상이 필요합니다.
#include <iostream>
#include <memory>
#include <utility>
struct Tracked {
int value;
explicit Tracked(int v) : value(v) { std::cout << "create " << value << '\n'; }
~Tracked() { std::cout << "destroy " << value << '\n'; }
};
int main() {
auto owner = std::make_unique<Tracked>(7);
Tracked* observer = owner.get(); // 관찰만 하고 해제 책임은 갖지 않음
std::cout << observer->value << '\n';
auto next_owner = std::move(owner);
std::cout << bool(owner) << " : " << bool(next_owner) << '\n';
next_owner.reset(); // destroy7은 여기서 발생
observer = nullptr; // 해제 뒤에는 역참조하지 않음
std::cout << bool(owner) << " : " << bool(next_owner) << '\n';
auto first = std::make_shared<Tracked>(9);
auto second = first;
first.reset(); // second가 아직 강한 소유권을 유지
std::cout << second->value << '\n';
second.reset(); // 마지막 강한 소유권이 사라져 destroy9
}출력 순서는 create 7, 7, 0 : 1, destroy 7, 0 : 0, create 9, 9, destroy 9입니다. owner를 이동해도 그 객체는 바로 소멸하지 않고 next_owner가 책임을 받습니다. reset 뒤 observer의 옛 주소를 역참조하면 안 됩니다. 포인터 변수에 nullptr를 넣는 행위가 힙 객체를 해제한 것이 아니라, 앞선 소유자의 reset이 해제했습니다.
독립 확인: next_owner.reset을 호출하기 전에 owner.reset을 먼저 호출하면 Tracked(7)이 소멸하나요? first.reset 뒤 second가 살아 있을 때 Tracked(9)은 소멸하나요? 끝으로 next_owner.release로 raw 주소를 받았다면 reset과 달리 누구에게 해제 책임이 남는지도 설명하세요.
소유권별 정리 확인
이동 후 owner는 비었으므로 owner.reset만으로 Tracked(7)은 소멸하지 않습니다. next_owner가 여전히 소유합니다. first.reset 뒤에는 second가 소유하므로 Tracked(9)도 아직 살아 있습니다. release는 소유자를 비우고 주소를 반환하지만 객체를 정리하지 않습니다. 반환 주소를 새 소유자에게 즉시 연결하거나 적절히 해제할 책임을 이어야 하며, 주소를 버리면 누수가 될 수 있습니다.
새로운 C++ 코드를 작성할 때는 특별한 이유가 없다면 항상 스마트 포인터를 사용하여 동적 메모리를 관리하는 습관을 들여야 합니다.
메모리 누수 탐지 도구 (Memory Leak Detectors)
아무리 스마트 포인터를 사용해도 레거시 코드나 특정 라이브러리 사용 시, 또는 복잡한 시나리오에서는 여전히 메모리 누수가 발생할 수 있습니다.
이때 메모리 누수 탐지 도구를 함께 사용합니다.
- Linux/macOS 환경에서 가장 강력하고 널리 사용되는 도구 중 하나입니다.
Memcheck라는 도구를 통해 메모리 누수, 유효하지 않은 메모리 접근, 해제된 메모리 사용 등을 탐지합니다.- 사용법:
valgrind --leak-check=full ./your_program - 장점: 매우 정확하고 상세한 보고서를 제공하며, 메모리 관련 거의 모든 종류의 오류를 탐지합니다.
- 단점: 프로그램 실행 속도가 매우 느려집니다 (5배~100배).
// leaky_program.cpp
#include <iostream>
#include <vector>
void create_leak() {
int* p_int = new int(10); // 할당했지만 delete 하지 않음
// p_int를 사용 후 버려짐.
std::cout << "Leaked int at " << p_int << " with value " << *p_int << std::endl;
std::vector<char>* p_vec = new std::vector<char>(1024); // 할당했지만 delete 하지 않음
std::cout << "Leaked vector at " << p_vec << std::endl;
// p_vec도 사용 후 버려짐
}
int main() {
create_leak();
std::cout << "Program exiting.\n";
return 0;
}g++ -g leaky_program.cpp -o leaky_program
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./leaky_programValgrind는 누수된 메모리 블록의 크기, 할당된 위치(함수명, 라인 번호), 누수 종류(definitely lost, indirectly lost, possibly lost 등)를 상세하게 보고해 줍니다.
- Visual Studio IDE에 통합된 진단 도구 (Diagnostic Tools)는 메모리 사용량을 모니터링하고 스냅샷을 찍어 메모리 누수를 비교 분석할 수 있는 기능을 제공합니다.
- 사용법: 디버깅 세션 중 진단 도구 창에서 메모리 사용량 탭을 활성화하고, 스냅샷 찍기 버튼을 사용하여 프로그램의 특정 시점에서 메모리 스냅샷을 찍습니다. 두 스냅샷을 비교하여 어떤 객체가 할당된 후 해제되지 않았는지 추적할 수 있습니다.
- CRT Debug Heap Functions: Microsoft Visual C++ 런타임 라이브러리는 디버그 모드에서 메모리 할당/해제 추적 기능을 제공합니다. (
_CrtDumpMemoryLeaks,_CrtSetDbgFlag등)
- GCC, Clang 컴파일러에 내장된 메모리 오류 탐지 도구입니다.
- 컴파일 시점에 계측(instrumentation)을 삽입하여 런타임에 메모리 오류를 탐지합니다.
- 사용법: 컴파일 시
g++ -fsanitize=address your_program.cpp -o your_program추가. - 장점: Valgrind보다 훨씬 빠릅니다 (보통 2배 오버헤드). 메모리 누수뿐만 아니라 Use-After-Free, Double-Free, Buffer Overflow/Underflow 등 다양한 메모리 오류를 탐지합니다.
- 단점: 컴파일러 지원이 필요하고, 때로는 더 많은 메모리를 사용합니다.
g++ -fsanitize=address -g leaky_program.cpp -o leaky_program
./leaky_programASan은 계측된 잘못된 메모리 접근 등을 실행 중 탐지합니다. 누수 검사는 지원 플랫폼에서 통합된 LSan이 주로 프로세스 종료 시 수행하며, 탐지 시점과 지원 범위를 구별해야 합니다. ASan의 누수 검사 안내와 LSan 문서의 현재 지원 조건을 확인합니다. 도구가 설치되지 않았거나 해당 플랫폼에서 검사가 지원되지 않으면, 일반 실행의 정상 종료를 누수 검사 통과로 기록하지 않습니다.
- 메모리 누수 탐지에 특화되어 있습니다. 지원 플랫폼에서는 ASan과 통합되며 -fsanitize=leak로 독립 사용할 수도 있습니다.
- UBSan: 정수 오버플로우, 잘못된 형변환, 정렬 위반 등 정의되지 않은 동작을 빠르게 발견합니다.
- TSan: 멀티스레드 코드의 데이터 레이스를 탐지합니다.
# UB 탐지
g++ -fsanitize=undefined -g your_program.cpp -o your_program_ubsan
./your_program_ubsan
# 데이터 레이스 탐지
g++ -fsanitize=thread -g your_program.cpp -o your_program_tsan
./your_program_tsan누수 의심 경로와 플랫폼에 맞춰 동적 계측·Memcheck·메모리 스냅샷을 선택합니다.
정적 분석으로 사전 차단하기 (clang-tidy)
런타임 탐지 도구와 함께 정적 분석을 병행하면, 실행 전에 잠재 결함을 줄일 수 있습니다.
clang-tidy는 널리 쓰이는 C++ 정적 분석 도구로, 메모리/성능/코드 스타일 문제를 자동 점검합니다.- CI에
clang-tidy를 포함하면 코드 리뷰 이전에 결함을 빠르게 차단할 수 있습니다.
clang-tidy src/main.cpp -- -std=c++20cmake -DCMAKE_CXX_CLANG_TIDY="clang-tidy;-checks=bugprone-*,performance-*,modernize-*" -S . -B build
cmake --build build실무에서는 정적 분석(clang-tidy) + 동적 계측(ASan/UBSan/TSan) + 회귀 테스트 조합이 가장 안정적입니다.
메모리 누수 탐지 전략
- 가장 기본적인 방법입니다.
new나malloc이 사용된 곳을 찾아delete나free가 적절히 호출되는지 확인합니다. - 포인터의 소유권(ownership)이 명확한지 확인합니다.
- RAII(Resource Acquisition Is Initialization) 원칙이 잘 지켜지고 있는지 확인합니다.
- 새로운 코드 작성 시에는
std::unique_ptr과std::shared_ptr을 기본적으로 사용합니다.
- 스트레스 테스트(Stress Test) 또는 장시간 실행 테스트를 통해 프로그램이 안정적으로 동작하는지 확인하고, 이 과정에서 메모리 누수 탐지 도구를 사용합니다.
- 메모리 스냅샷을 주기적으로 찍어 메모리 사용량의 추이를 분석합니다.
new / delete 오버로드 (Advanced)- 디버그 모드에서 전역
new및delete연산자를 오버로드하여 할당/해제되는 모든 메모리 블록을 추적하는 커스텀 메모리 관리자를 구현할 수 있습니다. 이는 복잡하지만, 특정 환경에 최적화된 누수 탐지 시스템을 구축할 때 유용합니다.
누수된 메모리 블록 이해하기
탐지 도구가 보고하는 메모리 누수는 다양한 상태로 분류될 수 있습니다.
- Definitely lost: 프로그램이 해당 메모리 블록에 대한 포인터를 전혀 가지고 있지 않아, 해당 메모리를 해제할 방법이 완전히 사라진 경우. (가장 심각한 누수)
- Indirectly lost: 해당 블록을 가리키는 블록들 자체가 이미 lost인 경우. 트리의 루트 블록을 잃으면 자식 블록들이 이 상태가 될 수 있어, 최초로 잃은 소유 경로부터 추적합니다.
- Possibly lost: 도달 경로에 블록 시작이 아닌 내부 주소를 가리키는 포인터가 포함되어, 실제 소유 경로인지 우연한 값인지 추가 확인이 필요한 경우.
- Still reachable: 루트에서 시작 포인터를 따라 블록에 도달할 수 있는 경우. 도구가 앞으로 사용할지 여부까지 판단하는 것은 아닙니다. (일반적으로는 누수가 아니지만, 의도치 않은 경우도 있음)
Definitely lost는 반드시 해결해야 할 문제입니다. 이 분류는 Memcheck의 누수 분류에 해당하며, 다른 도구의 보고 용어와 일대일로 같다고 가정하지 않습니다.
예시
가상 보고에서 시작 포인터를 잃은 루트 블록 16 B와 그 루트만이 가리키던 자식 32 B는 각각 definitely lost·indirectly lost로 분류될 수 있습니다. 전역 캐시가 시작 포인터를 유지한 64 B는 still reachable일 수 있지만, 업무상 계속 필요한 캐시인지까지 도구가 판단하지는 않습니다. 실제 블록 크기나 원 예제의 vector 할당 크기를 이 숫자로 바꾸어 읽지 않습니다.
독립 확인: 한 블록의 시작 주소 대신 내부 원소 주소만 남았다는 possibly lost 보고를 보면 무엇을 확인해야 하나요? 반복 요청 뒤 still reachable 64 B가 그대로인 경우와 64·128·192 B로 늘어나는 경우 중 어느 쪽이 추가 수명 조사의 우선 후보인가요?
보고서와 수명 조건 확인
내부 포인터가 의도된 참조인지, 시작 주소와 해제 책임을 별도로 유지하는지 조사합니다. possibly lost만으로 해제 방법을 확정할 수는 없습니다. 동일 조건에서 계속 늘어나는 64·128·192 B가 우선 후보이지만 정상 캐시의 확장일 수도 있으므로 할당 경로·최대 크기·정리 시점을 함께 확인합니다. 한 번의 reachable 보고나 빈 보고서만으로 모든 경로의 누수 유무를 판정하지 않습니다.
Possibly lost와 Still reachable은 코드의 맥락에 따라 중요도가 달라지지만, 대부분의 경우 조사하고 해결해야 합니다.
메모리 문제는 누수뿐 아니라 해제 후 사용, 버퍼 침범, 데이터 레이스처럼 여러 형태로 나타납니다.
아래 다이어그램은 예방, 정적 분석, 동적 계측, 장시간 검증을 어떤 순서로 조합할지 보여줍니다.
C++ · MEMORY DIAGNOSTICS
예방·분석·실행·장시간 관측을 함께 사용한다
RAII 예방, 정적 분석, 동적 도구, 장시간 관측을 서로 다른 증거 층으로 사용한다.
- 값·RAII·소유권
해제 책임을 설계에 명시한다. shared_ptr 순환과 관찰 포인터의 유효 수명도 확인한다.
- 경고·정적 분석
clang-tidy 등으로 실행 전에 의심되는 경로를 분석한다.
- sanitizer·플랫폼 도구
ASan은 잘못된 접근, LSan은 지원 환경의 누수, TSan은 데이터 레이스를 검사한다. Memcheck는 플랫폼에 맞게 사용한다.
- 부하·스냅샷·지표
스냅샷과 반복 부하의 증가 추세를 비교한다. 증가 자체만으로 누수를 단정하지 않는다.
ASan과 LSan의 지원·탐지 시점을 구분한다. LSan은 보통 종료 시 누수를 검사한다. 증가 추세나 보고서가 없는 한 번의 실행만으로 누수 유무를 단정하지 않는다.
누수 보고서는 블록 상태를 구분해 해제 방법을 잃은 메모리부터 우선순위를 매겨 처리해야 합니다.