0

Web アプリケーションのユーザーが情報を検証するために必要なデータを含む 2 つの大きな CSV ファイルがあります。ArrayList< String[] > を定義し、ユーザーがログインしてアプリケーションを使用するたびにそれらを読み取る必要がないように、両方のファイルの内容をメモリに保持するつもりでした。

ただし、アプリケーションを初期化して 2 番目のファイルを読み取ろうとすると、java.lang.OutOfMemoryError: Java heap space が発生します。(最初のファイルの読み取りは正常に終了しますが、2番目のファイルの読み取り時にハングし、しばらくするとその例外が発生します)

ファイルを読み取るためのコードは非常に単純です。

ArrayList<String[]> tokenizedLines = new ArrayList<String[]>();

public void parseTokensFile() throws Exception {
    BufferedReader bRead = null;
    FileReader fRead = null;

    try {
        fRead = new FileReader(this.tokensFile);
        bRead = new BufferedReader(fRead);
        String line;
        while ((line = bRead.readLine()) != null) {
            tokenizedLines.add(StringUtils.split(line, fieldSeparator));
        }
    } catch (Exception e) {
        throw new Exception("Error parsing file.");
    } finally {
        bRead.close();
        fRead.close();
    }
}

部分文字列関数は元の文字列を参照するため、Java の分割関数は大量のデータを読み取るときに大量のメモリを使用する可能性があることを読みました。数文字だけが必要なので、これを回避するために単純な分割関数を作成しました。

public String[] split(String inputString, String separator) {
    ArrayList<String> storage = new ArrayList<String>();
    String remainder = new String(inputString);
    int separatorLength = separator.length();
    while (remainder.length() > 0) {
        int nextOccurance = remainder.indexOf(separator);
        if (nextOccurance != -1) {
            storage.add(new String(remainder.substring(0, nextOccurance)));
            remainder = new String(remainder.substring(nextOccurance +  separatorLength));
        } else {
            break;
        }
    }

    storage.add(remainder);
    String[] tokenizedFields = storage.toArray(new String[storage.size()]);
    storage = null;

    return tokenizedFields;

}

ただし、これにより同じエラーが発生するため、メモリリークではなく、メモリ内に非常に多くのオブジェクトを持つ構造を持つことができないのではないかと考えています。1 つのファイルの長さは約 600,000 行で、1 行あたり 5 つのフィールドがあり、もう 1 つのファイルは約 900,000 行の長さで、1 行あたりのフィールド数はほぼ同じです。

完全なスタック トレースは次のとおりです。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at xxx.xxx.xxx.StringUtils.split(StringUtils.java:16)
    at xxx.xxx.xxx.GFTokensFile.parseTokensFile(GFTokensFile.java:36)

それで、長い投稿の後 (申し訳ありません:P)、これは私の JVM に割り当てられたメモリ量の制限ですか、それとも明らかな何かが欠けていて、どこかでリソースを無駄にしていますか?

4

5 に答える 5

4

JVM は、4 GB の RAM を搭載した 32 ビット オペレーティング システムで 2 GB を超えることはありません。それが1つの上限です。

2 つ目は、JVM の起動時に指定する最大ヒープ サイズです。その -Xmx パラメータを見てください。

3 つ目は、X > Y の Y サイズのコンテナーに X 単位の何かを収めることができないという現実です。ファイルのサイズはわかっています。それぞれを個別に解析して、消費しているヒープの種類を確認してください。

Visual VMをダウンロードし、利用可能なすべてのプラグインをインストールして、実行中のアプリケーションを監視することをお勧めします。ヒープ全体、perm gen スペース、GC コレクション、最もメモリを消費しているオブジェクトなどを確認できます。

データを取得することは、すべての問題に非常に役立ちますが、特にこのような問題には役立ちます。それがなければ、あなたはただ推測しています。

于 2012-06-02T14:48:09.123 に答える
2

プログラムの元のバージョンではストレージ リークが見られません。

および同様の方法で大量のストレージがリークする可能性があるシナリオはsplit、かなり限られています。

  1. 分割した元の文字列への参照を保持する必要はありません。

  2. 文字列分割によって生成された文字列のサブセットへの参照を保持する必要があります。

が呼び出されると、元の String のバッキング配列を共有String.substring()する新しい String オブジェクトが作成されます。元の String 参照がガベージ コレクションされると、部分文字列 String は、部分文字列に含まれていない文字を含む文字の配列を保持することになります。これは、部分文字列が保持される期間によっては、ストレージ リークになる可能性があります。

あなたの例では、フィールド区切り文字のためにすべての文字を別々に含む文字列を保持しています。これが実際にスペースを節約している可能性は十分にあります...各部分文字列が独立した文字列である場合に使用されるスペースと比較して。確かに、お使いのバージョンの でsplit問題が解決されないのは当然のことです。

ヒープサイズを増やすか、すべてのデータを同時にメモリに保持する必要がないようにアプリケーションを変更する必要があると思います。

于 2012-06-02T14:59:19.543 に答える
1

コードを改善してみるか、データ処理をデータベースに任せてください。

  1. コードは処理されたデータの冗長コピーを作成するため、メモリ使用量はファイル サイズに応じて大きくなります。処理対象のものと処理済みの部分データがあります。String は不変です。ここを参照してください。結果を格納するためにnew String(...)を使用する必要はありません。split はそのコピーを既に行っています。

  2. 可能であれば、データ ストレージ全体と検索をデータベースに委任します。CSV ファイルはデータベースに簡単にインポート/エクスポートでき、すべての面倒な作業を行います。

于 2012-06-02T15:11:36.050 に答える
0

あなたが行っていることに対して実際の文字列インターンをお勧めしませんが、そのテクニックの背後にあるアイデアを使用するのはどうですか? HashSet または HashMap を使用して、データに同じ文字シーケンスが含まれている場合は常に単一の String インスタンスのみを使用するようにすることができます。つまり、データには何らかの重複があるはずですよね?

一方、ここに表示されているのは、ヒープの断片化の悪いケースである可能性があります。JVM がこれらのケースをどのように処理するかはわかりませんが、Microsoft CLR では、より大きなオブジェクト (特に配列) が別のヒープに割り当てられます。ArrayList などの成長戦略では、より大きな配列が作成され、以前の配列の内容がコピーされてから、その配列への参照が解放されます。ラージ オブジェクト ヒープ (LOH) は CLR で圧縮されないため、この拡張戦略では、ArrayList が使用できなくなる巨大な空きメモリ領域が残ります。

それがどれだけ Lava VM に当てはまるかはわかりませんが、最初に LinkedList を使用してリストを作成してから、リストの内容を ArrayList にダンプするか、配列に直接ダンプすることができます。そうすれば、断片化を引き起こすことなく、行の大きな配列が一度だけ作成されます。

于 2012-06-02T15:20:23.450 に答える
0

両方のファイルの合計の長さがヒープ サイズよりも小さいことを確認してください。JVM オプションを使用して、最大ヒープ サイズを設定できます-Xmx

次に、コンテンツが非常に多い場合は、完全にメモリにロードしないでください。ある時、同様の問題が発生し、大きなファイルに情報のインデックスを格納するインデックス ファイルを使用して修正しました。次に、適切なオフセットで1行を読み取る必要がありました。

また、分割方法にはいくつかの奇妙なことがあります。

String remainder = new String(inputString);

コピーを使用して保持する必要はありませんinputString。文字列は不変であるため、変更は分割メソッドのスコープにのみ適用されます。

于 2012-06-02T15:14:37.317 に答える