"のように明示的に設定することができる。制限なし)で、既定ではヒープの最大値と同じようになるため、通常は気にしなくてもよいらしい。MBのダイレクトバッファを確保することが可能であった。(Java8u60で確認。byte[...]するだけのオーバーヘッドと同等である。ヒープメモリは事前にJavaVMがOSよりメモリをもらっており、そこでJavaVMが領域を割り当てるだけである。これに対して、ダイレクトバッファはネイティブ領域に都度、OSよりバッファを確保してもらっているような動きになっている。(実際は、もう少し効率は良いかもしれないが、前述の実験からすると、要求されてから都度、OSからメモリをもらっている感じである。)両者はメモリの確保方法が異なるため、確保するまでにかかる処理時間も異なると予想される。ベンチマークそこで、以下のようなベンチマークコードを書いて計測してみた。...}ByteBuffer.alloc()と、ByteBuffer.allocDirect()をバッファサイズを変えて計測している。(過度な最適化が起きないように、得られたByteBufferは状態インスタンスに1世代のみ保存している。-XX:MaxDirectMemorySize=256mを指定したものである。104.658数値は、1秒あたりのベンチマークメソッドの実行回数の平均である。激しくガベージコレクタが動作することが予想されるので、gcによる揺らぎを考慮して測定回数(イテレーション)は50回まわすことにした。これをチャート化すると、以下のようになる。チャート化するにあたり、数値は実行回数ではなく確保したバイト数に換算している。確保したバイト数)これにより、バッファ確保の効率を比較できるようにしている。これを見ると、明らかにダイレクトバッファはヒープバッファよりも時間がかかっている。差が10倍ほど開いてしまったので、縦軸は対数表示している。ダイレクトバッファは、確保するまでの時間が、むちゃくちゃ遅い。この点については、そもそもJavaDocに書かれている。呼び出すと作成されます。通常は、こちらのバッファーのほうが、非ダイレクトバッファーよりも割り当ておよび解放コストがやや高くなります。ダイレクトバッファーの内容が標準のガベージコレクトされたヒープの外部にあるなら、アプリケーションのメモリーフットプリントに対する影響はわずかです。このことから、ダイレクトバッファーには、基本となるシステム固有の入出力操作に従属する、寿命が長く容量の大きいバッファーを指定することをお勧めします。一般に、ダイレクトバッファーの割り当ては、プログラムの性能を十分に改善できる見込みがある場合にのみ行うべきです。http://docs.oracle.com/javase/jp/8/docs/api/java/nio/ByteBuffer.htmlチャートを見ると揺らぎがあるが、だいたいメモリ確保のスピードは確保するバッファサイズに係わらず、ほぼ一定のように思われるので、バッファが小さければ、あるいは、大きければバッファ確保が速くなるとか、そうゆうことはなさそうである。いずれにしても、ヒープとのダイレクトバッファの確保時のパフォーマンスの差はかなり大きい。DirerctByteBufferの連続確保と破棄の問題点ヒープ上のbyte[]配列であるヒープバッファは連続確保・破棄したとしても、その動きは通常のJavaオブジェクトと変わらないことが予想される。OSから見た使用メモリには増減は発生しないはずである。(すでに確保済みのヒープ内で処理されるため。)これに対して、ダイレクトバッファはOSより都度メモリを借り、そのメモリを解放するためには、そのダイレクトバッファのハンドルをもつヒープ上のByteBufferオブジェクトがgcされるまで待たなければならないはずである。(ByteBufferのメソッドにはI/O処理につきものの「close」のようなメソッドがないため、バッファが使用中であるかどうかを判断するにはgcによるしかない。)また、OSから見た使用メモリは、ダイレクトバッファは確保されるたびにコミットサイズが増加し、gcが発生するたびに解放されるような動きを示すはずである。実験そこで、以下のようなコードを書いて試してみた。}}JDK1.8u60で実行した。テストコードでは2スレッドの同時バッファ確保を行っている。コマンドは以下のオプションで実行した。Bytes(≒コミットサイズ)と、ワーキングセットサイズの推移グラフである。以下のパフォーマンスモニタのチャートはすべて同様である。byte[]の破棄と解放はJavaVMの中の出来事であり、OSから見てメモリ消費の増減はないのが分かる。Mbytesを割り当て、直近の1つのByteBufferだけを保存し、それ以前のByteBufferは参照がなくなりガベージコレクト対象としている。Mbytesのバッファ以外は不要のはずである。Mbytesほどのコミットサイズになっている。最大値になっていると思われる。)つまり、不要になったネイティブメモリは、すぐには消えていない。memory」とメモリ不足エラーが発生しやすくなる。スレッド数を増やすと、如実にOutOfMemoryErrorが発生するようになる。MBもあり、十分に空きがあると考えられるにも係わらず、である。確保するスピードが解放するスピードを上回っているのだろう。原理的には、ByteBufferがgcされるまでネイティブに確保されたダイレクトバッファも解放されないであろうため、最大ダイレクトメモリサイズよりヒープが大きい場合は、場合によってはヒープのgcが発生する前にダイレクトメモリが枯渇する可能性があるように思われる。(まさか、そんなナイーブな実装ではないとは思うし、OutOfMemoryをあげる前にgcは試行されるはずだが...。)現実に、Windows7/8.1上のjdk1.8u60では、MaxDirectMemorySizeが十分に大きくないと、このテストでは、すぐにOutOfMemoryが発生する状況である。System.gc()によるOutOfMemoryの回避ダイレクトバッファを確保する直前にSystem.gc()をしてあげると、この事象はでてこない。ByteBufferがgcされ、ネイティブの解放も逐次行われて、パフォーマンスモニタの波形も比較的穏やかなものになる。オプションによりgcが無視される可能性もある。)、というような課題はある。実は、どうやらダイレクトバッファの破棄が遅延されてOutOfMemoryErrorが発生する問題は、かなりメジャーな問題のようで、Stackoverflowあたりを検索すると沢山出てくる。System.gc()を使うのが、おそらくもっとも正式な方法だが、DirectByteBufferが内部で持っているcleanerメソッドを呼び出して、それに対してcleanを実行することで、ネイティブのメモリ破棄処理を強制的に早期に実行させる、という方法もあるようである。以下に引用する、ApacheのHadoopのソースにも、ダイレクトバッファを破棄するためのコードが書かれている。(および、どうしてダイレクトバッファの破棄が遅延するのかの理由も。}このコメントによると、参照がすべて切れたからといって、ただちにネイティブメモリが解放されるわけではなく、ダイレクトバッファはファントムリファレンスによって保持され、そのリファレンスキューをJavaVMがチェックして、gcによりキューに入れられたバッファがあれば、そのネイティブメモリを解放しているようである。このコメントから察するに、タイミングは不明だが、ともかくリファレンスキューを見に行く必要のあるパッシブな動きになっているのだろう。ためしに、このcleanerを呼び出すコードを使ってみると、パフォーマンスモニタの波形は、大変穏やかなものとなり、スレッド数を増やしてもOutOfMemoryも発生しなくなる。実運用コードの中にJavaVMの特定の実装にべったり依存するかのようなコードを書いてよいものかどうか、そのあたりの課題は残る。以上の考察から、ダイレクトバッファを使うには2つ注意点があることが分かった。ダイレクトバッファは確保するのが遅い。ダイレクトバッファは破棄するのが難しい。ファイルを開くたびに、ダイレクトバッファを毎回確保するような方法では、バッファ確保時のオーバーヘッドと、バッファ破棄のトラブルによって、あまり良いパフォーマンスは得られない可能性がある。「単にファイルをシーケンシャルに読み込みたい」という、ありふれたニーズであれば、これを対策するのは比較的容易である。ダイレクトバッファは確保したらキャッシュし、破棄しなければ良いだけである。(同時アクセスするファイルの最大数分だけダイレクトバッファをキャッシュすれば良い。)*1これを実装した上で、ダイレクトバッファとヒープバッファでのファイル読み取りのパフォーマンスを比較してみたいと思う。Mbytesのファイルである。Endianの2種類作成するデータはIntとDoubleの2種類作成する。MBytes程度のファイルならOSのファイルキャッシュに確実に乗っかることになる。今回はディスクの性能を比較するわけではないので、これで良いものと考える。ある意味、理想的なディスクを使った場合の読み取り方式の違いによるパフォーマンスの違いを計測するようなものといえるだろう。データ作成コード(Intデータ部のみ。(ウォームアップイテレーション、および計測イテレーション中には呼び出されない。}※このあと、バッファサイズ、LE/BEごとのコンテキストクラスが延々と続く。テスト実行部FileChannelを使ってByteBufferでファイルの中身を取り出しテストする部分。}データファイルを読み取り、ファイルチャネルを取り出して中身を走査する。連携でデータを読み取るテスト。DataInputStreamはBigEndian専用なので、BigEndianのデータのみテストする。DataInputStreamは一般的にいって使用頻度が高いとはいえないと思うが、byte[]からint値を取り出すための、昔からある、由緒正しい方法の1つなので、とりあえず、これで計測してみる。}なお、Java7から、FilesクラスのnewInputStreamメソッドを使って、簡易にファイルを開くことができるようになったが、従来のFileInputStreamのコンテストラクタで開く場合と変わりないのか気になったので、そのテストも追加している。FileInputStreamをBufferedInputStreamでラップしなかった場合、パフォーマンスの低下がどの程度なものなのか気になったので、こちらもテストを追加している。ops/sパフォーマンスの比較10MbytesのファイルのInt値の連続読み取りパフォーマンスのチャート。MBytesファイルの読み取り)の1秒間あたりの実行回数の平均を示す。ダイレクトバッファはヒープバッファよりも2倍以上性能が高い。KiB以上確保したほうが読み込み性能は良いように見える。KiB以上確保しても、それ以上は性能は変わらないようでもある。MbytesのファイルのDouble値の連続読み取りパフォーマンスのチャート。MBytesファイルの読み取り)の1秒間あたり実行回数の平均を示す。ダイレクトバッファはヒープバッファよりも2倍以上性能が高い。KiB以上確保したほうが読み込み性能は良いように見える。KiB以上確保しても、やはり、それ以上は性能は変わらないようでもある。OldBufferedIOの性能が思った以上に低い今回のベンチマークで思い知ったことは、Javaの古典的なストリーム系のBufferedInputStreamとDataInputStreamの組み合わせが、相当にパフォーマンスが低い、ということだった。本当に予想外に性能がかなり悪い。ヒープ上のByteBufferと比較しても、ここまでパフォーマンスが悪いとは思わなかった。うっかりBufferedInputStreamで包むのを忘れてしまったFileInputStreamの性能が大変悲惨であることをチャートで見るとよく分かる。(これは想定されていたことだが、実際にチャートで比較してみると"悲惨"という言葉に尽きる性能である。FileInputStream()の代わりにすぎなかったようだ。(ドキュメントにかかれてないので、まあ、そうだろう、とは思っていたけれど、全然コンビニエンスじゃないところが違和感があるというか。KiBのバッファサイズをもっているが、これを増やしても、ほとんど性能は向上しなさそうだということも判明した。(なので、たぶん、大多数の人が引数でバッファサイズを指定していないのだろう。今回の実験により、以下のことが分かった。ダイレクトバッファは確保と破棄に注意が必要である。ダイレクトメモリサイズはヒープとは別のJavaVMオプションで設定できる。確保と破棄が連続する場合では解放が追い付かずOutOfMemoryErrorの発生の恐れがある。System.gc()または内部クラスのCleanerを利用する対策がある。ダイレクトバッファは逐次構築するのではなくキャッシュすると効率が良い。いう性能差のようである。実のところ、ダイレクトバッファのベンチマークをとるのは、これで2〜3回目ぐらいである。過去のテストでは、ダイレクトバッファは使いどころが難しい、うまくパフォーマンスを発揮させられない、という結論を出してしまっていた。(今思えばベンチマークとして適切でない部分の時間も含んでいたような気もする。)また、ヒープ上のByteBufferについても、同じくヒープ上で動作する古典的ストリームI/Oとパフォーマンスに大きな差はないだろう、という先入観のようなものもあったのかもしれない。今回のテスト結果をみると、どうも過去のテストは明らかに何かを間違えていたかのように思われる。今回、確実に、かなり高速でデータの読み込みができることの確証を得た。今後は、ByteBufferがうまくマッチする場合は、優先的にByteBufferによるアクセスに切り替えて行く方が良さそうだ、という教訓が得られたと思う。今思うことは、どうして、もっと早く再評価してみようと思わなかったのか、ということだろうか。