2015/05/18

[Java][Android]なぜガーベージコレクションなのか


なぜ、Javaはガーベージコレクション(GC)を採用したのかおさらいしたいと思います。
ポインタを隠ぺいしたかった、スマートポインタを言語的に実現したかった、というのは的を得ていません。
それらはが逐次処理型のメモリマネージャでも実現可能です。

理由は至ってシンプルです。それはGCが”早い”からです。
GCの”遅さ”に辟易している方々には意外な話かもしれませんが、少し説明を聞いてください。

逐次処理の場合、メモリの解放が行われるタイミングで必ずその処理を実行しなければなりません。
処理をするということは、時間がかかるということです。
一方ガーベージコレクションを持っているメモリマネージャは参照カウンタを更新するだけで、メモリの解放という処理の本体を後回しにすることができます。
つまり、メモリの解放というアクションについては逐次処理より早いのです。
後回しにした処理は、CPUに余裕があるときに行えばユーザーにストレスを与えることはありません。

ガーベージコレクションはメモリとCPUのトレードオフにおいて、メモリ負荷を上げることでCPU負荷を減らす仕組みです。
メモリが潤沢にある環境ではとても効果的ですが、メモリにもCPUにも限界がある組込機器との相性は、いいとは言えません。
GCは時々、目に見えて遅くなります。時には未処理のメモリが詰まってOutOfMemoryExceptionを出すことさえあります。
どちらもメモリ限界を超えてしまったときに起こります。組込ソフトウェアの宿命なのです。

なんで逐次処理にしてくれなかったんだろうと恨み事の一つも言いたくなる気持ちは分からないでもありませんが、考えてください。
もし逐次処理だったらその遅さはアプリの動作全体にまんべんなくかかります。
最新のAndroid端末でさえ十分に快適な処理速度を持っているとは言えません。
大げさに言うともし、Androidがガーベージコレクションを持つJavaを選択していなかったら、今の普及はなかったのではないでしょうか。

AndroidアプリがOutOfMemoryに陥るのは組込ソフトウェアだからです。GCだからではありません。
メモリマネージャがいるからと言ってメモリのことを考えなくていいわけではありません。
GCにはよいところもたくさんあります。
逐次管理なら起こらないだろう不思議な現象に悩まされることもあると思いますが、GCと仲良くすることで、よりよいシステムを構築することもまたできると思います。
GCと正しく付き合っていれば、GCのせいで起こる問題っていうのは、実はそんなに多くないのではないかと思っています。
仲良くしましょう。

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専門家の方々の声をお聞かせくだされば幸いです。

2015/05/10

[Java]シャッフルの話

面白い例外を出したので、話のタネに。

配列をシャッフルする必要がありました。
Arrays.shuffle()という関数があればそれを使って終わりだったのですが、残念ながらはCollectionsshuffle()はあってもArrays.shuffle()はありません。シャッフルのためだけにわざわざコレクションにデータを移しかえるのは無駄なので(この認識は間違いです。正解はページの最後でw)別の方法を模索しました。
別の理由でこの配列をsortする必要がありましたので、おなじくsort()を使ってシャッフルができないかと思い付き、以下のようなComparatorを作ってみました。

class Comparison implements Comparator<Player> {
 @Override
 public int compare(Player lhs, Player rhs) {
  return rand.int(3)-1;
 }
}

compare()は2つのオブジェクトを比較して、大小の判定をします。
通常はデータを解析して適切な判断をするのですが、このComparatorは結果を乱数で返します。
比較結果がめちゃくちゃなのですから、まっとうに並ぶわけがないですが、シャッフルしたいのですから狙い通りです。
これは一見上手くいきました。
一見上品にもみえました。

しかし、このロジックは稀に例外を起こします。

java.lang.IllegalArgumentException: Comparison method violates its general contract!

general contract、一般契約に違反しています、というもの。
この場合の一般契約とは、Java.lang.Objectのメソッド、equalsが同じものは常に同じであることを保証するというものを指すのだと思います。
equalsとhashCoedeはObjectのメソッド、Java世界のすべてのオブジェクトが備えていなければならないものです。
例えばこれらはHashMap等にオブジェクトに格納する際に使用されます。
一般契約が守られなければ、これらJavaの根幹に当たる仕組みの動作が保証されなくなります。
とても大事なルールなのです。

さて、今回私はObjectをいじっているわけではありませんが、Objectが一般規約を守っているという前提で使用されているsortという仕組みを使っています。
どのようにして私のハッキングに気がついたのか定かではありませんが、恐らくはsortのロジックがあり得ない状態に陥ったのは想像に難くありません。
この仕掛けはやってはいけないものでした。

この話の怖いところは、上記の例外が以外と低確率でしかでないということです。
件のアプリでは、データ8つの抽選で遭遇することはまずなくて、80個程度の抽選で時々出るというくらいでした。
この致命的なバグは、試験が甘ければ気付かれることなく製品に入り込む可能性が十分にあります。

では、当初の命題、配列をシャッフルするにはどうするのが正しいのでしょうか。
今、私は以下のロジックを使っています。

Collections.shuffle(Arrays.asList(players));

別の記事で書きましたが、asListはplayersをラップする固定長のリストを返します。asListで返されたリストの内容を操作するとplayersの内容も操作されることが仕様上保証されています。シャッフルの場合ももちろんplayersのインスタンス内容が直接シャッフルされます。
リスト化する際にコピーが発生しませんから、配列をシャッフルするのとほぼ等価なパフォーマンスが期待できます。

ところで、こんな仕掛けを思いついてしまいました。

class Comparison implements Comparator<Player> {
 @Override
 public int compare(Player lhs, Player rhs) {
  if(lhs==rhs){
   return 0;
  }else{
   return rand.int(2)==0?1:-1;
  }
 }
}

同じインスタンスなら0(同じ)を返し、それ以外は0以外をランダムで返します。
比較は乱数ですが、equalsについての動作は保証されています。
まぁ、単なる興味で、使う気はありませんが。

[Android][Java]ArrayAdapterはアダプター

AndroidにはArrayAdapterというクラスがあります。
Adapterの実装の一つで、ListViewやSpinnerを使ったUIを実装する際には必須といえるクラスなので、触ったことがないという人は少ないと思います。
しかし、一方Adapterという非常に抽象的なネーミングだったり、文字列を表示するだけならカスタマイズの必要がなかったりして、サンプルコードをコピペしたまま良くわからずに使っているという人も多いのではないかと思います。
実は私もそうでした。
全容がわかっていなくても、表面上に出てくるところをいじっていると目前の目的が達成されるので、ついつい疑問を後回しにしていました。

このクラスを使っていると、不可解な現象に遭遇します。
前述のasListと同じく、追加ができる場合と例外が発生する場合があるのです。
個の現象について、仕様はなにも語っていません。
いろいろ整理していくと初期化の違いが影響しているらしいということが分かりました。

ArrayAdapterはいくつかのコンストラクタを持っています。

ArrayAdapter(Context context, int resource)// 1
ArrayAdapter(Context context, int resource, int textViewResourceId)// 2
ArrayAdapter(Context context, int resource, T[] objects)// 3
ArrayAdapter(Context context, int resource, int textViewResourceId, T[] objects)// 4
ArrayAdapter(Context context, int resource, List<T> objects)// 5
ArrayAdapter(Context context, int resource, int textViewResourceId, List<T> objects)// 6

1,2は要素を初期化しないコンストラクタ、3,4は初期値を配列、5,6はリストでそれぞれ提供するものです
下はそれぞれを動作確認するためのコードです。

private void test(Context context){
    String[]labels = {"test1", "test2", "test3"};
    ArrayList<String>array = new ArrayList<String>();
    for(String label : labels){
        array.add(label);
    }

    ArrayAdapter<String> adapter = ArrayAdapter<String>(context, 0);
    adapter.add("test4");//7

    adapter = ArrayAdapter<String>(context, 0, labels);
    adapter.add("test4");// 8

    adapter = ArrayAdapter<String>(context, 0, Arrays.asList(labels));
    adapter.add("test4");// 9

    adapter = ArrayAdapter<String>(context, 0, array);
    adapter.add("test4");// 10
}

このコードを実行すると7,10は成功し、8,9は例外が発生します。
この振る舞いは仕様に特に記述されていません。
この違いがなぜ起こるのか、しばらく疑問でした。

実は答えは簡単でした。
ArrayAdapterは外部にあるデータソースをGUIが操作できるインターフェイスに適合(Adapt)させるアダプターなのです。
私の誤認識はArrayAdapterが実装形式として内部に配列(Array)を持っているアダプタ(Adapter)だと思い込んでいたところにありました。
言い訳を許してもらえるなら、ArrayAdapterでなくCollectionAdapterだったなら、この誤解はなかったでしょう。
ArrayAdapterがアダプトするのはArrayだけではありません。ArrayListだけでもなく、すべてのコレクションを対象にしているのですから。

もうひとつ混乱を引き起こしたのは1,2のコンストラクタです。
こちらはデータソースのオリジナルがない状態を想定したコンストラクタだと思われます。つまり以下のように書くことができます。

ArrayAdapter<String>(this, 0, 0, new ArrayList<String>());

親切ではありますが、Adapterとしては過剰サービスだったのではないでしょうか。

ArrayListener.add()はデータソースのadd()をデリゲートしています。ArrayAdapterがデータの変更タイミングを察知するために必要なデリゲーションです。
こちらを通さない変更は都度notifyDataSetChanged()で知らせてやる必要があるのです。
構造が分かればいろいろなことがクリアになります。
実装ではありません、構造です。

2015/05/09

[Java]asListが返すリストは固定長

言語が進化していくにつれ、配列とコレクションの垣根は低くなっていきますが、依然としてそれは同じではありません。
さまざまな言語がさまざまなアプローチをしている分野で、この部分を俯瞰で見ていると、言語毎の思想が見えてきて面白いのです。

配列で保持しているデータをシャッフルして別なデータを生成するコードを書いていました。
具体的にはマッチメーカープログラムで、プレイヤーを一時的にシャッフルして対戦の組み合わせを決めるプログラムです。
プレイヤーのリストは順位で並んでいるので、これを壊したくはありません。
Javaに配列をシャッフル関数があれば良かったのですが、Arraysにはsortはあるもののshuffleはありません。
やむを得ずCollections.shuffleを使うことにしました。

Player[] generateShuffledArray(){
 List<Player>list = Arrays.asList(players);
 list.shuffle();
 return list.toArray();
}

直感的にasListは配列から新たにリストのインスタンスを作ると思っていましたから、シャッフルしたリストをさらにtoArrayで配列に戻して返しています。
効率の悪いロジックだなぁと思っていたのですが、実行してみると予想外の振る舞いになっていました。
この関数を実行した後、返り値の配列はともかく、オリジナルの配列playersまでがシャッフルされていたのです。
最初はどこかでgenerateShuffledArrayの結果を上書きしてしまったんだろうと思いましたが、その形跡はありません。
発想の転換が必要でしたが、listの変化がplayersに反映していると考えるしかありませんでした。
さて、仕様をみると、ちゃんと記述がありました。

As List
指定された配列に連動する固定サイズのリストを返します。返されたリストへの変更は、そのまま配列に書き込まれます。このメソッドは、Collection.toArray() と組み合わせることで、配列ベースの API とコレクションベースの API の橋渡し役として機能します。また、返されるリストは直列化可能で、RandomAccess を実装します。 

実装についての記述はありませんが、Arrays.asListから返ってくるListは公開されているどんなListサブクラスとも異なる、生の配列をラップする実装となっているクラスであると考えるのが妥当です。
(手元で確認したところ、クラス名はArrays$ArrayListとなっていましたので、ここではその名前を使います。名前は実装依存です。)
固定長であるのは、サイズ拡張によって与えられた配列と別インスタンスにならないための制限でしょう。

通常、実装に依存するようなデザインはぜい弱で、美しくありません。
ネット上にはこのデザインについて、”初期実装のバグを引き継いだ”ような説を唱えている方もいるようですが、これには理由があり、Javaという言語を理解する上で大事なポイントだと思っています。

開発初期のJavaは今よりはるかにパフォーマンスについての要求が厳しいものでした。
ネイティブコードを生成するCやFortranでさえ満足できる処理速度が出せず、クリティカルな個所はアセンブラで書くというのがまだ選択肢にあった時代です。
バーチャルマシン上で動作するというコンセプトのJavaはおもちゃ扱いされていました。
配列の処理は実装効率をてきめんに反映します。
データを物理的にフラットにバイト単位で並べてオフセット値でアクセスする配列は、ある意味抽象化からもっとも遠い存在です。
Javaが仮装言語であることに固執したなら、配列そのものを否定する選択肢もあったはずです。
その場合、配列は必ずコレクションの内部形式にコピーされますから、パフォーマンスは目に見えて落ちます。
理想と現実のせめぎあいの中で、配列をそのままラップするArrays$ArrayListという発明は絶妙だと思います。
実行効率をほぼ落とすことなくリストとしての機能を提供することができるからです。
Arraysをビルダーにしたデザインも賢明でした。実装に強依存するクラスがオーバーライドされて拡散するのとを防いでいます。
Javaにはコレクションを初期化する際にコレクションを引数に取るコンストラクタはありますが、配列を引数にとるコンストラクタはありません。恐らくasListのデザインとひとつながりなのでしょう。初期化の違いで動作が変わるか、実行効率を最適化するチャンスを逃すか、究極の選択をしないという判断です。
実にうまくできています。

さて、当初の想定通り、オリジナルをシャッフルしないなら以下のように修正します。


Player[] generateShuffledArray(){

 List<Player>
 list = Arrays.asList(players.clone());
 list.shuffle();
 return list.toArray();

}

オリジナルがシャッフルされることを容認するなら実装はこれだけです。もうメソッドにする必要すらありません。

void generateShuffledArray(){
 Arrays.asList(players).shuffle();
}
どちらのコードもパフォーマンスは悪くない筈です。少なくとも言語的にはそういう実装ができるデザインになっています。