挨拶
初めまして、日本システム開発株式会社の鈴木です。
技術者として更なる向上を目指すためQiitaアウトプットをする取り組みを行っています。
技術者としては経験が浅く発信内容はとにかく試したものの覚書になります。
今回は業務内でJVMに32GB以上のメモリを割り当てる必要が発生したため、その時の注意点と解決策を覚書していきます。
32GBの壁
そもそもJVMに32GB以上のメモリを割り当てる際の問題点についてまとめます。
JVMに割り当てるメモリ(ここではヒープ領域のみを指す)はJVM引数の-Xms,-Xmxオプションで設定することができます。理論的にはここで指定するメモリサイズには上限がありませんので32GB以上を割り当てることができます。
しかし、JVMのメモリ管理は圧縮OOPsという仕組みになっています。これは簡単にいうと、64bitシステムにおいてすべてのメモリアドレスを管理しようとするとアドレス部だけでメモリ領域を圧迫してしまうため、まずメモリ全体を8バイトごとに区切りアドレスはアドレス部×8が先頭であるとすることでアドレス部を圧縮するという考え方です。例えば0~255の256通りのアドレスを管理したいとなった時そのままでは8ビットのアドレス部が必要ですが×8で管理すれば0~31の32通り=5ビットで管理ができるのでアドレス部分が短くなってメモリの節約となります。この手法では代わりに、130のように8で割り切れないアドレスからデータの格納を行うことができず、例えば0から始めて12バイトのデータを格納したい場合圧縮をしなければ次のデータはアドレス13から始めることができますが、8バイト区切りの圧縮OOPsでは戦闘アドレスは8の倍数である必要があるため次のデータはアドレス16から始まります(13~16までは0で埋めることが一般的です)。そのため、メモリを隙間なく使うという観点では無駄が発生しますが、それよりもアドレス部の圧縮の方が恩恵が大きいという判断です。
ここで32GBの壁の問題に話を戻します。圧縮OOPsではアドレス部を32bitにしアドレスを8倍にして管理することでアドレス部のメモリ節約を行っております。ではこの方式で表現できるアドレスの最大値は
32bit×8B=2^32×2^3B=2^35B=34,359,738,368B≒32GBになります。つまり圧縮OOPsでは高々34GBがアドレス管理できる限界になるということで、これ以上のメモリを割り当てても圧縮OOPsでは参照することができません。
しかし、冒頭で述べた通りJVMに-Xms,-Xmxオプションで32GB以上のメモリを割り当てることは可能です。ただし、そのままではメモリを管理しきれないためヒープ領域が32GBを超える場合JVMは自動で圧縮OOPsを無効にしてしまいます。結果、アドレス部の圧縮がされなくなりメモリを大量に割り当てたにもかかわらずメモリ使用効率が落ち、見た目使用できるメモリが減ったように振舞うことがあります。これを本稿では32GBの壁と呼んでいます。
対応策
私の環境ではメモリ不足から38GBをJVMに割り当てようとしましたが、32GBの壁によりメモリ圧迫問題は改善しない可能性がありました。そこで、32GB以上は割り当てつつ圧縮OOPsを有効にする方針でJVMを設定しました。この方針では2通りの方法が考えられます。アドレス部を1ビット増やすかメモリの区切りを8から16にすることです(ビット単位の節約をしないといけないため、区切りは2のべき乗である必要があります)。今回は後者で対応しました。
JVMでは圧縮OOPsの区切り1単位をアラインメント(Alignment)でと呼んでおりデフォルトは8Byteです。このサイズは-XX:ObjectAlignmentInBytesオプションで指定することができるため、今回は16byteで指定しました。
設定後JVMを立ち上げヒープサイズをgcログから確認し、標準出力に警告が出ていないことをもって無事メモリが認識され圧縮OOPsが働いていることを確認しました(今回の環境ではJava内で他のログを出したりログレベルの変更が困難であったため標準出力のエラーログから確認しておりますが、gcログをDebugレベルにしてCompressedMemory情報を確認したり外部ライブラリから起動時にメモリ管理情報を出力する方が確認方法としては適切かと思います)。
32GBの壁章で述べた通り、アラインメントを大きくすることはアラインメントに達しないために0埋めされる領域が増加するリスクを伴います。そのため、小さいデータを大量にメモリに格納するシステムでは今回の方法ではかえってメモリ効率が32GBを下回る可能性があります。これらの点を考慮してJVMのオプションを設定する必要があります