Java Language
Javaメモリ管理
サーチ…
備考
Javaでは、オブジェクトはヒープ内に割り当てられ、ヒープメモリは自動ガベージコレクションによって再利用されます。アプリケーションプログラムは、Javaオブジェクトを明示的に削除することはできません。
Javaガベージコレクションの基本原則は、 ガベージコレクションの例で説明しています。他の例では、ファイナライズ、手でガベージコレクタをトリガする方法、およびストレージリークの問題について説明しています。
終了
Javaオブジェクトはfinalize
メソッドを宣言することができます。このメソッドは、Javaがオブジェクトのメモリを解放する直前に呼び出されます。通常、次のようになります。
public class MyClass {
//Methods for the class
@Override
protected void finalize() throws Throwable {
// Cleanup code
}
}
しかし、Javaファイナライズの動作にはいくつかの重要な注意点があります。
- Javaは、
finalize()
メソッドがいつ呼ばれるかを保証しません。 - Javaは、実行中のアプリケーションの存続期間中に
finalize()
メソッドがしばらく呼び出されることさえ保証しません。 - 保証されるのは、オブジェクトが削除される前にメソッドが呼び出されるということだけです。オブジェクトが削除された場合です。
上記の注意点は、 finalize
メソッドを使用してタイムリーに実行する必要があるクリーンアップ(またはその他の)アクションを実行することは悪い考えであることを意味します。ファイナライズに過度に依存すると、ストレージリークやメモリリークなどの問題が発生する可能性があります。
要するに、ファイナライズが実際には良い解決策である状況はほとんどありません。
ファイナライザは1回だけ実行されます
通常、オブジェクトはファイナライズされた後に削除されます。しかし、これは常に起こるわけではありません。次の例1を考えてみましょう。
public class CaptainJack {
public static CaptainJack notDeadYet = null;
protected void finalize() {
// Resurrection!
notDeadYet = this;
}
}
CaptainJack
のインスタンスが到達不能になり、ガベージコレクタがそれを再要求しようとすると、 finalize()
メソッドはインスタンスへの参照をnotDeadYet
変数にnotDeadYet
ます。これにより、もう一度インスタンスに到達できるようになり、ガベージコレクタはそれを削除しません。
質問:キャプテンジャックは不滅ですか?
回答:いいえ。
キャッチは、JVMはオブジェクトのファイナライザを一生だけ実行するということです。 notDeadYet
にnull
をnotDeadYet
と、resurectされたインスタンスが再び到達不能になるため、ガベージコレクタはオブジェクトに対してfinalize()
を呼び出しません。
1 - https://en.wikipedia.org/wiki/Jack_Harknessを参照してください 。
GCの手動トリガ
ガベージコレクタを呼び出すことによって手動でトリガすることができます
System.gc();
ただし、コールが戻ったときにJavaがガベージコレクタを実行したことを保証するものではありません。このメソッドは、JVM(Java仮想マシン)にガベージコレクタを実行させたいと単純に「示唆」しますが、実行するよう強制しません。
一般的にガベージコレクションを手動で起動しようとするのは悪い習慣と考えられます。 JVMは、 -XX:+DisableExplicitGC
オプションを指定して実行すると、 System.gc()
への呼び出しを無効にできます。 System.gc()
呼び出してガベージコレクションをトリガすると、JVMが使用する特定のガベージコレクタ実装の通常のガベージ管理/オブジェクト昇格処理が中断される可能性があります。
ガベージコレクション
C ++のアプローチ - 新規と削除
C ++のような言語では、アプリケーションプログラムは、動的に割り当てられたメモリによって使用されるメモリを管理します。 new
演算子を使用してC ++ヒープ内にオブジェクトが作成されると、そのオブジェクトを破棄するためのdelete
演算子を対応して使用する必要があります。
プログラムがオブジェクトを
delete
することを忘れて、単にそれを忘れると、関連するメモリはアプリケーションに失われます。この状況の用語はメモリリークであり、 メモリリークが多すぎると、アプリケーションがメモリを使いやすくなり、最終的にクラッシュする可能性があります。一方、アプリケーションが同じオブジェクトを2回
delete
しようとした場合、またはオブジェクトが削除された後にそのオブジェクトを使用すると、メモリ破損の問題によりアプリケーションがクラッシュする可能性があります
複雑なC ++プログラムでは、 new
とdelete
を使用してメモリ管理を実装すると時間がかかることがあります。実際、メモリ管理はバグの共通ソースです。
Javaのアプローチ - ガベージコレクション
Javaには異なるアプローチがあります。明示的なdelete
演算子ではなく、Javaはガベージコレクションと呼ばれる自動メカニズムを提供して、不要になったオブジェクトが使用するメモリを再利用します。 Java実行時システムは、処理されるオブジェクトを見つける責任があります。このタスクは、 ガベージコレクタ (GC)と呼ばれるコンポーネントによって実行されます。
Javaプログラムの実行中にいつでも、既存のすべてのオブジェクトのセットを2つの異なるサブセット1に分割できます。
到達可能なオブジェクトは、JLSによって次のように定義されます。
到達可能なオブジェクトは、任意のライブスレッドからの潜在的な継続的な計算においてアクセス可能な任意のオブジェクトである。
実際には、これはスコープ内のローカル変数から始まる一連の参照や、あるコードがオブジェクトに到達できる
static
変数があることを意味します。到達不能オブジェクトは、上記のように到達できないオブジェクトです。
到達不能なオブジェクトはガベージコレクションの対象となります 。これがガベージコレクションされるわけではありません。実際には:
- 到達不能なオブジェクトは到達不能になると直ちに収集されません 1 。
- 到達不能なオブジェクトはガベージコレクションされることはありません 。
Java言語仕様は、到達不能オブジェクトをいつ収集するかを決定するために、JVM実装に多くの寛容さを与えます。また、(実際には)到達不能なオブジェクトをどのように検出するかについて、JVM実装が慎重になることを許可します。
JLSが保証することの1つは、 到達可能なオブジェクトがガベージコレクションされないことです。
オブジェクトが到達不能になるとどうなるか
まず第一に、オブジェクトが到達不能になると 、特に何も起こりません。ガベージコレクタが実行され、それは、オブジェクトが到達不能であることを検出したときに物事にのみ起こります。さらに、GC実行が到達不能なオブジェクトをすべて検出しないことが一般的です。
GCが到達不能オブジェクトを検出すると、次のイベントが発生する可能性があります。
オブジェクトを参照する
Reference
オブジェクトがある場合、それらの参照はオブジェクトが削除される前にクリアされます。オブジェクトがファイナライズ可能な場合は、 ファイナライズされます。これは、オブジェクトが削除される前に発生します。
オブジェクトは削除することができ、それが占めるメモリは再利用することができます。
上記のイベントが発生する可能性のある明確なシーケンスがありますが、ガベージコレクタは特定の時間枠内の特定のオブジェクトの最終削除を実行する必要はありません。
到達可能なオブジェクトと到達不能なオブジェクトの例
以下のサンプルクラスを考えてみましょう。
// A node in simple "open" linked-list.
public class Node {
private static int counter = 0;
public int nodeNumber = ++counter;
public Node next;
}
public class ListTest {
public static void main(String[] args) {
test(); // M1
System.out.prinln("Done"); // M2
}
private static void test() {
Node n1 = new Node(); // T1
Node n2 = new Node(); // T2
Node n3 = new Node(); // T3
n1.next = n2; // T4
n2 = null; // T5
n3 = null; // T6
}
}
test()
が呼び出されたときに何が起きるか調べてみましょう。ステートメントT1、T2、T3はNode
オブジェクトを作成し、オブジェクトはそれぞれn1
、 n2
、 n3
変数を介してすべて到達可能です。ステートメントT4は、2番目のNode
オブジェクトへの参照を最初のNode
オブジェクトのnext
フィールドに割り当てます。これが完了すると、2つ目のNode
は2つのパス経由で到達できます。
n2 -> Node2
n1 -> Node1, Node1.next -> Node2
ステートメントT5では、 n2
にnull
を代入しnull
。これにより、 Node2
の到達可能性チェインの最初のものが破損しNode2
が、2番目のものは破られていないので、 Node2
はまだ到達可能です。
ステートメントT6では、 n3
にnull
を代入しnull
。これにより、 Node3
到達可能性チェーンがNode3
、 Node3
到達できなくなります。ただし、 Node1
とNode2
両方にn1
変数を使用して到達可能です。
最後に、 test()
メソッドが返ってくると、ローカル変数n1
、 n2
、 n3
は範囲外になり、したがって何もアクセスすることはできません。これにより、 Node1
とNode2
の残りの到達可能性チェーンが破られ、すべてのNode
オブジェクトにアクセスできなくなり、ガベージコレクションが可能になります 。
1 - これは、ファイナライズを無視する単純化であり、 Reference
クラスです。 偽的に、Javaの実装はこれを行うことができますが、これを行うパフォーマンスコストは実用的ではありません。
ヒープ、PermGen、およびスタックサイズの設定
Java仮想マシンが起動するときには、ヒープを作成する大きさとスレッドスタックのデフォルトサイズを知る必要があります。これらは、 java
コマンドのコマンドラインオプションを使用して指定できます。 Java 8より前のバージョンのJavaでは、ヒープのPermGen領域のサイズを指定することもできます。
PermGenはJava 8で削除されており、PermGenサイズを設定しようとすると、オプションは無視されます(警告メッセージ付き)。
ヒープとスタックのサイズを明示的に指定しない場合、JVMはバージョンとプラットフォーム固有の方法で計算されるデフォルトを使用します。これにより、アプリケーションのメモリ使用量が少なすぎたり多すぎたりすることがあります。通常、これはスレッドスタックでは問題ありませんが、大量のメモリを使用するプログラムでは問題になる可能性があります。
ヒープ、PermGen、およびデフォルトのスタックサイズの設定:
次のJVMオプションは、ヒープサイズを設定します。
-
-Xms<size>
- 初期ヒープサイズを設定します。 -
-Xmx<size>
- 最大ヒープサイズを設定します。 -
-XX:PermSize<size>
- 初期-XX:PermSize<size>
設定します。 -
-XX:MaxPermSize<size>
- PermGenの最大サイズを設定します。 -
-Xss<size>
- デフォルトのスレッドスタックサイズを設定します。
<size>
パラメータはバイト数でも、 k
、 m
またはg
接尾辞を持つこともできます。後者は、サイズをそれぞれキロバイト、メガバイト、およびギガバイト単位で指定します。
例:
$ java -Xms512m -Xmx1024m JavaApp
$ java -XX:PermSize=64m -XX:MaxPermSize=128m JavaApp
$ java -Xss512k JavaApp
デフォルトサイズを見つける:
-XX:+printFlagsFinal
オプションを使用すると、JVMを起動する前にすべてのフラグの値を表示できます。これは、ヒープサイズとスタックサイズの設定のデフォルトを次のように出力するために使用できます。
Linux、Unix、Solaris、Mac OSXの場合
$ java -XX:+ PrintFlagsFinal -version | grep -iE 'HeapSize | PermSize | ThreadStackSize'
Windowsの場合:
java -XX:+ PrintFlagsFinal -version | findstr / i "HeapSize PermSize ThreadStackSize"
上記のコマンドの出力は、次のようになります。
uintx InitialHeapSize := 20655360 {product}
uintx MaxHeapSize := 331350016 {product}
uintx PermSize = 21757952 {pd product}
uintx MaxPermSize = 85983232 {pd product}
intx ThreadStackSize = 1024 {pd product}
サイズはバイト数で与えられます。
Javaでのメモリリーク
ガベージコレクションの例では、Javaがメモリリークの問題を解決することを暗示しました。これは実際には当てはまりません。 Javaプログラムはメモリリークを起こす可能性がありますが、リークの原因はかなり異なります。
到達可能なオブジェクトが漏れる
以下の単純なスタックの実装を考えてみましょう。
public class NaiveStack {
private Object[] stack = new Object[100];
private int top = 0;
public void push(Object obj) {
if (top >= stack.length) {
throw new StackException("stack overflow");
}
stack[top++] = obj;
}
public Object pop() {
if (top <= 0) {
throw new StackException("stack underflow");
}
return stack[--top];
}
public boolean isEmpty() {
return top == 0;
}
}
オブジェクトをpush
てすぐにpop
すると、 stack
配列内のオブジェクトへの参照が引き続き存在しstack
。
スタック実装のロジックは、その参照をAPIのクライアントに返すことができないことを意味します。オブジェクトがポップされている場合、 ライブスレッドからの潜在的な継続的な計算ではアクセスできないことを証明できます 。問題は、現在の世代のJVMではこれを証明できないということです。現世代のJVMは、参照が到達可能かどうかを判断する際にプログラムのロジックを考慮しません。 (初めは実用的ではありません)
しかし、 到達可能性がどういう意味なのかを別にすれば、 NaiveStack
実装が再利用すべきオブジェクトに「ぶら下がっている」状況があることは明らかです。それはメモリリークです。
この場合、解決策は簡単です。
public Object pop() {
if (top <= 0) {
throw new StackException("stack underflow");
}
Object popped = stack[--top];
stack[top] = null; // Overwrite popped reference with null.
return popped;
}
キャッシュはメモリリークの可能性があります
サービスのパフォーマンスを向上させるための一般的な戦略は、結果をキャッシュすることです。一般的な要求とその結果の記録を、キャッシュと呼ばれるメモリ内のデータ構造に保存するという考え方です。次に、要求が行われるたびに、キャッシュ内の要求をルックアップします。ルックアップが成功すると、対応する保存された結果が返されます。
この戦略は、適切に実装すれば非常に効果的です。しかし、正しく実装されていないと、キャッシュがメモリリークになる可能性があります。次の例を考えてみましょう。
public class RequestHandler {
private Map<Task, Result> cache = new HashMap<>();
public Result doRequest(Task task) {
Result result = cache.get(task);
if (result == null) {
result == doRequestProcessing(task);
cache.put(task, result);
}
return result;
}
}
このコードの問題は、 doRequest
呼び出しdoRequest
キャッシュに新しいエントリが追加される可能性がある間に、それらを削除するものがないことです。サービスが継続的に異なるタスクを取得している場合、キャッシュは最終的にすべての使用可能なメモリを消費します。これはメモリリークの一種です。
これを解決する1つのアプローチは、最大サイズのキャッシュを使用し、キャッシュが最大値を超えたときに古いエントリを廃棄することです。 (最も最近使用されたエントリをWeakHashMap
は良い戦略です)。もう一つのアプローチはWeakHashMap
を使ってキャッシュを構築して、ヒープがいっぱいになってもJVMがキャッシュエントリをWeakHashMap
できるようにすることです。