Javaでメモリリークを作成する方法


3223

面接を受けたところ、Javaでメモリリークを起こすように言われました。
言うまでもありませんが、作成を開始する方法すらまったく手がかりがありません。

例は何でしょうか?


275
Javaはガベージコレクターを使用することを伝え、「メモリリーク」の定義についてもう少し具体的に説明してもらいます。JVMのバグがない限り、Javaはまったく同じ方法でメモリをリークできないということを説明します。/ C ++はできます。どこかにオブジェクトへの参照が必要です。
ダリアン、2011

371
ほとんどの回答で、人々がこれらのエッジケースやトリックを探していて、ポイント(IMO)を完全に逃しているように見えるのは面白いと思います。それらは、二度と使用されないオブジェクトへの無用な参照を保持し、同時にそれらの参照を決して落とさないコードを示すことができます。それらのケースにはまだオブジェクトへの参照があるため、これらのケースは「本当の」メモリリークではないと言えるかもしれませんが、プログラムがそれらの参照を再び使用せず、削除もしない場合は、「」と完全に同じです真のメモリリーク」。
ehabkost

62
正直言って、私が "Go"について尋ねた同様の質問が-1に反対投票されたとは信じられません。ここに:stackoverflow.com/questions/4400311/… 基本的に、私が話していたメモリリークは、OPに+200の賛成票を獲得したものですが、攻撃を受けて、「Go」に同じ問題があるかどうか尋ねて侮辱されました。どういうわけか、すべてのwikiがうまく機能しているとは思えません。
SyntaxT3rr0r

74
@ SyntaxT3rr0r-ダリエンの答えはファンボイズムではありません。彼は、特定のJVMにバグがあり、メモリリークが発生する可能性があることを明確に認めました。これは、メモリリークを許容する言語仕様自体とは異なります。
Peter Recore

31
@ehabkost:いいえ、同等ではありません。(1)あなたはメモリを再利用する能力を持っていますが、「真のリーク」では、C / C ++プログラムは割り当てられた範囲を忘れてしまい、安全に回復する方法はありません。(2)「膨張」に関連するオブジェクトを確認できるため、プロファイリングの問題を非常に簡単に検出できます。(3)「真のリーク」は明確なエラーですが、終了するまで多くのオブジェクトを保持するプログラムは、意図したとおりに動作する意図の一部である可能性があります。
ダリアン、

回答:


2309

純粋なJavaで真のメモリリーク(コードを実行してもオブジェクトにアクセスできないが、メモリに格納されているオブジェクト)を作成する良い方法を次に示します。

  1. アプリケーションは長時間実行されるスレッドを作成します(またはスレッドプールを使用してさらに速くリークします)。
  2. スレッドは(オプションでカスタム)を介してクラスをロードしますClassLoader
  3. クラスはメモリの大きなチャンク(例:)を割り当て、new byte[1000000]それへの強い参照を静的フィールドに格納し、それ自体への参照をに格納しますThreadLocal。追加のメモリの割り当てはオプションです(クラスインスタンスをリークするだけで十分です)が、リークが大幅に速くなります。
  4. アプリケーションは、カスタムクラスまたはClassLoaderそれがロードされた元のクラスへのすべての参照をクリアします。
  5. 繰り返す。

この方法ThreadLocalはOracleのJDKに実装されているため、メモリリークが発生します。

  • それぞれThreadにプライベートフィールドthreadLocalsがあり、実際にはスレッドローカル値が格納されます。
  • このマップの各キーThreadLocalオブジェクトへの弱い参照であるため、そのThreadLocalオブジェクトがガベージコレクションされた後、そのエントリはマップから削除されます。
  • しかし、それぞれの値は、強い参照であるので、値に(直接的または間接的に)ポイント場合ThreadLocal、そのあるオブジェクトキーは、そのオブジェクトはどちらもしないであろうガベージコレクト長いスレッド生活などとしてもマップから除去されます。

この例では、強い参照のチェーンは次のようになります。

Threadオブジェクト→ threadLocalsマップ→サンプルクラスのインスタンス→サンプルクラス→静的ThreadLocalフィールド→ ThreadLocalオブジェクト。

(これClassLoaderは実際にリークの作成に役割を果たしませんClassLoader。これは、この追加の参照チェーンのためにリークを悪化させます。例:クラス→→ ロードしたすべてのクラス。多くのJVM実装では、特に以前はさらに悪かったです。ClassLoaderJava7。これは、クラスとsがpermgenに直接割り当てられ、ガベージコレクションがまったく行われなかったためです。)

このパターンのバリエーションはThreadLocal、何らかの理由でs を使用するアプリケーションを頻繁に再デプロイすると、アプリケーションコンテナー(Tomcatなど)がふるいのようにメモリリークする可能性がある理由です。これは、いくつかの微妙な理由で発生する可能性があり、デバッグや修正が困難な場合があります。

更新:多くの人が求め続けているため、この動作を実際に示すコード例を次に示します


186
+1 ClassLoaderリークは、JEEの世界で最も一般的に痛みを伴うメモリリークの一部であり、多くの場合、データを変換するサードパーティのライブラリ(BeanUtils、XML / JSONコーデック)が原因です。これは、libがアプリケーションのルートクラスローダーの外部に読み込まれているが、クラスへの参照を保持している場合(キャッシュなど)に発生する可能性があります。アプリをアンデプロイ/再デプロイすると、JVMはアプリのクラスローダー(つまり、アプリによってロードされるすべてのクラス)をガベージコレクションできないため、繰り返しデプロイすると、アプリサーバーが最終的に停止します。運が良ければ、ClassCastExceptionを使用して手掛かりを得る場合、zxyAbcをzxyAbcにキャストできません
イヤーカム

7
tomcatはトリックを使用し、読み込まれたすべてのクラスのすべての静的変数をnilします。tomcatには多くのデータ競合と不適切なコーディングがありますが(時間をかけて修正を送信する必要があります)、内部(小さな)オブジェクトのキャッシュとして非常に気が遠くなるようなConcurrentLinkedQueueがあります。非常に小さいため、ConcurrentLinkedQueue.Nodeでもより多くのメモリを必要とします。
bestsss

57
+1:クラスローダーのリークは悪夢です。私はそれらを理解するために何週間も費やしました。悲しいことに、@ earcamが言ったように、それらは主にサードパーティのライブラリが原因であり、ほとんどのプロファイラーはこれらのリークを検出できません。このブログには、Classloaderリークに関する適切で明確な説明があります。blogs.oracle.com/fkieviet/entry/…–
エイドリアンM

4
@ニコラス:よろしいですか?JRockitはデフォルトでGCクラスオブジェクトを実行し、HotSpotは実行しませんが、AFAIK JRockitは、ThreadLocalによって参照されるクラスまたはClassLoaderをGCできません。
Daniel Pryden

6
Tomcatはこれらのリークを検出し、警告を表示します:wiki.apache.org/tomcat/MemoryLeakProtection。最新バージョンでは、リークが修正されることもあります。
Matthijs Bierman

1212

オブジェクト参照を保持する静的フィールド[esp finalフィールド]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

String.intern()長い文字列を呼び出す

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

(閉じられていない)オープンストリーム(ファイル、ネットワークなど...)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

閉じられていない接続

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

ネイティブメソッドを介して割り当てられたメモリなど、JVMのガベージコレクターから到達できない領域

Webアプリケーションでは、アプリケーションが明示的に停止または削除されるまで、一部のオブジェクトはアプリケーションスコープに保存されます。

getServletContext().setAttribute("SOME_MAP", map);

不適切または不適切なJVMオプションnoclassgc未使用のクラスガベージコレクションを防止するIBM JDK のオプションなど、

IBM jdk設定を参照してください。


178
コンテキストとセッションの属性が「リーク」であることには同意しません。それらは長命の変数です。そして、静的最終フィールドは多かれ少なかれ定数です。たぶん、大きな定数は避けるべきですが、メモリリークと呼ぶのは公平ではないと思います。
Ian McLaird

80
(閉じられていない)オープンストリーム(ファイル、ネットワークなど)、クローズ()(次のGCサイクルの後になる)ファイナライズ中に、本当のために漏洩しない(予定されようとしているclose()通常ファイナライザで呼び出されていませんブロッキング操作の可能性があるため、スレッド)。クローズしないことは悪い習慣ですが、リークの原因にはなりません。閉じられていないjava.sql.Connectionも同じです。
bestss

33
ほとんどの健全なJVMでは、Stringクラスはinternハッシュテーブルの内容に弱い参照しか持っていないように見えます。このように、それがされるごみ漏れを適切に収集していません。(IANAJPを除く)mindprod.com/jgloss/interned.html#GC
Matt B.

42
オブジェクト参照を保持する静的フィールド[esp finalフィールド]はどのようにメモリリークですか?
Kanagavelu Sugumar 2012

5
@cHaoはい。私が遭遇した危険は、Streamsによってメモリがリークされることによる問題ではありません。問題は、それらによって十分なメモリがリークされないことです。多くのハンドルをリークする可能性がありますが、それでも十分なメモリがあります。ガベージコレクターは、十分なメモリがあるため、完全なコレクションを実行しないように決定する場合があります。つまり、ファイナライザが呼び出されないため、ハンドルが不足します。問題は、ストリームのリークによってメモリが不足する前にファイナライザが(通常)実行されることですが、メモリ以外のものが不足する前にファイナライザが呼び出されない可能性があります。
Patrick M

460

簡単なことは、正しくない(または存在しない)hashCode()またはequals()でHashSetを使用し、「重複」を追加し続けることです。必要に応じて重複を無視する代わりに、セットは成長し続けるだけで、それらを削除することはできません。

これらの不正なキー/要素をぶら下げたい場合は、次のような静的フィールドを使用できます

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

68
実際には、要素クラスがhashCodeを取得して等しくない場合でも、HashSetから要素を削除できます。イテレータは実際には要素ではなく基礎となるエントリ自体を操作するため、セットのイテレータを取得してそのremoveメソッドを使用するだけです。(実装されていない hashCode / equals ではリークをトリガーするのに十分ではないことに注意してください。デフォルトでは単純なオブジェクトIDが実装されているため、要素を取得して通常どおりに削除できます。)
Donal Fellows

12
@Donal私が言おうとしているのは、おそらく、メモリリークの定義に同意できないことです。私は(アナロジーを続けるために)あなたのイテレータ削除テクニックをリークの下のドリップパンだと考えます。ドリップパンに関係なく、漏れはまだ存在しています。
corsiKa

94
私は同意します。これはメモリの「リーク」ではありません。ハッシュセットへの参照を削除して、GCが起動するのを待つだけで済むためです。メモリが戻ります。
user541686 2011

12
@ SyntaxT3rr0r、私はあなたの質問を自然にメモリリークにつながる何かが言語にあるかどうかを尋ねると解釈しました。答えはノーだ。この質問は、メモリリークのようなものを作成する状況を引き起こすことが可能かどうかを尋ねます。これらの例はどれも、C / C ++プログラマが理解できるような方法でのメモリリークではありません。
Peter Lawrey、

11
@Peter Lawrey:また、これについてどう思いますか:「C言語には、割り当てたメモリを手動で解放することを忘れない限り、自然にメモリリークにリークするものはありません」。それは知的不正直にとってどうでしょうか?とにかく、私は疲れています。最後の言葉を言うことができます。
SyntaxT3rr0r

271

以下に、Javaがリークする明らかでないケースがあります。これは、忘れられたリスナー、静的参照、ハッシュマップの偽の/変更可能なキー、またはライフサイクルを終了する可能性のないスレッドだけのスタックの標準ケースに加えてです。

  • File.deleteOnExit() -常に文字列をリークし、 文字列が部分文字列の場合、リークはさらに悪化します(基になるchar []もリークされます)- Javaで7また、コピーサブストリングchar[]後には適用されませんので、。@ダニエル、しかし、投票の必要はありません。

スレッドに集中して、管理されていないスレッドの危険性を主に示します。スイングには触れたくありません。

  • Runtime.addShutdownHook削除しないでください...そして、まだ開始されていないスレッドに関するThreadGroupクラスのバグのためにremoveShutdownHookを使用しても、収集されず、事実上ThreadGroupをリークします。JGroupのGossipRouterにリークがあります。

  • 作成するが開始しない Thread。上記と同じカテゴリに入ります。

  • スレッドを作成するContextClassLoaderAccessControlContext、and 、およびThreadGroupandおよびanyが継承されInheritedThreadLocalます。これらの参照はすべて、クラスローダーによってロードされたクラス全体とすべての静的参照、およびja-jaとともに潜在的なリークです。効果は、超シンプルな機能を備えたjucExecutorフレームワーク全体で特に顕著です。ThreadFactoryインターフェースほとんどの開発者には潜んでいる危険の手掛かりがありません。また、多くのライブラリはリクエストに応じてスレッドを開始します(あまりに多くの業界で人気のあるライブラリ)。

  • ThreadLocalキャッシュ; それらは多くの場合悪です。スレッドローカルに基づく単純なキャッシュをかなり多くの人が見てきたと思いますが、それは悪いニュースです。スレッドがコンテキストClassLoaderの寿命を予想以上に継続している場合、それは純粋に小さな小さなリークです。本当に必要でない限り、ThreadLocalキャッシュを使用しないでください。

  • 呼び出す ThreadGroup.destroy()スレッドグループは何のスレッド自体を持っていませんが、それはまだ子供のスレッドグループを保持したとき。ThreadGroupをその親から削除できないようにする悪いリークですが、すべての子は列挙できなくなります。

  • WeakHashMapと値を使用すると、キーが(間接的に)直接参照されます。これは、ヒープダンプなしでは見つけるのが難しいものです。それはすべての拡張に適用されますWeak/SoftReference、保護されたオブジェクトへのハード参照を保持し続ける可能性があるます。

  • java.net.URLHTTP(S)プロトコルで使用し、from(!)からリソースをロードします。これは特別なもので、KeepAliveCacheはシステムのThreadGroupに新しいスレッドを作成し、現在のスレッドのコンテキストクラスローダーをリークします。スレッドは、有効なスレッドが存在しないときに最初のリクエストで作成されるため、幸運になるか、単にリークする可能性があります。リークはJava 7ですでに修正されており、スレッドを作成するコードはコンテキストクラスローダーを適切に削除します。さらにいくつかのケースがあります(ImageFetcherのような同様に修正された)同様のスレッドを作成する。

  • コンストラクターをInflaterInputStream渡すことnew java.util.zip.Inflater()PNGImageDecoderたとえば)を使用しend()、インフレーターを呼び出さない。まあ、もしあなたがコンストラクタを単にnewで渡したとしても、チャンスはありません...そしてはい、close()それがコンストラクタパラメータとして手動で渡された場合、ストリームを呼び出してもインフレータは閉じません。ファイナライザによってリリースされるため、これは本当のリークではありません...必要と思われる場合。その瞬間まで、ネイティブメモリを非常に大量に消費するため、Linux oom_killerがプロセスを強制終了できなくなります。主な問題は、Javaでのファイナライズが非常に信頼性が低く、G1が7.0.2まで悪化させたことです。話の教訓:できるだけ早くネイティブリソースを解放してください。ファイナライザはあまりにも貧弱です。

  • と同じケースjava.util.zip.Deflater。DeflaterはJavaでメモリを大量に消費するため、これははるかに悪いものです。つまり、常に15ビット(最大)と8つのメモリレベル(9は最大)を使用して、数百KBのネイティブメモリを割り当てます。幸い、Deflater広く使用されておらず、私の知る限り、JDKには誤用はありません。またはend()を手動で作成する場合は常に呼び出します。最後の2つの最良の部分は、通常のプロファイリングツールでは入手できないことです。DeflaterInflater

(私は私が要求に応じて遭遇したいくつかの時間浪費者をさらに追加することができます。)

頑張って安全を守ってください。漏れは悪です!


23
Creating but not starting a Thread...何百年も前に私はこれにひどく噛まれました!(Java 1.3)
leonbloy

@leonbloy、スレッドがスレッドグループに直接追加されてさらに悪化する前に、開始しないと非常にハードなリークが発生しました。unstartedカウントを増やすだけでなく、スレッドグループが破壊されるのを防ぎます(それほど悪ではありませんが、それでもリークです)
bestsss

ありがとうございました!ThreadGroup.destroy()ThreadGroup自体にスレッドがない場合の呼び出し...」は、非常に微妙なバグです。私はこれを何時間も追跡してきました。制御GUIでスレッドを列挙しても何も表示されませんでしたが、スレッドグループと、おそらく少なくとも1つの子グループは消えません。
Lawrence Dol 2017年

1
@bestsss:気になるのですが、JVMシャットダウンで実行されている場合、シャットダウンフックを削除する理由は何ですか?
ローレンスドル2017年

203

ここでのほとんどの例は「複雑すぎる」です。彼らはエッジケースです。これらの例では、プログラマーは(equals / hashcodeを再定義しないように)間違いを犯したか、JVM / JAVA(静的なクラスのロード...)のコーナーケースに噛まれました。それは、インタビュアーが望んでいるタイプの例でも、最も一般的なケースでさえないと思います。

しかし、メモリリークのケースは本当に単純です。ガベージコレクターは、参照されなくなったものだけを解放します。Java開発者である私たちはメモリを気にしません。必要なときに割り当てて、自動的に解放します。いいよ

ただし、長期間有効なアプリケーションはすべて状態を共有する傾向があります。静的、シングルトンなど、何でもかまいません。多くの場合、重要なアプリケーションは複雑なオブジェクトグラフを作成する傾向があります。参照をnullに設定することを忘れたり、コレクションから1つのオブジェクトを削除することを忘れたりするだけで、メモリリークが発生します。

もちろん、あらゆる種類のリスナー(UIリスナーなど)、キャッシュ、または長期間有効な共有状態は、適切に処理されないとメモリリークを発生させる傾向があります。理解すべきことは、これはJavaのコーナーケースではなく、ガベージコレクターの問題でもないということです。設計上の問題です。長寿命のオブジェクトにリスナーを追加するように設計しますが、不要になったときにリスナーを削除しません。オブジェクトをキャッシュしますが、キャッシュから削除する戦略はありません。

計算に必要な以前の状態を保存する複雑なグラフがあるかもしれません。しかし、以前の状態自体が以前の状態にリンクされています。

SQL接続またはファイルを閉じる必要があるように。適切な参照をnullに設定し、コレクションから要素を削除する必要があります。適切なキャッシング戦略(最大メモリサイズ、要素数、またはタイマー)を用意します。リスナーへの通知を許可するすべてのオブジェクトは、addListenerメソッドとremoveListenerメソッドの両方を提供する必要があります。また、これらの通知機能が使用されなくなった場合は、リスナーリストをクリアする必要があります。

メモリリークは実際に起こり得、完全に予測可能です。特別な言語機能や特別なケースは必要ありません。メモリリークは、何かが欠落している可能性があること、またはデザインの問題であることを示しています。


24
他の回答で人々がそれらのエッジケースとトリックを探していて、ポイントを完全に逃しているように見えるのは面白いと思います。それらは、二度と使用されないオブジェクトへの無用な参照を保持し、それらの参照を削除しないコードを表示するだけです。それらのオブジェクトへの参照がまだ残っているため、これらのケースは「真の」メモリリークではないと言えるかもしれませんが、プログラムがそれらの参照を再び使用せず、削除もしない場合は、「」と完全に同等です真のメモリリーク」。
ehabkost

@Nicolas Bousquet:「メモリリークは確かに完全に可能です」 どうもありがとうございました。+15票。いいね。Go言語についての質問の前提として、その事実を述べたことで私はここで怒鳴られました:stackoverflow.com/questions/4400311 この質問にはまだ否定的な反対投票があります:(
SyntaxT3rr0r

Javaおよび.NETのGCは、ある意味で、他のオブジェクトへの参照を保持するオブジェクトの前提グラフに基づいており、他のオブジェクトを「考慮」するオブジェクトのグラフと同じです。実際には、「思いやりのある」関係を表さないエッジが参照グラフに存在する可能性があり、直接または間接の参照パスWeakReferenceが存在しない場合でも(を使用して)、オブジェクトが別のオブジェクトの存在を気にする可能性があります一方から他方へ。オブジェクト参照に予備のビットがある場合、「ターゲットを気遣うこと」インジケーターがあると役に立ちます...
supercat

...そしてPhantomReference、オブジェクトがそのオブジェクトを気にかけている人がいないことが判明した場合、システムに通知を提供します(と同様の方法を使用)。 WeakReference多少似ていますが、使用する前に強参照に変換する必要があります。強い参照が存在するときにGCサイクルが発生した場合、ターゲットは有用であると見なされます。
スーパーキャット2013年

これは私の意見では正しい答えです。シミュレーションは数年前に書きました。どういうわけか、以前の状態を現在の状態に誤ってリンクして、メモリリークを引き起こしました。期限のため、メモリリークを解決することはありませんでしたが、それを文書化することで«機能»にしました。
nalply

163

答えは完全に面接官が彼らが尋ねていたと思ったものに依存します。

実際にJavaリークを作成することは可能ですか?もちろんそうです、そして他の答えにはたくさんの例があります。

しかし、質問されている可能性がある複数のメタ質問がありますか?

  • 理論的には「完璧な」Java実装はリークに対して脆弱ですか?
  • 候補者は理論と現実の違いを理解していますか?
  • 候補者はガベージコレクションの仕組みを理解していますか?
  • または、理想的な場合にガベージコレクションはどのように機能するはずですか?
  • 彼らはネイティブインターフェースを介して他の言語を呼び出すことができることを知っていますか?
  • 彼らはそれらの他の言語でメモリをリークすることを知っていますか?
  • 候補者はメモリ管理とは何か、Javaの舞台裏で何が行われているのかを知っていますか?

私はあなたのメタ質問を「このインタビューの状況で私が使うことができたであろう答えは何か」と読んでいます。したがって、Javaではなくインタビュースキルに焦点を当てます。あなたは、Javaリークを作成する方法を知る必要がある場所にいるよりも、インタビューの質問に対する答えを知らないという状況を繰り返す可能性が高いと思います。ですから、うまくいけば、これが役立ちます。

面接のために開発できる最も重要なスキルの1つは、質問に積極的に耳を傾けることを学び、面接官と協力して彼らの意図を引き出すことです。これは彼らが望む方法で彼らの質問に答えさせるだけでなく、あなたがいくつかの重要なコミュニケーションスキルを持っていることも示しています。同様に才能のある多くの開発者の間での選択ということになると、毎回応答する前に、耳を傾け、考え、理解する人を採用します。


22
私がその質問をしたときはいつでも、私はかなり単純な答えを探しています-キューを大きくし続け、最終的にデータベースを閉じないなど、奇妙なクラスローダー/スレッドの詳細ではなく、GCがあなたのためにできることとできないことを理解していることを意味します。面接している仕事次第ではないでしょうか。
DaveC


130

JDBCを理解していない場合、次の例はかなり無意味です。または、少なくともJDBCが開発者にとインスタンスのクローズを期待する方法ConnectionStatementおよびResultSetインスタンスを破棄する前、またはインスタンスへの参照を失う前に、の実装に依存する代わりにfinalize

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

上記の問題は、Connectionオブジェクトが閉じていないため、ガベージコレクターが近づいて到達できないことがわかるまで、物理的な接続が開いたままになることです。GCはfinalizeメソッドを呼び出しますがfinalize、少なくとも実装されているのと同じ方法でConnection.closeはなく、を実装していないJDBCドライバーがあります。結果として生じる動作は、到達不能なオブジェクトが収集されるためにメモリが再利用される一方で、関連するリソース(メモリを含む)がConnectionオブジェクトにます。

Connectionfinalizeメソッドがすべてをクリーンアップしないようなイベントでは、データベースサーバーへの物理的な接続は、データベースサーバーが接続が生きていないことを最終的に把握するまで(実際にする)、そして閉じられるべきです。

JDBCドライバーがを実装する場合でも、finalizeファイナライズ中に例外がスローされる可能性があります。結果として生じる動作は、finalize1回だけ呼び出されることが保証されているため、現在「休止」オブジェクトに関連付けられているメモリは再利用されません。

オブジェクトのファイナライズ中に例外が発生する上記のシナリオは、メモリリークにつながる可能性のある別のシナリオ、つまりオブジェクトの復活に関連しています。オブジェクトの復活は、別のオブジェクトからのファイナライズからオブジェクトへの強い参照を作成することによって、意図的に行われることがよくあります。オブジェクトの復活を誤用すると、メモリリークの他のソースと組み合わせてメモリリークが発生します。

あなたが思いつくことができるもっとたくさんの例があります-のような

  • Listリストに追加するだけでリストから削除しないインスタンスの管理(不要になった要素は削除する必要があります)、または
  • SocketsまたはFilesを開くが、不要になった場合は閉じない(上記のConnectionクラスに関する例と同様)。
  • Java EEアプリケーションを停止するときにシングルトンをアンロードしない。明らかに、シングルトンクラスをロードしたクラスローダーはクラスへの参照を保持するため、シングルトンインスタンスは収集されません。アプリケーションの新しいインスタンスがデプロイされると、通常、新しいクラスローダーが作成され、シングルトンにより、以前のクラスローダーが引き続き存在します。

98
通常、メモリ制限に達する前に、開いている接続の最大制限に達します。なぜ私が知っているのか、聞かないでください
Hardwareguy

Oracle JDBCドライバーは、これを行うことで悪名高くなっています。
chotchki 2013

@Hardwareguy Connection.close最終的にすべてのSQL呼び出しのブロックに入るまで、SQLデータベースの接続制限に何度も到達しました。さらに面白くするために、データベースへの呼び出しが多すぎるのを防ぐためにJava側でのロックを必要とする長時間実行されているOracleストアドプロシージャを呼び出しました。
Michael Shopsin 2014

@Hardwareguy興味深いことですが、実際の接続制限がすべての環境で満たされる必要はありません。たとえば、weblogicアプリサーバー11gにデプロイされたアプリケーションの場合、大規模な接続リークが発生しました。しかし、リークされた接続を収集するオプションがあるため、メモリリークが導入されている間、データベース接続は引き続き使用可能でした。すべての環境についてはわかりません。
Aseem Bansal 2014

私の経験では、接続を閉じてもメモリリークが発生します。最初にResultSetとPreparedStatementを閉じる必要があります。OutOfMemoryErrorsが原因で、数時間後または数日後も正常に動作した後、私がそれを開始するまでサーバーが繰り返しクラッシュしました。
ビョルンステンフェルト2017

119

おそらく、潜在的なメモリリークの最も単純な例の1つと、それを回避する方法は、ArrayList.remove(int)の実装です。

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

自分で実装している場合、使用されなくなった配列要素をクリアすることを考えましたelementData[--size] = nullか()?その参照は巨大なオブジェクトを存続させるかもしれません...


5
そして、ここでのメモリリークはどこですか?
RDS

28
@maniek:このコードがメモリリークを示すことを示唆するつもりはありませんでした。偶発的なオブジェクトの保持を回避するために、時には自明ではないコードが必要になることを示すために引用しました。
メリトン2011

RangeCheck(index);とは ?
Koray Tugay 14年

6
Joshua Blochは、スタックの単純な実装を示す効果的なJavaでこの例を示しました。非常に良い答えです。
2015年

しかし、それは忘れられても、実際のメモリリークではありません。要素は依然としてリフレクションで安全にアクセスされますが、それは明らかではなく、Listインターフェースから直接アクセスできますが、オブジェクトと参照はまだそこにあり、安全にアクセスできます。
DGoiko

68

不要になったオブジェクトへの参照を保持するたびに、メモリリークが発生します。Javaでメモリリークがどのように発生するか、およびJavaでできることの例については、Javaプログラムでのメモリリークの処理を参照してください。


14
これが「リーク」だとは思わない。これはバグであり、プログラムと言語の仕様によるものです。リークは、オブジェクトへの参照なしでぶらぶらしているオブジェクトです。
user541686

29
@Mehrdad:これは、すべての言語に完全に適用されるわけではない1つの狭い定義にすぎません。メモリリークはすべて、プログラムの設計の不備が原因のバグであると私は主張します
リザードの請求

9
@Mehrdad:...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. 私はあなたがその結論をどのように描いているのかわかりません。定義によっては、Javaでメモリリークを作成する方法は少なくなります。それは間違いなくまだ有効な質問です。
リザードの請求

7
@ 31eee384:プログラムがオブジェクトをメモリ内に保持し、決して使用できない場合、技術的にはリークされたメモリです。あなたがより大きな問題を抱えているという事実は、それを本当に変えません。
リザードの請求

8
@ 31eee384:そうでないという事実を知っているなら、それは不可能です。プログラムは、記述されているように、データにアクセスすることはありません。
トカゲに請求する

51

sun.misc.Unsafeクラスを使用すると、メモリリークを発生させることができます。実際、このサービスクラスは、さまざまな標準クラス(たとえば、java.nioクラス)で使用されています。このクラスのインスタンスを直接作成することはできませんが、リフレクションを使用してそれを行うことができます。

コードはEclipse IDEでコンパイルされません-コマンドを使用javacしてコンパイルします(コンパイル中に警告が表示されます)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}

1
割り当てられたメモリは、ガベージコレクタからは見えません
ステムは、

4
割り当てられたメモリもJavaに属していません。
bestss 11/07/17

これはsun / oracle jvm固有ですか?たとえば、これはIBMで機能しますか?
ベルリンブラウン

2
メモリは確かに「Javaに属している」ことは確かです。少なくともi)他の誰も利用できないという意味です。ii)Javaアプリケーションが終了すると、システムに返されます。これは、JVMのすぐ外にあります。
Michael Anderson

3
これは(少なくとも最近のバージョンでは)Eclipseでビルドされますが、コンパイラー設定を変更する必要があります。 「。
マイケルアンダーソン、

43

私の回答をここからコピーできます: Javaでメモリリークを発生させる最も簡単な方法は?

「コンピューターサイエンスにおけるメモリーリーク(またはこのコンテキストにおけるリーク)は、コンピュータープログラムがメモリーを消費したが、それをオペレーティングシステムに解放できない場合に発生します。」(ウィキペディア)

簡単な答えは次のとおりです。できません。Javaは自動メモリ管理を実行し、不要なリソースを解放します。これを防ぐことはできません。常にリソースを解放できます。手動メモリ管理を使用するプログラムでは、これは異なります。malloc()を使用してCでメモリを取得することはできません。メモリを解放するには、mallocが返したポインタが必要であり、その上でfree()を呼び出します。しかし、ポインタがなくなった(上書きされた、または寿命を超えた)場合、残念ながらこのメモリを解放できず、メモリリークが発生します。

これまでの他のすべての答えは私の定義にあり、実際にはメモリリークではありません。それらはすべて、無意味なものでメモリを高速に埋めることを目的としています。しかし、いつでも作成したオブジェクトを逆参照してメモリを解放することができます->リークなし。acconradの答えはかなり近いですが、彼の解決策は事実上、ガベージコレクターを無限ループで強制的に「クラッシュ」させることなので、認めざるを得ません。

長い答えは次のとおりです。JNIを使​​用してJava用のライブラリを作成すると、メモリリークが発生する可能性があります。JNIは、手動でメモリを管理できるため、メモリリークが発生する可能性があります。このライブラリを呼び出すと、Javaプロセスでメモリリークが発生します。または、JVMにバグがあり、JVMがメモリを失う可能性があります。おそらくJVMにバグがあります。ガベージコレクションはそれほど簡単ではないため、既知のバグが存在することもありますが、それでもバグです。設計上、これは不可能です。あなたはそのようなバグによって影響を受けるいくつかのJavaコードを求めているかもしれません。申し訳ありませんが私はそれを知りません。それはとにかく次のJavaバージョンではもうバグではないかもしれません。


12
これは、メモリリークの非常に限定された(そしてあまり有用ではない)定義です。実用的な目的で意味のある唯一の定義は、「メモリリークとは、プログラムが保持するデータが不要になった後、プログラムが割り当てられたメモリを保持し続ける状態です」です。
メイソンウィーラー

1
上記のacconradの回答が削除されましたか?
トマーシュZato -復活モニカ

1
@TomášZato:いいえ、そうではありません。上記の参照をリンクに向けたので、簡単に見つけることができます。
ヤンキー

オブジェクトの復活とは何ですか?デストラクタは何回呼び出されますか?これらの質問はこの答えをどのように反証しますか?
自閉症の

1
確かに、GCにもかかわらず、Java内でメモリリークを作成し、それでも上記の定義を満たすことができます。追加のみのデータ構造を持ち、外部アクセスから保護されているので、他のコードがそれを削除することはありません。コードがないため、プログラムはメモリを解放できません。
toolforger 2018年

39

これはhttp://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29を介した単純な/不吉なものです。

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

部分文字列は元の非常に長い文字列の内部表現を参照するため、元の文字列はメモリに残ります。したがって、StringLeakerが動作している限り、1文字の文字列を保持していると思っていても、元の文字列全体がメモリに保持されます。

元の文字列への不要な参照を保存しないようにするには、次のようにします。

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

さらに悪い点として.intern()、部分文字列を使用することもできます。

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

これにより、StringLeakerインスタンスが破棄された後でも、元の長い文字列と派生した部分文字列の両方がメモリに保持されます。


4
それ自体をメモリリークとは呼びません。場合muchSmallerString(ためfreeedれるStringLeakerオブジェクトが破棄される)、長い文字列はよくとして解放されます。私がメモリリークと呼んでいるのは、JVMのこのインスタンスでは解放できないメモリです。ただし、メモリを解放する方法を示しましたthis.muchSmallerString=new String(this.muchSmallerString)。実際のメモリリークでは、何もできません。
RDS

2
@rds、それは公平な点です。非internケースは、「メモリリーク」ではなく「メモリサプライズ」の可能性があります。.intern()ただし、部分文字列を使用すると、長い文字列への参照が保持され、解放できないという状況が確実に発生します。
Jon Chambers

15
substring()メソッドは、java7で新しい文字列を作成します(これは新しい動作です)
anstarovoyt

自分でsubstring()を実行する必要すらありません。正規表現マッチャーを使用して、巨大な入力の小さな部分を照合し、「抽出された」文字列を長時間持ち歩きます。巨大な入力は、Java 6に生きアップをまま
Bananeweizen

37

GUIコードでのこの一般的な例は、ウィジェット/コンポーネントを作成し、静的/アプリケーションスコープオブジェクトにリスナーを追加し、ウィジェットが破棄されたときにリスナーを削除しない場合です。メモリリークが発生するだけでなく、イベントをリッスンしているイベントが発生すると、古いリスナーもすべて呼び出されるため、パフォーマンスが低下します。


1
Androidプラットフォームは、ビューの静的フィールドにビットマップをキャッシュすることによって作成されたメモリリークの例を提供します
RDS

36

任意のサーブレットコンテナ(Tomcat、Jetty、Glassfishな​​ど)で実行されている任意のWebアプリケーションを取得します。アプリを10回または20回続けて再デプロイします(サーバーのautodeployディレクトリにあるWARを単にタッチするだけで十分な場合があります)。

誰かが実際にこれをテストしていない限り、アプリケーションはそれ自体をクリーンアップするための注意を払っていなかったため、数回の再デプロイメント後にOutOfMemoryErrorが発生する可能性が高くなります。このテストでは、サーバーのバグを見つけることもできます。

問題は、コンテナの寿命がアプリケーションの寿命よりも長いことです。コンテナーがアプリケーションのオブジェクトまたはクラスに対して持つ可能性のあるすべての参照がガベージコレクションできることを確認する必要があります。

Webアプリケーションのアンデプロイを切り抜けた参照が1つしかない場合、対応するクラスローダーと、結果としてWebアプリケーションのすべてのクラスはガベージコレクションされません。

アプリケーションによって開始されたスレッド、ThreadLocal変数、ロギングアペンダーは、クラスローダーリークを引き起こす可能性がある通常の疑いの一部です。


1
これはメモリリークが原因ではなく、クラスローダーが以前のクラスセットをアンロードしないためです。そのため、サーバー(物理マシンではなくアプリサーバー)を再起動せずにアプリケーションサーバーを再デプロイすることはお勧めしません。WebSphereでも同じ問題が発生しました。
スヴェン

35

多分JNIを通して外部のネイティブコードを使用することによって?

純粋なJavaでは、ほとんど不可能です。

しかし、これは「標準」タイプのメモリリークに関するものであり、メモリにアクセスできなくなっても、アプリケーションによって所有されます。代わりに、未使用のオブジェクトへの参照を保持するか、後でオブジェクトを閉じずにストリームを開くことができます。


22
それは「メモリリーク」の定義に依存します。「保持されているが不要になったメモリ」であれば、Javaで簡単に実行できます。「割り当てられているがコードからまったくアクセスできないメモリ」の場合は、少し難しくなります。
ヨアヒムザウアー

@ヨアヒムザウアー-私は2番目のタイプを意味しました。最初のものはかなり簡単に作成できます:)
Rogach

6
「純粋なJavaでは、ほとんど不可能です。」まあ、私の落とし穴は、ここでの落とし穴に気づいていない人がキャッシュを実装することに関しては特に別です。
Fabian Barney

4
@Rogach:基本的に+10 000人の担当者によるさまざまな回答に対する+400の賛成票があり、どちらの場合もJoachim Sauerが非常に可能であるとコメントしています。したがって、「ほとんど不可能」は意味がありません。
SyntaxT3rr0r

32

かつて、PermGenとXMLの構文解析に関連して「メモリリーク」が発生しました。使用したXMLパーサー(どちらであったか思い出せません)は、比較を高速化するために、タグ名に対してString.intern()を実行しました。私たちの顧客の1人は、XML属性やテキストではなく、タグ名としてデータ値を保存するという素晴らしいアイデアを持っていたため、次のようなドキュメントがありました。

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

実際、彼らは数字を使用せず、より長いテキストID(約20文字)を使用しました。これらは一意であり、1日に1000万から1500万の割合で着信しました。これにより、1日あたり200 MBのゴミが発生します。これは二度と必要なく、GCも行われません(PermGenにあるため)。permgenを512 MBに設定したため、メモリ不足例外(OOME)が到着するまでに約2日かかりました...


4
サンプルコードをひっくり返すために:数値(または数値で始まる文字列)は、XMLの要素名として許可されていません。
パウロEbermann

これは、ヒープで文字列のインターンが発生するJDK 7+には当てはまらないことに注意してください。詳細については、この記事を参照してください:java-performance.info/string-intern-in-java-6-7-8
jmiserez

では、Stringの代わりにStringBufferを使用すると、この問題が解決すると思いますか?ね?
anubhs

24

メモリリークとは:

  • バグまたは設計の誤りが原因です。
  • それは記憶の浪費です。
  • 時間の経過とともに悪化します。
  • ガベージコレクタはそれをクリーンアップできません。

典型的な例:

オブジェクトのキャッシュは、物事を台無しにする良い出発点です。

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

キャッシュが大きくなります。そしてすぐに、データベース全体がメモリに吸い込まれます。より優れた設計ではLRUMapを使用します(最近使用したオブジェクトのみをキャッシュに保持します)。

もちろん、物事をもっと複雑にすることができます:

  • ThreadLocalコンストラクションを使用する。
  • より複雑な参照ツリーを追加します。
  • またはサードパーティのライブラリが原因のリーク。

よく起こること:

このInfoオブジェクトに他のオブジェクトへの参照がある場合、このオブジェクトにも他のオブジェクトへの参照があります。ある意味では、これはある種のメモリリークと考えることもできます(設計が不適切なため)。


22

内部クラスの例を誰も使用しなかったのは面白いと思いました。内部クラスがある場合; それは本質的に包含クラスへの参照を維持します。もちろん、Javaは最終的にクリーンアップするため、技術的にはメモリリークではありません。ただし、これにより、クラスが予想よりも長く滞留する可能性があります。

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

ここで、Example1を呼び出して、Example2がExample1を破棄するようにした場合、本質的にはまだExample1オブジェクトへのリンクがあります。

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

また、特定の時間よりも長く存在する変数がある場合、その噂を聞いたことがあります。Javaはそれが常に存在することを想定しており、コードで到達できない場合は実際にはクリーンアップを試みません。しかし、それは完全に検証されていません。


2
内部クラスが問題になることはめったにありません。それらは簡単なケースであり、検出が非常に簡単です。噂も単なる噂です。
bestsss

2
「噂」は、世代別GCがどのように機能するかについて誰かが半分読んだように聞こえます。存続期間は長くても到達できないオブジェクトは、実際にまとまってしばらくの間領域を占有する可能性があります。JVMがオブジェクトを若い世代から昇格させたため、すべてのパスのチェックを停止できるためです。彼らは、意図的に、「私の5000の一時的な文字列をクリーンアップする」パスを、気まぐれに回避します。しかし、彼らは不滅ではありません。それらはまだ収集の対象であり、VMがRAMに縛られている場合、最終的には完全なGCスイープを実行し、そのメモリを取り戻します。
cHao 2013

22

最近、log4jが原因でメモリリークが発生しました。

Log4jには、ネストされた診断コンテキスト(NDC)と呼ばれるこのメカニズム があり、インターリーブされたログ出力をさまざまなソースから区別する手段です。NDCが機能する粒度はスレッドであるため、異なるスレッドからのログ出力を個別に区別します。

スレッド固有のタグを格納するために、log4jのNDCクラスは、(スレッドIDとは対照的に)スレッドオブジェクト自体がキーとするHashtableを使用するため、NDCタグがメモリに留まるまで、スレッドからハングするすべてのオブジェクトを保持しますオブジェクトもメモリに残ります。Webアプリケーションでは、NDCを使用してログ出力にリクエストIDをタグ付けし、ログを単一のリクエストと区別します。NDCタグをスレッドに関連付けるコンテナは、リクエストから応答を返すときにも削除します。この問題は、リクエストの処理中に次のコードのような子スレッドが生成されたときに発生しました。

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

したがって、NDCコンテキストは、生成されたインラインスレッドに関連付けられていました。このNDCコンテキストのキーとなったスレッドオブジェクトは、hugeListオブジェクトがぶら下がっているインラインスレッドです。したがって、スレッドが実行中の処理を終了した後でも、hugeListへの参照がNDCコンテキストHastableによって保持され、メモリリークが発生していました。


それは最悪だ。:あなたは、ファイルへのロギングしながら、ZEROのメモリを割り当てるこのロギングライブラリを確認する必要がありmentalog.soliveirajr.com
TraderJoeChicago

+1 slf4j / logback(同じ作者による後継製品)のMDCに同様の問題があるかどうか、わかりますか?ソースについて詳しく説明しますが、最初に確認したいと思います。いずれにせよ、これを投稿してくれてありがとう。
sparc_spread 14

20

インタビュアーはおそらく、以下のコードのような循環参照を探していました(偶然、参照カウントを使用していた非常に古いJVMでのみメモリリークしますが、これはもう当てはまりません)。しかし、これはかなり漠然とした質問であるため、JVMメモリー管理についての理解を披露する絶好の機会です。

class A {
    B bRef;
}

class B {
    A aRef;
}

public class Main {
    public static void main(String args[]) {
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
    } /* main */
}

次に、参照カウントを使用すると、上記のコードでメモリリークが発生することを説明できます。しかし、ほとんどの最近のJVMは参照カウントをもはや使用せず、ほとんどがスイープガベージコレクターを使用しており、実際にはこのメモリを収集します。

次に、次のように、基になるネイティブリソースを持つオブジェクトの作成について説明します。

public class Main {
    public static void main(String args[]) {
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    }
}

次に、これは技術的にはメモリリークであると説明できますが、リークは実際には、Javaコードによって解放されなかった、基になるネイティブリソースを割り当てるJVMのネイティブコードが原因です。

結局のところ、最新のJVMでは、JVMの認識の通常の範囲外にネイティブリソースを割り当てるJavaコードを記述する必要があります。


19

誰もが常にネイティブコードルートを忘れています。リークの簡単な式は次のとおりです。

  1. ネイティブメソッドを宣言します。
  2. ネイティブメソッドでを呼び出しますmalloc。電話しないでくださいfree
  3. ネイティブメソッドを呼び出します。

ネイティブコードでのメモリ割り当ては、JVMヒープから行われることに注意してください。


1
実話に基づいて。
登録

18

静的マップを作成し、ハード参照を追加し続けます。それらは決してGCされません。

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}

87
そのリークはどうですか?それはあなたがそれをするように求めていることを正確に行っています。それがリークの場合、オブジェクトをどこかに作成して保存するとリークになります。
Falmarri

3
@Falmarriに同意します。リークはありません。オブジェクトを作成しているだけです。確かに、「removeFromCache」という別のメソッドで割り当てたメモリを「再利用」できます。リークとは、メモリを再利用できない場合です。
カイル

3
私のポイントは、オブジェクトを作成し続け、おそらくそれらをキャッシュに入れている誰かが、注意しないとOOMエラーが発生する可能性があるということです。
duffymo 2012

8
@duffymo:しかし、それは実際に質問されていたものではありません。単にすべてのメモリを使い果たすこととは関係ありません。
Falmarri

3
絶対に無効です。Mapコレクション内のオブジェクトの束を収集しているだけです。マップが保持しているため、参照は保持されます。
gyorgyabraham 2013年

16

クラスのfinalizeメソッドでクラスの新しいインスタンスを作成することにより、移動メモリリークを作成できます。ファイナライザが複数のインスタンスを作成する場合のボーナスポイント。ヒープサイズに応じて、数秒から数分の間にヒープ全体をリークする単純なプログラムを次に示します。

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}

15

私はまだ誰もこれを言っていないと思います:finalize()がこれの参照をどこかに格納するようにfinalize()メソッドをオーバーライドすることでオブジェクトを復活させることができます。ガベージコレクターはオブジェクトに対して一度だけ呼び出されるため、その後はオブジェクトが破棄されることはありません。


10
これは真実ではありません。finalize()呼び出されませんが、参照がなくなるとオブジェクトが収集されます。ガベージコレクタも呼び出されません。
bestss

1
この答えは誤解を招くものであり、finalize()メソッドはJVMによって1回しか呼び出せませんが、オブジェクトが復活してから再び逆参照された場合に、そのメソッドがガベージコレクションされないという意味ではありません。finalize()メソッドにリソースを閉じるコードがある場合、このコードは再度実行されず、メモリリークが発生する可能性があります。
Tom Cammann

15

最近、より微妙なリソースリークに遭遇しました。クラスローダーのgetResourceAsStreamを介してリソースを開きましたが、入力ストリームのハンドルが閉じられていませんでした。

ええと、あなたは言うかもしれません、ばかです。

さて、これが興味深いのは、この方法では、JVMのヒープからではなく、基礎となるプロセスのヒープメモリをリークできるということです。

必要なのは、Javaコードから参照されるファイルを含むjarファイルだけです。jarファイルが大きいほど、割り当てられるメモリが速くなります。

このようなjarは、次のクラスで簡単に作成できます。

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

BigJarCreator.javaという名前のファイルに貼り付け、コマンドラインからコンパイルして実行するだけです。

javac BigJarCreator.java
java -cp . BigJarCreator

Etvoilà:現在の作業ディレクトリに2つのファイルが入ったjarアーカイブを見つけます。

2つ目のクラスを作成しましょう。

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

このクラスは基本的に何もしませんが、参照されていないInputStreamオブジェクトを作成します。これらのオブジェクトはすぐにガベージコレクションされるため、ヒープサイズには影響しません。この例では、jarファイルから既存のリソースをロードすることが重要であり、サイズはここで重要です!

疑わしい場合は、上記のクラスをコンパイルして開始してください。ただし、適切なヒープサイズ(2 MB)を選択してください。

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

ここではOOMエラーは発生しません。参照が保持されないため、上記の例でITERATIONSを選択しても、アプリケーションは実行を継続します。プロセス(上部(RES / RSS)またはプロセスエクスプローラーに表示)のメモリ消費量は、アプリケーションが待機コマンドに到達しない限り増加します。上記の設定では、約150 MBのメモリが割り当てられます。

アプリケーションを安全に再生したい場合は、作成された場所で入力ストリームを閉じます。

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

また、反復回数に関係なく、プロセスは35 MBを超えません。

かなりシンプルで驚くべきことです。


14

多くの人が示唆しているように、リソースリークは、JDBCの例のように、かなり簡単に引き起こされます。実際のメモリリークは少し難しいです-特に、JVMの壊れたビットに依存していない場合は...

フットプリントが非常に大きいオブジェクトを作成し、それらにアクセスできないという考えは、実際のメモリリークでもありません。何もアクセスできない場合はガベージコレクションされ、何かがアクセスできる場合はリークではありません...

以前は機能していましたが、それでも機能するかどうかはわかりませんが、3つの深さの循環チェーンを使用する方法があります。オブジェクトAにはオブジェクトBへの参照があり、オブジェクトBにはオブジェクトCへの参照があり、オブジェクトCにはオブジェクトAへの参照があります。GCは、A <-> B -AとBが他からアクセスできないが、3方向チェーンを処理できなかった場合、安全に収集できます...


7
しばらくの間、そうではありませんでした。最近のGCは循環参照の処理方法を知っています。
assylias 2013年

13

潜在的に巨大なメモリリークを作成する別の方法は、への参照を保持Map.Entry<K,V>することTreeMapです。

これがTreeMapsにのみ適用される理由を評価することは困難ですが、実装を調べると、その理由は次のようになりますTreeMap.Entry。aはその兄弟への参照を格納しているため、aをTreeMap収集する準備ができているが、他のクラスがそのMap.Entry場合、マップ全体がメモリに保持されます。


実際のシナリオ:

ビッグTreeMapデータ構造を返すdbクエリがあるとします。TreeMap要素の挿入順序が保持されるため、通常はsを使用します。

public static Map<String, Integer> pseudoQueryDatabase();

クエリが何度も呼び出され、クエリごと(つまりMap返されたクエリごと)にEntryどこかに保存した場合、メモリは常に増加し続けます。

次のラッパークラスを考えてみます。

class EntryHolder {
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) {
        this.entry = entry;
    }
}

応用:

public class LeakTest {

    private final List<EntryHolder> holdersCache = new ArrayList<>();
    private static final int MAP_SIZE = 100_000;

    public void run() {
        // create 500 entries each holding a reference to an Entry of a TreeMap
        IntStream.range(0, 500).forEach(value -> {
            // create map
            final Map<String, Integer> map = pseudoQueryDatabase();

            final int index = new Random().nextInt(MAP_SIZE);

            // get random entry from map
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue().equals(index)) {
                    holdersCache.add(new EntryHolder(entry));
                    break;
                }
            }
            // to observe behavior in visualvm
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    public static Map<String, Integer> pseudoQueryDatabase() {
        final Map<String, Integer> map = new TreeMap<>();
        IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
        return map;
    }

    public static void main(String[] args) throws Exception {
        new LeakTest().run();
    }
}

pseudoQueryDatabase()呼び出しの後、mapインスタンスは収集の準備ができているはずですが、少なくとも1つEntryは別の場所に格納されているため、インスタンスは収集されません。

jvm設定によっては、が原因でアプリケーションが初期段階でクラッシュする場合がありますOutOfMemoryError

このvisualvmグラフから、メモリが増加し続ける様子を確認できます。

メモリダンプ-TreeMap

ハッシュされたデータ構造(HashMap)では同じことが起こりません。

これは、を使用した場合のグラフHashMapです。

メモリダンプ-HashMap

ソリューション?を保存するのではなく、(おそらくすでに行っているように)キー/値を直接保存するだけですMap.Entry


ここで、より広範なベンチマークを作成しました


11

スレッドは、終了するまで収集されません。それらはガベージコレクションのルーツとして機能します。それらは、単にそれらを忘れたり、それらへの参照をクリアしたりするだけでは回収されない数少ないオブジェクトの1つです。

考えてみてください。ワーカースレッドを終了するための基本的なパターンは、スレッドから見える条件変数を設定することです。スレッドは定期的に変数をチェックし、それを終了のシグナルとして使用できます。変数が宣言されていない場合、変数volatileへの変更はスレッドに表示されない可能性があるため、終了することはわかりません。あるいは、一部のスレッドが共有オブジェクトを更新したいが、それをロックしようとしているときにデッドロックが発生したとします。

スレッドの数が少ない場合、プログラムが適切に動作しなくなるため、これらのバグはおそらく明らかです。必要に応じてより多くのスレッドを作成するスレッドプールがある場合、古くなった/スタックしたスレッドは認識されず、無期限に蓄積され、メモリリークを引き起こします。スレッドはアプリケーションで他のデータを使用する可能性が高いため、スレッドが直接参照するものは収集されなくなります。

おもちゃの例として:

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

System.gc()あなたが好きなすべてを呼び出しますが、渡されたオブジェクトleakMeは決して死にません。

(*編集*)


1
@Spidey「スタック」するものはありません。呼び出し元のメソッドはすぐに戻り、渡されたオブジェクトは回収されません。それはまさにリークです。
Boann 2013

1
プログラムの存続期間中、スレッドは「実行中」(またはスリープ状態)になります。それは私への漏れとは見なされません。また、プールを完全に使用しなくても、プールはリークとしてカウントされません。
Spidey 2013

1
@Spidey「プログラムの存続期間中、[もの]があります。それは、私へのリークとは見なされません。」聞こえますか?
Boann 2013

3
@Spideyプロセスがリークしていないと認識しているメモリをカウントする場合、プロセスは常に仮想アドレス空間のどのページがマップされているかを追跡するため、ここでのすべての答えは間違っています。プロセスが終了すると、OSはページを空きページスタックに戻すことにより、すべてのリークをクリーンアップします。これをさらに極端に進めるには、RAMチップまたはディスク上のスワップ領域の物理ビットが物理的に誤って配置または破壊されていないことを指摘することで、あらゆるリークを打ち負かし、コンピューターをオフにすることができます。そして、再び漏れをクリーンアップします。
Boann 2013

1
リークの実際的な定義は、メモリが失われているために分からないため、メモリを再利用するために必要な手順を実行できないことです。メモリスペース全体を破棄して再構築する必要があります。このような不正なスレッドは、デッドロックまたは危険なスレッドプールの実装によって自然に発生する可能性があります。そのようなスレッドによって参照されるオブジェクトは、間接的にも収集されないようになりました。そのため、プログラムの存続期間中に自然に再利用または再利用できないメモリがあります。私はそれを問題と呼びます。具体的には、メモリリークです。
Boann 2013

10

有効な例は、スレッドがプールされている環境でThreadLocal変数を使用することだと思います。

たとえば、サーブレットでThreadLocal変数を使用して他のWebコンポーネントと通信し、コンテナによってスレッドが作成され、プール内のアイドルスレッドを維持します。ThreadLocal変数は、正しくクリーンアップされない場合、おそらく同じWebコンポーネントがそれらの値を上書きするまでそこに存在します。

もちろん、特定されれば、問題は簡単に解決できます。


10

インタビュアーは循環参照ソリューションを探している可能性があります。

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

これは、参照カウントガベージコレクターの典型的な問題です。次に、JVMはこの制限のないはるかに洗練されたアルゴリズムを使用することを丁寧に説明します。

-ウェスタール


12
これは、参照カウントガベージコレクターの典型的な問題です。15年前でも、Javaは参照カウントを使用していませんでした。参照 カウントもGCよりも遅くなります。
bestsss

4
メモリリークではありません。ただの無限ループ。
Esben Skov Pedersen、2011

2
@Esben各反復で、前のコードfirstは役に立たないため、ガベージコレクションする必要があります。で参照カウント(単独で)その上にアクティブな参照があるため、ガベージコレクタ、オブジェクトがfreeedされないであろう。無限ループは、リークを示すためにここにあります。プログラムを実行すると、メモリが無制限に増加します。
RDS

@rds @ Wesley Tarleは、ループが無限ではなかったと仮定します。それでもメモリリークはありますか?
nz_21 2018年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.