1. 序章

Javaの主な利点の1つは、組み込みのガベージコレクター(または略して GC )を使用した自動メモリ管理です。 GCは暗黙的にメモリの割り当てと解放を処理するため、メモリリークの問題の大部分を処理できます。

GCはメモリのかなりの部分を効果的に処理しますが、メモリリークに対する絶対確実な解決策を保証するものではありません。 GCはかなりスマートですが、完璧ではありません。 良心的な開発者のアプリケーションでも、メモリリークが発生する可能性があります。

アプリケーションが大量の不要なオブジェクトを生成し、重要なメモリリソースを使い果たし、アプリケーション全体が失敗する場合があります。

メモリリークはJavaの真の問題です。 このチュートリアルでは、メモリリークの潜在的な原因、実行時にそれらを認識する方法、およびアプリケーションでそれらを処理する方法を確認します。

2. メモリリークとは

メモリリークは、使用されなくなったオブジェクトがヒープ内に存在するが、ガベージコレクタがそれらをメモリから削除できないため、不必要に維持される状況です。

メモリリークは、メモリリソースをブロックし、時間の経過とともにシステムパフォーマンスを低下させるため、問題があります。 そして、処理されない場合、アプリケーションは最終的にそのリソースを使い果たし、最終的に致命的なjava.lang.OutOfMemoryErrorで終了します。

ヒープメモリに存在するオブジェクトには、参照型と非参照型の2種類があります。 参照されるオブジェクトとは、アプリケーション内でまだアクティブな参照を持っているオブジェクトですが、参照されていないオブジェクトにはアクティブな参照がありません。

ガベージコレクタは、参照されていないオブジェクトを定期的に削除しますが、まだ参照されているオブジェクトを収集することはありません。ここで、メモリリークが発生する可能性があります。

 

メモリリークの症状

  • アプリケーションを長時間継続して実行すると、パフォーマンスが大幅に低下します。
  • OutOfMemoryErrorアプリケーションのヒープエラー
  • 自発的で奇妙なアプリケーションがクラッシュする
  • アプリケーションで接続オブジェクトが不足することがあります

これらのシナリオのいくつかとそれらに対処する方法を詳しく見てみましょう。

3. Javaでのメモリリークの種類

どのアプリケーションでも、さまざまな理由でメモリリークが発生する可能性があります。 このセクションでは、最も一般的なものについて説明します。

3.1. staticフィールドを介したメモリリーク

潜在的なメモリリークを引き起こす可能性がある最初のシナリオは、static変数の多用です。

Javaでは、静的フィールドの有効期間は通常実行中のアプリケーションの全有効期間と一致します( ClassLoader がガベージコレクションの対象にならない限り)。

static List:にデータを取り込む簡単なJavaプログラムを作成しましょう。

public class StaticTest {
    public static List<Double> list = new ArrayList<>();

    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add(Math.random());
        }
        Log.info("Debug Point 2");
    }

    public static void main(String[] args) {
        Log.info("Debug Point 1");
        new StaticTest().populateList();
        Log.info("Debug Point 3");
    }
}

このプログラムの実行中にヒープメモリを分析すると、予想どおり、デバッグポイント1と2の間でヒープメモリが増加していることがわかります。

ただし、 populateList()メソッドをデバッグポイント3のままにしておくと、このVisualVM応答でわかるように、ヒープメモリはまだガベージコレクションされていません

 

ただし、上記のプログラムの2行目で、キーワード static を削除すると、メモリ使用量が大幅に変化します。このVisualVMの応答は次のようになります。

 

デバッグポイントまでの最初の部分は、 静的。 しかし、今回は私たちが去った後 PopulateList() 方法、 リストへの参照がないため、リストのすべてのメモリはガベージコレクションされます

したがって、static変数の使用に細心の注意を払う必要があります。 コレクションまたはラージオブジェクトがstaticとして宣言されている場合、それらはアプリケーションの存続期間を通じてメモリに残り、他の場所で使用される可能性のある重要なメモリをブロックします。

それを防ぐ方法は?

  • static変数の使用を最小限に抑える
  • シングルトンを使用する場合は、熱心にロードするのではなく、オブジェクトを遅延ロードする実装に依存してください

3.2. 閉じられていないリソースを介して

新しい接続を確立するか、ストリームを開くたびに、JVMはこれらのリソースにメモリを割り当てます。 いくつかの例には、データベース接続、入力ストリーム、およびセッションオブジェクトが含まれます。

これらのリソースを閉じるのを忘れると、メモリがブロックされ、GCの到達範囲から外れる可能性があります。 これは、プログラムの実行がこれらのリソースを閉じるためのコードを処理しているステートメントに到達するのを妨げる例外の場合でも発生する可能性があります。

いずれの場合も、リソースから残されたオープン接続はメモリを消費し、それらを処理しないと、パフォーマンスが低下し、OutOfMemoryErrorになる可能性があります。

それを防ぐ方法は?

  • リソースを閉じるには、常にfinallyブロックを使用してください
  • リソースを閉じるコード( finally ブロック内でも)自体に例外がないようにする必要があります
  • Java 7以降を使用する場合、 try-with-resourcesブロックを使用できます。

3.3. 不適切なequals()および hashCode()の実装

新しいクラスを定義するとき、非常に一般的な見落としは、 equals()および hashCode()メソッドの適切なオーバーライドされたメソッドを記述していないことです。

HashSetおよびHashMapは多くの操作でこれらのメソッドを使用し、正しくオーバーライドされない場合、潜在的なメモリリークの問題の原因となる可能性があります。

些細なPersonクラスの例を取り上げて、HashMapのキーとして使用してみましょう。

public class Person {
    public String name;
    
    public Person(String name) {
        this.name = name;
    }
}

次に、このキーを使用する Map に、重複するPersonオブジェクトを挿入します。

Mapに重複するキーを含めることはできないことに注意してください。

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map<Person, Integer> map = new HashMap<>();
    for(int i=0; i<100; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertFalse(map.size() == 1);
}

ここでは、Personをキーとして使用しています。 Map は重複キーを許可しないため、キーとして挿入した多数の重複 Person オブジェクトは、メモリを増やすべきではありません。

ただし、適切なequals()メソッドを定義していないため、重複するオブジェクトが積み重なってメモリが増加します。そのため、メモリ内に複数のオブジェクトが表示されます。 このためのVisualVMのヒープメモリは次のようになります。

 

ただし、 equals()メソッドとhashCode()メソッドを適切にオーバーライドした場合、このMapには1つのPersonオブジェクトしか存在しません。

Personクラスのequals()および hashCode()の適切な実装を見てみましょう。

public class Person {
    public String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Person)) {
            return false;
        }
        Person person = (Person) o;
        return person.name.equals(name);
    }
    
    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + name.hashCode();
        return result;
    }
}

この場合、次のアサーションが当てはまります。

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map<Person, Integer> map = new HashMap<>();
    for(int i=0; i<2; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertTrue(map.size() == 1);
}

equals() hashCode()を適切にオーバーライドすると、同じプログラムのヒープメモリは次のようになります。

 

もう1つの例は、 equals()および hashCode()メソッドを使用してオブジェクトを分析し、それらをキャッシュに保存するHibernateなどのORMツールを使用する例です。

これらのメソッドがオーバーライドされない場合、メモリリークの可能性が非常に高くなります。Hibernateはオブジェクトを比較できず、キャッシュが重複オブジェクトでいっぱいになるためです。

それを防ぐ方法は?

  • 経験則として、新しいエンティティを定義するときは、常に equals()および hashCode()メソッドをオーバーライドします
  • オーバーライドするだけで十分ではありませんが、これらのメソッドも最適な方法でオーバーライドする必要があります

詳細については、チュートリアル Eclipse を使用してequals()とhashCode()を生成し、 Java でhashCode()のガイドを参照してください。

3.4. 外部クラスを参照する内部クラス

これは、非静的内部クラス(匿名クラス)の場合に発生します。 初期化のために、これらの内部クラスは常にそれを囲むクラスのインスタンスを必要とします。

すべての非静的内部クラスには、デフォルトで、それを含むクラスへの暗黙の参照があります。 この内部クラスのオブジェクトをアプリケーションで使用すると、含まれているクラスのオブジェクトがスコープ外になった後でも、ガベージコレクションは行われません

多くのかさばるオブジェクトへの参照を保持し、非静的な内部クラスを持つクラスについて考えてみます。 内部クラスのみのオブジェクトを作成すると、メモリモデルは次のようになります。

 

ただし、内部クラスを静的として宣言するだけの場合、同じメモリモデルは次のようになります。

これは、内部クラスオブジェクトが外部クラスオブジェクトへの参照を暗黙的に保持し、それによってガベージコレクションの無効な候補になるために発生します。 匿名クラスの場合も同じことが起こります。

それを防ぐ方法は?

  • 内部クラスが含まれているクラスメンバーにアクセスする必要がない場合は、それをstaticクラスに変換することを検討してください。

3.5. finalize()メソッドを介して

ファイナライザーの使用は、潜在的なメモリリークの問題のさらに別の原因です。 クラスのfinalize()メソッドがオーバーライドされると、そのクラスのオブジェクトはすぐにガベージコレクションされません。代わりに、GCはそれらをファイナライズのためにキューに入れます。これは後で発生します。時間内に。

さらに、 finalize()メソッドで記述されたコードが最適でなく、ファイナライザーキューがJavaガベージコレクターに追いつかない場合、遅かれ早かれ、アプリケーションはOutOfMemoryErrorを満たすように運命づけられます。

これを示すために、 finalize()メソッドをオーバーライドしたクラスがあり、メソッドの実行に少し時間がかかると考えてみましょう。 このクラスの多数のオブジェクトがガベージコレクションされると、VisualVMでは次のようになります。

 

ただし、オーバーライドされた finalize()メソッドを削除しただけの場合、同じプログラムは次の応答を返します。

それを防ぐ方法は?

  • ファイナライザーは常に避けるべきです

finalize()の詳細については、 Javaのfinalizeメソッドガイドのセクション3(ファイナライザーの回避)を参照してください。

3.6. インターン文字列

Java String プールは、PermGenからHeapSpaceに転送されたときに、Java7で大きな変更が加えられました。 ただし、バージョン6以下で動作するアプリケーションの場合、大きな文字列を操作するときは、より注意を払う必要があります。

巨大な巨大なStringオブジェクトを読み取り、そのオブジェクトに対してintern()を呼び出すと、PermGen(永続メモリ)にある文字列プールに移動し、アプリケーションが実行されている限りそこに留まります。[ X211X]これにより、メモリがブロックされ、アプリケーションで大きなメモリリークが発生します。

JVM 1.6でのこの場合のPermGenは、VisualVMでは次のようになります。

 

これとは対照的に、メソッドでは、ファイルから文字列を読み取るだけで、それをインターンしない場合、PermGenは次のようになります。

 

それを防ぐ方法は?

  • この問題を解決する最も簡単な方法は、文字列プールがJavaバージョン7以降からHeapSpaceに移動されるため、最新のJavaバージョンにアップグレードすることです。
  • 大きなStringsで作業している場合は、潜在的な OutOfMemoryErrors を回避するために、PermGenスペースのサイズを大きくしてください。
    -XX:MaxPermSize=512m

3.7. ThreadLocalの使用

ThreadLocal Java チュートリアルでのThreadLocalの概要で詳しく説明されています)は、状態を特定のスレッドに分離する機能を提供し、スレッドセーフを実現できるようにする構造です。

この構成を使用する場合、各スレッドはThreadLocal変数のコピーへの暗黙の参照を保持し、スレッドが生きている限り、複数のスレッド間でリソースを共有するのではなく、独自のコピーを維持します。

その利点にもかかわらず、 ThreadLocal 変数の使用は、適切に使用されない場合にメモリリークを引き起こすことで悪名高いため、物議を醸しています。 Joshua Blochは、スレッドローカル使用法についてかつてコメントしました。

「スレッドプールのずさんな使用とスレッドローカルのずさんな使用は、多くの場所で指摘されているように、意図しないオブジェクトの保持を引き起こす可能性があります。 しかし、スレッドローカルに責任を負わせるのは不当です。」

ThreadLocalsでメモリリークが発生する

ThreadLocals は、保持しているスレッドがアライブでなくなると、ガベージコレクションされることになっています。 ただし、 ThreadLocals を最新のアプリケーションサーバーと一緒に使用すると、問題が発生します。

最新のアプリケーションサーバーは、新しいリクエストを作成する代わりに、スレッドのプールを使用してリクエストを処理します(たとえば、ApacheTomcatの場合はExecutor )。 さらに、別のクラスローダーも使用します。

アプリケーションサーバーのスレッドプールはスレッドの再利用の概念に基づいて機能するため、ガベージコレクションされることはなく、代わりに別のリクエストを処理するために再利用されます。

これで、クラスが ThreadLocal 変数を作成したが、それを明示的に削除しなかった場合、そのオブジェクトのコピーは、Webアプリケーションが停止した後も、ワーカーThreadに残ります。オブジェクトがガベージコレクションされるのを防ぎます。

それを防ぐ方法は?

  • ThreadLocals が使用されなくなったら、クリーンアップすることをお勧めします— ThreadLocals は、 remove()メソッドを提供します。これにより、現在のスレッドの値が削除されます。この変数
  • ThreadLocal.set(null)を使用して値をクリアしないでください —実際には値をクリアしませんが、代わりに現在のスレッドに関連付けられている Map を検索し、キーを設定します-現在のスレッドとしての値のペアとnullそれぞれ
  • ThreadLocal は、例外の場合でも常に閉じられるようにするために、finallyブロックで閉じる必要があるリソースと見なすとさらによいでしょう。
    try {
        threadLocal.set(System.nanoTime());
        //... further processing
    }
    finally {
        threadLocal.remove();
    }

4. メモリリークに対処するための他の戦略

メモリリークに対処する際に万能の解決策はありませんが、これらのリークを最小限に抑える方法がいくつかあります。

4.1. プロファイリングを有効にする

Javaプロファイラーは、アプリケーションを介したメモリリークを監視および診断するツールです。 アプリケーションの内部で何が起こっているか(たとえば、メモリがどのように割り当てられているか)を分析します。

プロファイラーを使用すると、さまざまなアプローチを比較して、リソースを最適に使用できる領域を見つけることができます。

このチュートリアルのセクション3では、 JavaVisualVMを使用しました。 Mission Control、JProfiler、YourKit、Java VisualVM、Netbeans Profilerなどのさまざまなタイプのプロファイラーについては、Javaプロファイラーガイドをご覧ください。

4.2. Verboseガベージコレクション

詳細なガベージコレクションを有効にすることで、GCの詳細なトレースを追跡しています。 これを有効にするには、JVM構成に以下を追加する必要があります。

-verbose:gc

このパラメータを追加することで、GC内で起こっていることの詳細を確認できます。

 

4.3. 参照オブジェクトを使用してメモリリークを回避する

また、 java.lang.ref パッケージに組み込まれているJavaの参照オブジェクトを使用して、メモリリークを処理することもできます。 オブジェクトを直接参照する代わりに、 java.lang.ref パッケージを使用して、オブジェクトへの特別な参照を使用して、オブジェクトを簡単にガベージコレクションできるようにします。

参照キューは、ガベージコレクターによって実行されたアクションを認識できるように設計されています。 詳細については、Java Baeldungチュートリアルのソフトリファレンス、特にセクション4を参照してください。

4.4. Eclipseのメモリリーク警告

JDK 1.5以降のプロジェクトの場合、Eclipseは、メモリリークの明らかなケースが発生するたびに、警告とエラーを表示します。 したがって、Eclipseで開発する場合は、定期的に[問題]タブにアクセスして、メモリリークの警告(ある場合)についてより注意を払うことができます。

 

4.5. ベンチマーク

ベンチマークを実行することで、Javaコードのパフォーマンスを測定および分析できます。 このようにして、同じタスクを実行するための代替アプローチのパフォーマンスを比較できます。 これは、より良いアプローチを選択するのに役立ち、メモリを節約するのに役立つ可能性があります。

ベンチマークの詳細については、Javaによるマイクロベンチマークチュートリアルをご覧ください。

4.6. コードレビュー

最後に、単純なコードウォークスルーを実行するための古典的な昔ながらの方法が常にあります。

場合によっては、この些細な見た目の方法でさえ、いくつかの一般的なメモリリークの問題を排除するのに役立ちます。

5. 結論

素人の言葉で言えば、メモリリークは、重要なメモリリソースをブロックすることによってアプリケーションのパフォーマンスを低下させる病気と考えることができます。 そして、他のすべての病気と同様に、治癒しないと、時間の経過とともに致命的なアプリケーションのクラッシュが発生する可能性があります。

メモリリークを解決するには注意が必要です。メモリリークを見つけるには、Java言語に対する複雑な習熟とコマンドが必要です。 メモリリークを処理している間、リークはさまざまなイベントを通じて発生する可能性があるため、万能の解決策はありません。

ただし、ベストプラクティスに頼り、厳密なコードウォークスルーとプロファイリングを定期的に実行すると、アプリケーションでのメモリリークのリスクを最小限に抑えることができます。

いつものように、このチュートリアルで示されているVisualVM応答を生成するために使用されるコードスニペットは、GitHub利用できます。