자바스크립트 엔진의 메모리 관리
소개
이 문서는 QML에서 자바스크립트 엔진의 동적 메모리 관리에 대해 설명합니다. 다소 기술적이고 심도 있는 설명입니다. QML에서 자바스크립트 메모리 관리의 정확한 특성에 관심이 있는 경우에만 이 문서를 읽으면 됩니다. 특히 애플리케이션의 성능을 극대화하기 위해 최적화하려는 경우 유용할 수 있습니다.
참고: QML 코드를 C++로 컴파일할 때 Qt Quick Compiler 를 사용하여 C++로 컴파일하면 자바스크립트 힙 사용량을 상당 부분 줄일 수 있습니다. 생성된 C++ 코드는 객체와 값을 저장하는 데 익숙한 C++ 스택과 힙을 사용합니다. 그러나 JavaScript 호스트 환경은 사용 여부에 관계없이 항상 일부 JavaScript 관리 메모리를 사용합니다. C++로 컴파일할 수 없는 기능을 사용하는 경우 엔진은 해석 또는 JIT 컴파일로 돌아가서 자바스크립트 힙에 저장된 자바스크립트 객체를 사용합니다.
기본 원칙
QML의 JavaScript 엔진에는 운영 체제에서 여러 페이지 단위로 주소 공간을 요청하는 전용 메모리 관리자가 있습니다. 그러면 자바스크립트에서 생성된 객체, 문자열 및 기타 관리되는 값은 자바스크립트 엔진의 자체 할당 체계를 사용하여 이 주소 공간에 배치됩니다. 자바스크립트 엔진은 자바스크립트 객체의 메모리를 할당할 때 C 라이브러리의 malloc() 및 free(), C++의 new 및 delete의 기본 구현을 사용하지 않습니다.
주소 공간에 대한 요청은 일반적으로 유닉스 계열 시스템에서는 mmap(), 윈도우에서는 VirtualAlloc()을 통해 이루어집니다. 이러한 프리미티브의 플랫폼별 구현은 여러 가지가 있습니다. 이렇게 예약된 주소 공간은 물리적 메모리에 즉시 커밋되지 않습니다. 오히려 운영 체제는 메모리 페이지가 실제로 액세스되는 시점을 파악한 다음 커밋합니다. 따라서 주소 공간은 사실상 무료이며, 주소 공간을 많이 확보하면 자바스크립트 메모리 관리자가 자바스크립트 힙에 객체를 효율적으로 배치하는 데 필요한 활용도를 높일 수 있습니다. 또한, 주소 공간의 일부가 아직 예약되어 있지만 당분간 물리적 메모리에 매핑할 필요가 없음을 운영 체제에 알리는 플랫폼별 기술이 있습니다. 그러면 운영 체제는 필요에 따라 메모리를 커밋 해제하고 다른 작업에 사용할 수 있습니다. 결정적으로, 대부분의 운영 체제는 이러한 커밋 해제 요청에 대해 즉각적인 조치를 보장하지 않습니다. 실제로 메모리가 다른 용도로 필요할 때만 메모리를 커밋 해제합니다. 유닉스 계열 시스템에서는 일반적으로 madvise()를 사용합니다. Windows에서는 VirtualFree()에 특정 플래그를 지정하여 동일한 작업을 수행할 수 있습니다.
참고: 이 메커니즘을 이해하지 못하고 자바스크립트 메모리 사용량을 과도하게 보고하는 메모리 프로파일링 도구가 있습니다.
자바스크립트 힙에 저장된 모든 값은 가비지 컬렉션의 대상이 됩니다. 어떤 값도 범위를 벗어나거나 다른 방식으로 '삭제'되더라도 즉시 '삭제'되지는 않습니다. 가비지 수집기만이 자바스크립트 힙에서 값을 제거하고 메모리를 반환할 수 있습니다(작동 방식은 아래의 가비지 수집을 참조하세요).
Q객체 기반 타입
QObject-기반 타입, 특히 QML 요소로 표현할 수 있는 모든 타입은 C++ 힙에 할당됩니다. 자바스크립트에서 QObject 에 액세스할 때 포인터를 둘러싼 작은 래퍼만 자바스크립트 힙에 배치됩니다. 그러나 이러한 래퍼는 포인터가 가리키는 QObject 을 소유할 수 있습니다. QJSEngine::ObjectOwnership 을 참조하세요. 래퍼가 객체를 소유하고 있는 경우 래퍼가 가비지 수집될 때 삭제됩니다. 그런 다음 destroy() 메서드를 호출하여 수동으로 삭제를 트리거할 수도 있습니다. destroy()는 내부적으로 QObject::deleteLater()를 호출합니다. 따라서 객체를 즉시 삭제하지 않고 다음 이벤트 루프 반복을 기다립니다.
QML로 선언된 객체의 프로퍼티는 자바스크립트 힙에 저장됩니다. 이러한 프로퍼티는 해당 프로퍼티가 속한 객체가 존재하는 한 유지됩니다. 그 후에는 다음 가비지 수집기가 실행될 때 제거됩니다.
객체 할당
자바스크립트에서 구조화된 타입은 모두 객체입니다. 여기에는 함수 객체, 배열, 정규 표현식, 날짜 객체 등이 포함됩니다. QML에는 위에서 언급한 QObject 래퍼와 같은 여러 가지 내부 객체 유형이 있습니다. 객체가 생성될 때마다 메모리 관리자는 자바스크립트 힙에서 객체를 위한 일부 저장소를 찾습니다.
자바스크립트 문자열도 관리되는 값이지만, 해당 문자열 데이터는 자바스크립트 힙에 할당되지 않습니다. QObject 래퍼와 마찬가지로 문자열용 힙 객체는 문자열 데이터에 대한 포인터를 둘러싼 얇은 래퍼일 뿐입니다.
객체에 대한 메모리를 할당할 때 객체의 크기는 먼저 32바이트 정렬로 반올림됩니다. 32바이트의 주소 공간 조각을 각각 "슬롯"이라고 합니다. "거대한 크기" 임계값보다 작은 객체의 경우, 메모리 관리자는 객체를 메모리에 배치하기 위해 일련의 시도를 수행합니다:
- 메모리 관리자는 이전에 해제된 힙 조각의 연결된 목록을 "빈"이라고 합니다. 각 빈에는 빈당 크기가 고정된 힙 조각이 슬롯에 들어 있습니다. 올바른 크기의 빈이 비어 있지 않으면 첫 번째 항목을 선택하고 거기에 객체를 배치합니다.
- 아직 사용되지 않은 메모리는 범퍼 할당자를 통해 관리됩니다. 범퍼 포인터는 점유된 주소 공간 너머의 바이트를 가리킵니다. 아직 사용하지 않은 주소 공간이 충분하면 범퍼는 그에 따라 증가하고 오브젝트는 사용되지 않은 공간에 배치됩니다.
- 위에서 언급한 특정 크기보다 큰 다양한 크기의 힙에서 이전에 해제된 조각을 위해 별도의 빈이 유지됩니다. 메모리 관리자는 이 목록을 탐색하여 새 객체를 수용하기 위해 분할할 수 있는 조각을 찾으려고 합니다.
- 메모리 관리자는 할당할 오브젝트보다 큰 특정 크기의 빈 목록을 검색하고 그 중 하나를 분할하려고 시도합니다.
- 마지막으로, 위의 방법 중 어느 것도 효과가 없으면 메모리 관리자는 더 많은 주소 공간을 확보하고 범퍼 할당기를 사용하여 객체를 할당합니다.
거대한 객체는 자체 얼로케이터가 처리합니다. 각각의 객체에 대해 하나 이상의 개별 메모리 페이지를 OS에서 가져와 별도로 관리합니다.
또한 메모리 관리자가 OS에서 가져오는 각각의 새로운 주소 공간 청크에는 각 슬롯에 대한 여러 플래그를 포함하는 헤더가 있습니다:
- object: 객체가 차지하는 첫 번째 슬롯에는 이 비트가 플래그가 지정됩니다.
- 확장: 객체가 차지하는 모든 추가 슬롯은 이 비트로 플래그가 지정됩니다.
- mark: 가비지 컬렉터가 실행될 때 객체가 아직 사용 중이면 이 비트를 설정합니다.
내부 클래스
자바스크립트 엔진은 객체가 보유한 멤버에 대한 메타데이터에 필요한 저장 공간을 최소화하기 위해 각 객체에 "내부 클래스"를 할당합니다. 다른 자바스크립트 엔진에서는 이를 "숨겨진 클래스" 또는 "모양"이라고 부릅니다. 내부 클래스는 중복이 제거되어 트리에 보관됩니다. 객체에 프로퍼티가 추가되면 현재 내부 클래스의 하위 클래스를 검사하여 동일한 객체 레이아웃이 이전에 발생했는지 확인합니다. 그렇다면 결과 내부 클래스를 바로 사용할 수 있습니다. 그렇지 않으면 새로 만들어야 합니다.
내부 클래스는 자바스크립트 힙의 자체 섹션에 저장되며, 그 외에는 위에서 설명한 일반적인 객체 할당과 동일한 방식으로 작동합니다. 이는 내부 클래스를 사용하는 객체가 수집되는 동안 내부 클래스가 살아 있어야 하기 때문입니다. 그런 다음 내부 클래스는 별도의 패스로 수집됩니다.
하지만 내부 클래스에 저장된 실제 속성 속성은 자바스크립트 힙에 유지되지 않고 새로 만들기 및 삭제를 사용하여 관리됩니다.
가비지 컬렉션
자바스크립트 엔진에서 사용되는 가비지 컬렉터는 움직이지 않는 마크 앤 스윕 디자인입니다. 마크 단계에서는 객체에 대한 라이브 참조를 찾을 수 있는 모든 알려진 위치를 순회합니다. 특히
- 자바스크립트 전역
- QML 및 JavaScript 컴파일 단위에서 삭제할 수 없는 부분
- 자바스크립트 스택
- 영구 값 저장소. QJSValue 및 이와 유사한 클래스가 JavaScript 객체에 대한 참조를 보관하는 곳입니다.
이러한 위치에서 발견되는 모든 객체에 대해 참조하는 모든 항목에 대해 재귀적으로 마크 비트가 설정됩니다.
그런 다음 스윕 단계에서 가비지 컬렉터는 전체 힙을 탐색하고 이전에 표시되지 않은 모든 객체를 해제합니다. 이렇게 해제된 메모리는 추후 할당을 위해 사용할 빈으로 분류됩니다. 주소 공간의 한 덩어리가 완전히 비어 있으면 커밋 해제되지만 주소 공간은 유지됩니다(위의 기본 원칙 참조). 메모리 사용량이 다시 증가하면 동일한 주소 공간이 재사용됩니다.
가비지 수집기는 gc() 함수를 호출하여 수동으로 트리거하거나 다음 측면을 고려한 휴리스틱에 의해 트리거됩니다:
- 문자열 및 내부 클래스 멤버 데이터와 같이 JavaScript 힙에 직접 할당되지 않고 JavaScript 힙에서 객체가 관리하는 메모리 양. 이러한 메모리에 대해서는 동적 임계값이 유지됩니다. 임계값을 초과하면 가비지 수집기가 실행되고 임계값이 증가합니다. 관리되는 외부 메모리의 양이 임계값에 훨씬 못 미치면 임계값이 감소합니다.
- 예약된 총 주소 공간. JavaScript 힙의 내부 메모리 할당은 최소 일부 주소 공간이 예약된 후에만 고려됩니다.
- 마지막 가비지 수집기 실행 이후 예약된 추가 주소 공간입니다. 주소 공간의 양이 마지막 가비지 컬렉터 실행 후 사용된 메모리의 두 배 이상이면 가비지 컬렉터를 다시 실행합니다.
메모리 사용량 분석
주소 공간과 그 안에 할당된 객체의 수를 모두 관찰하려면 전문 도구를 사용하는 것이 가장 좋습니다. 의 QML Profiler 는 여기에 도움이 되는 시각화 기능을 제공합니다. 일반적인 도구는 JavaScript 메모리 관리자가 예약한 주소 공간 내에서 수행하는 작업을 볼 수 없으며 주소 공간의 일부가 물리적 메모리에 커밋되지 않은 것을 알아차리지 못할 수도 있습니다.
메모리 사용량을 디버깅하는 또 다른 방법은 logging categories qt.qml.gc.statistics 및 qt.qml.gc.allocatorStats입니다. qt.qml.gc.statistics에 대해 디버그 수준을 활성화하면 가비지 수집기가 실행될 때마다 몇 가지 정보를 인쇄합니다:
- 예약된 총 주소 공간의 양
- 가비지 수집 전후에 사용된 메모리 양
- 지금까지 할당된 다양한 크기의 객체 수
가비지 수집기가 트리거된 방법, 마크 및 스윕 단계의 타이밍, 바이트 및 주소 공간 청크별 메모리 사용량에 대한 자세한 분석을 포함하는 더 자세한 통계를 인쇄하는 qt.qml.gc.allocatorStats의 디버그 수준입니다.
© 2025 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.