PR

クラスを変えるとメモリー使用量は1/3に

 問題3はメモリー不足になった結果起こったことですが,元のプログラムは極めてメモリー使用効率の悪い方法です。

 図3(1)は元のDAO部分を模式的に示したものです。列名は各行で同じであるにもかかわらず,行ごとに列名の文字列を持っており,無駄にメモリーを消費しています。同じ文字列を複数持たないようにする方法として,図3(2)のようにする方法があります。これは「String.intern()」を用いる方法で,複数の同じ文字列を一つのインスタンスにまとめることができます(JDBCドライバ・ソフトによっては,この処理を行うものがある)。

図3●実装方式の違いによるメモリー使用量の差
図3●実装方式の違いによるメモリー使用量の差
[画像のクリックで拡大表示]

 またHashMapは,単なるデータの格納庫としてはスペース効率が悪いです。同じMap構造でも,問題2で説明したEnumを用いた「EnumMap」の方がメモリー使用量は少なくなります。試しに筆者の手元のPCで試してみました。三つの列で5万件の行のある表を用意し,プログラム実行時に消費されるメモリー量を計ったところ,HashMapを用いた図3(1)の形式では27Mバイトになりました。同(2)の形式では13Mバイト,EnumMapを用いた同(3)では9Mバイトで済みました。EnumMapを用いることで,メモリー使用量を1/3に節約することができます。行クラスを用いると,さらにメモリー使用量は削減できます(同(4))。

 EnumMapを用いたとしても,DBの行数が増えれば,常に「OutOfMemoryError」の危険がつきまとうことに変わりはありません。そこで検討すべきことは,全行を一度にメモリーに展開する必要があるのかどうかです。もしDB内のデータを先頭から最後まで走査して終了という単純な動作しか行わないのであれば,そもそも全行をメモリー上に保持する必要はありません。こうしたケースでは,「Iterator」を使いましょう。Iterator は,一連のデータを一方向に操作する必要最低限のインタフェースを提供します。具体的には,

hasNext() 次の要素があるかどうか
next()  次の要素を返す
remove() 直前に返した要素を削除(オプション)

といったインタフェースです。今回の場合はDBからのデータを保持する「ResultSet」(JDBCでデータにアクセスするためのインタフェース)を利用し,次のようなインタフェースを用意すればよいでしょう。

hasNext() ResultSetからまだ行が取得可能か
next() ResultSetから行を一つ取得して返す
remove() (今回は実装しない)

 ResultSetをIteratorのインタフェースに合わせるため,「アダプタ・パターン」という設計パターンを用い,ResultSetのインタフェースをIteratorに変換します。これで,全行を一度にメモリーに展開する必要は無くなります。

 筆者の手元のPC環境で先ほどと同じDBを使ったところ,使用メモリー量は3Mバイトほどになりました。しかも,これはDBの行数によらず一定です。

 Iteratorを用いることで使用メモリー量は大幅に削減できますが,Iteratorは一方向に順番にアクセスすることしかできません。これがデメリットです。そのほか,DBとのコネクションの管理なども複雑になります。DBのデータを全部一度に読み込んでしまうと,読み込んだ後はDBとのコネクションを切断してもよく,制御は簡単です。ところがIteratorではアプリケーションがDBアクセスする間,DBとのコネクションを維持する必要があります。コネクションを開いている間にDBエラーが起こる可能性を考慮するなど,制御が難しくなる可能性があります。

宇野 るいも(本名:花井 志生/はない しせい)
日本IBM グローバル・ビジネス・サービス ソフトウェア・エンジニアリング ITスペシャリスト
1991年日本IBM入社。アセンブラ/C/C++を中心とした,組み込みシステムの開発を担当。現在はJava EEを利用したシステム開発における,システム設計および実装作業,トラブル時の火消しとして,インスペクションやチューニング作業を中心に担当。「Strutsプログラミング講座」(アスキー)など,主にJava EEに関連した書籍の執筆多数