以前の開発者からいくつかのアプリケーションを引き継いでいます。 Eclipseを介してアプリケーションを実行すると、メモリ使用量とヒープサイズが大幅に増加します。さらに調査したところ、彼らはオブジェクトをループ内で何度も繰り返し作成していることがわかりました。
私は通り抜け始め、いくつかのクリーンアップを行いました。しかし、私が経験すればするほど、「これは実際に何かをしますか?」という質問が増えました。
たとえば、上記のループの外側で変数を宣言し、その値をループで設定するのではなく、オブジェクトをループで作成しました。つまり、
for(int i=0; i < arrayOfStuff.size(); i++) {
String something = (String) arrayOfStuff.get(i);
...
}
versus
String something = null;
for(int i=0; i < arrayOfStuff.size(); i++) {
something = (String) arrayOfStuff.get(i);
}
ボトムループの方がいいと言っても不正確ですか?おそらく私は間違っています。
また、上記の2番目のループの後、「何か」をnullに戻しましたか?それはいくつかのメモリをクリアしますか?
どちらの場合でも、アプリケーションでのメモリ使用量を低く抑えるのに役立つ、メモリ管理のベストプラクティスにはどのようなものがありますか?
更新:
これまで皆さんのフィードバックに感謝します。しかし、私は上記のループについて実際に尋ねていませんでした(ただし、あなたのアドバイスにより、最初のループに戻りました)。私は目を離さないことができるいくつかのベストプラクティスを取得しようとしています。 「コレクションの使用が完了したら、それを一掃する」ということの何か。これらのアプリケーションがメモリを多く使用していないことを確認する必要があります。
VMを裏切ろうとしないでください。最初のループは、パフォーマンスと保守性の両方で推奨されるベストプラクティスです。ループ後に参照をnullに戻すと、メモリがすぐに解放されるとは限りません。可能な限り最小のスコープを使用すると、GCが最も効果的に機能します。
これらのことを(ユーザーの観点から)詳細に説明している本は Effective Java 2 and Implementation Patterns です。
パフォーマンスとVMの内部についてもっと知りたい場合は、講演を見るか、 Brian Goetz から本を読む必要があります。
これら2つのループは、something
のスコープを除いて同等です。詳細は この質問 を参照してください。
一般的なベストプラクティスは?ええと、見てみましょう。正当な理由がない限り、大量のデータを静的変数に格納しないでください。ラージオブジェクトを使い終わったら、コレクションから削除します。そしてそうそう、「測定、推測しないでください」。プロファイラーを使用して、メモリが割り当てられている場所を確認します。
両方のコードサンプルで作成されたオブジェクトはありません。オブジェクト参照を、すでにarrayOfStuffにある文字列に設定するだけです。したがって、メモリに関しては違いはありません。
JVMは、存続期間の短いオブジェクトを解放するのに最適です。不要なオブジェクトを割り当てないようにしてください。ただし、ワークロード、オブジェクトのライフタイム、オブジェクトのサイズを理解するまで、メモリ使用量を最適化することはできません。プロファイラーがこれを教えてくれます。
最後に、避けるべき1番目のことは、ファイナライザを使用しないことです。ファイナライザはガベージコレクションを妨害します。オブジェクトを解放するだけではなく、ファイナライズのためにキューに入れる必要があるためです。ファイナライザを使用しないことが最善です。
Eclipseに表示されるメモリ使用量に関しては、必ずしも関連があるとは限りません。 GCは、空きメモリの量に基づいてその仕事をします。空きメモリが多い場合、アプリがシャットダウンする前に単一のGCが表示されないことがあります。アプリのメモリが不足している場合は、実際のプロファイラーだけがリークや非効率の場所を教えてくれます。
2つのループは基本的に同じ量のメモリを使用します。違いは無視できます。 「何かを文字列化する」ことは、オブジェクト自体への参照ではなく、オブジェクトへの参照を作成するだけなので、使用される追加のメモリはわずかです。さらに、コンパイラー/ JVMと組み合わせると、生成されたコードが最適化される可能性があります。
メモリ管理の実践では、実際にボトルネックがどこにあるかを理解するために、メモリをよりよくプロファイリングする必要があります。特にメモリの大きなチャンクを指す静的参照を探します。これは決して収集されないためです。
弱参照やその他の特殊なメモリ管理クラスも確認できます。
最後に、アプリケーションがメモリを消費する場合、それには理由があるかもしれないことを覚えておいてください...
更新メモリ管理の鍵は、データ構造と、必要なパフォーマンスの量とタイミングです。多くの場合、トレードオフはメモリとCPUサイクルの間です。
たとえば、キャッシュによって大量のメモリが占有される可能性があります。これは、コストのかかる操作を回避しようとしているため、特にパフォーマンスを向上させるために存在します。
したがって、データ構造をよく検討し、必要以上に長くメモリに保持しないようにしてください。それがWebアプリの場合、セッション変数に大量のデータを格納することを避け、メモリの巨大なプールへの静的参照を持たないようにします。
最初のループの方が優れています。なぜなら
しかし、記憶の点からは、これは無関係です。
メモリの問題がある場合は、それが消費されている場所をプロファイルする必要があります。
私の意見では、このようなマイクロ最適化は避けるべきです。彼らは多くの脳周期を要しますが、ほとんどの場合ほとんど影響を与えません。
アプリケーションにはおそらくいくつかの中心的なデータ構造があります。それらはあなたが心配するべきものです。たとえば、基礎となる構造のサイズ変更が繰り返されないようにするために、適切なサイズの見積もりを事前に割り当てます。これは特にStringBuffer
、ArrayList
、HashMap
などに当てはまります。それらの構造へのアクセスをうまく設計して、たくさんコピーする必要がないようにしてください。
適切なアルゴリズムを使用して、データ構造にアクセスします。最下位レベルでは、前述のループのように、Iterator
sを使用するか、少なくとも常に.size()
を呼び出さないようにします。 (はい、リストのサイズを毎回尋ねていますが、ほとんどの場合、そのサイズは変わりません。)ところで、私はMap
sで同様の間違いをよく見ました。人々は、最初にkeySet()
を単に反復するのではなく、entrySet()
およびget
の各値を反復します。メモリマネージャーは、追加のCPUサイクルに感謝します。
上記の1つのポスターが示唆したように、プロファイラーを使用して、プログラムの特定の部分のメモリ(またはCPU)の使用量を推測するのではなく測定します。見つけたものに驚くかもしれません!
これには追加の利点もあります。あなたはあなたのプログラミング言語とあなたのアプリケーションについてもっと理解するでしょう。
私はプロファイリングにVisualVMを使用しています。 jdk/jreディストリビューションが付属しています。
まあ、何かのスコープが小さいので、最初のループは実際にはより良いです。メモリ管理に関しては、大きな違いはありません。
ほとんどのJavaメモリの問題は、オブジェクトをコレクションに格納するときに発生しますが、それらを削除するのを忘れます。それ以外の場合、GCは彼の仕事を非常によくします。
最初の例は問題ありません。ループを介して毎回スタック変数の割り当てと割り当て解除を行うことを除いて、メモリの割り当ては行われていません(非常に安価で迅速)。
その理由は、「割り当てられている」のは参照であり、それは4バイトのスタック変数です(とにかくほとんどの32ビットシステムで)。スタック変数は、スタックの先頭を表すメモリアドレスに追加することで「割り当て」られるため、非常に高速で安価です。
注意が必要なのは、次のようなループの場合です。
for (int i = 0; i < some_large_num; i++)
{
String something = new String();
//do stuff with something
}
実際にはメモリ割り当てを行っているからです。
まだインストールしていない場合は、 Eclipse Test&Performance Tools Platform (TPTP)をインストールすることをお勧めします。ヒープをダンプして検査する場合は、SDK jmapおよびjhat ツールを確認してください。 監視と管理Java SE 6プラットフォームアプリケーション も参照してください。
「ボトムループの方が優れていると言って間違いはありませんか?」、答えはNOです。優れているだけでなく、同じケースが必要です...変数の定義(内容ではありません)はメモリヒープで作成され、制限付き、最初の例では、各ループがこのメモリにインスタンスを作成し、「arrayOfStuff」のサイズが大きい場合、「メモリ不足エラー:Javaヒープスペース」が発生する可能性があります。
あなたが見ていると私が理解していることから、ボトムループは良くありません。理由は、単一の参照(Ex-何か)を再利用しようとしている場合でも、オブジェクト(Ex-arrayOfStuff.get(i))がまだリスト(arrayOfStuff)から参照されているためです。オブジェクトをコレクションの対象にするためには、どこからも参照しないでください。この後のリストの寿命が確かな場合は、別のループ内でオブジェクトをリストから削除/解放することができます。
静的な観点から実行できる最適化(つまり、他のスレッドからこのリストに変更が加えられていない)の場合、size()の呼び出しを繰り返し回避することをお勧めします。これは、サイズが変化することを期待していない場合に、サイズを何度も計算する理由です。結局、それはarray.lengthではなく、list.size()です。