Javaとキャッシュの付き合い方で考えてた - 日々量産

Javaとキャッシュの付き合い方で考えてた

キャッシュっていろんな意味があってアレだけど、ここでは「苦労して得た情報なんだから使いまわしたい」時に使うキャッシュということで。

仕事でキャッシュ使ったほうがいいよなぁ、とぼんやり思ったので考えまとめるためにメモっとく。

アルゴリズムの勉強しかけたけど、LRUとLFUぐらいしかわからんし、効率よく実装するにゃ頭が足らんかった。

なぜキャッシュを使いたいと思ったか

設定ファイル読み込んだり、独自の定義ファイル(xmlとか独自フォーマットなファイルとか)をいくつも読む必要があった。
これらのファイルを1リクエスト中に1〜5回ぐらいファイルを読むようで、応答速度も「現在は」気になってない。
でも、ファイルのアクセスは時間がかかる!遅い!!クソザコ!!!という固定概念を持ってるので、キャッシュしたほうが早くなるよなーと思った。

キャッシュのメリット

  • 実行時間が短縮できる(応答速度が上がる)

キャッシュのデメリット

  • 同期問題
    • トランザクションを跨いで処理してたらキャッシュの内容が変わってました!みたいな事になると困る場合がある。
    • センシティブな情報には向いてない。と思う。
    • この辺りは要件次第で採用するか否か判断するべきかなー。
  • メモリ管理
    • メモリにどれだけのせるか
    • いつまでメモリにキャッシュするか
    • GCあるのになんでメモリ意識しなきゃならないの。辛い。

キャッシュとか考えるのめんどいんだけど

全部キャッシュに乗ればみんなが幸せになれるのは間違いないと思う。
そもそもディスクにあるのが悪い。全部L1キャッシュに乗れば超爆速だよ!
でも現実世界のリソースは限られている。

キャッシュのメリット・デメリットを理解してうまくやりくりしないといけない。

Javaでのキャッシュ戦略

色々ありますキャッシュ戦略。

戦略といえば大仰すぎるものもあるけど、何度も同じ計算・処理しないための機構は全部キャッシュだと思ってるので。

Singletonパターンを使う

Javaならstaticフィールド作ってstaticブロックとかでnewすれば初回のみ初期化して使いまわすことができる。

  • メリット
    • 早い!
    • 簡単!
  • デメリット
    • 値を1個しかもてない
    • チャンスは最初の1回だけ
      • ならsetter用意すれば良いじゃない!ってのもある。
    • 例外は実行時例外でオネシャス!と言う感じになりがち
    • staticフィールドはメモリに残り続ける(クラスローダがある限り。それぐらい知ってるよ!)
Singletonパターン + HashMap

キ ー バ リ ュ ー ス ト ア

  • メリット
    • キーで返す値を変えられる
    • 多分早い
  • デメリット
    • メモリを食いつぶす可能性がある
      • 対応するには削除を意識する必要がある。キャッシュなのに。
LinkedHashMap
new LinkedHashMap<String,String>(16 * 2, 0.75F, true){ // 第3引数をtrueにするとセット時だけでなくアクセス時にも、そのエントリが最新の情報という扱いにしてくれる。
     protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > 16; // 16個まで保持するよ
     }
};

とかやると、LRUなキャッシュ機構が作れる!すごい!楽!!
(コンストラクタ引数の数を適当に2倍にしておくと、factorの制限に引っかからずに再配置されなくなる、はず。上記の例だと32個の領域に16個までしかデータが載らないことが保証されてるので、使用率は0.5になる)

  • メリット
    • メモリも割りと食うし、数も多いけど、よく使われるのがある程度決まってて、最悪のケースでもメモリに乗り切る場合にはうまくいく
      • 上記のケースだと、キャッシュヒット率が高くなりやすい
  • デメリット
    • そうでないパターンだとうまくいかない
ResourceBundleを使う

ResourceBundle自体は1.1ぐらいからあったらしい。ResourceBundle.Controlクラスが1.6から用意されてからが本番。
最初キャッシュ機能なんてあるの?と思ったけどデフォルトで勝手にキャッシュしてくれる。Wow!!
あと一定時間でキャッシュを破棄して、次回呼び出し時に再読み込みできる状態にできるようにいじることもできる。

  • メリット
    • 公式が用意してくれた機能なので安心
    • 言語リソース扱うならこれ1択でいいと思う
    • ちょっと有効期限設定できるし、そうでなくてもキャッシュに載せ続けることもできる。
  • デメリット
    • どう扱われてるかわかんなくてちょっと怖い(わからない故の不安)
    • たくさんリソースを扱うのには向いてない気がする
    • リソースといってもpropertiesとかxmlとかファイル向け。
      • Groovyのmemorizeみたいに関数の処理結果をキャッシュする用途に作られてない、はず。
      • コントローラでformatを定義すればいくらでも拡張できると思うけど...。
    • 型安全じゃない。getObject("zzz");したらキャストしなきゃだめ。
      • 臭いものには蓋(wrap)するなり文字列しか扱わなければgetString("yyy");で良いと思う
SoftReferenceを使う

弱参照の仕組みを使って、あるだけSoftReferenceにする。SoftReferenceだとWeakReferenceと違いGCで即回収とはならない。WeakReferenceは即回収になる。
どうしようもないくらいメモリを使い込むとSoftReferenceを優先的に開放しようとする(らしい)。

あなたのJVMはどうか、試してみましょう。

/**
 * java.lang.NegativeArraySizeExceptionが出るようだったら、java -Xmx256mとかやって、メモリの割当量を減らす
 * 
 */
public class SoftReferenceSample {

	public static long getRealFreeMemory() {
		Runtime r = Runtime.getRuntime();
		return r.maxMemory() - (r.totalMemory() - r.freeMemory());
	}

	public static void dumpReference(List<Reference<byte[]>> refList) {
		System.err.println("FREE MEMORY: " + getRealFreeMemory());
		for (int i = 0; i < refList.size(); ++i) {
			System.err.println("refList[" + i + "] => " + refList.get(i).get());
		}
	}

	public static void main(String[] args) {
		long freeMemory = getRealFreeMemory();
		int size = 8; // n等分する
		ArrayList<Reference<byte[]>> refList = new ArrayList<Reference<byte[]>>(size);

		for (int i = 0; i < size; ++i) {
			long start = System.currentTimeMillis();
			refList.add(new SoftReference<byte[]>(new byte[(int) (freeMemory / size)]));
			long time = System.currentTimeMillis() - start;
			if (time != 0) {
				System.err.println("add[" + i + "] time: " + time + " ms");
			}
			dumpReference(refList);
			System.gc(); // 終わるたびにGC. kuso code
		}
	}
}
  • メリット
    • JavaGC機構に丸投げできるので、マジでメモリを意識しなくて良い!やった!!
  • デメリット
    • 開放される順序は保証されていない(使われてないものから開放するように実装して、という"推奨"はされている)
      • 手元のjava.runtime.version=1.8.0-ea-b88, java.vm.name=Java HotSpot(TM) 64-Bit Server VMの環境では、そのときが来ると、とりあえず全部消すみたい。
        • この挙動から、頻繁にメモリいっぱいまで使うケースがあると、キャッシュヒット率が悪くなる。実装依存なのは怖い。
        • 逆にあまりメモリを使い切らない(ピーク時にたまに使い切ってキャッシュアウトして間に合う程度)なら、悪くなさそう
      • コード見たらtimestampを持ってて、それっぽいことしてそうだったけど、違うじゃないか(憤怒)
    • 突然のNullPointerExceptionになる可能性があり、よくない。
      • SoftReferenceを使っているのを隠して、なければ取得処理を行う、とかにして値が変えることを実装で保証する方法もある

実装依存は本当に怖いので、これでReferenceでキャッシュを書こうとか思っちゃダメだと思いました!

キャッシュを実現するライブラリを使う

最後の戦略です。なぜ最後かって、これ使えばOKというかんじのものがない。

Ehcacheが人気?
JCS(JSR-107)の実装であるJCacheはEE 7で入らなかったのでEE 8でくるんじゃないか、とか話はあるけどどうだろう。

キャッシュライブラリ事情は以下の記事が良くまとまってて良かった(こなみかん)
http://d.hatena.ne.jp/Kazuhira/20130723/1374587549

Ehcacheは設定でムカムカしてしまって、じっくり使う気になれなかった。最低限のget/putはやったが、それだけ。
凄く多機能なのはわかるけど・・・これ使ってる人って何で使ってるんだろう、ってちょっと思った。他に選択肢なかったのかな?

  • メリット
    • よく使われているので自分で書くより良いと思う。
  • デメリット
    • 設定めんどい。
      • なんとかStateExceptionとか言われてもわからん。
    • コードめんどい。

あとディスクに逃がすとかありえないとおもうんすよ。キャッシュとはなんだったのか考えちゃうね。
キャッシュってそんな複雑なもんなのかねー。

あきらめる

ディスクキャッシュ君!CPU君!後は任せた!!

おわり

僕が考えた最強のキャッシュライブラリ

  • もっとカジュアルにget/putもしくはget or loadできる
  • メモリに乗るだけ乗り、古い・利用頻度が少ないものから削除
  • プログラムとかファイル関係ない
  • キャスト不要
  • 設定はオプショナル(何もしなくても満足できる速度で動く)

イメージコードでいうとこんなかんじ。

Cache<Type> cache = new Cache<Type>();
cache.put(new Type("AAAAAA"));
Type value = cache.get("key"); // new Type("AAAAAA")
Cache<Type> cache = new Cache<Type>(new Loader<Type>());
Type value = cache.getOrLoad("hogehoge"); // なければ"hogehoge"をキーにして取って来る
// Loaderの実装次第でリロードとかもできるといいなー

総称型はラップすればどうにでもできる感じで。

もっと簡単に使えて、速くて、多少融通の聞く(リロード機能とか盛り込める)感じのがほしいなー。
SoftReferenceダメとか言ったけど、SoftReferenceに入れた値をLRUで強参照しておけば利用頻度が高いやつは保持できてとりあえずいいんじゃないかなーとか思ってるけども、SoftReferenceの動きの問題と参照握っちゃう事でOutOfMemoryになっちゃう問題の落としどころが整理できず。良い感じにLRUでの保持数を変化させたりしないと、良い感じに使えない気がする。

設定多いのは個人的には悪だと思ってる。「こうすると速くなる」とかはダメ。
簡単に使えておいしいのが一番。ディスクキャッシュとか結構複雑なことやってそうだけどあんまり意識しなくても利用できてる感じじゃないですかー。
プログラム側もどうにかなりませんかね!