【2023年】Javaのメモリオーバーフローとメモリリークコードのテストと検出

1. 基本的な考え方

  • メモリオーバーフロー:
    • 簡単に言えば、メモリ オーバーフローとは、プログラムの実行中に要求されたメモリがシステムが提供できるメモリを超え、その結果、十分なメモリを適用できなくなり、メモリ オーバーフローが発生することを意味します。
    • メモリ不足例外が発生します
  • メモリリーク:
    • メモリ リークとは、プログラムの実行中に一時変数にメモリが割り当てられることを意味します。使用後は GC によって再利用されません。メモリは常に占有されており、他のプログラムに使用したり割り当てたりすることはできません。これはメモリ リークと呼ばれます (つまり、メモリを占有しているが他のプログラムに割り当てていないのと同じです。メモリを浪費するまで管理することはできません)。
    • メモリ リークは、短期的には大きな影響を与えませんが、メモリ リークが蓄積すると重大な影響を及ぼし、使用可能なメモリを占有し続け、メモリ オーバーフローが発生します

1. メモリリーク

メモリー・リークの根本的な原因は、存続期間の長いオブジェクトが存続期間の短いオブジェクトへの参照を保持していることです。存続期間の短いオブジェクトはもう必要ありませんが、存続期間の長いオブジェクトがそれらのオブジェクトへの参照を保持しているため、リサイクルできません。
メモリ リークは発生方法によって分類され、次の 4 つのカテゴリに分類できます。

  1. メモリリークが頻繁に発生します。メモリ リークのあるコードは複数回実行され、実行されるたびにメモリ リークが発生します。
  2. 散発的なメモリ リーク。メモリ リークを引き起こすコードは、特定の状況または操作手順でのみ発生します。頻繁と散発は相対的なものです。特定の環境では、散発的な状態が定期的な状態になる場合があります。したがって、メモリ リークを検出するには、テスト環境とテスト方法が重要です。
  3. 1 回限りのメモリ リーク。メモリ リークのあるコードは 1 回だけ実行されるか、アルゴリズムの欠陥により常に 1 つのメモリ リークのみが発生します。たとえば、メモリはクラスのコンストラクターで割り当てられますが、デストラクターでは解放されないため、メモリ リークは 1 回だけ発生します。
  4. 暗黙的なメモリ リーク。プログラムは実行中に継続的にメモリを割り当てますが、最後までメモリは解放されません。厳密に言えば、プログラムは最終的に割り当てられたメモリをすべて解放するため、ここでメモリ リークは発生しません。しかし、数日、数週間、さらには数か月にわたって実行する必要があるサーバー プログラムの場合、メモリを期限内に解放しないと、最終的にシステムのメモリがすべて使い果たされてしまう可能性があります。したがって、このタイプのメモリ リークを暗黙的メモリ リークと呼びます。

プログラムを使用するユーザーからすれば、メモリリーク自体は何の害もありませんし、一般ユーザーとしては > > メモリリークの存在を全く感じません。本当に有害なのはメモリ リークの蓄積であり、最終的にはシステムのメモリをすべて消費してしまいます。この観点から見ると、1 回限りのメモリ リークは累積しないため有害ではありませんが、暗黙的なメモリ リークは頻繁で散発的なメモリ リークよりも検出が難しいため、非常に有害です。

2.1. 静的コレクションクラスによって引き起こされるメモリリーク:

HashMap、Vector などの使用は、メモリ リークが最も発生しやすいです。これらの静的変数のライフ サイクルはアプリケーションのライフ サイクルと一致しており、それらが参照するすべてのオブジェクトを解放できず、メモリ リークが発生します。 Vector などで常に使用されます。

Vector<Object> v=new Vector<Object>(100);
for (int i = 1; i<100; i++)
{
    
    
Object o = new Object();
v.add(o);
o = null;
}

この例では、Object オブジェクトを循環的に適用し、適用されたオブジェクトを Vector に配置します。参照自体 (o=null) だけが解放された場合、Vector は引き続きオブジェクトを参照するため、このオブジェクトは GC で再利用できません。したがって、オブジェクトを Vector に追加した後に Vector から削除する必要がある場合、最も簡単な方法は Vector オブジェクトを null に設定することです。

2.2. HashSet 内のオブジェクトのパラメータ値を変更します。パラメータはハッシュ値の計算に使用されるフィールドです。

オブジェクトが HashSet コレクションに格納された後、オブジェクト内のハッシュ値の計算に関連するフィールドを変更すると、オブジェクトのハッシュ値は、コレクションに元々格納されていたものとは異なります。この場合、contains メソッドを使用することはできません。コレクション内のオブジェクトを取得するときに、このオブジェクトが見つかりません。これにより、HashSet から現在のオブジェクトを削除できなくなり、メモリ リークが発生します。例:

public static void main(String[] args){
    
    
        Set<Person> set = new HashSet<>();
        Person p1 = new Person("张三","1",25);
        Person p2 = new Person("李四","2",26);
        Person p3 = new Person("王五","3",27);

        set.add(p1);
        set.add(p2);
        set.add(p3);
        System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!

        System.out.println("修改前的哈希值:"+p3.hashCode());
        p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
        System.out.println("修改后的哈希值:"+p3.hashCode());
        
        set.remove(p3); //此时remove不掉,造成内存泄漏
        set.add(p3); //重新添加,可以添加成功
        
        System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!
        for (Person person : set){
    
    
            System.out.println(person);
        }
}

ログ結果の印刷: 変更前と変更後ではハッシュコードが変更されています。
ここに画像の説明を挿入します

2.3. 各種接続

たとえば、データベース接続 (dataSourse.getConnection())、ネットワーク接続 (ソケット)、および IO 接続は、接続を閉じるために close() メソッドが明示的に呼び出されない限り、GC によって自動的にリサイクルされません。Resultsset オブジェクトと Statement オブジェクトを明示的にリサイクルする必要はありませんが、Connection はいつでも自動的にリサイクルできるわけではなく、Connection がリサイクルされると、Resultset オブジェクトと Statement オブジェクトはすぐに NULL になるため、Connection は明示的にリサイクルする必要があります。ただし、接続プールを使用する場合は状況が異なります。接続を明示的に閉じるだけでなく、Resultset Statement オブジェクトも明示的に閉じる必要があります (いずれかを閉じると、もう一方も閉じられます)。多数の Statement オブジェクトを閉じることができず、解放され、メモリ リークが発生します。この場合、通常、try で接続が行われ、finally で接続が解放されます。

2.4. シングルトンモード

シングルトン オブジェクトが外部オブジェクトへの参照を保持している場合、外部オブジェクトは jvm によって通常どおりリサイクルされず、メモリ リークが発生します。

  不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露,考虑下面的例子:
class A{
    
    
    public A(){
    
    
        B.getInstance().setA(this);
    }
    ....
}
//B类采用单例模式
class B{
    
    
    private A a;
    private static B instance=new B();
    public B(){
    
    }
    
    public static B getInstance(){
    
    
        return instance;
    }
    
    public void setA(A a){
    
    
        this.a=a;
    }
    //getter...
}

2. メモリ オーバーフローの一般的な状況

メモリ オーバーフローには、次のような一般的な状況があります。

1.1、java.lang.OutOfMemoryError: PermGen スペース (永続的なオーバーフロー)

JVM は Java 仮想マシン仕様のメソッド領域を永続化によって実装しており、ランタイム定数プールはメソッド領域に格納されていることがわかっています。したがって、この種のオーバーフローは、ランタイム定数プールのオーバーフローまたは使用法が原因である可能性があります。 jar またはクラスの数が多いと、メソッド領域に保存されたクラス オブジェクトが時間内にリサイクルされないか、クラス情報によって占有されるメモリが設定されたサイズを超えます。

1.2、java.lang.OutOfMemoryError: Java ヒープ領域 (ヒープ オーバーフロー)

この種のオーバーフローの原因は一般に、作成されるオブジェクトが多すぎて、ガベージ コレクションの前にオブジェクトの数が最大ヒープの容量制限に達することです。
この領域の異常を解決する方法は、Dump からのヒープ ダンプ スナップショットをメモリ イメージ解析ツールで解析し、メモリ オーバーフローかメモリ リークかを確認する方法が一般的です。メモリ リークの場合は、ツールを使用して、リークしたオブジェクトから GC ルートまでの参照チェーンを表示し、リークしたコードの場所を特定し、プログラムまたはアルゴリズムを変更できます。リークがない場合は、オブジェクトがメモリ内にメモリがまだ残っている必要がある場合は、仮想マシンのヒープ パラメータ -Xmx (最大ヒープ サイズ) と -Xms (初期ヒープ サイズ) を確認し、それらをマシンの物理メモリと比較して、それらが保存できるかどうかを確認する必要があります。増えた。

  • コードの実装:
    まず、仮想マシンのヒープ パラメーターを設定します: -Xmx (最大ヒープ サイズ) および -Xms (初期ヒープ サイズ)
    ここに画像の説明を挿入します
  • テストコード
    public static void main(String[] args) {
    
    
        List list = new ArrayList();
        for (int i = 0; i < 1024; i++) {
    
    
            System.out.println(i);
            list.add(new byte[1024 * 1024]);
        }
    }

ここに画像の説明を挿入します

1.3. 仮想マシンスタックとローカルメソッドスタックのオーバーフロー

スレッドによって要求されたスタックの深さが、仮想マシンによって許可される最大の深さよりも大きい場合、StackOverflowError がスローされます。

スタックを拡張するときに仮想マシンが十分なメモリ領域を適用できない場合、OutOfMemoryError がスローされます。

メモリ オーバーフローの状況を検出する方法

Jプロファイル

idea を使用している場合は、プラグインまたはアプリで JProfiler を使用して検出および監視できます。

  • 実装のメモリ使用量とオブジェクトが占有するメモリのサイズを表示できます。
    ここに画像の説明を挿入します

ここに画像の説明を挿入します
中国語版ダウンロードリンク:
リンク: https://pan.baidu.com/s/1Qjhx7A7x6vK3VBy5ns2Udg?pwd=8omj
抽出コード: 8omj

マット

  • Eclipse を使用している場合は、検出と監視に対応する MAT ツールを使用できます。

おすすめ

転載: blog.csdn.net/weixin_52315708/article/details/131791255