高度な検索
Developer Connection
Member Login ログイン | ご入会 ADC連絡先

Technote 1042

Strategies for Dealing with Low-Memory Conditions


目次


メモリ予約の維持管理

メモリ割り当てのチェック

ユーザへのメモリ不足状態の通知

一時メモリの使い方

68K コードセグメントの処理

malloc および new に関する特殊な問題

要約
Macintosh 対応のソフトウェアを書くにあたって不変なものの一つは、開発したアプリケーションが遅かれ早かれ使用可能なメモリが不足した状態に陥るであろうということです。他のオペレーティングシステム上で実行されるアプリケーションは、グローバルプール(仮想メモリによって支援される) から割り当てられたメモリを使用し、ほとんど制限のない状態で必要なメモリを引き出すことができますが、Macアプリケーションは、Process Manager によって与えられた一定量のメモリを使い切らないように動作する必要があります。Toolboxの呼び出しはアプリケーションの呼び出しと同じヒープを共有しており、さらによく知られているように、Toolboxの一部ではメモリ割り当てができない状態を適切に処理することができないため、問題を管理するために幅広い戦略を立てることが重要になります。メモリ不足の状態に陥ったアプリケーションはなかなか言うことを聞いてくれません。

この TECHNOTE では、メモリ不足の状態を処理するためにアプリケーションがとるべきさまざまな戦略について説明します。また、このTECHNOTE は、Macintosh 対応ソフトウェアの開発にかかわって間もないデベロッパや、他のプラットフォームからアプリケーションをポーティングしようとしているデベロッパにとっては、メモリ管理の入門書として役立つはずです。

この TECHNOTE は、『Inside Macintosh:Memory』「Introduction to Memory Management」の章を拡張したものです。このため、この TECHNOTE の予備知識として少なくとも 1-37 ページから 1-49 ページを読んでおくことをお勧めします。また、68K セグメンテーションに関する情報については、『Macintosh:Processes』第 7 章を読んでいることが前提になります。


メモリ予約の維持管理
メモリ不足の状態を処理するための第 1 の戦略はメモリの予約を維持管理することです。この予約されていたメモリが底をつくとき、適度なサイズのメモリ割り当て部分には十分なメモリを供給すると同時に、アプリケーションのその他の部分には情報を提供します。予約状況をチェックすることにより、メモリ供給のステータスを判断することができます。予約されていたメモリの供給がだんだん少なくなるにつれ、アプリケーションでは、メモリが不足していることをユーザに通知して、さらに、選択肢を限定することで、ユーザが実行できる操作に制限を加えることができます。

パージャブルクッションの使い方
パージャブルクッションというテクニックを使用する利点はその単純さにあります。つまり、ごくわずかなコードを書くだけですみます。一方、不利な点は、アプリケーションのメモリの大部分が Toolbox によって間接的に割り当てられているときに、全体的な安全性を確保することが困難であるということです。

メモリ予約を作成するには、アプリケーションのスタートアップコードの最初でハンドルを割り当てます。このハンドルのサイズは、通常、32K バイトから 64K バイトの範囲に設定してください。また、パージャブルなものとしてマークされている必要があります。その後、Memory Manager でより多くのフリースペースを作成することが必要になるとき、パージャブルなハンドルやリソースは自動的にパージされます。

メインイベントループの実行中に、アプリケーションはそのハンドルがパージされているかどうかをチェックすることができます (ハンドルのマスタポインタがゼロの場合、ハンドルはパージされています)。また、ReallocateHandle を使って再割り当てを行い、オリジナルのサイズに戻すこともできます。この処理に失敗すると、残された連続するメモリの容量が少なくなり、アプリケーションではグローバルフラグをセットして、通常の状態で実行を続けるにはメモリが十分ではないことを示します。これに対する応答として、不要な書類を閉じるようにユーザに警告を出したり、メモリを浪費する機能を選択するメニューを無効にしたり、あるいはより一般的に、ユーザにメモリの制限を圧迫させないようにすることができます。ハンドルをもう一度再割り当てできると、アプリケーションはグローバルフラグをクリアして、以前と同じステップにしたがって予約を行います。

次のコード例に示すように、オリジナルのメモリを回復して、さらに余分なメモリ容量を確保できた場合にだけグローバルフラグをクリアするテクニックは役立ちます。

extern Handle gReserveHandle;
if (*gReserveHandle == nil)
{
  ReallocateHandle(gReserveHandle, kReserveSize + kSlopFactor);
  if (MemError()!= noErr)
  {
    SetHandleSize(gReserveHandle, kReserveSize);
    HPurge(gReserveHandle);
    SetReserveWasRecoveredFlag();
  }
}

アプリケーションがメモリ不足の状態に陥るぎりぎりのところで正常に実行されている場合、いわゆる "スロップファクター" を使って、アプリケーションが "揺れ動いている" ことをユーザに知らせる警告をあまり頻繁に表示しないようにすることができます。

作成するハンドルのサイズはアプリケーションの必要に応じて異なります。GWorlds やピクチャなど、サイズの大きな Toolbox のデータ構造体が日常的にすべての使用可能なメモリを消費する場合は、64K を超えるサイズが必要になることもあります。必要なサイズを知る唯一の方法は、さまざまなサイズで実験をしてみて、必要最小限のメモリでアプリケーションの機能をテストすることです。アプリケーションがフリーズすれば、予約サイズを多くします。また、アプリケーションがフリーズしなければ、少ないサイズを試してみます。

拡大ゾーンプロシージャのインストール
SetGrowZone を使って拡大ゾーンプロシージャ (Grow Zone Procedure) をインストールするという若干複雑なテクニックもあります。このプロシージャは、パージャブルなハンドルやリソースのパージ、ヒープのコンパクト化など、フリースペースを見つけるためのあらゆる戦略を試みた後で、Memory Manager によって呼び出されます。拡大ゾーンプロシージャは最後の手段として呼び出されます。しばしば拡大ゾーンプロシージャは割り当てが引き金になった問題とは無関係に使用される場合があります。拡大ゾーンプロシージャは、メモリ不足エラーを適切に処理するアプリケーションコードだったり、十分なチェックができなかった Toolbox ルーチンを処理するアプリケーションコードであったりします。

拡大ゾーンプロシージャの戦略
拡大ゾーンプロシージャは、1 つの "クッション" メモリブロックを単純に解放したり、パージャブルのマーキングを行い、前述のようにメインイベントループにその再割り当てを行わせることで、追加のメモリを提供しようとします。また、拡大ゾーンプロシージャは SetHandleSize を使ってクッションハンドルを縮小し、メインイベントループの中でそれを拡大することもできます。

さらに、拡大ゾーンプロシージャは、単純なクッションテクニックよりも複雑かつインテリジェントな戦略を立て、使用可能なメモリを供給することができます。アプリケーションは縮小可能なバッファを使用し、ハンドルやポインタに格納されているデータ構造体を解放したり、その他のリソースをパージャブルとしてマークします。また、段階的な方法でメモリ必要量を削減することもできます。アプリケーションは、メモリブロックやリソースが表しているものを正しく認識しているため、パージャブルなバッファの解放、縮小、またはマーキングを行い、Memory Manager では解放できないメモリを使用可能な状態にすることができます。

拡大ゾーンプロシージャにより一部のメモリが解放されたことが示されると、Memory Manager はパージや圧縮の別のサイクルを再度試みます。Memory Manager は、繰り返し拡大ゾーンプロシージャを呼び出し、十分なメモリが使用可能な状態になるか、拡大ゾーンプロシージャの戻り値がゼロになるまでパージと圧縮を継続します。拡大ゾーンプロシージャの戻り値がゼロになると、Memory Manager は処理をやめ、メモリ割り当てを試みたコードに 'memFullErr' を返します。

メモリを回復する試みは、メインイベントループの実行中に行います。予約されていたメモリが使用され、さらにそれが回復されると、グローバル変数またはグローバルステートにより、予約されていたメモリがどの程度残されているかを追跡することができ、さらにアプリケーションのその他の部分でこの情報を使用することもできます。

安全な Memory Manager 呼び出しのリスト
次に、拡大ゾーンプロシージャの内部で安全に呼び出すことのできる Memory Manager 呼び出しの一覧を示します。

SetHandleSize (ただし比較的小さなサイズのみ)
SetPtrSize (ただし比較的小さなサイズのみ)
DisposeHandle
DisposePtr
EmptyHandle
HPurge、または同等の HSetState
HUnlock、または同等の HSetState
68K アプリケーションでは、現在の呼び出しチェーン内にないセグメントを対象に UnloadSeg を呼び出すこともできます。詳細については、後述の「68K セグメントの処理」を参照してください。

GZSaveHnd
GZSaveHnd は常に呼び出す必要があります。GZSaveHnd は、Memory Manager が現在オペレーション中であるハンドルを返します。ただし、このハンドルを使って、上述の呼び出しを行わないでください。

拡大ゾーンプロシージャが行ってはいけないこと
基本的に、直接または間接的にメモリの移動を行う呼び出しや、メモリ解放のリクエストのトリガーとなる呼び出しを実行することはできません。これらは、Memory Manager や拡大ゾーンプロシージャに対する再帰呼び出しになってしまいます。Memory Manager はリエントラントではありません。すでにヒープの再配置を行っているのに、強制的にヒープの再配置を行わせようとすると、確実に、誰もが望まない予測不可能な結果に陥ることになります。

拡大ゾーンプロシージャで避けるべき処理の例
前の段落で述べた禁止事項を考慮しつつ、次に、拡大ゾーンプロシージャでは実行を避けるべき処理の例を示します。これらの問題の多くは、長年にわたるデベロッパからの質問に基づいています。

メモリを割り当てないでください
これは明らかなことのように思われますが、さまざまな状況によって間接的にメモリの割り当てが行われることもあるので注意してください。この後にあげる問題の大部分は、結果的にメモリの割り当て、あるいはヒープの再配置を引き起こします。十分なメモリが小さなブロックでも使用可能であるかどうかを完全にチェックすることは不可能です。Memory Manager を混乱させるようなリスクを犯さないのが賢明です。

同期ファイル I/O を実行しないでください
メモリを移動しないことが保証されているのは、File Manager を非同期的に呼び出したときだけです。ただし残念なことに、非同期ファイル I/O を実行しても、メモリが解放されるわけではありません。このため、データをディスクに保存することで、より多くの使用可能なメモリを確保することは不可能です。

リソースを更新しないでください
ChangedResource を呼び出したり、その他多くの Resource Manager 呼び出しを実行することは避けてください。Resource Manager は、拡大ゾーンから呼び出されているかどうかを考慮することなく、Memory Manager を使用するためです。

ダイアログを表示しないでください
メモリが不足していることをユーザに通知する必要がある場合は、グローバルフラグをセットして、それをメインイベントループの中でチェックします。GrowZone の実行中に何らかのユーザインタフェースコードを呼び出すと、確実にメモリの割り当てが行われます。拡大ゾーンプロシージャの内部からユーザに書類を保存させようとしても、まずはうまくいきません。このことは考えない方がよいでしょう。

68K Macintosh では、セグメントのロードを強制するルーチンを呼び出さないでください
この問題は、非常に微妙な方法で発生する場合があります。たとえば、C++ で、Memory Manager のヒープハンドルまたはポインタであるメンバ変数を含んだいくつかのオブジェクトを削除し、その結果、いくらかのメモリ空間を解放しようとするとします。この場合、オブジェクトを対象に削除の呼び出しを実行します。しかし残念ながら、オブジェクトのクラスのためのデストラクタ、あるいはスーパークラスのためのデスクトラクタは、現在メモリ内にないコードセグメントに常駐します。このため、このコードを呼び出してメモリ空間を解放しようとしても、実際にはより多くのメモリが消費されることになり、拡大ゾーンプロシージャへの再帰呼び出しが行われ、最終的にクラッシュを引き起こすことになります。拡大ゾーンプロシージャから呼び出すものがすべてレジデントセグメントにあることを確認してください。

メモリ割り当てのチェック
メモリ不足に対処する戦略を立てた場合でも、メモリ割り当てが正常に行われているかどうかを常にチェックすることは非常に重要です。ただし、パージャブルクッションや拡大ゾーンプロシージャを注意深く使用することで、失敗した割り当ての処理を非常に簡略化することができます。クッションまたはメモリ予約の現在のステータスを返すグローバルフラグまたは関数があれば、アプリケーションにタスクやオペレーションを実行するために十分なメモリがあるかどうかをすばやくチェックできます。予約されていたメモリが残り少ない場合は、最初からタスクを中止することができます。予約されていたメモリが完全な状態で残されている場合は、使用可能なメモリが最低限、存在することになり、確実に処理を継続することができます。

予約またはクッションにより、必要とされるメモリが適切に供給されているということが確実にわかっている場合にかぎり、割り当てが正常に行われているかどうかチェックする必要がありません。十分なメモリが使用可能であるかどうか判定する PurgeSpace のような、潜在的に処理時間を浪費する呼び出しの実行を避けるようにします。このタイプの呼び出しは、予約またはクッションによって供給される容量以上のメモリが必要であるかどうかをチェックする必要が出てくるまで、保持しておくことができます。

ユーザへのメモリ不足状態の通知
メモリが不足していることをユーザに通知するには、デリケートなテクニックが必要です。アプリケーションがごく少ないメモリを使って実行されている場合、"メモリ不足" のアラートが繰り返し表示される状況に陥る場合がありますが、頻繁に表示されるアラートほどユーザにとって迷惑なものはありません。

2 段階プランの開発
一定の時間待機した後で次の警告を表示するようにすれば、ユーザはアプリケーションを終了したり、書類を閉じたり、あるいはその他の操作を実行することができるようになります。また、次のような 2 段階のメッセージを使用すると便利です。
1. メモリが残り少なくなっていることの警告 (予約が縮小されて、再割り当てされたときに表示します)

2. メモリがまったく残っていないということを通知する、もっと深刻な警告 (メモリ予約を回復できないときに表示します)
第 2 段階では、書類のクローズや保存などに関連した機能を除き、大部分のプログラム機能を使用できないようにしてください。"アプリケーションはメモリ不足です。いますぐ終了してください" といった悲しげな情報を表示するくらいなら、ユーザがアクセスできる機能に絶対的な制限を加える方が望ましい対処方法といえます。そうでないと、最悪の場合、ユーザは爆弾マークの攻撃にさらされてしまいます。

重要
メモリアラートは作業に使用する十分なメモリが残されていないときに表示されるため、これらのアラートを表示するために必要なリソースとコードは、メモリ内に常駐させるようにしてください。多くのアプリケーションでは、アラート文字列を 1 つの STR# リソースの中にまとめています。メモリ不足のメッセージがこのようにサイズの大きなリソースの中にあると、リソースをロードして個別の文字列を抽出することが不可能になってしまう場合もあります。このため、これらのアラートは、アラート内の静的項目である文字列とともに、できるかぎりメモリ内に常駐させることをお勧めします。



ユーザが書類を保存することの保証
アプリケーションがメモリ不足に陥ったときに、ユーザが作業中の書類を保存できることを保証する必要があります。これは明らかなことのように思えますが、実際には、書類を保存すると、メモリ内に格納されているデータをファイルフォーマットに変換したり、編集記録やプレビューイメージなどのリソースを保存するためにかなりの量のメモリが必要になります。このため、書類を確実に保存できるように、メモリの一部を別個に予約しておく必要があります。

また、メモリ不足の危機が去ったとき、つまり必要な書類を閉じたため、ユーザは安心して作業を続けることができるときにも、そのことをユーザに知らせる必要があります。

注意
メモリの使用状況を“About”ボックスに表示するアプリケーションでは、メモリ不足に陥ったときにも、“About”ボックスを開くためのメモリだけは確保しておく必要があります。


一時メモリの使い方
一時メモリを割り当てると、メモリ不足になっているアプリケーションの役に立つ場合があります。メモリ不足を検出した場合、アプリケーションでは、一時メモリが使用可能であるかどうをチェックし、TempNewHandle を使って、一部の割り当てを一時メモリに切り替えることができます。ただし、この方法は一時的な性質の項目にしか適用できず、アプリケーションの実行時全体にわたって存在するデータには適用できないという障害があります。

メモリを一時ヒープ (実際には Process Manager ヒープ) に割り当てるとき、ユーザが別のアプリケーションを起動することは困難になります。これは、アプリケーションがこのヒープの中で起動されるためです。また、アプリケーションで、常に使用可能な一時メモリが存在することをあてにすることはできません。このため、一時メモリが存在しなかったときのために、あらかじめ代替戦略を立てておく必要があります。

68K コードセグメントの処理
Motorola 68000 ファミリをサポートし続けるアプリケーションは、コードセグメントの管理という重荷をさらに背負い込むことになります。セグメントローダがセグメントをロードできない場合 (通常は十分なメモリがないため)、システムエラー #15 が返されます。また、コードセグメントに使用するメモリ容量がある時点で必要だった最小限の容量に制限されるという別の問題もあります。

次に、68K のコードセグメントを処理するために役立ついくつかの戦略を示します。

いくつかの役立つ戦略
すべての戦略は、コードを "レジデント" および "非レジデント" セグメントに分割することから始まります。レジデントセグメントには、メインイベントループ、拡大ゾーンプロシージャ、任意の割り込み時コード、および頻繁に参照されるその他のコードが含まれます。残ったセグメントは、コードのどのセクションが一緒に呼び出されるかを分析して決定します。

最終的な目標は、アプリケーションの各オペレーションや機能に必要となるセグメント (およびメモリ) の数を最小限にすることです。これを行うためには、アプリケーションのどの部分がどのオペレーションを実行するために呼び出されるのかを理解している必要があります。Metrowerks の ZoneRanger のようなユーティリティを使用すると、アプリケーションを実行するときに、メモリセグメントがどこに常駐しているかを目で確認することができます。

単一セグメントの使用
最も単純な戦略は、MPW の model far のようなオプションを使ってアプリケーションをビルドし、単一のセグメントを使用することです。この方法を使えば、すべてのものがメモリ内に格納され、コードを書き足す必要もありません。

メモリの足跡をきちんと追跡できる場合は、セグメンテーションを使ってメモリ管理を複雑にするのは無意味な操作です (単一セグメント CFM アプリケーションでは、仮想メモリを利用するにしろ、常時すべてのコードをロードすることが選択できる唯一のオプションとなります)。

アプリケーションのサイズが小さく、必要なメモリ容量がそれほど問題にならない場合は、この戦略が理想的です。

イベントループでのセグメントのアンロード
次に単純な戦略は、イベントループを通過するたびに (イベントが処理された後、または WaitNextEvent の直前で)、それぞれの非レジデントセグメントのルーチンとともに UnloadSeg を呼び出すことです。アプリケーションがコードによって必要とされるメモリをわずかに超えるメモリを使用している場合、特に、WaitNextEvent の呼び出しの間に、サイズの小さなパーマネント割り当てだけを行う場合は、この方法だけで十分です。コードが適切にセグメントに分割されているとすれば、それぞれのセグメントについて、UnloadSeg 呼び出しの中で使用するルーチンを決定することだけが問題になります。次に、2 つの例を示します。

1. 各セグメントの中に、そのセグメントの名前を持つ特殊なルーチンを作成し、UnloadSeg の中ではそのルーチンのアドレスを使用します。セグメントの内容はいろいろと変更できますが、使用する新しいルーチンを検索する必要もなく、何か足りないものがあるかどうかを心配する必要もありません。

2. 各セグメントに含まれる先頭のルーチンに対するジャンプテーブルをスキャンして、直接アドレスを取得します。この方法は、開発環境によって作成されるジャンプテーブルのフォーマットに大きく依存します。サンプルについては、MacApp ソースファイルの "USegments.cp" を参照してください。
任意のタイミングでのセグメントのアンロード
アプリケーション実行時の任意のタイミングで、現在呼び出しチェーンに入っていない任意のコードセグメントを意識的にアンロードすることができます。

実行時にこの処理を実行するには、ロードされているセグメント内に含まれる任意のアドレスに対してスタックをスキャンします。次に、毎回、実行するステップを示します。
1. 各セグメントに対するエントリを含むテーブルを構築します。この中には、メモリ内の各セグメントに対する現在のメモリ位置とサイズ、および 'keep' フラグが含まれます。すべてのレジデントセグメントのフラグに True をセットし、その他のフラグには False をセットします。

2. レジスタ A7 の現在の値から出発し、一度に 2 バイトずつスキップしながら、LMGetCurStackBase によって返されたアドレスをスキャンします。それぞれの値はポインタと見なします。この値がアプリケーションのヒープゾーンの内部に含まれている場合、それがセグメント内に含まれているかどうかをチェックします (ステップ #1 で構築したテーブルを使って)。もしそうであれば、そのセグメントに対する keep フラグを True にセットします。ここでは基本的に、サブルーチン呼び出し中にスタック上に置かれたリターンアドレスを見ていることになります。サブルーチン呼び出しは、呼び出しチェーンの中にあるコードを示します。

3. スタック全体をスキャンした後、keep フラグが False に設定されているセグメントをアンロードします (セグメントのアドレスを取得するために選択したテクニックを使って)。
この戦略の多少、複雑なサンプルは、MacApp ソースファイル "USegments.cp" の中に含まれています。

LoadSeg のパッチ
さらにもう一つの代替ソリューションは、LoadSeg にパッチを当てることです。このパッチは、セグメントをディスクからロードするために (そのセグメントがパージされている場合に) 十分なメモリが存在するかどうかをチェックします (このためには、GetResource を呼び出して単純にセグメントリソースをロードするという方法があります)。十分なスペースを検出すると、パッチは実際の LoadSeg を呼び出します。また、十分なメモリが存在しない場合は、バッファの解放、予約領域の縮小などの拡大ゾーンプロシージャの動作を実行したり、現在使用されていないコードセグメントをアンロードするなどして、使用可能なメモリを確保しようとします。この後で、パッチは実際の LoadSeg を呼び出します。このような方法により、セグメントのロードは常に成功します (処理の基礎となる予約メモリおよびセグメンテーションのスキーマが適切である場合に)。セグメントをロードすることができない場合は、(C++) 例外処理を実行します。

malloc および new に関する特殊な問題
C++ で書かれたアプリケーションや、標準的な C のアロケータ (特に、他のプラットフォームからポートされたもの) を使ったアプリケーションでは、それら独自のメモリ割り当てと Memory Manager を使ったメモリ割り当てを混在させると特殊な問題が発生します。

すべての C/C++ 開発環境には、NewPtr によって作成される Memory Manager ブロックの外に二次的な割り当て (sub-allocate) を行う malloc および new のバージョンが用意されています。これらのブロックは non-relocatable であり、特にメモリが不足するときにはヒープを断片化してしまう可能性があります。

さらに、new および malloc のこれらのインプリメンテーションの一部では、これらの NewPtr ブロックに二次的に割り当てられた情報が含まれないとき、NewPtr ブロックを正常に解放できないという問題もあります。これらのブロックがヒープにたまり始めると、ウィンドウ、メニュー、リソースなどの Toolbox の項目に使用するメモリがだんだんと少なくなります。Memory Manager 割り当てに使用する十分なメモリ容量を確保するためには、これらのブロックの増大に制限を設けることが重要です。

malloc および new の一部のインプリメンテーションでは、これらの動作をある程度制御することができます。詳細については、それぞれのコンパイラのマニュアルを参照してください。また、Smartheap などの市販の代替アロケータを使用することもできます。これらのツールは、この問題を解決するために役立つ場合があります。さらに別のソリューションとしては、アプリケーションゾーンの内部に代替ヒープゾーンを作成し、その中に非 Memory Manager 割り当てをすべて格納するという方法もあります。

要約
メモリ不足の状態を処理することは、Macintosh アプリケーションの開発を成功させる上で絶対に必要です。この TECHNOTE では、メモリ不足の状態を処理するために利用できるいくつかの戦略の概要を説明しています。たとえば、メモリ予約を維持管理することは、メモリ不足になったときにアプリケーションがクラッシュするのを防ぐために必要です。68K コードセグメントの管理や、割り当てに失敗したメモリのチェックなどの、その他の戦略も、Macintosh に対応した高品質のアプリケーションをビルドする上で重要です。適切なメモリ戦略を立てることに失敗すると、動作が不良で、ユーザの欲求不満を高める製品しか開発できません。

参考文献