escape analysis を一言でいうと
escape analysis(エスケープ解析)は、JIT コンパイラが
「このオブジェクトはメソッドの外に“逃げる”か?」
「このオブジェクトはスレッドの外に“逃げる”か?」
を解析する仕組みです。
逃げない(メソッド内・スレッド内に閉じている)と分かれば、
ヒープに置かずスタック上に置いたり、
そもそもオブジェクトを作らずに分解して最適化したり、
不要な同期を外したりできます。
つまり、「本来ならヒープに new されるはずのオブジェクトを、JVM が勝手にもっと安く扱ってくれる」ための土台が escape analysis です。
「オブジェクトが逃げる/逃げない」とは何か
メソッドの外に逃げるかどうか
あるメソッドの中で new したオブジェクトが、
そのメソッドの外から参照される可能性があるかどうか、
これが「メソッドからのエスケープ」です。
例えば次のコードを見てください。
class Point {
int x, y;
}
Point createPoint(int x, int y) {
Point p = new Point();
p.x = x;
p.y = y;
return p;
}
Javaこの Point は、メソッドの戻り値として外に返されています。
呼び出し元からも参照されるので、「メソッドの外に逃げている」と判断されます。
一方、次のようなコードではどうでしょう。
int sumLength(String a, String b) {
Point p = new Point();
p.x = a.length();
p.y = b.length();
return p.x + p.y;
}
Javaここでは Point はメソッドの外に返されていません。p はこのメソッドの中だけで使われ、メソッドが終われば完全に消えます。
この場合、「メソッドの外には逃げていない(non-escaping)」と判断できます。
JIT が「逃げない」と判断できれば、
この Point をヒープに置かず、
単なる二つのローカル変数(int x, int y)として扱うように最適化できます。
結果として、実行時には本当に new Point() が行われないこともあります。
スレッドの外に逃げるかどうか
もう一段踏み込むと、「スレッドの外に逃げるか」も重要です。
あるオブジェクトが、他のスレッドから絶対に触られないと分かれば、
そのオブジェクトに対する同期(synchronized など)は不要になります。
例えば、次のようなコードを考えます。
class Counter {
private int value;
public synchronized void inc() {
value++;
}
public synchronized int get() {
return value;
}
}
int useCounter() {
Counter c = new Counter();
c.inc();
c.inc();
return c.get();
}
JavaCounter 自体はスレッドセーフに書かれていますが、
この useCounter メソッドの中で作られた Counter は、
他のスレッドに渡されていません。
escape analysis によって「この Counter はこのスレッドから逃げない」と分かれば、
JIT は inc や get の synchronized を実質的に外してしまうことができます。
つまり、「スレッドセーフに書いておいても、単一スレッドでしか使っていない場面では同期コストを払わなくて済む」ようにしてくれるわけです。
具体例で「スタック割り当て」イメージを掴む
メソッド内だけで完結するオブジェクト
先ほどの sumLength の例をもう少し噛み砕きます。
int sumLength(String a, String b) {
Point p = new Point();
p.x = a.length();
p.y = b.length();
return p.x + p.y;
}
Javaソースコード上は new Point() していますが、
JIT が「p はこのメソッド内だけで使われ、外に逃げない」と判断すると、
内部的には次のようなイメージに変換されます(擬似コードです)。
int sumLength(String a, String b) {
int px = a.length();
int py = b.length();
return px + py;
}
Javaつまり、Point というオブジェクト自体が消え、
単なる二つのローカル変数に分解されます。
このとき、ヒープへの割り当ても GC の対象も発生しません。
「オブジェクトを作っているように見えるけれど、実行時には作っていない」
これが escape analysis による最適化の典型パターンです。
ループ内での一時オブジェクト
ループの中で一時オブジェクトを作るコードも、
escape analysis の恩恵を受けることがあります。
int sum(List<String> list) {
int total = 0;
for (String s : list) {
Wrapper w = new Wrapper(s.length());
total += w.value;
}
return total;
}
JavaWrapper が単なる値の入れ物で、
ループの中だけで使われているなら、
JIT はこれを「毎回 new するオブジェクト」ではなく、
「ループ内の一時的なローカル変数」として扱える可能性があります。
もちろん、実際に最適化されるかどうかは JVM のバージョンや状況次第ですが、
「逃げないオブジェクトはスタックやレジスタに押し込める」という方向性で頑張ってくれる、
というイメージを持っておくとよいです。
escape analysis が効くと何が嬉しいのか
ヒープ割り当てが減る → GC の負荷が下がる
ヒープにオブジェクトを割り当てるということは、
GC の対象を増やすということでもあります。
escape analysis によって、
「本当はメソッド内だけで完結しているオブジェクト」が
ヒープではなくスタックやレジスタで処理されるようになると、
ヒープ上のオブジェクト数が減ります。
その結果、Young 領域の GC の頻度やコストが下がり、
Stop The World の時間も短くなりやすくなります。
同期の除去でロックコストが減る
先ほどの Counter の例のように、
「スレッドセーフに書かれているけれど、実際には単一スレッドでしか使っていない」
というケースはよくあります。
escape analysis によって「このインスタンスはスレッドの外に逃げない」と分かれば、
JIT はそのインスタンスに対する synchronized を実質的に外してしまえます。
これにより、ロック取得・解放のオーバーヘッドがなくなり、
マルチスレッド環境でもスループットが向上することがあります。
プログラマは escape analysis をどう意識すればいいか
「JVM が勝手にやってくれる最適化」として信頼する
escape analysis 自体は、JIT コンパイラ内部の話です。
「ここは絶対スタック割り当てされるはず」と決め打ちして設計するのは危険ですし、
JVM の実装やバージョンによって挙動が変わることもあります。
なので、基本スタンスは
「逃げないオブジェクトは JVM がうまくやってくれるかもしれない」
くらいに留めておき、
それを前提にしない設計を心がけるのが安全です。
それでも意識しておくと得をする書き方
とはいえ、「逃げないオブジェクト」を増やすような書き方は、
パフォーマンス的にも、コードの分かりやすさ的にもプラスになることが多いです。
例えば、次のような意識は持っておくと良いです。
メソッドの戻り値やフィールドに安易にオブジェクトを晒さない。
本当に必要な場合だけ、外に渡す。
一時的な入れ物オブジェクトは、メソッド内で完結させる。
スレッド間で共有しなくていいものは、ローカルに閉じ込める。
こういう設計は、
escape analysis にとって「解析しやすい」形になりますし、
人間にとっても「スコープが狭くて理解しやすい」コードになります。
まとめ:escape analysis を自分の言葉で説明するなら
あなたの言葉で整理すると、こうなります。
「escape analysis は、JIT コンパイラが『このオブジェクトはメソッドやスレッドの外に逃げるか?』を解析する仕組み。
逃げないと分かったオブジェクトは、ヒープに置かずスタックやレジスタに置いたり、オブジェクト自体を分解してしまったり、不要な同期を外したりできる。
その結果、ヒープ割り当てや GC の負荷、ロックのオーバーヘッドが減り、パフォーマンスが良くなる。
プログラマは escape analysis を直接制御する必要はないが、
『一時オブジェクトはメソッド内に閉じ込める』『不要にオブジェクトを外に晒さない』といった設計を心がけることで、
JVM がこの最適化をしやすいコードになる。」
