Javaのメモリリークについて

Javaのメモリリークについて

1. 前書き

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

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

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

メモリリークはJavaの真の問題です。 このチュートリアルでは、what the potential causes of memory leaks are, how to recognize them at runtime, and how to deal with them in our applicationが表示されます。

2. メモリリークとは

メモリリークはwhen there are objects present in the heap that are no longer used, but the garbage collector is unable to remove them from memoryの状況であるため、不必要に維持されます。

blocks memory resources and degrades system performance over timeであるため、メモリリークは不良です。 そして、処理されない場合、アプリケーションは最終的にそのリソースを使い果たし、最終的に致命的なjava.lang.OutOfMemoryErrorで終了します。

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

The garbage collector removes unreferenced objects periodically, but it never collects the objects that are still being referenced.これは、メモリリークが発生する可能性がある場所です。

 

image

メモリリークの症状

  • アプリケーションが長時間連続して実行されると、パフォーマンスが大幅に低下する

  • アプリケーションでのOutOfMemoryErrorヒープエラー

  • 自発的および奇妙なアプリケーションのクラッシュ

  • アプリケーションで接続オブジェクトが不足する場合があります

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

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

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

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

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

Javaでは、static fields have a life that usually matches the entire lifetime of the running applicationClassLoaderがガベージコレクションの対象にならない限り)。

staticList:を設定する簡単なJavaプログラムを作成しましょう

public class StaticTest {
    public static List 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応答に示されているようにthe heap memory isn’t yet garbage collectedが表示されます。

 

image

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

 

image

デバッグポイントまでの最初の部分は、static.の場合に取得したものとほぼ同じですが、今回はpopulateList()メソッドを終了した後、all the memory of the list is garbage collected because we don’t have any reference to itになります。

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

それを防ぐ方法は?

  • static変数の使用を最小限に抑える

  • シングルトンを使用する場合、積極的にロードするのではなく、オブジェクトを遅延ロードする実装に依存します

3.2. クローズドリソースを通じて

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

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

いずれの場合も、the open connection left from resources consumes memoryであり、それらを処理しないと、パフォーマンスが低下し、OutOfMemoryErrorになる可能性があります。

それを防ぐ方法は?

  • リソースを閉じるには、常にfinallyブロックを使用してください

  • リソースを閉じるコード(finallyブロック内であっても)自体に例外があってはなりません

  • Java 7以降を使用する場合、try-with-resourcesブロックを利用できます。

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

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

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

些細な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 map = new HashMap<>();
    for(int i=0; i<100; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertFalse(map.size() == 1);
}

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

ただし、since we haven’t defined proper equals() method, the duplicate objects pile up and increase the memoryであるため、メモリ内に複数のオブジェクトが表示されます。 このためのVisualVMのヒープメモリは次のようになります。

 

image

ただし、if we had overridden the equals() and hashCode() methods properly, then there would only exist one Person object in this*Map*.

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 map = new HashMap<>();
    for(int i=0; i<2; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertTrue(map.size() == 1);
}

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

 

image

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

The chances of memory leak are quite high if these methods are not overriddenは、Hibernateがオブジェクトを比較できず、キャッシュが重複するオブジェクトでいっぱいになるためです。

それを防ぐ方法は?

  • 経験則として、新しいエンティティを定義するときは、常にequals()およびhashCode()メソッドをオーバーライドします

  • オーバーライドするだけで十分ではありませんが、これらのメソッドも最適な方法でオーバーライドする必要があります

詳細については、チュートリアルのGenerate equals() and hashCode() with EclipseGuide to hashCode() in Javaをご覧ください。

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

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

すべての非静的内部クラスには、デフォルトで、含まれるクラスへの暗黙的な参照があります。 この内部クラスのオブジェクトをアプリケーションで使用する場合、even after our containing class' object goes out of scope, it will not be garbage collected

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

 

image

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

image

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

それを防ぐ方法は?

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

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

ファイナライザーの使用は、メモリリークの潜在的な問題のもう1つの原因です。 クラスのfinalize()メソッドがオーバーライドされると、objects of that class aren’t instantly garbage collected.が代わりに、GCはそれらをキューに入れてファイナライズします。これは、後の時点で発生します。

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

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

 

image

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

image

それを防ぐ方法は?

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

finalize()の詳細については、セクション3(Guide to the finalize Method in JavaAvoiding Finalizers) )を参照してください。

3.6. インターンStrings

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

If we read a huge massive String object, and call intern() on that object, then it goes to the string pool, which is located in PermGen (permanent memory) and will stay there as long as our application runs.これにより、メモリがブロックされ、アプリケーションで重大なメモリリークが発生します。

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

 

image

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

image

 

それを防ぐ方法は?

  • この問題を解決する最も簡単な方法は、文字列プールがJavaバージョン7以降からHeapSpaceに移動されるため、最新のJavaバージョンにアップグレードすることです。

  • 大きなStringsで作業している場合は、PermGenスペースのサイズを増やして、潜在的なOutOfMemoryErrorsを回避します。

    -XX:MaxPermSize=512m

3.7. ThreadLocalsの使用

ThreadLocalIntroduction to ThreadLocal in Javaチュートリアルで詳細に説明)は、状態を特定のスレッドに分離する機能を提供し、スレッドセーフを実現できるようにする構造です。

この構成を使用する場合、each thread will hold an implicit reference to its copy of a ThreadLocal variable and will maintain its own copy, instead of sharing the resource across multiple threads, as long as the thread is alive.

その利点にもかかわらず、ThreadLocal変数の使用は、適切に使用されないとメモリリークが発生することで悪名高いため、物議を醸しています。 Joshua Blochonce commented on thread local usage

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

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

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

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

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

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

それを防ぐ方法は?

  • 使用されなくなったThreadLocalsをクリーンアップすることをお勧めします—ThreadLocalsは、この変数の現在のスレッドの値を削除するremove()メソッドを提供します

  • Do not use ThreadLocal.set(null) to clear the value —実際には値をクリアしませんが、代わりに現在のスレッドに関連付けられているMapを検索し、キーと値のペアをそれぞれ現在のスレッドとnullとして設定します

  • ThreadLocal を、例外の場合でも常に閉じていることを確認するために、finallyブロックで閉じる必要があるリソースと見なすとさらによいでしょう。

    try {
        threadLocal.set(System.nanoTime());
        //... further processing
    }
    finally {
        threadLocal.remove();
    }

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

メモリリークを処理する際に万能なソリューションはありませんが、これらのリークを最小限に抑える方法がいくつかあります。

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

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

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

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

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

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

-verbose:gc

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

image

 

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

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

参照キューは、ガベージコレクターによって実行されるアクションを認識できるように設計されています。 詳細については、Soft References in Javaのサンプルチュートリアル、特にセクション4をご覧ください。

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

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

image

 

4.5. ベンチマーク

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

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

4.6. コードレビュー

最後に、単純なコードウォークスルーを行う従来の古い方法が常にあります。

場合によっては、この簡単な方法でも、一般的なメモリリークの問題を解決するのに役立ちます。

5. 結論

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

メモリリークは解決が困難であり、それらを見つけるには、Java言語に対する複雑な習熟とコマンドが必要です。 While dealing with memory leaks, there is no one-size-fits-all solution, as leaks can occur through a wide range of diverse events.

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

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