【JVM】OOM(Out Of Memory)障害対応のための必要な知識

# はじめに
OOMの障害対応をする際に、多くのサイトをみてインプットを重ねた。
その時、体系的に知識を頭に入れることにすごく苦労した。
なので同様に苦労してる人が、参考になればいいと思い、この記事を書いた。

すでにJVMの知識があり、OOMの障害対応方法を知りたい人はこちらの記事を参照
[【JVM】OOM(Out Of Memory)障害対応に挑戦しよう](https://qiita.com/yoshinori_hisakawa/items/ed754eea24ff01e5983d)
**こちら追加しましたのでご確認ください**

## 対象読者
・JVM？俺そんなの知らないよ！って人
・OOMなどの同じ問題を抱えているエンジニアさん
・JVMのログ設定などをせずにプロセスを起動する破壊神

## この記事で得られること
・最低限の起動オプションを設定してOOMの障害対応に備えられるようになる
・OOMの原因を特定して、解決するアクションを取れるようになる
・Linuxの絶対に知っておかなければいけない事を知れる

# OOMの障害対応をする命を受けた
ある機能の改修を1ヶ月程度していた。
テストやリリース準備も終えて、さあリリースするぞ！という時に、
**検証環境にあげているサービスが週に1度落ちるという事象が発生した。**
理由はOut Of Memory(以下：OOM)ということはわかっていたが、なぜそれが起きたかはわからない。
それにJVMに明るい人は周りにいないし、ヒープダンプ（後で詳しく説明します）などのデータも全くない状況。
例えるなら、無人島にノートPCと一緒に捨てられた気分だった。

**JVMとかGCとかヒープとか、単語は聞いたことはあるが、意味がわかんねーって状態から
このOOMの問題を解決するまでの学んだことを、この記事にまとめることにした。**
※ ツールの使い方や細かい部分は書くと長くなるので、書きません。
　 参考になったリンクを貼っておくので気になる方は読んでください。

# インプット編
まずはインプット編から。
調査するにも必要な知識がないとまず無理です。
ここではJVMやGCの仕組みにフォーカスして学んで行くことにする。

## JVMとは
一言で言うと、Javaのプログラムを動かすために必要なソフトウェアのこと。
RubyやScalaもJVM上で動かしたりしている。
JVMの特徴はプログラムをプラットフォームに依存しないで動かすことができる。
**つまり、OS（Windows、Mac、Linuxなど）環境に依存せずに動かせる。**

## JVMの構成
JVMは起動時にOSのメモリ領域が割り当てられる。
これはあとで説明するが、起動オプション(例：java -Xms100m)によって設定できる。

JVMは以下のような構成で成り立っている。
![JVM.png](https://qiita-image-store.s3.amazonaws.com/0/234760/7cc23584-a632-ff3d-f648-b62152d0d992.png)

以下でこれらの領域について詳しく説明して行く。

### Javaヒープ
**一言で言うと、ユーザが作ったプログラムが利用するメモリ領域のこと。**
他の言い方で言うと、Javaのプログラム内で使用されるオブジェクトや配列が格納されるメモリ空間。
プログラムを動かす際にオブジェクトをインスタンス化すると思うが、その際にメモリが割り当てられる。

そしてJavaヒープの中は厳密にはNew領域、Old領域と役割を分けている。

#### New領域
**新しく作られたオブジェクトが最初に格納される領域のこと**
実はNew領域の中でもEdenとsurvivorで領域を分けている。
新規に作成されたオブジェクト等が、まずはEden領域に格納される。
Eden領域のメモリがいっぱいになると、オブジェクトを退避するためSurvivor（厳密には「From領域」と「To領域」）に格納される。
※ FromとToが入れ変わるなど、詳しくここで知る必要はない。

詳しくはあとで説明するが、少し頭の隅に入れて置いて欲しいのはScavenge GCはNew領域に対して行われる。

#### Old領域
**New領域に格納されているオブジェクトの中で、寿命が長いオブジェクトが格納される領域。**
寿命が長いと言うのは、処理の中で長く使われているオブジェクトのことを言う。
例えば、処理の中のメソッドの中でしか使われてないオブジェクトは寿命が短いオブジェクトだ。

``` java
puclic hoge() {
    Fuga fuga = new fuga();
}
```

詳しくはあとで説明するが、少し頭の隅に入れて置いて欲しいのは**Full GCはOld領域に対して行われる。**

### Cヒープ
JVMがネイティブライブラリを実行する際に使用するメモリ空間。
※ Full GCの対象

### Permanent領域
クラスやメソッドの情報が格納されるメモリ空間。
最初に格納されるため、サイズはそんなに変わらない。
※ Full GCの対象

### スレッドスタック
Javaスレッドのスタック領域

## GCの仕組み
使われなくなったオブジェクトを削除してくれる仕組み。

イメージがわかない人のために、例えて説明すると
まずは田んぼを想像してほしい。
田んぼには苗を植えて、水をやると稲になる。（オブジェクトを生成してるイメージ）
苗から稲になる途中でダメになってしまう奴がいるとしよう。（オブジェクトが参照されなくなったイメージ）
このダメになってくれた奴を自動で摂ってくれるのがGCの役割だ。
そうすれば新しい苗を植える面積が増えるので、田んぼの経営者にとってありがたい仕組みだ。

GCが削除してくれるオブジェクトになるには、以下の点を満たしてる必要がある。
**・オブジェクトがどこからも参照されていない。**
　※ nullを代入することで参照をされない状態を作れる。

また、GCには大きく分けて、2つのパターンがある。
Scavenge GC（コピーGC・マイナーGCとも言う）とFull GC（メジャーGCとも言う）だ。

※ GCのタイプ（シリアル・パラレルとか）についてはここでは触れない。
詳しくは以下のサイトを参照
http://gihyo.jp/dev/serial/01/jvm-arc/0004

### Scavenge GC
生成されたオブジェクトはNew領域に割り当てられる。
**New領域がに空きがなくなってくると、New領域に対してGCを実施する。**
これをScavenge GCと言う。
Scavenge GCによって削除対象のオブジェクトは削除され、まだ使用されている寿命が長いオブジェクトはOld領域へ移る。
こうして、New領域を確保し、新たなオブジェクトを受け入れる準備をする。
短期のホームステイのようなものだ。

※ **New領域のメモリサイズが小さいとScavenge GCが頻繁に起こりパフォーマンスが悪くなる**

### Full GC
**Old領域とParmanent領域の空きがなくなってくると、これからの領域に対して、GCを実施する。**
これがFull GCだ。

※ Old領域のメモリサイズが小さいとFull GCが頻繁に起こりパフォーマンスが悪くなる。
Full GCの数は少し注意したほうがいい。アプリケーションが止まる時間が長いため。
**また、Scavenge GCが15回程度に対して、Full GCが1回走るくらいが丁度いいらしい。**
合ってなかったら、起動オプションで領域のサイズをチューニングすれば良い。
基本的には1:1の割合で、New領域とOld領域が分けられている。

### OOMとは？
ざっくり言うと、
上記で紹介した領域のいずれかがメモリ不足してしまうと、JVMがOut Of Memory Errorを引き起こす。

ここで重要なのは、どこの領域でOOMが発生したのか判断すること
**まずは、判断し、領域を特定することがOOMの障害対応で一番最初にするべきこと！！！！！**

## Javaの起動オプション（ここだけ読んで起動オプションを設定するだけでも為になります）
絶対にするべき最小限のオプションのみ説明する。
・Javaヒープのサイズを指定

```
-Xms　-Xmx
```

-XmsはJavaヒープ初期サイズ。一般的に、-Xmxと同値にする。
-XmxはJavaヒープ最大サイズ。
以下のコマンドでJavaプロセスを起動した時の上記の情報が見れる。

```
ps aux | grep java
```


・GC発生状況、メモリリーク状況を確認

```
-verbose:gc(-Xloggc:ファイルパスをここに記載)、-XX:+PrintGCTimeStamps、-XX:+PrintGCDetails
```

これを設定しておくことで、GCのログの情報を詳細に表示してくれる。
起動しているJavaプロセスのGCログを見たいときは、以下のコマンドで確認する。

```
# javaプロセスを確認
jcmd -l

# 上記の<pid>を入力してGCログの確認
jstat -gcutil -h10 <pid> 10000
```

・ヒープダンプの取得

```
-XX:+HeapDumpOnOutOfMemoryError
```

これを設定しておくことでOOMで停止するときにヒープダンプを取得してくれる。
**ヒープダンプと言うのは、ヒープ状況のスナップショットみたいなもの。**
ある時点でのヒープサイズを取得してくれるので、起動直後、1日後など比較することでメモリの使用率の
遷移などがわかり原因を掴みやすい。

また、任意のタイミングで撮りたいときは、以下のコマンドを実行し取得する。

```
# javaプロセスを確認
jcmd -l

# 上記の<pid>を入力してヒープダンプを取得
jmap -dump:format=b,file=filename <pid>
```

詳しくはこちらのリンクが参考になる。
http://d.hatena.ne.jp/learn/touch/20090218/p1

## 一旦まとめ
ここまでで、JVMの仕組みとGCの仕組みについての説明は終わりにする。
出来るだけこの記事でJVMについて体系的に理解し、詳細は他のサイトで掘り下げてもらえるように書いた。
※ linuxのことについてはインプット編で触れませんでした。アウトプット編で触れます。

## アウトプット編
少し長くなりすぎたので別の記事にまとめることにする。

[【JVM】OOM(Out Of Memory)障害対応に挑戦しよう](https://qiita.com/yoshinori_hisakawa/items/ed754eea24ff01e5983d)