자바스크립트 엔진의 메모리 관리
소개
이 문서는 QML에서 자바스크립트 엔진의 동적 메모리 관리에 대해 설명합니다. 다소 기술적이고 심도 있는 설명입니다. QML에서 자바스크립트 메모리 관리의 정확한 특성에 관심이 있는 경우에만 이 문서를 읽으시면 됩니다. 특히, 애플리케이션의 성능을 극대화하기 위해 최적화를 시도하고 있다면 이 문서가 도움이 될 수 있습니다.
참고: Qt Quick Compiler JavaScript 힙 사용량을 상당 부분 줄일 수 있습니다. 생성된 C++ 코드는 객체와 값을 저장하기 위해 익숙한 C++ 스택과 힙을 사용합니다. 그러나 JavaScript 호스트 환경은 이를 사용하든 안 하든 상관없이 항상 JavaScript가 관리하는 메모리를 일부 사용합니다. 단, C++로 컴파일할 수 없는 기능을 사용하는 경우, 엔진은 해석 모드나 JIT 컴파일 방식으로 전환되어 자바스크립트 힙에 저장된 자바스크립트 객체를 사용하게 됩니다.
기본 원리
QML의 JavaScript 엔진은 운영 체제에서 여러 페이지 단위로 주소 공간을 요청하는 전용 메모리 관리자를 갖추고 있습니다. JavaScript에서 생성된 객체, 문자열 및 기타 관리되는 값은 JavaScript 엔진 고유의 할당 방식을 사용하여 이 주소 공간에 배치됩니다. 자바스크립트 엔진은 자바스크립트 객체에 대한 메모리를 할당할 때 C 라이브러리의 malloc() 및 free(), 또는 C++의 new 및 delete의 기본 구현을 사용하지 않습니다.
주소 공간 요청은 일반적으로 유닉스 계열 시스템에서는 mmap(), 윈도우에서는 VirtualAlloc()을 통해 이루어집니다. 이러한 기본 함수들에 대해서는 여러 플랫폼별 구현 방식이 존재합니다. 이러한 방식으로 예약된 주소 공간은 즉시 물리 메모리에 할당되지 않습니다. 대신, 운영 체제는 메모리 페이지가 실제로 액세스될 때 이를 감지하고, 그때서야 비로소 할당합니다. 따라서 주소 공간은 사실상 무료이며, 이를 충분히 확보해 두면 자바스크립트 메모리 관리자가 자바스크립트 힙에 객체를 효율적으로 배치하는 데 필요한 유연성을 얻을 수 있습니다. 또한, 특정 주소 공간 블록이 아직 예약되어 있더라도 당분간 물리 메모리에 매핑될 필요가 없음을 운영 체제에 알리는 플랫폼별 기법들이 존재합니다. 그러면 운영 체제는 필요에 따라 해당 메모리의 할당을 해제하고 다른 작업에 사용할 수 있습니다. 중요한 점은, 대부분의 운영 체제가 이러한 할당 해제 요청에 대해 즉각적인 처리를 보장하지 않는다는 것입니다. 운영 체제는 해당 메모리가 실제로 다른 용도로 필요할 때만 할당을 해제합니다. 유닉스 계열 시스템에서는 일반적으로 madvise()를 사용하여 이를 처리합니다. Windows에서는 VirtualFree()에 특정 플래그를 지정하여 이와 동등한 기능을 수행합니다.
참고: 이 메커니즘을 제대로 이해하지 못해 자바스크립트 메모리 사용량을 과대 보고하는 메모리 프로파일링 도구가있습니다 .
자바스크립트 힙에 저장된 모든 값은 가비지 컬렉션의 대상이 됩니다. 어떤 값도 스코프를 벗어나거나 다른 방식으로 “제거”되더라도 즉시 “삭제”되지는 않습니다. 오직 가비지 컬렉터만이 자바스크립트 힙에서 값을 제거하고 메모리를 반환할 수 있습니다(이 작동 방식에 대해서는 아래의 ‘가비지 컬렉션’ 섹션을 참조하십시오).
QObject 기반 유형
QObject- 기반 유형, 특히 QML 요소로 표현할 수 있는 모든 것은 C++ 힙에 할당됩니다. JavaScript에서 ` QObject `에 접근할 때, 포인터를 감싸는 작은 래퍼만 JavaScript 힙에 배치됩니다. 그러나 이러한 래퍼는 자신이 가리키는 ` QObject `를 소유할 수 있습니다. QJSEngine::ObjectOwnership 를 참조하십시오. 래퍼가 객체를 소유하고 있는 경우, 래퍼가 가비지 컬렉션될 때 해당 객체도 삭제됩니다. 또한 래퍼의 destroy() 메서드를 호출하여 수동으로 삭제를 트리거할 수도 있습니다. destroy()는 내부적으로 QObject::deleteLater()를 호출합니다. 따라서 객체를 즉시 삭제하지는 않고, 다음 이벤트 루프 반복을 기다립니다.
객체의 QML로 선언된 속성은 JavaScript 힙에 저장됩니다. 이 속성들은 속한 객체가 존재하는 동안 유지됩니다. 이후 가비지 컬렉터가 다음에 실행될 때 제거됩니다.
객체 할당
자바스크립트에서 구조화된 유형은 모두 객체입니다. 여기에는 함수 객체, 배열, 정규식, 날짜 객체 등이 포함됩니다. QML에는 앞서 언급한 QObject 래퍼와 같은 여러 내부 객체 유형이 있습니다. 객체가 생성될 때마다 메모리 관리자는 자바스크립트 힙에서 해당 객체를 위한 저장 공간을 할당합니다.
자바스크립트 문자열도 관리되는 값이지만, 문자열 데이터 자체는 자바스크립트 힙에 할당되지 않습니다. QObject 래퍼와 마찬가지로, 문자열에 대한 힙 객체는 문자열 데이터를 가리키는 포인터를 감싸는 얇은 래퍼에 불과합니다.
객체에 대한 메모리를 할당할 때, 먼저 객체의 크기를 32바이트 정렬 단위로 올림합니다. 32바이트 단위의 주소 공간을 “슬롯(slot)”이라고 합니다. “거대 크기(huge size)” 임계값보다 작은 객체의 경우, 메모리 관리자는 해당 객체를 메모리에 배치하기 위해 일련의 시도를 수행합니다:
- 메모리 관리자는 이전에 해제된 힙 영역을 "빈(bin)"이라고 부르는 연결 리스트로 관리합니다. 각 빈은 슬롯 단위로 빈당 고정된 크기의 힙 영역을 포함합니다. 적절한 크기의 빈이 비어 있지 않으면, 첫 번째 항목을 선택하여 객체를 그곳에 배치합니다.
- 아직 사용되지 않은 메모리는 범퍼 할당기를 통해 관리됩니다. 범퍼 포인터는 점유된 주소 공간의 바로 다음 바이트를 가리킵니다. 사용되지 않은 주소 공간이 여전히 충분하다면, 범퍼가 그에 따라 확장되고 객체는 사용되지 않은 공간에 배치됩니다.
- 앞서 언급한 특정 크기보다 크며 다양한 크기를 가진, 이전에 해제된 힙 영역을 위해 별도의 빈이 유지됩니다. 메모리 관리자는 이 목록을 순회하며, 새로운 객체를 수용하기 위해 분할할 수 있는 영역을 찾으려고 시도합니다.
- 메모리 관리자는 할당될 객체보다 큰 특정 크기의 빈 목록을 검색하여 그중 하나를 분할하려고 시도합니다.
- 마지막으로, 위의 방법 중 어느 것도 성공하지 못하면 메모리 관리자는 더 많은 주소 공간을 확보하고 범퍼 할당기를 사용하여 객체를 할당합니다.
대용량 객체는 전용 할당기를 통해 처리됩니다. 이러한 객체마다 OS로부터 하나 이상의 별도 메모리 페이지를 확보하여 별도로 관리합니다.
또한, 메모리 관리자가 OS로부터 확보한 새로운 주소 공간 청크마다 각 슬롯에 대한 여러 플래그를 포함하는 헤더가 부여됩니다:
- object: 객체가 차지하는 첫 번째 슬롯에는 이 비트가 설정됩니다.
- extends: 객체가 점유하는 그 이후의 모든 슬롯에는 이 비트가 설정됩니다.
- mark: 가비지 컬렉터가 실행될 때, 객체가 여전히 사용 중이면 이 비트를 설정합니다.
내부 클래스
객체가 어떤 멤버를 가지고 있는지에 대한 메타데이터에 필요한 저장 공간을 최소화하기 위해, 자바스크립트 엔진은 각 객체에 “내부 클래스”를 할당합니다. 다른 자바스크립트 엔진들은 이를 “숨겨진 클래스” 또는 “셰이프(shape)”라고 부릅니다. 내부 클래스는 중복 제거되어 트리 구조로 관리됩니다. 객체에 속성이 추가되면, 현재 내부 클래스의 자식 노드들을 확인하여 동일한 객체 레이아웃이 이전에 발생했는지 확인합니다. 만약 그렇다면, 결과로 나온 내부 클래스를 바로 사용할 수 있습니다. 그렇지 않으면 새로운 내부 클래스를 생성해야 합니다.
내부 클래스는 자바스크립트 힙의 별도 구역에 저장되며, 이 힙의 작동 방식은 앞서 설명한 일반적인 객체 할당과 동일합니다. 이는 내부 클래스를 사용하는 객체가 가비지 컬렉션되는 동안에도 내부 클래스는 유지되어야 하기 때문입니다. 내부 클래스는 별도의 가비지 컬렉션 단계에서 처리됩니다.
하지만 내부 클래스에 저장된 실제 속성 값들은 자바스크립트 힙에 보관되지 않고, `new` 및 `delete`를 사용하여 관리됩니다.
가비지 컬렉션
자바스크립트 엔진에서 사용되는 가비지 컬렉터는 비이동형(non-moving) 마크 앤 스윕(Mark and Sweep) 방식입니다. Qt 6.8부터는 기본적으로 증분 방식으로 실행됩니다( QV4_GC_TIMELIMIT이 0으로 설정된 경우는 제외). 마킹 단계에서는 객체에 대한 활성 참조를 찾을 수 있는 모든 알려진 위치를 순회합니다. 특히:
- 자바스크립트 전역 변수
- QML 및 자바스크립트 컴파일 유닛의 삭제 불가능한 부분
- 자바스크립트 스택
- 영구 값 저장소. 이곳은 ` QJSValue ` 및 이와 유사한 클래스가 자바스크립트 객체에 대한 참조를 보관하는 곳입니다.
이러한 위치에서 발견된 모든 객체에 대해, 해당 객체가 참조하는 모든 항목에 대해 마크 비트가 재귀적으로 설정됩니다.
스윕 단계에서 가비지 컬렉터는 전체 힙을 탐색하여 이전에 마크되지 않은 모든 객체를 해제합니다. 그 결과 해제된 메모리는 향후 할당에 사용될 수 있도록 빈(bin)으로 분류됩니다. 주소 공간의 일부가 완전히 비어 있는 경우, 해당 영역은 해제되지만 주소 공간 자체는 유지됩니다(위의 ‘기본 원리’ 참조). 메모리 사용량이 다시 증가하면 동일한 주소 공간이 재사용됩니다.
가비지 컬렉터는 ` gc()` 함수를 호출하여 수동으로 실행하거나, 다음 요소를 고려하는 휴리스틱에 의해 트리거됩니다:
- JavaScript 힙에서 객체에 의해 관리되지만 JavaScript 힙에 직접 할당되지는 않은 메모리 양(예: 문자열 및 내부 클래스 멤버 데이터). 이에 대해서는 동적 임계값이 유지됩니다. 이 임계값을 초과하면 가비지 컬렉터가 실행되고 임계값이 증가합니다. 관리되는 외부 메모리 양이 임계값보다 훨씬 낮아지면 임계값이 감소합니다.
- 예약된 총 주소 공간. 자바스크립트 힙 내부의 메모리 할당은 최소한의 주소 공간이 예약된 후에만 고려됩니다.
- 마지막 가비지 컬렉터 실행 이후 추가로 예약된 주소 공간입니다. 주소 공간의 양이 마지막 가비지 컬렉터 실행 후 사용된 메모리 양의 두 배를 초과하면 가비지 컬렉터를 다시 실행합니다.
메모리 사용량 분석
주소 공간의 변화 추이와 그 안에 할당된 객체의 수를 관찰하려면 전용 도구를 사용하는 것이 가장 좋습니다. QML Profiler 는 이에 도움이 되는 시각화 기능을 제공합니다. 보다 일반적인 도구는 자바스크립트 메모리 관리자가 할당된 주소 공간 내에서 수행하는 작업을 파악할 수 없으며, 주소 공간의 일부가 물리적 메모리에 할당되지 않았다는 사실조차 감지하지 못할 수 있습니다.
메모리 사용량을 디버깅하는 또 다른 방법은 logging categories 인 qt.qml.gc.statistics 및 qt.qml.gc.allocatorStats를 사용하는 것입니다. qt.qml.gc.statistics에 대해 디버그(Debug) 레벨을 활성화하면, 가비지 컬렉터가 실행될 때마다 다음과 같은 정보를 출력합니다:
- 총 주소 공간 중 예약된 양
- 가비지 컬렉션 전후로 사용 중이던 메모리 양
- 지금까지 할당된 다양한 크기의 객체 수
qt.qml.gc.allocatorStats의 디버그 수준은 가비지 컬렉터가 어떻게 트리거되었는지, 마크(mark) 및 스윕(sweep) 단계의 소요 시간, 바이트 및 주소 공간 청크 단위로 나눈 메모리 사용량의 상세 내역 등을 포함한 더욱 상세한 통계를 출력합니다.
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.