2015/05/17

[Java][Android]メモリリーク

最初に断っておかなければならないことがあります。
私は組込機器とJVMに若干見識があるものの、AndroidもそのほかのJavaも業務として扱った経験は0に等しいエンジニアです。
Androidを生業にされている方々には是非、実経験に基づいたご意見をいただきたいと思っています。

Androidの開発者さんの間でごく自然に「メモリリークが…」という会話をされているのを聞くたびに違和感を感じています。
Javaには理論的にメモリリークは発生しないはずです。言語上でメモリの生成消滅の管理を行わないからです。
実際にAndroidの開発をしていると簡単にOutOfMemoryExceptionに見舞われるのは知っています。
Java界面から見て既に不要になっているメモリがその原因になっているときに、それを既知のシステムにならって「メモリリーク」と言いたくなる気持ちは分からないではありません。
しかし、それは「リーク(漏れ)」ではないはずです。

ネイティブ言語においてのメモリリークは、確保したメモリのハンドルへの参照を失うことで、当該メモリを操作するすべを失うことを言います。

void test(){
 void*p = malloc(BUFFER_SIZE);

// free(p);
}

free();に引き渡されることなくスコープから外れてしまったpに格納されていたアドレスは、C言語の界面で拾うことはもうできません。
ハンドルを失ったメモリは、言語の界面から操作することが不可能になります。メモリープールからこぼれ落ちたメモリの断片をリーク(漏れ)と呼んだのはまさに言い得て妙です。

一方Javaはガーベージコレクタを備えたメモリ管理機能を持つ言語です。
Javaのメモリマネージャは参照を失ったメモリ(Javaの場合Object)を定期的に整理することができます。
こうしてJavaは常に必要なサイズのメモリだけを保持します。

OutOfMemoryが発生する理由はシンプルです。OutOfMemorey(メモリ不足)になったからです。
Androidは概ね組込機器上で動作しますから、メモリに限界はあります。使い過ぎればOntOfMemoryとなります。これは当然です。

問題はOutOfMemoryの原因を探っていった時に、その原因が既に不要になったObjectに起因していた場合、メモリリークと呼びたくなる気持ちは分からないでもありません。
例えば、Listに情報を詰め込むようなロジックで、設計者が意図しないデータが蓄積してOutOfMemoryが発生するものをメモリリークの例に挙げているページがありましたが、それはバグです、リークと呼ぶのはJVMに対しての言いがかりだと思います。

自分が意図しない参照がもとでメモリが過剰に消費されることはいくらもあると思います。
例えばstaticでないインナークラスが暗黙で持っている親クラスの参照について意識していない人はそこそこいるかもしれません。
(余談ですが、C#はインナークラスに親クラスのプライベートにアクセスする権限を与えましたが、親クラスのインスタンスは明示的に引き渡さなければならないように設計されています。この設計はJavaの失敗(?)を踏まえてか良く練らていると感心しました。)
シングルトンにするべくstaticに保存したインスタンスもそう簡単に消えません。
Viewサブクラスが便利だからという理由だけで上位クラスのViewを保存してしまうと簡単に循環参照になります。
それらは未熟な設計者の意図を超えた寿命を持ちますが、それは設計者の頭からリークしているだけで、システム的にはリークしていません。

とても大きなリソースを消費してしまうので、GCで処理されるのを待っていることができないクラスは存在します。
Bitmapは典型的な例です。
画像を展開するためメガ単位のバッファを使います。
参照が切れてもGCが走るまでバッファを維持してしまうのは、組込機器上許されることではありません。
ですので、Bitmapにはrecycle()というメソッドが用意されています。
不要になったBitmapは速やかにrecycle()をコールしなければなりません。
もうひとつの例はファイルアクセスです。
JavaはGCがあるから明示的にclose()しなくても大丈夫、と思っていると痛い目にあいます。
C++をバックグラウンドに持っているエンジニア-私もそうですが-は、スコープを外れた刹那に暗黙的に処理をしてくれるデストラクタに慣れているので、最初にJavaにデストラクタがないことに驚愕し、finalize()が同じように動いてくれないことに失望します。
CからC++にグレードアップしたときに縁が切れたとおもっていたclose()と再び付き合わなければならなくなって、がっかりしたものです。
正直、Javaの設計段階に参照カウンタが0になった時に呼び出されるメソッドがあっても良かったのではないかと思っていますが、後の祭りです。(C#はusingという仕組みを持っています。これもまたうまく作ったものだと感心しています。)
いずれにしても、GCを待たずに後処理を行わなければならないクラスは存在します。OutOfMemoryの原因にはなり得ますが、リークではありません。

循環参照等でガーベージコレクションの効率が著しく低下することはあると思います。
その場合、参照が切れてからもGCに掛かるまでにそれなりの寿命を持つことは想像に難くありません。
しかし、それはまだ相応の時間があれば処理できる可能性があるもので、リークではありません。
相応の時間が取れず、ギブアップするJVMの実装もないとは言えません。それは現実問題を解決しなければならない組込Javaとして妥当な判断であると思います。
循環参照は設計で避けることができます。(循環参照を病的に避けようとする、ある種のプロセスはまた有害だと思うのですが…)
万が一循環参照が解決できないメモリマネージャを持ったJVMがあるとしたら、それはリークではなくJVMのバグです。

OutOfMemoryになりやすい状況をメモリリークと呼ぶんだよ、と言われればそうなのかもしれません。
それならそれは、あまりいい傾向ではないと思っています。
上であげた例はほぼすべて、正しい知識、特にメモリマネジメントの知識があれば回避できる問題です。
もうひとつ、Androidエンジニアの方々には、失礼かもしれませんが、組込ソフトウェアを作っているという意識が不足しているように思えてなりません。
スマートフォンは組込機器なのです。限られたリソースとパフォーマンスの上で以下に効率よくサービスを構築するかというのが技術なのです。
それはCPUやローメモリが見えていなくても変わりはありません。
バーチャルマシン上で実行されるプログラムですので、実装依存の部分に深入りできないという事情もあり、そこの部分は1つのCPU上で動けばOKなネイティブ実装より問題は複雑です。
「リーク」という言葉を聞くと、その言葉に逃げているように思えてしまうのです。

最初の言葉に戻りますが、私はJavaをかじっている組込屋です。
Android専門家の方々の声をお聞かせくだされば幸いです。

0 件のコメント:

コメントを投稿