JavaScriptエンジンのメモリ管理

はじめに

このドキュメントでは、QMLにおけるJavaScriptエンジンの動的メモリ管理について説明します。かなり専門的で深い説明です。QMLにおけるJavaScriptのメモリ管理の正確な特徴に関心のある方のみお読みください。特に、アプリケーションのパフォーマンスを最大化するために最適化しようとしている場合に役立ちます。

注意: Qt Quick Compilerを使って QML コードを C++ にコンパイルすることで、JavaScript のヒープ使用の多くを避けることができます。生成された C++ コードでは、オブジェクトや値の保存に使い慣れた C++ のスタックとヒープが使用されます。しかし、JavaScriptホスト環境は、使用するしないに関わらず、常にJavaScriptが管理するメモリを使用します。C++にコンパイルできない機能を使用する場合、エンジンは解釈またはJITコンパイルにフォールバックし、JavaScriptヒープに格納されたJavaScriptオブジェクトを使用しますが。

基本原則

QMLのJavaScriptエンジンは、専用のメモリマネージャを持ち、オペレーティング システムから複数ページ単位のアドレス空間を要求されます。そして、JavaScript で生成されたオブジェクトや文字列などの管理値は、JavaScript エンジン独自のアロケーションスキームを使って、このアドレス空間に配置されます。JavaScript エンジンは C ライブラリの malloc() や free() を使用しませんし、C++ の new や delete のデフォルトの実装も JavaScript オブジェクトにメモリを割り当てるために使用しません。

アドレス空間へのリクエストは通常、Unix 系システムでは mmap() を、ウィンドウズでは VirtualAlloc() を使って行われます。これらのプリミティブにはいくつかのプラットフォーム固有の実装があります。この方法で予約されたアドレス空間は、すぐに物理メモリにコミットされるわけではない。むしろ、オペレーティング・システムは、メモリ・ページが実際にアクセスされたときにそれに気づき、それから初めてコミットする。そのため、アドレス空間は実質的に自由であり、それをたくさん持つことで、JavaScriptメモリマネージャはJavaScriptヒープ上にオブジェクトを効率的に配置するために必要な力を得ることができます。さらに、オペレーティングシステムに、アドレス空間の塊はまだ予約されているものの、当面は物理メモリにマッピングする必要がないことを伝える、プラットフォーム固有のテクニックもあります。オペレーティング・システムは、必要に応じてメモリーをデコミットし、他のタスクに使用することができる。重要なのは、ほとんどのオペレーティング・システムは、このようなデコミット・リクエストに対する即時のアクションを保証していないことだ。OSは、そのメモリーが実際に他のことに必要になったときだけ、そのメモリーをデコミットする。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つを分割しようとする。
  • 最後に、上記のどれもうまくいかない場合、メモリマネージャはより多くのアドレス空間を確保し、バンパアロケータを使ってオブジェクトを割り当てる。

巨大なオブジェクトは独自のアロケータで処理される。それらのそれぞれについて、1つ以上の別々のメモリページがOSから取得され、別々に管理される。

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

  • オブジェクトが占有する最初のスロット:オブジェクトによって占有される最初のスロットは、このビットでフラグが立てられる。
  • extends:object:オブジェクトが最初に占有するスロットにはこのフラグが付けられる。
  • マーク:ガベージコレクタが実行されるとき、オブジェクトがまだ使用中であれば、このビットがセットされます。

内部クラス

オブジェクトがどのようなメンバーを持っているかというメタデータのために必要なストレージを最小限にするために、JavaScriptエンジンは各オブジェクトに「内部クラス」を割り当てます。他のJavaScriptエンジンではこれを "隠しクラス "や "シェイプ "と呼んでいます。内部クラスは重複排除され、ツリーで管理される。オブジェクトにプロパティが追加されると、現在の内部クラスの子クラスがチェックされ、以前に同じオブジェクト・レイアウトが発生したかどうかが確認される。もしそうなら、出来上がった内部クラスをすぐに使うことができる。そうでない場合は、新しい内部クラスを作成しなければなりません。

内部クラスはJavaScriptのヒープ内の独自のセクションに格納され、それ以外は上で説明した一般的なオブジェクトの割り当てと同じように動作します。これは、内部クラスを使用するオブジェクトが収集される間、内部クラスを存続させなければならないからです。内部クラスは、別のパスで収集されます。

しかし、内部クラスに格納されている実際のプロパティ属性はJavaScriptのヒープ上には保持されず、newとdeleteを使って管理されます。

ガベージコレクション

JavaScriptエンジンで使用されるガベージコレクタは、移動しない、ストップ・ザ・ワールドのマークとスイープのデザインです。マーク段階では、オブジェクトへのライブ参照が見つかる既知の場所をすべて巡回します。特に

  • JavaScriptグローバル
  • QMLとJavaScriptのコンパイルユニットの削除不可能な部分
  • JavaScriptスタック
  • 永続値ストレージ。これはQJSValue や同様のクラスが JavaScript オブジェクトへの参照を保持する場所です。

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

掃引フェーズでは、ガベージコレクタはヒープ全体を走査し、以前にマークされなかったオブジェクトを解放します。結果として解放されたメモリは、次の割り当てに使用するビンにソートされる。アドレス空間のチャンクが完全に空になった場合、それはデコミットされるが、アドレス空間は保持される(上記の基本原則を参照)。メモリ使用量が再び増加した場合、同じアドレス空間が再利用される。

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

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

メモリ使用量の分析

アドレス空間とそこに割り当てられたオブジェクト数の両方の発展を観察するためには、専用のツールを使うのが一番です。QML Profilerはこのような場合に役立つ可視化機能を提供します。より汎用的なツールでは、JavaScript のメモリマネージャが確保したアドレス空間の中で何を行っているのかを見ることはできませんし、アドレス空間の一部が物理メモリにコミットされていないことにすら気づかないかもしれません。

メモリ使用量をデバッグするもう一つの方法はlogging categories qt.qml.gc.statisticsqt.qml.gc.allocatorStats です。qt.qml.gc.statisticsのDebugレベルを有効にすると、ガベージコレクタは実行するたびに情報を表示します:

  • どれだけのアドレス空間が確保されているか
  • ガベージコレクションの前後に使用されたメモリの量
  • これまでに割り当てられた様々なサイズのオブジェクトの数

qt.qml.gc.allocatorStatsのDebugレベルでは、ガベージコレクタがどのようにトリガされたか、マークフェーズとスイープフェーズのタイミング、アドレス空間のバイトとチャンクによるメモリ使用量の詳細な内訳を含む、より詳細な統計情報を表示します。

本ドキュメントに含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。