このページでは

JavaScript エンジンにおけるメモリ管理

はじめに

このドキュメントでは、QMLにおけるJavaScriptエンジンの動的メモリ管理について説明します。これはかなり技術的で詳細な説明です。QMLにおけるJavaScriptのメモリ管理の正確な特性に関心がある場合のみ、このドキュメントを読む必要があります。特に、アプリケーションのパフォーマンスを最大化するために最適化を図ろうとしている場合には、参考になるでしょう。

注: Qt Quick コンパイラを使用してQMLコードをC++にコンパイルすることで、 JavaScriptヒープの使用量の多くを回避できます。生成されたC++コードは、オブジェクトや値を格納するために、お馴染みのC++のスタックとヒープを使用します。ただし、JavaScriptホスト環境は、それを使用しているかどうかに関わらず、常にJavaScriptが管理するメモリをある程度使用します。 ただし、C++にコンパイルできない機能を使用する場合、エンジンは解釈またはJITコンパイルに切り替わり、JavaScriptヒープ上に格納されたJavaScriptオブジェクトを使用することになります。

基本原則

QML の JavaScript エンジンには、オペレーティングシステムから複数のページ単位でアドレス空間を要求する専用のメモリマネージャが備わっています。JavaScript で作成されたオブジェクト、文字列、およびその他の管理対象値は、JavaScript エンジン独自の割り当て方式を用いて、このアドレス空間に配置されます。 JavaScript エンジンは、JavaScript オブジェクトのメモリ割り当てに、C ライブラリの malloc() や free()、あるいは C++ の new および delete のデフォルトの実装は使用しません。

アドレス空間の要求は、一般的にUnix系システムではmmap()、WindowsではVirtualAlloc()を用いて行われます。これらのプリミティブには、プラットフォーム固有の実装がいくつか存在します。この方法で確保されたアドレス空間は、直ちに物理メモリにコミットされるわけではありません。むしろ、オペレーティングシステムは、メモリのページが実際にアクセスされたことを検知して初めて、それをコミットします。 したがって、アドレス空間は実質的に「無料」であり、それを十分に確保しておくことで、JavaScript メモリ管理者はオブジェクトを JavaScript ヒープ上に効率的に配置するために必要な余裕を得ることができます。さらに、まだ予約されているものの、当面は物理メモリにマッピングする必要がないアドレス空間のブロックについて、オペレーティングシステムにその旨を伝えるプラットフォーム固有の手法も存在します。 そうすることで、オペレーティングシステムは必要に応じてそのメモリの割り当てを解除し、他のタスクに利用できるようになります。重要な点として、ほとんどのオペレーティングシステムは、このような割り当て解除要求に対して即座の対応を保証していません。メモリは、実際に他の用途で必要になったときにのみ、割り当てが解除されます。Unix系システムでは、一般的にmadvise()関数を使用してこれを行います。 Windows では、VirtualFree() に特定のフラグを指定することで同等の処理を行うことができます。

注: この仕組みを理解しておらず、JavaScriptのメモリ使用量を過大に報告してしまうメモリプロファイリングツールが存在します 。

JavaScriptヒープに格納されたすべての値は、ガベージコレクションの対象となります。スコープ外になったり、その他の理由で「破棄」されたりしても、どの値も即座に「削除」されることはありません。JavaScriptヒープから値を削除し、メモリを解放できるのはガベージコレクタのみです(その仕組みについては、以下の「ガベージコレクション」を参照してください)。

QObject ベースの型

QObject- ベースの型、特に QML 要素として記述できるものはすべて、C++ ヒープ上に割り当てられます。JavaScript から `QObject ` にアクセスする際、ポインタを囲む小さなラッパーのみが JavaScript ヒープ上に配置されます。ただし、そのようなラッパーは、それが指す `QObject ` を所有している場合があります。QJSEngine::ObjectOwnership を参照してください。ラッパーがオブジェクトを所有している場合、ラッパーがガベージコレクションの対象となった際にオブジェクトは削除されます。また、そのラッパーの destroy() メソッドを呼び出すことで、手動で削除をトリガーすることも可能です。destroy() は内部でQObject::deleteLater() を呼び出します。したがって、オブジェクトは即座に削除されるわけではなく、次のイベントループの反復を待ちます。

オブジェクトのQMLで宣言されたプロパティは、JavaScriptのヒープ上に格納されます。これらは、所属するオブジェクトが存続している間、存続します。その後、ガベージコレクタが次に実行された際に削除されます。

オブジェクトの割り当て

JavaScript では、構造化された型はすべてオブジェクトです。これには、関数オブジェクト、配列、正規表現、日付オブジェクトなどが含まれます。QML には、前述の `QObject ` ラッパーなど、いくつかの内部オブジェクト型があります。オブジェクトが作成されるたびに、メモリマネージャは JavaScript ヒープ上にそのオブジェクト用の領域を確保します。

JavaScriptの文字列も管理対象の値ですが、その文字列データ自体はJavaScriptヒープ上に割り当てられません。QObject ラッパーと同様に、文字列用のヒープオブジェクトは、文字列データへのポインタを包むだけの薄いラッパーに過ぎません。

オブジェクトのメモリを割り当てる際、まずオブジェクトのサイズが32バイト単位に切り上げられます。32バイト単位のアドレス空間は「スロット」と呼ばれます。「巨大サイズ」の閾値未満のオブジェクトの場合、メモリマネージャはオブジェクトをメモリ内に配置するために一連の試行を行います:

  • メモリマネージャは、以前に解放されたヒープ領域の連結リスト(「ビン」と呼ばれる)を保持しています。各ビンには、ビンごとに固定されたサイズのスロットが割り当てられています。適切なサイズのビンが空でない場合、そのビンの先頭エントリを選択し、そこにオブジェクトを配置します。
  • まだ使用されていないメモリは、バンパーアロケータによって管理されます。バンパーポインタは、占有されているアドレス空間の直後のバイトを指します。まだ十分な未使用のアドレス空間がある場合、バンパーはそれに応じて拡張され、オブジェクトは未使用の領域に配置されます。
  • また、前述の特定のサイズよりも大きく、さまざまなサイズの、以前に解放されたヒープ領域用に、別のビンが保持されています。メモリマネージャはこのリストを走査し、新しいオブジェクトを収容するために分割できる領域を探します。
  • メモリマネージャは、割り当て対象のオブジェクトよりも大きい特定のサイズのビンのリストを検索し、そのうちの1つを分割できないか試みます。
  • 最終的に、上記のいずれの方法も機能しない場合、メモリマネージャは追加のアドレス空間を確保し、バンパーアロケータを使用してオブジェクトを割り当てます。

巨大なオブジェクトは、専用のアロケータによって処理されます。それらについては、OS から 1 つ以上の個別のメモリページが取得され、個別に管理されます。

さらに、メモリマネージャが OS から取得する新しいアドレス空間の各チャンクには、各スロットに対する一連のフラグを保持するヘッダーが付与されます。

  • object:オブジェクトが占有する最初のスロットには、このビットが設定されます。
  • extends: オブジェクトが占有するそれ以降のスロットには、このビットが設定されます。
  • mark: ガベージコレクタが実行された際、オブジェクトがまだ使用中の場合はこのビットが設定されます。

内部クラス

オブジェクトが保持するメンバに関するメタデータの必要ストレージを最小限に抑えるため、JavaScriptエンジンは各オブジェクトに「内部クラス」を割り当てます。他のJavaScriptエンジンでは、これを「隠しクラス」または「シェイプ」と呼ぶこともあります。 内部クラスは重複排除され、ツリー構造で管理されます。オブジェクトにプロパティが追加されると、現在の内部クラスの子ノードがチェックされ、以前に同じオブジェクトレイアウトが存在したかどうかが確認されます。もし存在すれば、その結果として得られる内部クラスをすぐに使用できます。そうでない場合は、新しい内部クラスを作成する必要があります。

内部クラスは、JavaScriptヒープ内の専用のセクションに格納されますが、その動作は前述の一般的なオブジェクトの割り当てと同じです。これは、内部クラスを参照しているオブジェクトがガベージコレクションの対象となる間も、内部クラス自体は存続させなければならないためです。その後、内部クラスは別のパスでガベージコレクションの対象となります。

ただし、内部クラスに格納される実際のプロパティ属性は、JavaScriptヒープ上には保持されず、`new`および`delete`を使用して管理されます。

ガベージコレクション

JavaScript エンジンで使用されるガベージコレクタは、非移動型の「マーク・アンド・スイープ」方式を採用しています。Qt 6.8 以降、デフォルトでは増分方式で実行されます(QV4_GC_TIMELIMIT が0 に設定されていない限り)。マークフェーズでは、オブジェクトへの有効な参照が見つかる可能性のある既知の場所をすべて走査します。具体的には:

  • JavaScriptのグローバル変数
  • QML および JavaScript コンパイルユニットの削除不可能な部分
  • JavaScriptスタック
  • 永続的な値の格納領域。これは、QJSValue や類似のクラスがJavaScriptオブジェクトへの参照を保持する場所です。

これらの場所で検出されたオブジェクトについては、そのオブジェクトが参照するすべてのオブジェクトに対して、再帰的にマークビットが設定されます。

スイープフェーズでは、ガベージコレクタがヒープ全体を走査し、それまでマークされていなかったオブジェクトをすべて解放します。 その結果解放されたメモリは、さらなる割り当てに使用されるようビンにソートされます。アドレス空間の一部が完全に空の場合、その領域は解放されますが、アドレス空間自体は保持されます(上記の「基本原則」を参照)。メモリ使用量が再び増加した場合、同じアドレス空間が再利用されます。

ガベージコレクタは、gc() 関数を呼び出すことで手動で起動されるか、以下の側面を考慮したヒューリスティックによって起動されます:

  • JavaScriptヒープ上でオブジェクトによって管理されているが、JavaScriptヒープ上に直接割り当てられていないメモリの量(文字列や内部クラスのメンバーデータなど)。これらに対しては動的な閾値が維持されます。閾値を超えた場合、ガベージコレクタが実行され、閾値が引き上げられます。管理されている外部メモリの量が閾値を大幅に下回った場合、閾値は引き下げられます。
  • 予約済みのアドレス空間の合計。JavaScript ヒープ上の内部メモリ割り当ては、少なくともある程度のアドレス空間が予約された後にのみ考慮されます。
  • 前回のガベージコレクタ実行以降に追加で確保されたアドレス空間。アドレス空間の量が、前回のガベージコレクタ実行後の使用済みメモリ量の2倍を超える場合、ガベージコレクタを再度実行します。

メモリ使用状況の分析

アドレス空間の推移と、そこに割り当てられたオブジェクト数の推移を観察するには、専用のツールを使用するのが最適です。 QML Profiler は、この分析に役立つ可視化機能を提供しています。より汎用的なツールでは、JavaScript メモリマネージャが確保したアドレス空間内で何を行っているかを把握できず、アドレス空間の一部が物理メモリにコミットされていないことさえ認識できない場合があります。

メモリ使用状況をデバッグする別の方法として、logging categories の`qt.qml.gc.statistics`および`qt.qml.gc.allocatorStats`があります。`qt.qml.gc.statistics`のデバッグレベルを有効にすると、ガベージコレクタは実行されるたびに以下の情報を出力します:

  • 予約されているアドレス空間の合計量
  • ガベージコレクションの前後で、使用されていたメモリの量
  • これまでに割り当てられた各種サイズのオブジェクトの数

qt.qml.gc.allocatorStats のDebugレベルでは、ガベージコレクタがどのようにトリガーされたか、マークフェーズとスイープフェーズの所要時間、バイト単位およびアドレス空間のチャンク単位でのメモリ使用量の詳細な内訳など、より詳細な統計情報が出力されます。

© 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.