2014年5月21日水曜日

Javaでメモリリークする件

Java プログラムでメモリー・リークが発生するのでしょうか?
その通りです。
http://www.ibm.com/developerworks/jp/java/library/j-leaks/

メモリはリークするか

まとめ

相互参照は問題ない。
問題なのはライフサイクルの不一致。

Javaは「メモリリーク」しない

Javaはすごい。

ガーベッジ・コレクション(GC)という仕組みによって、
参照できなくなったメモリ領域を自動的に開放してくれる。
だからC/C++のようなメモリリークは起こらない。

Javaがメモリリークしない理由

なにを解放するか。

参照できなくなったメモリ領域とは、どのようなものだろう。
Javaではうまいことなっているのだが、まずはすごーく素朴なものを考えてみよう。

たとえば、そのオブジェクトの参照されている数を記録しておき、0になったら破棄するというのはどうだろう。これをリファレスカウンタ方式という。

http://ja.wikipedia.org/wiki/参照カウント

すごーく素朴な方式で軽量ではあるが、これには 相互参照 の問題がある。
理解しやすくするため、
3つのオブジェクト「キミ」と「ゴリラ」と「バナナ」を考えよう。

解放が上手くいく場合

キミはメインクラスなので被参照数が1ある。(キミの被参照数=1)
キミはゴリラを見ている。(ゴリラの被参照数=1)
ゴリラはバナナを見ている。(バナナの被参照数=1)

これですべてのオブジェクトは解放されない。

ここで、ゴリラが要らなくなったとしよう。
キミはゴリラから視線を外す。
すると、誰にも見られていないゴリラが解放される。
続いて、誰にも見られていないバナナが解放される。

ゴリラとバナナは正しく解放できた。

解放が失敗する場合

さて、ここで問題だ。
バナナがゴリラを見ていたら、何が起こるだろう。

キミがゴリラへの視線を外しても、
ゴリラとバナナの被参照数は1となり、解放されないのだ!

もうゴリラはいらないのに、ゴリラは居座り続けるし、
なにより、バナナも残ってしまう。

相互参照とは、バナナとゴリラが残ること、である。

JavaのGC

さて、JavaのGCはいかにして上記の問題を解決するのだろうか。
それはルートオブジェクトの導入である。

http://ja.wikipedia.org/wiki/マーク・アンド・スイープ

まずキミに色を塗る。
そしてキミの見ているもの、つまりゴリラに色を塗る。
そしてゴリラの見ているバナナに色を塗る。

で、適当なタイミングで色が塗られていないものを解放する。

これならバナナがゴリラを見ていても
ゴリラが二度塗りされるだけだ。いわゆる二度塗りゴリラだ。

キミがゴリラから視線を外せば
ゴリラもバナナも色塗りされずに解放される。

純粋なキミは、「え、じゃあみんなこれやればいいじゃん」と思うだろう。
しかし、リファレスカウンタ方式に比べて処理が重いとか
ダメなところもあるので一概には言えない。

じゃあJavaで起こるメモリリークってなに

端的に言えば、ライフサイクルの不一致だ。
メンバー間の音楽性の不一致によって解散するバンドも数多い。

キミは80年生きる。
そのなかでもう使わないのに参照し続けているものがあるのではないか。
たとえば、部屋の隅にある小5のときに書いた文集だ。
当時は確かに使った。
友達のやつは読んだし、自分のも読んだし、あの子のも読んだ。
中学の時に読み返して、なんでこんなこと書いたんだと不思議に思った。
しかし小6ならまだしも、小5である。もう読むことはないだろう。

  • 子どもに見せるため?
  • 親の小5の文集を見たことがあるか?
  • 結婚相手はいるのか?
  • 頭は大丈夫か?

それはいわばゴミなのだ。Garbageなのだ。Collectされるべきなのだ。
しかし、キミから参照されているために
このゴミはお母さんによって片付けられることはない。
(なお、お母さんがマーク・アンド・スイープ方式を採用しているかは議論の余地が残る)

Androidでは

AndroidではAndroidの用意してくれたフレームワークという舞台上で
キミのイカしたActivityやServiceその他を踊らせていくことになる。
このフレームワークの生存期間は(たいてい)キミのアプリが起動している期間と等しい。
やっかいなことにもっと長いものもある。たとえばタイマー処理だ。
たとえばタイマー処理にゴリラを参照させると、そのタイマーが解放されるまでゴリラはメモリ上に残る。

ゴリラだったらいい。

これがもしActivityだとすると、ActivityはViewをたくさん持っているし、画像も持ってるかもしれないしメモリが大きいので問題になる。
あと、タイマーの時間にもよる。タイマーが24時間後だったらやばい。
24時間ゴリラがメモリ上にいるのだ!!!危ない!!!

Androidでは何がどれくらいの長さ保持されているかが不明瞭なため、
こういったメモリリークが起こりやすい。

ライフサイクルを一致させろ。

Activityで使うもの(ゴリラ)はActivityに持たせる。
そうすればActivityが解放されるタイミングで
それ(ゴリラ)も解放される。

ライフサイクル、つまり

いつ作られて、いつ要らなくなるか

という世界で閉じた作りになっていれば、メモリリークは怖くないのだ。

付録

面接で質問された人もいるという
http://stackoverflow.com/questions/6470651/creating-a-memory-leak-with-java