「書ける」から「できる」になれる!
~Javaメモリ節約ノウハウ話~
JJUG CCC 2017 Fall
2017/11/18
#jjug_ccc
#ccc_g5
1
JUSTSYSTEMS
自己紹介
• 株式会社ジャストシステム 猪鼻哲也
• 担当商品履歴
• MS-DOS上のOffice系アプリケーション
• Windows上のOffice系アプリケーション
• Windows上の情報活用系アプリケーション
• ストレージ系インターネットサービス
• オンプレミス文書管理系サーバ
• オンプレミス情報活用系サーバ
• ...
• Java歴18年
• 低レイヤーの実装まわりが好き
2
JUSTSYSTEMS
本日の内容
• Javaオブジェクトの構造
• メモリ使用量の削減
• ライブラリの利用
• まとめ
• 付録
3
JUSTSYSTEMS
Java進化の歴史
• 機能面はもちろんのこと、性能面の進化が普及できた
大きな要因(特にサーバーサイド)
• 速度はJIT/HotSpotの進化で商用レベルに(JDK1.3~)
• メモリについては、H/Wの進化と64bit VMにより、over 2GB
の大容量が実現可能に
• エンタープライズ検索やインメモリDBによる集計・分析といっ
た、昔では考えられなかった機能も実現
<弊社製品例>
https://www.justsystems.com/jp/products/cbes
https://www.justsystems.com/jp/products/actionista/
4
JUSTSYSTEMS
メモリ不足はサービス安定運用の大敵
• メモリ使用量やリークに注意しないとSTW(Stop the
world)やOOM(OutOfMemoryError)が発生
• リークしてなくても-Xmxの8~9割程度使ってしまうと
メモリ確保/GCの負荷上昇でスループットが落ちてく
る(経験上)
• スループット低下やサービスダウン→緊急の対応が必要
 最初からメモリ使用量を抑えておくに越したことはない
5
JUSTSYSTEMS
Javaオブジェクトの構造
6
JUSTSYSTEMS
Javaオブジェクトの構造
/* フィールドのオフセットをsun.misc.Unsafe.objectFieldOffset()で取得すると、
管理用データのサイズを反映した値が返る */
class A {
public int a;
};
...
Unsafe unsafe = ...(略); // sun.misc.Unsafeの”theUnsafe”をリフレクションで取得
Field aField = A.class.getDeclaredField(“a”); // class Aのフィールドaを取得
long offset = unsafe.objectFieldOffset(aField); // フィールドaのオフセットを取得
// 32bitVMで8 (4 + 4)
// 64bitVM -XX:+UseCompressedOopsで12 (8 + 4)
// 64bitVM -XX:–UseCompressedOops(または-Xmx32g以上)で16 (8 + 8)
Javaソースで定義
したフィールド...
(8バイトアラインメント)
_mark
_klass
• 管理用のデータ領域を先頭に持つ
• _mark → オブジェクトの状態
• 32bit VMの場合4バイト、64bit VMで8バイト
• _klass →クラスデータへの参照
• 32bit VMの場合4バイト、64bit VMで8バイト
• 64bitでもCompressedOops(オブジェクト参照を圧縮す
る仕組み)が有効な場合は4バイト
ど
の
オ
ブ
ジ
ェ
ク
ト
も
必
ず
持
っ
て
い
る
領
域
7
JUSTSYSTEMS
基本型とラッパークラス
• ラッパークラスのオブジェクトは管理領域+保持している基
本型サイズ+パディングのメモリを消費
基本型とそのサイズ(バイト) 対応するラッパークラスとそのメモリ使用量(バイト)
型名 サイズ クラス名 32bit JVM
64bit JVM
32GB未満
64bit JVM
32GB以上
boolean 1 Boolean 16 16 24
byte 1 Byte 16 16 24
char 2 Character 16 16 24
short 2 Short 16 16 24
int 4 Integer 16 16 24
float 4 Float 16 16 24
long 8 Long 16 24 24
double 8 Double 16 24 24
8
boolean Boolean(32bit JVM) Boolean(64bit JVM<32GB) Boolean(64bit JVM≧32GB)
val ue _mark _klass _mark _mark
val ue _klass val ue _klass
val ue
long
Long(32bit
JVM)
Long(64bit JVM<32GB) Long(64bit JVM≧32GB)
value _mark _klass _mark _mark
value _klass _klass
value value
詳細は
チラシで
JUSTSYSTEMS
基本型とラッパークラス
• -Xmx32g(厳密には-Xmx32767m~)を境にメモリ使
用量が増える理由
 圧縮Oops(CompressedOops)が無効になり、オブジェクト参
照(klassヘッダー)が4→8バイトになるため
• 4バイト(32bit)で識別可能なアドレス空間は4GB(== 2^32)
• Javaオブジェクトのアドレスは8バイトでアラインメントされているため、
下位3bitは常に0
• (bbbbbbbb bbbbbbbb bbbbbbbb bbbbb000のビットパターン)
• 3bitシフトで35bitのアドレス空間を32bitで識別できる(35bit→32GB)
• (-Xmx32g以下でも-XX:-UseCompressedOopsで無効にできる)
9
JUSTSYSTEMS
Javaオブジェクトの構造(再)
• たいていの場合、単一ではなくオブジェクト内にオブジェク
トへの参照を持っている
• 例えば形態素解析辞書の場合
• 一つの単語は表記・品詞・解析用情報からなる
class 単語 {
String[] 表記; // 表記ゆれ対応のため複数
int 品詞;
int 解析用パラメータ;
}
• 辞書は単語の集合
class 辞書 {
単語[] words = new 単語[100万とか];
}
10
JUSTSYSTEMS
Javaオブジェクトの構造(再)
"バイオリン", "ヴァイオリン"表記を持つ単語の場合
• 132バイト中58バイト(44%)が管理情報またはパディング
• 構造はJVMの実装により異なる(上記は32bit JVM)
• 32bitポインタ/8バイトアラインメント
オブジェクトの数が増えると、想定以上にメモリ消費量が増大
11
class 単語 class String[] String char[]
単語: _mark _klass _mark _klass _mark _klass _mark _klass
表記: 品詞:xxxx length:2 value: hash:xx length:5 'バ' 'イ'
解析:yyyy value: value: 'オ' 'リ' 'ン'
String char[]
_mark _klass _mark _klass
value: hash:yy length:6 'ヴ' 'ァ'
'イ' 'オ' 'リ' 'ン'
JUSTSYSTEMS
いろいろなオブジェクトのメモリ使用量
クラス/オブジェクト 32bit JVM
64bit JVM
32GB未満
64bit JVM
32GB以上
new String("https://[^/]+/") ※Java8 56 72 88
new String("https://[^/]+/") ※Java9 56 72
"https://[^/]+/".toCharArray() 40 48 56
"https://[^/]+/".getBytes("UTF-8") 32 32 40
Pattern.compile("https://[^/]+/") 1080 1128 1256
new Date() 24 24 32
new Timestamp() 24 32 40
Calendar.getInstance() ※GregorianCalendar 424 448 544
ZonedDateTime.now() 88 120 152
LocalDateTime.now() 48 72 80
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S") 1232 1296 1880
DateTimeFormatter.of("yyyy-MM-dd HH:mm:ss.S") 432 456 704
• StringはJava9でサイズが小さくなった(Latin-1の文字は1byteで保持するため)
• Patternなどはstaticに持っておくとよい。
• GregorianCalendarが意外と大きい。
• SimpleDateFormatはスレッドセーフでないので、同期化するかDateTimeFormatterに乗り換える。
• etc...
12
詳細は
チラシで
JUSTSYSTEMS
メモリ使用量の削減
13
JUSTSYSTEMS
基本型を使う
• 単語を全て基本型の配列に押し込む
class 辞書(メモリ節約版) {
char[] 全単語の表記; // 表記揺れ数→表記長→表記の順に格納
int[] 全単語の表記への開始位置の配列;
int[] 品詞の配列;
int[] 解析用パラメータの配列;
}
• 前述の辞書のメモリ使用量は・・・3分の1以下に!
• ただし、ソース可読性・メンテナンス性が犠牲に・・・
クラス
メモリ使用量 [MB]
32bitVM
64bitVM
32GB未満
64bitVM
32GB以上
辞書 109.3 132.0 180.0
辞書(メモリ節約版) 32.0 32.0 32.0
14
JUSTSYSTEMS
基本型の配列をさらに詰める
• Boolean[ ] → 4または8バイト x 要素数
• Boolean.TRUE/FALSEを格納した場合。new Boolean(true/false)はやめましょう 。
• boolean[ ] → 1バイト x 要素数
• BitSet → 1ビット x 要素数 (内部はlong[ ]配列)
• 要素数100万の場合、BitSetはBoolean[ ]の16分の1以下、
125KBに!
32bitVM
64bitVM
32GB未満
64bitVM
32GB以上
new Boolean[100万] 4MB 4MB 8MB
new boolean[100万] 1MB 1MB 1MB
new BitSet(100万) 125kB 125kB 125kB
• BitSetをさらに圧縮する方法もあるが、難しいのでここでは割愛
15
詳細は
チラシで
JUSTSYSTEMS
基本型の配列をさらに詰める
• booleanをBitSetに詰めたのと同様、整数配列の最小値~最大値が N
bit(N≪整数型のビット幅)で表現できるとあらかじめ分かっている場合、配列
に隙間なく詰め込むことで圧縮できる(ただし取得にbit演算が必要になる)
• データ依存で効果が異なる
• どんなデータもコンスタントに圧縮できる方法はなかなかない
• 伸張速度も重要、用途によってはランダムアクセスも必要
long: val1 long: val2 long: val3 long: val4
val1 val2 val3 val4 ...
...
 long値の配列の最小値~最大値が64bitよりも小さなbitで表現できる場合
 値のバリエーションが少ない場合も、値→IDの変換表を作ることでも圧縮
ID 値
1 123456
2 98765432
3 123456789
123456789 123456 98765432 123456
3 1 2 1 ...
...
16
JUSTSYSTEMS
ライブラリの利用
17
JUSTSYSTEMS
Collection @since 1.2
• Java Collection Framework
• List, Map, Set等の頻出するデータ集合に対する
統一されたインターフェイスを提供
• 検索やソート等の高性能実装
• 一般には、使いやすく、高品質、高性能
• ただし、メモリ使用量に関しては・・・
18
JUSTSYSTEMS
Autoboxing @since 1.5
• 一見基本型を使っているようだがラッパークラスに
Set<Integer> set = new HashSet<Integer>();
set.add(1234); // 実際にはnew Integer(1234) が追加
• 基本型を指定しても、自動的にIntegerが生成され格納さ
れる(4バイトではなく16バイト以上消費)
• さらに、HashSet/HashMapは内部でMap.Entryを生成して
key/valueを格納するため、その分のメモリも消費・・・
19
JUSTSYSTEMS
省メモリコレクションライブラリ
• fastutil, Eclipse(GS) Collections, HPPC, koloboke, troveなど
• Map/Set<? extends Number>の値を基本型で持つことで省メモリ化
• JDKのHashMap<K,V>/HashSet<K>に比べ1/5以下のメモリ使用量
■100万個のint値を保持した集合のメモリ使用量と検索時間(実測値)
コレクションクラス
メモリ使用量 [MB] 検索時間 [ns/エントリ]
32bitVM
64bitVM
32GB未満
64bitVM
32GB以上
32bitVM
64bitVM
32GB未満
64bitVM
32GB以上
int[ ] (検索はバイナリサーチ) 4.0 4.0 4.0 165 154 159
java.util.HashSet<Integer> 48.4 56.4 88.8 166 68 74
it.unimi.dsi.fastutil.ints.IntOpenHashSet *1 8.4 8.4 8.4 30 39 25
org.eclipse.collections.impl.set.mutable.
primitive.IntHashSet *2
8.4 8.4 8.4 53 50 33
com.koloboke.collect.impl.hash.
MutableLHashIntSet *3
8.4 8.4 8.4 31 23 23
com.carrotsearch.hppc.IntHashSet *4 8.4 8.4 8.4 35 28 21
*1 fastutil → http://fastutil.di.unimi.it/ *2 Eclipse Collections → https://www.eclipse.org/collections/ja/
*2 koloboke → https://koloboke.com/ *4 hppc → https://labs.carrotsearch.com/hppc.html 20
JUSTSYSTEMS
省メモリコレクションライブラリ
• オブジェクトを入れる場合も、key/valueの格納にMap.Entry<K,V>を使わず
Object[] に詰め込んでいるため、JDKのHashMap/HashSetより省メモリ
// char[]とbyte[]を両方扱えるHash.Strategy(fastutil用)
// ObjectOpenCustomHashSetに設定して利用
// ※Latin-1の文字列の場合byte[ ]、
// それ以外はchar[ ] をput()/add()する
class PrimitiveArrayHashStrategy
implements Hash.Strategy<Object> {
public int hashCode(Object o) {
if (o == null) { return 0; }
if (o instanceof byte[]) {
return Arrays.hashCode((byte[]) o);
} else if (o instanceof char[]) {
return Arrays.hashCode((char[]) o);
} else { /* エラー */ }
}
public boolean equals(Object a, Object b) {
if (a == b) { return true; }
if (a instanceof byte[]) {
return b instanceof byte[]
&& Arrays.equals((byte[]) a, (byte[]) b);
} else if (a instanceof char[]) {
return b instanceof char[]
&& Arrays.equals((char[]) a, (char[]) b);
}
return false;
}
}
コレクションクラス
メモリ使用量 [MB]
32bitVM
64bitVM
32GB未満
64bitVM
32GB以上
Java8 Java8 Java9 Java8 Java9
HashSet<String> 111.6 128.4 104.4 168.8 144.8
ObjectOpenHashSet
<String>(fastutil)
87.6 96.4 72.4 120.8 96.8
ObjectOpenCustom
HashSet<Object> *1
48.4 48.4 48.4 64.8 64.8
■Latin-1の文字列100万個を保持したSetのメモリ使用量
• さらに、fastutilのHash.StrategyやEclipse(GS)
CollectionsのHashingStrategy, kolobokeのEquivalence
を使うと、hashCode()やequals()を持たないクラスのオブ
ジェクトをMapやSetに格納できる
• プリミティブ配列も使えるので、Stringの代わりにchar[ ]
やbyte[ ]を入れられるMap/Setが作れる
*1 全てbyte[ ]で格納されているため、省メモリ
21
JUSTSYSTEMS
省メモリコレクションライブラリ
• (先ほどの辞書クラスと同様)文字列ごとのbyte[ ] を1つのbyte[ ]に詰め込む
HashMap/HashSetを考えると、さらにメモリが節約できる(→32.5MB)
• さらに圧縮するなら、アルゴリズム/データ構造から変える必要がある
// byte[]に全文字列を詰め込むHashSet/Map
class ByteArrayStringHashSet {
// Latin-1かどうかと文字列長と文字列本体を
// 詰め込んだbyte配列
private byte[] buffer;
// エントリごとのbufferへのオフセット
// ハッシュ値ごとにまとめて格納
private int[] offsets;
// 詳細は
→https://en.wikipedia.org/wiki/Hash_table#O
pen_addressing
// Mapとして使う場合に値を格納する配列
// private Object[] values;
}
コレクションクラス
メモリ使用量 [MB]
32bitVM
64bitVM
32GB未満
64bitVM
32GB以上
Java8 Java8 Java9 Java8 Java9
HashSet<String> 111.6 128.4 104.4 168.8 144.8
ObjectOpenHashSet
<String>(fastutil)
87.6 96.4 72.4 120.8 96.8
ObjectOpenCustom
HashSet<Object>
(前ページ)
48.4 48.4 48.4 64.8 64.8
ByteArrayStringHash
Set (右図)
32.5 32.5 32.5 32.5 32.5
org.trie4j.louds.
TailLOUDSTrie *1
24.6 24.7 24.5 24.7 24.5
■Latin-1の文字列100万個を保持したSetのメモリ使用量
• 例えばLOUDS Trieなど(→ 24.6MB)
*1 trie4j → https://github.com/takawitter/trie4j
22
JUSTSYSTEMS
まとめ
• Javaのオブジェクトは1つ1つ参照としてしか生成できず、大量の
インスタンスを生成するとメモリ消費のオーバーヘッドが無視でき
なくなる
• メモリ使用量が高止まりするとガベージコレクションの負荷も上がってしまう。
• 用途によってはプリミティブ型や高効率のOSSライブラリを活用す
ることでメモリ使用量を節約できる
• プリミティブ型を使うとソースの可読性やメンテナンス性が犠牲になる場合も。
• 実装時間が余計にかかったり、バグが残存しやすいのも悩みどころ。
• いくらがんばってもメモリに乗らないものは乗らない
• コレクションを使うときに、何をどれくらいの入れるかやスコープ・ライフサイクルを
常に意識することが大切。
• そのためにもメモリ使用量を見積もったり計測するノウハウを持っておく。
• 乗らないものはあきらめて一時ファイルやDBを使う。
 結局は適材適所
23
JUSTSYSTEMS
付録
24
JUSTSYSTEMS
メモリ使用量の計測方法
① Runtime.totalMemory() – Runtime.freeMemory()
• 一番お手軽。GC前のメモリも計測されるので、System.gc()してから計測する。(が、頻繁に計
測すると性能に影響が)
② MemoryMXBean (ManagementFactory.getMemoryMXBean())
• init, used, committed, max がそれぞれ取得可能。
③ MemoryPoolMXBean (ManagementFactory.getMemoryPoolMXBeans())
• Eden, Survivor, Tenuredそれぞれ取得可能 (G1GCだと名前が変わる。 G1 Old Genとか)
④ com.sun.management.ThreadMXBean (Oracle JDK限定)
• getThreadAllocatedBytes()でオブジェクト生成前後のヒープ量が差分が取れる。
• オブジェクト生成で一時的に確保されたヒープも計測されてしまうようなので注意が必要。
⑤ Instrumentation.getObjectSize()
• オブジェクトの正確なメモリ使用量が計測可能。
• premain()を実装したjarを-javaagent:...でJVM引数に指定する。
• 参照先のオブジェクトは計測できない
→Reflectionを駆使して参照を辿りながら計測する必要あり。
⑥ sun.misc.Unsafe.objectFieldOffset(Field f)
• Fieldのオフセットが取得できる→全フィールドのオフセットの最大値+サイズでオブジェクトの
サイズが計測できる。(アラインメントの考慮は必要)
• 参照先をReflectionで辿って合計する必要があるのはInstrumentationと同じ。 25
JUSTSYSTEMS
今後に向けて
• JEP 169: Value Objects http://openjdk.java.net/jeps/169
• クラスインスタンスの配列がメモリ上で連続して取れる
• new 単語[100万]でもメモリ効率が落ちない。
• ネストするオブジェクト(単語.表記など)の扱いは別途工夫する必要があるかも。
• mark/klassのオーバーヘッドがなく、メモリ上でも近接しているため速度向上も期待できる
• JEP 218: Generics over Primitive Types http://openjdk.java.net/jeps/218
• Map<int, int>とか。実装はサードパーティ任せ?
• Array 2.0(Project Panama) http://openjdk.java.net/projects/panama/
• 2次元配列(従来だと1次元配列のオブジェクト配列)をメモリ上で連続して確保。
• メモリ効率を上げるためにnew int[m x n] のように1次元に詰め込んでいたのが、普通の2次
元配列(int[m][n])のようにでき、可読性と性能向上が期待できる。
• これらの導入で、自然言語処理や大規模データ処理でますますJavaが使え
るようになるはず・・・
26
JUSTSYSTEMS
その他の小技
• 省メモリな固定数コレクション
• 空の場合、Collections.emptyList()/emptySet()/emptyMap()
• シングルトンの場合、
Collections.singletonList()/singleton()/singletonMap()
• 2個以上の場合、Eclipse Collectionsの
org.eclipse.collections.impl.list.fixed.DoubletonListなど
• 同じオブジェクトをN個返す場合、Collections.nCopies(Object o)
• Enumの場合、HashMap/SetよりEnumMap/Setが省メモリ
• 数値型オブジェクトのキャッシュ
• -128(Characterの場合は0)~127の場合、
Byte/Short/Character/Integer/Longはstaticにキャッシュされたインスタ
ンスを返す
• Integerのみ、-XX:AutoBoxCacheMax=<size>で上限を増やせる。
• BigDecimalの0~10も同様。BigIntegerは-1,0,1,2,10だけ。
27
JUSTSYSTEMS
ご清聴ありがとうございました
今回はメモリ節約についてご紹介しました
株式会社ジャストシステムでは多種多様な商品を
開発しており、さまざまな技術を活用しています
興味のある方、お声がけください
• 「できる」になれるコンテンツ:Java100本ノック
• https://github.com/JustSystems/java-100practices
• ジャストシステムのエンジニア特集ページ
• https://www.justsystems.com/jp/employ/engineer/
28

「書ける」から「できる」になれる! ~Javaメモリ節約ノウハウ話~