面接を受けたところ、Javaでメモリリークを起こすように言われました。
言うまでもありませんが、作成を開始する方法すらまったく手がかりがありません。
例は何でしょうか?
面接を受けたところ、Javaでメモリリークを起こすように言われました。
言うまでもありませんが、作成を開始する方法すらまったく手がかりがありません。
例は何でしょうか?
回答:
純粋なJavaで真のメモリリーク(コードを実行してもオブジェクトにアクセスできないが、メモリに格納されているオブジェクト)を作成する良い方法を次に示します。
ClassLoader
。new byte[1000000]
それへの強い参照を静的フィールドに格納し、それ自体への参照をに格納しますThreadLocal
。追加のメモリの割り当てはオプションです(クラスインスタンスをリークするだけで十分です)が、リークが大幅に速くなります。ClassLoader
それがロードされた元のクラスへのすべての参照をクリアします。この方法ThreadLocal
はOracleのJDKに実装されているため、メモリリークが発生します。
Thread
にプライベートフィールドthreadLocals
があり、実際にはスレッドローカル値が格納されます。ThreadLocal
オブジェクトへの弱い参照であるため、そのThreadLocal
オブジェクトがガベージコレクションされた後、そのエントリはマップから削除されます。ThreadLocal
、そのあるオブジェクトキーは、そのオブジェクトはどちらもしないであろうガベージコレクト長いスレッド生活などとしてもマップから除去されます。この例では、強い参照のチェーンは次のようになります。
Thread
オブジェクト→ threadLocals
マップ→サンプルクラスのインスタンス→サンプルクラス→静的ThreadLocal
フィールド→ ThreadLocal
オブジェクト。
(これClassLoader
は実際にリークの作成に役割を果たしませんClassLoader
。これは、この追加の参照チェーンのためにリークを悪化させます。例:クラス→→ ロードしたすべてのクラス。多くのJVM実装では、特に以前はさらに悪かったです。ClassLoader
Java7。これは、クラスとsがpermgenに直接割り当てられ、ガベージコレクションがまったく行われなかったためです。)
このパターンのバリエーションはThreadLocal
、何らかの理由でs を使用するアプリケーションを頻繁に再デプロイすると、アプリケーションコンテナー(Tomcatなど)がふるいのようにメモリリークする可能性がある理由です。これは、いくつかの微妙な理由で発生する可能性があり、デバッグや修正が困難な場合があります。
更新:多くの人が求め続けているため、この動作を実際に示すコード例を次に示します。
オブジェクト参照を保持する静的フィールド[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設定を参照してください。
close()
通常ファイナライザで呼び出されていませんブロッキング操作の可能性があるため、スレッド)。クローズしないことは悪い習慣ですが、リークの原因にはなりません。閉じられていないjava.sql.Connectionも同じです。
intern
ハッシュテーブルの内容に弱い参照しか持っていないように見えます。このように、それがされるごみ漏れを適切に収集していません。(IANAJPを除く)mindprod.com/jgloss/interned.html#GC
簡単なことは、正しくない(または存在しない)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.
以下に、Javaがリークする明らかでないケースがあります。これは、忘れられたリスナー、静的参照、ハッシュマップの偽の/変更可能なキー、またはライフサイクルを終了する可能性のないスレッドだけのスタックの標準ケースに加えてです。
File.deleteOnExit()
-常に文字列をリークし、 char[]
後には適用されませんので、。@ダニエル、しかし、投票の必要はありません。スレッドに集中して、管理されていないスレッドの危険性を主に示します。スイングには触れたくありません。
Runtime.addShutdownHook
削除しないでください...そして、まだ開始されていないスレッドに関するThreadGroupクラスのバグのためにremoveShutdownHookを使用しても、収集されず、事実上ThreadGroupをリークします。JGroupのGossipRouterにリークがあります。
作成するが開始しない Thread
。上記と同じカテゴリに入ります。
スレッドを作成するContextClassLoader
とAccessControlContext
、and 、およびThreadGroup
andおよびanyが継承されInheritedThreadLocal
ます。これらの参照はすべて、クラスローダーによってロードされたクラス全体とすべての静的参照、およびja-jaとともに潜在的なリークです。効果は、超シンプルな機能を備えたjucExecutorフレームワーク全体で特に顕著です。ThreadFactory
インターフェースほとんどの開発者には潜んでいる危険の手掛かりがありません。また、多くのライブラリはリクエストに応じてスレッドを開始します(あまりに多くの業界で人気のあるライブラリ)。
ThreadLocal
キャッシュ; それらは多くの場合悪です。スレッドローカルに基づく単純なキャッシュをかなり多くの人が見てきたと思いますが、それは悪いニュースです。スレッドがコンテキストClassLoaderの寿命を予想以上に継続している場合、それは純粋に小さな小さなリークです。本当に必要でない限り、ThreadLocalキャッシュを使用しないでください。
呼び出す ThreadGroup.destroy()
スレッドグループは何のスレッド自体を持っていませんが、それはまだ子供のスレッドグループを保持したとき。ThreadGroupをその親から削除できないようにする悪いリークですが、すべての子は列挙できなくなります。
WeakHashMapと値を使用すると、キーが(間接的に)直接参照されます。これは、ヒープダンプなしでは見つけるのが難しいものです。それはすべての拡張に適用されますWeak/SoftReference
、保護されたオブジェクトへのハード参照を保持し続ける可能性があるます。
java.net.URL
HTTP(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つの最良の部分は、通常のプロファイリングツールでは入手できないことです。Deflater
Inflater
(私は私が要求に応じて遭遇したいくつかの時間浪費者をさらに追加することができます。)
頑張って安全を守ってください。漏れは悪です!
Creating but not starting a Thread...
何百年も前に私はこれにひどく噛まれました!(Java 1.3)
unstarted
カウントを増やすだけでなく、スレッドグループが破壊されるのを防ぎます(それほど悪ではありませんが、それでもリークです)
ThreadGroup.destroy()
ThreadGroup自体にスレッドがない場合の呼び出し...」は、非常に微妙なバグです。私はこれを何時間も追跡してきました。制御GUIでスレッドを列挙しても何も表示されませんでしたが、スレッドグループと、おそらく少なくとも1つの子グループは消えません。
ここでのほとんどの例は「複雑すぎる」です。彼らはエッジケースです。これらの例では、プログラマーは(equals / hashcodeを再定義しないように)間違いを犯したか、JVM / JAVA(静的なクラスのロード...)のコーナーケースに噛まれました。それは、インタビュアーが望んでいるタイプの例でも、最も一般的なケースでさえないと思います。
しかし、メモリリークのケースは本当に単純です。ガベージコレクターは、参照されなくなったものだけを解放します。Java開発者である私たちはメモリを気にしません。必要なときに割り当てて、自動的に解放します。いいよ
ただし、長期間有効なアプリケーションはすべて状態を共有する傾向があります。静的、シングルトンなど、何でもかまいません。多くの場合、重要なアプリケーションは複雑なオブジェクトグラフを作成する傾向があります。参照をnullに設定することを忘れたり、コレクションから1つのオブジェクトを削除することを忘れたりするだけで、メモリリークが発生します。
もちろん、あらゆる種類のリスナー(UIリスナーなど)、キャッシュ、または長期間有効な共有状態は、適切に処理されないとメモリリークを発生させる傾向があります。理解すべきことは、これはJavaのコーナーケースではなく、ガベージコレクターの問題でもないということです。設計上の問題です。長寿命のオブジェクトにリスナーを追加するように設計しますが、不要になったときにリスナーを削除しません。オブジェクトをキャッシュしますが、キャッシュから削除する戦略はありません。
計算に必要な以前の状態を保存する複雑なグラフがあるかもしれません。しかし、以前の状態自体が以前の状態にリンクされています。
SQL接続またはファイルを閉じる必要があるように。適切な参照をnullに設定し、コレクションから要素を削除する必要があります。適切なキャッシング戦略(最大メモリサイズ、要素数、またはタイマー)を用意します。リスナーへの通知を許可するすべてのオブジェクトは、addListenerメソッドとremoveListenerメソッドの両方を提供する必要があります。また、これらの通知機能が使用されなくなった場合は、リスナーリストをクリアする必要があります。
メモリリークは実際に起こり得、完全に予測可能です。特別な言語機能や特別なケースは必要ありません。メモリリークは、何かが欠落している可能性があること、またはデザインの問題であることを示しています。
WeakReference
が存在しない場合でも(を使用して)、オブジェクトが別のオブジェクトの存在を気にする可能性があります一方から他方へ。オブジェクト参照に予備のビットがある場合、「ターゲットを気遣うこと」インジケーターがあると役に立ちます...
PhantomReference
、オブジェクトがそのオブジェクトを気にかけている人がいないことが判明した場合、システムに通知を提供します(と同様の方法を使用)。 WeakReference
多少似ていますが、使用する前に強参照に変換する必要があります。強い参照が存在するときにGCサイクルが発生した場合、ターゲットは有用であると見なされます。
答えは完全に面接官が彼らが尋ねていたと思ったものに依存します。
実際にJavaリークを作成することは可能ですか?もちろんそうです、そして他の答えにはたくさんの例があります。
しかし、質問されている可能性がある複数のメタ質問がありますか?
私はあなたのメタ質問を「このインタビューの状況で私が使うことができたであろう答えは何か」と読んでいます。したがって、Javaではなくインタビュースキルに焦点を当てます。あなたは、Javaリークを作成する方法を知る必要がある場所にいるよりも、インタビューの質問に対する答えを知らないという状況を繰り返す可能性が高いと思います。ですから、うまくいけば、これが役立ちます。
面接のために開発できる最も重要なスキルの1つは、質問に積極的に耳を傾けることを学び、面接官と協力して彼らの意図を引き出すことです。これは彼らが望む方法で彼らの質問に答えさせるだけでなく、あなたがいくつかの重要なコミュニケーションスキルを持っていることも示しています。同様に才能のある多くの開発者の間での選択ということになると、毎回応答する前に、耳を傾け、考え、理解する人を採用します。
JDBCを理解していない場合、次の例はかなり無意味です。または、少なくともJDBCが開発者にとインスタンスのクローズを期待する方法Connection
、Statement
および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
オブジェクトにます。
Connection
のfinalize
メソッドがすべてをクリーンアップしないようなイベントでは、データベースサーバーへの物理的な接続は、データベースサーバーが接続が生きていないことを最終的に把握するまで(実際にする)、そして閉じられるべきです。
JDBCドライバーがを実装する場合でも、finalize
ファイナライズ中に例外がスローされる可能性があります。結果として生じる動作は、finalize
1回だけ呼び出されることが保証されているため、現在「休止」オブジェクトに関連付けられているメモリは再利用されません。
オブジェクトのファイナライズ中に例外が発生する上記のシナリオは、メモリリークにつながる可能性のある別のシナリオ、つまりオブジェクトの復活に関連しています。オブジェクトの復活は、別のオブジェクトからのファイナライズからオブジェクトへの強い参照を作成することによって、意図的に行われることがよくあります。オブジェクトの復活を誤用すると、メモリリークの他のソースと組み合わせてメモリリークが発生します。
あなたが思いつくことができるもっとたくさんの例があります-のような
List
リストに追加するだけでリストから削除しないインスタンスの管理(不要になった要素は削除する必要があります)、またはSocket
sまたはFile
sを開くが、不要になった場合は閉じない(上記のConnection
クラスに関する例と同様)。Connection.close
最終的にすべてのSQL呼び出しのブロックに入るまで、SQLデータベースの接続制限に何度も到達しました。さらに面白くするために、データベースへの呼び出しが多すぎるのを防ぐためにJava側でのロックを必要とする長時間実行されているOracleストアドプロシージャを呼び出しました。
おそらく、潜在的なメモリリークの最も単純な例の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
か()?その参照は巨大なオブジェクトを存続させるかもしれません...
不要になったオブジェクトへの参照を保持するたびに、メモリリークが発生します。Javaでメモリリークがどのように発生するか、およびJavaでできることの例については、Javaプログラムでのメモリリークの処理を参照してください。
...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language.
私はあなたがその結論をどのように描いているのかわかりません。定義によっては、Javaでメモリリークを作成する方法は少なくなります。それは間違いなくまだ有効な質問です。
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();
}
}
}
私の回答をここからコピーできます: Javaでメモリリークを発生させる最も簡単な方法は?
「コンピューターサイエンスにおけるメモリーリーク(またはこのコンテキストにおけるリーク)は、コンピュータープログラムがメモリーを消費したが、それをオペレーティングシステムに解放できない場合に発生します。」(ウィキペディア)
簡単な答えは次のとおりです。できません。Javaは自動メモリ管理を実行し、不要なリソースを解放します。これを防ぐことはできません。常にリソースを解放できます。手動メモリ管理を使用するプログラムでは、これは異なります。malloc()を使用してCでメモリを取得することはできません。メモリを解放するには、mallocが返したポインタが必要であり、その上でfree()を呼び出します。しかし、ポインタがなくなった(上書きされた、または寿命を超えた)場合、残念ながらこのメモリを解放できず、メモリリークが発生します。
これまでの他のすべての答えは私の定義にあり、実際にはメモリリークではありません。それらはすべて、無意味なものでメモリを高速に埋めることを目的としています。しかし、いつでも作成したオブジェクトを逆参照してメモリを解放することができます->リークなし。acconradの答えはかなり近いですが、彼の解決策は事実上、ガベージコレクターを無限ループで強制的に「クラッシュ」させることなので、認めざるを得ません。
長い答えは次のとおりです。JNIを使用してJava用のライブラリを作成すると、メモリリークが発生する可能性があります。JNIは、手動でメモリを管理できるため、メモリリークが発生する可能性があります。このライブラリを呼び出すと、Javaプロセスでメモリリークが発生します。または、JVMにバグがあり、JVMがメモリを失う可能性があります。おそらくJVMにバグがあります。ガベージコレクションはそれほど簡単ではないため、既知のバグが存在することもありますが、それでもバグです。設計上、これは不可能です。あなたはそのようなバグによって影響を受けるいくつかのJavaコードを求めているかもしれません。申し訳ありませんが私はそれを知りません。それはとにかく次のJavaバージョンではもうバグではないかもしれません。
これは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インスタンスが破棄された後でも、元の長い文字列と派生した部分文字列の両方がメモリに保持されます。
muchSmallerString
(ためfreeedれるStringLeaker
オブジェクトが破棄される)、長い文字列はよくとして解放されます。私がメモリリークと呼んでいるのは、JVMのこのインスタンスでは解放できないメモリです。ただし、メモリを解放する方法を示しましたthis.muchSmallerString=new String(this.muchSmallerString)
。実際のメモリリークでは、何もできません。
intern
ケースは、「メモリリーク」ではなく「メモリサプライズ」の可能性があります。.intern()
ただし、部分文字列を使用すると、長い文字列への参照が保持され、解放できないという状況が確実に発生します。
GUIコードでのこの一般的な例は、ウィジェット/コンポーネントを作成し、静的/アプリケーションスコープオブジェクトにリスナーを追加し、ウィジェットが破棄されたときにリスナーを削除しない場合です。メモリリークが発生するだけでなく、イベントをリッスンしているイベントが発生すると、古いリスナーもすべて呼び出されるため、パフォーマンスが低下します。
任意のサーブレットコンテナ(Tomcat、Jetty、Glassfishなど)で実行されている任意のWebアプリケーションを取得します。アプリを10回または20回続けて再デプロイします(サーバーのautodeployディレクトリにあるWARを単にタッチするだけで十分な場合があります)。
誰かが実際にこれをテストしていない限り、アプリケーションはそれ自体をクリーンアップするための注意を払っていなかったため、数回の再デプロイメント後にOutOfMemoryErrorが発生する可能性が高くなります。このテストでは、サーバーのバグを見つけることもできます。
問題は、コンテナの寿命がアプリケーションの寿命よりも長いことです。コンテナーがアプリケーションのオブジェクトまたはクラスに対して持つ可能性のあるすべての参照がガベージコレクションできることを確認する必要があります。
Webアプリケーションのアンデプロイを切り抜けた参照が1つしかない場合、対応するクラスローダーと、結果としてWebアプリケーションのすべてのクラスはガベージコレクションされません。
アプリケーションによって開始されたスレッド、ThreadLocal変数、ロギングアペンダーは、クラスローダーリークを引き起こす可能性がある通常の疑いの一部です。
多分JNIを通して外部のネイティブコードを使用することによって?
純粋なJavaでは、ほとんど不可能です。
しかし、これは「標準」タイプのメモリリークに関するものであり、メモリにアクセスできなくなっても、アプリケーションによって所有されます。代わりに、未使用のオブジェクトへの参照を保持するか、後でオブジェクトを閉じずにストリームを開くことができます。
かつて、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日かかりました...
メモリリークとは:
典型的な例:
オブジェクトのキャッシュは、物事を台無しにする良い出発点です。
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を使用します(最近使用したオブジェクトのみをキャッシュに保持します)。
もちろん、物事をもっと複雑にすることができます:
よく起こること:
このInfoオブジェクトに他のオブジェクトへの参照がある場合、このオブジェクトにも他のオブジェクトへの参照があります。ある意味では、これはある種のメモリリークと考えることもできます(設計が不適切なため)。
内部クラスの例を誰も使用しなかったのは面白いと思いました。内部クラスがある場合; それは本質的に包含クラスへの参照を維持します。もちろん、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はそれが常に存在することを想定しており、コードで到達できない場合は実際にはクリーンアップを試みません。しかし、それは完全に検証されていません。
最近、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によって保持され、メモリリークが発生していました。
インタビュアーはおそらく、以下のコードのような循環参照を探していました(偶然、参照カウントを使用していた非常に古い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コードを記述する必要があります。
静的マップを作成し、ハード参照を追加し続けます。それらは決して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); }
}
クラスの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());
}
}
}
私はまだ誰もこれを言っていないと思います:finalize()がこれの参照をどこかに格納するようにfinalize()メソッドをオーバーライドすることでオブジェクトを復活させることができます。ガベージコレクターはオブジェクトに対して一度だけ呼び出されるため、その後はオブジェクトが破棄されることはありません。
finalize()
呼び出されませんが、参照がなくなるとオブジェクトが収集されます。ガベージコレクタも呼び出されません。
finalize()
メソッドはJVMによって1回しか呼び出せませんが、オブジェクトが復活してから再び逆参照された場合に、そのメソッドがガベージコレクションされないという意味ではありません。finalize()
メソッドにリソースを閉じるコードがある場合、このコードは再度実行されず、メモリリークが発生する可能性があります。
最近、より微妙なリソースリークに遭遇しました。クラスローダーの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を超えません。
かなりシンプルで驚くべきことです。
多くの人が示唆しているように、リソースリークは、JDBCの例のように、かなり簡単に引き起こされます。実際のメモリリークは少し難しいです-特に、JVMの壊れたビットに依存していない場合は...
フットプリントが非常に大きいオブジェクトを作成し、それらにアクセスできないという考えは、実際のメモリリークでもありません。何もアクセスできない場合はガベージコレクションされ、何かがアクセスできる場合はリークではありません...
以前は機能していましたが、それでも機能するかどうかはわかりませんが、3つの深さの循環チェーンを使用する方法があります。オブジェクトAにはオブジェクトBへの参照があり、オブジェクトBにはオブジェクトCへの参照があり、オブジェクトCにはオブジェクトAへの参照があります。GCは、A <-> B -AとBが他からアクセスできないが、3方向チェーンを処理できなかった場合、安全に収集できます...
潜在的に巨大なメモリリークを作成する別の方法は、への参照を保持Map.Entry<K,V>
することTreeMap
です。
これがTreeMap
sにのみ適用される理由を評価することは困難ですが、実装を調べると、その理由は次のようになります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
グラフから、メモリが増加し続ける様子を確認できます。
ハッシュされたデータ構造(HashMap
)では同じことが起こりません。
これは、を使用した場合のグラフHashMap
です。
ソリューション?を保存するのではなく、(おそらくすでに行っているように)キー/値を直接保存するだけですMap.Entry
。
スレッドは、終了するまで収集されません。それらはガベージコレクションのルーツとして機能します。それらは、単にそれらを忘れたり、それらへの参照をクリアしたりするだけでは回収されない数少ないオブジェクトの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
は決して死にません。
(*編集*)
有効な例は、スレッドがプールされている環境でThreadLocal変数を使用することだと思います。
たとえば、サーブレットでThreadLocal変数を使用して他のWebコンポーネントと通信し、コンテナによってスレッドが作成され、プール内のアイドルスレッドを維持します。ThreadLocal変数は、正しくクリーンアップされない場合、おそらく同じWebコンポーネントがそれらの値を上書きするまでそこに存在します。
もちろん、特定されれば、問題は簡単に解決できます。
インタビュアーは循環参照ソリューションを探している可能性があります。
public static void main(String[] args) {
while (true) {
Element first = new Element();
first.next = new Element();
first.next.next = first;
}
}
これは、参照カウントガベージコレクターの典型的な問題です。次に、JVMはこの制限のないはるかに洗練されたアルゴリズムを使用することを丁寧に説明します。
-ウェスタール
first
は役に立たないため、ガベージコレクションする必要があります。で参照カウント(単独で)その上にアクティブな参照があるため、ガベージコレクタ、オブジェクトがfreeedされないであろう。無限ループは、リークを示すためにここにあります。プログラムを実行すると、メモリが無制限に増加します。