メモリリークの検出
メモリリークが発生すると、アプリのパフォーマンスとユーザーエクスペリエンスが低下する可能性があります。効率的で応答性の高いアプリを作成するには、このようなリークを特定して解決することが重要です。
Vegaアプリを開発するための統合開発環境(IDE)であるVega Studioには、MemLabと呼ばれる機能があります。この機能は、JavaScriptヒープスナップショットを分析することでVega向けReact Nativeアプリのメモリリークを検出します。MemLabでは、次のような潜在的なメモリの問題を特定できます。
- React Nativeの仮想ドキュメントオブジェクトモデル(DOM)内にある、デタッチされたDOMのようなオブジェクト。
- JavaScriptオブジェクト内の循環参照。
- ガベージコレクションの妨げとなる、保持されたままのコンポーネントやイベントリスナー。
Vega向けReact NativeとReact Nativeとの関係
Vega向けReact Nativeは、Amazonによって開発された、クロスプラットフォームアプリを構築するための独自のスクリプト言語およびフレームワークです。これにより、さまざまなAmazonデバイスやプラットフォーム向けの効率的で応答性の高いアプリを作成できます。Vega向けReact NativeはReact Nativeを基盤としており、そのコンセプトと方法論の多くを継承しています。React Nativeと同様に、Vega向けReact Nativeではコンポーネントベースのアーキテクチャが使用され、一度作成すれば複数のプラットフォームにデプロイできます。
Vega向けReact Nativeでのメモリ管理
React Nativeと同様に、Vega向けReact Nativeアプリでは適切なメモリ管理が重要です。メモリの割り当てと割り当て解除の大部分はフレームワークによって処理されますが、存続期間の長いオブジェクト、イベントリスナー、複雑な状態管理を扱う場合は、潜在的なメモリリークに注意する必要があります。
メモリリーク検出の重要性
メモリリークの特定と修正は、特にリソースが限られているデバイスで、Vega向けReact Nativeアプリのパフォーマンスと安定性を維持するために不可欠です。そこで、MemLabのようなツールが開発プロセスに役立ちます。
このページでは、以下の手順の実行方法に関するガイダンスを提供します。
- 意図的にメモリリークを起こすサンプルのVega向けReact Nativeアプリを作成する。
- アプリの実行中のさまざまな段階でヒープスナップショットをキャプチャする。
- MemLabを使用してこれらのスナップショットを分析し、メモリリークを特定する。
- MemLabの出力を解釈して、リークの性質と原因を理解する。
前提条件
- Node.jsをインストールする。
-
MemLabをインストールする。
npm install -g memlab
手順1: サンプルアプリを作成する
次のコマンドを使用して、テスト目的で意図的にサンプルリークを起こすサンプルアプリを作成します。
kepler project generate --template hello-world --name keplersampleapp --packageId com.amazon.keplersampleapp --outputDir keplersampleapp
app.jsファイルの例
次のファイルには、メモリリークのシナリオを実行するサンプルアプリのコードスニペットと、メモリリークを作成、クリア、分析するツールが含まれています。
1 import React, { useRef, useState } from 'react';
2 import { Pressable, StyleSheet, Text, View } from 'react-native';
3
4 export default function App() {
5 const detachedDOMRef = useRef([]);
6 const [snapshotInfo, setSnapshotInfo] = useState(null);
7 const [focusedButton, setFocusedButton] = useState(null);
8
9 // デタッチされたDOM要素を作成する関数(強力なメモリリーク)
10 const createDetachedDOMLeak = () => {
11 console.log('Creating detached DOM elements...');
12 const detachedElements = [];
13 for (let i = 0; i < 10000; i++) {
14 // デタッチされたDOM要素を表すオブジェクトを作成します。
15 const element = { id: i, name: `デタッチされた要素${i}` };
16 detachedElements.push(element);
17 }
18 detachedDOMRef.current = detachedElements; // 実際のDOMではなく参照を保持
19 console.log('10,000要素のデタッチされたDOMによるリークが作成されました');
20 };
21
22 // メモリリークをクリアする関数
23 const clearLeaks = () => {
24 console.log('すべてのリークをクリアします...');
25 detachedDOMRef.current = []; // 参照をクリア
26 console.log('すべてのリークがクリアされました');
27 };
28
29 // ヒープスナップショットの取得のシミュレート
30 const takeHeapSnapshot = async () => {
31 console.log('ヒープスナップショットを取得します...');
32 const snapshot = {
33 detachedDOMSize: detachedDOMRef.current.length,
34 };
35 if (global.__CollectHeapSnapshot__) {
36 global.__CollectHeapSnapshot__();
37 }
38 setSnapshotInfo(snapshot);
39 console.log('ヒープスナップショットが取得されました:', snapshot);
40 };
41
42 return (
43 <View style={styles.container}>
44 <Text style={styles.text}>メモリリークのデモ</Text>
45
46 <View style={styles.boundary}>
47 <View style={styles.buttonRow}>
48 <Pressable
49 onPress={createDetachedDOMLeak}
50 onFocus={() => setFocusedButton('createDetachedDOMLeak')}
51 onBlur={() => setFocusedButton(null)}
52 style={[
53 focusedButton === 'createDetachedDOMLeak' ? styles.focusedButton : styles.button,
54 ]}
55 >
56 <Text style={styles.buttonText}>デタッチされたDOMによるリークを作成</Text>
57 </Pressable>
58
59 <Pressable
60 onPress={clearLeaks}
61 onFocus={() => setFocusedButton('clearLeaks')}
62 onBlur={() => setFocusedButton(null)}
63 style={[
64 focusedButton === 'clearLeaks' ? styles.focusedButton : styles.button,
65 ]}
66 >
67 <Text style={styles.buttonText}>リークをクリア</Text>
68 </Pressable>
69
70 <Pressable
71 onPress={takeHeapSnapshot}
72 onFocus={() => setFocusedButton('takeHeapSnapshot')}
73 onBlur={() => setFocusedButton(null)}
74 style={[
75 focusedButton === 'takeHeapSnapshot' ? styles.focusedButton : styles.button,
76 ]}
77 >
78 <Text style={styles.buttonText}>スナップショットを取得</Text>
79 </Pressable>
80 </View>
81 </View>
82
83 {snapshotInfo && (
84 <Text style={styles.snapshotText}>
85 スナップショットが作成されました:デタッチされたDOM要素{snapshotInfo.detachedDOMSize}個
86 </Text>
87 )}
88 </View>
89 );
90 }
91
92 const styles = StyleSheet.create({
93 container: {
94 flex: 1,
95 justifyContent: 'center',
96 alignItems: 'center',
97 backgroundColor: '#e0e0e0',
98 },
99 text: {
100 fontSize: 24,
101 marginBottom: 20,
102 textAlign: 'center',
103 },
104 boundary: {
105 padding: 20,
106 borderWidth: 2,
107 borderColor: '#333',
108 backgroundColor: '#ffffff',
109 borderRadius: 10,
110 },
111 buttonRow: {
112 flexDirection: 'row',
113 justifyContent: 'center',
114 },
115 button: {
116 width: 150,
117 height: 50,
118 marginHorizontal: 5,
119 borderRadius: 5,
120 alignItems: 'center',
121 justifyContent: 'center',
122 backgroundColor: '#007bff',
123 },
124 focusedButton: {
125 backgroundColor: '#003f7f',
126 color: '#000', // フォーカス時は黒いテキスト
127 },
128 buttonText: {
129 color: '#fff',
130 fontSize: 16,
131 },
132 snapshotText: {
133 marginTop: 20,
134 fontSize: 16,
135 color: '#333',
136 },
137 });
手順2: アプリをビルドする
次のコマンドを実行します。
npm install
npm run build:app
手順3: アプリの実行
-
Vega仮想デバイスを実行します。
kepler virtual-device start -
Vega仮想デバイス上でアプリを実行します。
kepler run-kepler <Vpkgのパス> <アプリID> -d VirtualDevice
手順4: ヒープスナップショットをキャプチャしてコピーする
- アプリの実行中に [Take Snapshot] ボタンをクリックして、デバイス上のヒープスナップショットをキャプチャします。
- [Create Detached DOM Leak] ボタンをクリックして、メモリリークを引き起こす要素を10,000個生成します。
- スナップショットをもう一度取得します。
- [Clear Leaks] ボタンをクリックして、メモリリークをシミュレートする要素を削除します。
-
最終的なスナップショットを取得します。

[Take Snapshot] ボタンをクリックすると、キャプチャしたスナップショットの保存先のファイルパスがログに表示されます。
ls /home/app_user/packages/<アプリ>/data/ -l total 39764 -rw-r--r-- 1 app_user 30097 26 2024-10-17 08:56 shared_preferences.json -rw-r--r-- 1 app_user 30097 6335533 2024-10-17 08:37 snapshot-17-10-2024-08-37-08.heapsnapshot -rw-r--r-- 1 app_user 30097 8081408 2024-10-17 08:37 snapshot-17-10-2024-08-37-25.heapsnapshot -rw-r--r-- 1 app_user 30097 6336519 2024-10-17 08:39 snapshot-17-10-2024-08-39-27.heapsnapshot -rw-r--r-- 1 app_user 30097 9492333 2024-10-17 08:39 snapshot-17-10-2024-08-39-36.heapsnapshot -rw-r--r-- 1 app_user 30097 10434707 2024-10-17 08:40 snapshot-17-10-2024-08-39-55.heapsnapshot -
スナップショットファイルをローカルコンピューターにコピーします。
取得したヒープスナップショットを使用して、潜在的なメモリリークを分析して特定できます。
手順5: MemLabを実行する
スナップショットに対してMemLabを実行するには、次のコマンド形式を使用します。
memlab find-leaks --baseline <初期のスナップショット> --target <リーク後のスナップショット> --final <クリア後のスナップショット> --trace-all-objects
このコマンドを実行すると、MemLabは次のアクションを実行します。
- すべてのスナップショットをメモリに読み込みます。
- ベースラインとターゲットのスナップショットを比較して、新しいオブジェクトを特定します。
- 特定した新しいオブジェクトを分析して、最終スナップショットまで存続しているかどうかを確認します。
- 保持パターンを理解するためにオブジェクト参照をトレースします。
MemLabコマンド構造の説明
memlab find-leaksコマンドは、メモリリークを特定するMemLabのコア機能です。
次のオプションコマンドを指定できます。
--baseline <初期のスナップショット>- 潜在的なリークが取り込まれる前に取得した、初期のヒープスナップショットを指定します。基準点として使用されます。--target <リーク後のスナップショット>- 潜在的なリークが取り込まれた後に取得したスナップショットです。MemLabはこれをベースラインと比較して、リークの可能性がある新しいオブジェクトを特定します。--final<post-clear-snapshot>-リークをクリアしようとした後に撮ったスナップショットを指定します。これは、オブジェクトが本当にリークしているか、それとも正常なアプリ状態の一部であるかをMemLabが判断するために役立ちます。--trace-all-objects- このオプションは、すべてのオブジェクトをトレースするようにMemLabに指示します。これによってより包括的な分析が提供されますが、処理時間が長くなる可能性があります。
MemLabの出力例
memlab find-leaks --baseline snapshot-17-10-2024-08-27-13.heapsnapshot --target snapshot-17-10-2024-08-27-29.heapsnapshot --final snapshot-17-10-2024-08-27-52.heapsnapshot --trace-all-objects
Alive objects allocated in target page:
┌─────────┬─────────────────────────────────────────────────────────┬───────────┬───────┬──────────────┐
│ (index) │ name │ type │ count │ retainedSize │
├─────────┼─────────────────────────────────────────────────────────┼───────────┼───────┼──────────────┤
│ 0 │ 'JSArray' │ 'object' │ 305 │ '1.1MB' │
│ 1 │ 'JSObject(id, name)' │ 'object' │ 10000 │ '1.1MB' │
│ 2 │ 'DictPropertyMap' │ 'object' │ 79 │ '122.1KB' │
│ 3 │ 'HiddenClass' │ 'object' │ 71 │ '92.6KB' │
│ 4 │ 'BoxedDouble' │ 'object' │ 1511 │ '36.2KB' │
│ 5 │ 'JSObject(memoizedState, baseState, baseQueue, queu...' │ 'object' │ 498 │ '23.9KB' │
│ 6 │ 'Environment' │ 'object' │ 287 │ '14.7KB' │
│ 7 │ 'JSFunction' │ 'closure' │ 185 │ '8.8KB' │
│ 8 │ 'JSObject(validated)' │ 'object' │ 155 │ '7.4KB' │
│ 9 │ 'JSObject($$typeof, type, key, ref, props, ...)' │ 'object' │ 155 │ '7.4KB' │
│ 10 │ 'JSObject(tag, create, destroy, deps, next)' │ 'object' │ 132 │ '6.3KB' │
│ 11 │ 'Detached FiberNode' │ 'object' │ 12 │ '5.5KB' │
│ 12 │ 'JSObject(value, children)' │ 'object' │ 66 │ '3.1KB' │
│ 13 │ 'HashMapEntry' │ 'object' │ 61 │ '2.9KB' │
│ 14 │ 'JSObject(style, children)' │ 'object' │ 58 │ '2.7KB' │
│ 15 │ 'warnAboutAccessingRef' │ 'object' │ 54 │ '2.5KB' │
│ 16 │ 'warnAboutAccessingRef' │ 'closure' │ 54 │ '2.5KB' │
│ 17 │ 'JSObject(lastEffect, stores)' │ 'object' │ 48 │ '2.3KB' │
│ 18 │ 'HiddenClass(Dictionary)' │ 'object' │ 45 │ '2.1KB' │
│ 19 │ 'HostObject' │ 'object' │ 43 │ '2KB' │
└─────────┴─────────────────────────────────────────────────────────┴───────────┴───────┴──────────────┘
Sampling trace due to a large number of traces:
Number of Traces: 10100
Sampling Ratio: 49.5%
MemLab found 5 leak(s)
Number of clusters loaded: 0
--Similar leaks in this run: 4956--
--Retained size of leaked objects: 1.1MB--
[(GC roots)] (synthetic) @3 [2.8MB]
--1 (element)---> [(Registers)] (synthetic) @5 [2.2KB]
--57 (element)---> [Environment] (object) @226263 [48 bytes]
--0 (internal)---> [JSObject(current)] (object) @47915 [1.1MB]
--directProp0 (internal)---> [JSArray] (object) @222185 [1.1MB]
--9999 (element)---> [JSObject(id, name)] (object) @191663 [112 bytes]
--class (internal)---> [HiddenClass] (object) @117709 [300 bytes]
--Similar leaks in this run: 22--
--Retained size of leaked objects: 2.9KB--
[(GC roots)] (synthetic) @3 [2.8MB]
--1 (element)---> [(Registers)] (synthetic) @5 [2.2KB]
--2 (element)---> [Detached FiberNode RCTView] (object) @69 [384 bytes]
--pendingProps (property)---> [JSObject(accessibilityViewIsModal, isTVSelectable, hitSlop, alexaEntityType, alexaExternalIds, ...)] (object) @228861 [680 bytes]
--onBlur (property)---> [onBlur] (closure) @45137 [128 bytes]
--prototype (property)---> [onBlur] (object) @224283 [48 bytes]
--Similar leaks in this run: 11--
--Retained size of leaked objects: 560 bytes--
[(GC roots)] (synthetic) @3 [2.8MB]
--1 (element)---> [(Registers)] (synthetic) @5 [2.2KB]
--34 (element)---> [JSObject(Dictionary)] (object) @133 [26.2KB]
--console (property)---> [JSObject(error, info, log, warn, trace, ...)] (object) @78609 [4.8KB]
--directProp1 (internal)---> [JSFunction] (closure) @87937 [256 bytes]
--environment (internal)---> [Environment] (object) @87913 [208 bytes]
--parentEnvironment (internal)---> [Environment] (object) @87801 [32 bytes]
--0 (internal)---> [JSObject(enable, disable, registerBundle, log, setup)] (object) @85777 [3.2KB]
--directProp4 (internal)---> [setup] (closure) @88235 [48 bytes]
--environment (internal)---> [Environment] (object) @88203 [2.6KB]
--11 (internal)---> [JSArray] (object) @88225 [2.1KB]
--4 (element)---> [JSArray] (object) @222491 [200 bytes]
--Similar leaks in this run: 1--
--Retained size of leaked objects: 64 bytes--
[(GC roots)] (synthetic) @3 [2.8MB]
--1 (element)---> [(Registers)] (synthetic) @5 [2.2KB]
--57 (element)---> [Environment] (object) @226263 [48 bytes]
--parentEnvironment (internal)---> [Environment] (object) @47913 [2KB]
--7 (internal)---> [JSObject(container, text, boundary, buttonRow, button, ...)] (object) @77759 [712 bytes]
--snapshotText (property)---> [JSObject(marginTop, fontSize, color)] (object) @77737 [708 bytes]
--class (internal)---> [HiddenClass] (object) @27707 [660 bytes]
--forInCache (internal)---> [SegmentedArray] (array) @224539 [64 bytes]
--Similar leaks in this run: 10--
--Retained size of leaked objects: 40 bytes--
[(GC roots)] (synthetic) @3 [2.8MB]
--3 (element)---> [(RuntimeModules)] (synthetic) @9 [15.4KB]
--378 (element)---> [, ] (symbol) @117483 [4 bytes]
MemLabの出力の解釈
MemLabの出力で重要な要素は以下のとおりです。
Alive objects allocated in target page
このセクションには、アプリの操作後または変更後にまだメモリに存在しているオブジェクトが表示されます。この一覧は潜在的なメモリ保持の問題を示し、各オブジェクトについて次の詳細を含んでいます。
- name - オブジェクト名またはその構造型。
- type - オブジェクトがobject(オブジェクト:JSObject、JSArrayなど)であるか、closure(クロージャ:メモリを保持する関数)であるか。
- count - 各オブジェクト型のインスタンスの数。
- retainedSize - これらのオブジェクトによって保持されているメモリの合計。
例
JSArray(オブジェクト型)- 305個のインスタンスがあり、1.1MBのメモリが保持されています。JSObject(id, name)- 10,000個のインスタンスがあり、同じく1.1MBのメモリが保持されています。
Sampling trace due to a large number of traces
このセクションは、トレースの数が多く(10,100件)、トレースの処理と表示を効率的に行うために、MemLabが49.5%のサンプリング比でサンプリングを適用したことを示しています。
MemLab found X leak(s)
この例では、MemLabによって5件のリークが検出されています。出力には、各メモリリークについて次の情報が表示されます。
- Number of similar leaks - 同様のリークパターンが検出された回数。
- Retained size of leaked objects - リークのあったオブジェクトによって保持されているメモリの合計。
- Leak trace - オブジェクトがどのように相互に接続し、なぜガベージコレクションが適用されないかを示します。これらのトレースから、オブジェクトの保持につながる経路がわかります。
ガベージコレクション(GC)ルート
これらは、メモリに残っていて、ほかのオブジェクトのガベージコレクションを妨げているオブジェクトのエントリポイントです。トレースは、GCルートからの参照によってオブジェクトがどのように有効なまま保持されるのかを、JSObject、JSArray、クロージャー参照につながる経路と共に示します。
例1
- リークパターン: 4,956件の同様のリークがあり、1.1MBのメモリが保持されています。
JSObject(current) → JSArray → JSObject(id, name)
これは、JSArrayが10,000個のJSObject(id, name)オブジェクトへの参照を保持していて、これらのオブジェクトのガベージコレクションを妨げていることを示します。
例2
リークパターン: 22件の同様のリークがあり、2.9KBのメモリが保持されています。
トレースには、React Nativeのビューシステムの一部であるDetached FiberNode RCTViewオブジェクトが含まれています。このリークは、onBlurなどのイベントハンドラーに関連している可能性があり、削除されたはずのオブジェクトが不要に保持されていると考えられます。
MemLabによるスナップショットの分析
- Alive objects allocated in target pageセクションを調べて、保持されているオブジェクトを確認します。
- 「MemLab found [X] leak(s)」という行を確認して、特定された固有のリークの数を把握します。各リークについて以下を確認します。
- 同様のリークの数と合計保持サイズを確認します。
- オブジェクトトレースを調べて、オブジェクトがどのように保持されているかを理解します。
- デタッチされたDOM要素や大きい配列など、想定外の保持に注意します。
リークへの対処
アプリ内のリークに対処するには、次の操作を実行します。
- リークのあるオブジェクトのパターン(デタッチされたDOM要素、クリアされないイベントリスナーなど)を特定します。
- アプリ内で対応するコードを特定します。
- 修正を実装します。たとえば、コンポーネントを適切にマウント解除し、配列をクリアし、イベントリスナーを削除します。
修正の確認
修正されたことを確認するには、次の操作を実行します。
- 修正を実装したアプリを再ビルドし、再実行します。
- スナップショットの取得と分析のプロセスを繰り返します。
- 以前に特定されたリークがMemLabの出力に表示されなくなったことを確認します。
関連トピック
- アプリのKPIの測定
- UIレンダリングの問題の特定
- オーバードローの検出
- Vega向けReact Nativeアプリでのコンポーネントの再レンダリングに関する問題の調査
- 💬 コミュニティ: アプリのメモリが不足しているかどうか、どうすれば確認できますか?
Last updated: 2025年9月30日

