4

CentOS で 500K の小さなファイル (それぞれ平均 40K) を作成するための Java コードを作成します。元のコードは次のようになります。

 package MyTest;

 import java.io.*;

 public class SimpleWriter {

public static void main(String[] args) {
    String dir = args[0];
    int fileCount = Integer.parseInt(args[1]);

    String content="@#$% SDBSDGSDF ASGSDFFSAGDHFSDSAWE^@$^HNFSGQW%#@&$%^J#%@#^$#UHRGSDSDNDFE$T#@$UERDFASGWQR!@%!@^$#@YEGEQW%!@%!!GSDHWET!^";
    StringBuilder sb = new StringBuilder();
    int count = 40 * 1024 / content.length();
    int remainder = (40 * 1024) % content.length();
    for (int i=0; i < count; i++)
    {
        sb.append(content);
    }
    if (remainder > 0)
    {
        sb.append(content.substring(0, remainder));
    }

    byte[] buf = sb.toString().getBytes();

    for (int j=0; j < fileCount; j++)
    {
        String path = String.format("%s%sTestFile_%d.txt", dir, File.separator, j);
        try{
            BufferedOutputStream fs = new BufferedOutputStream(new FileOutputStream(path));
            fs.write(buf);
            fs.close();
        }
        catch(FileNotFoundException fe)
        {
            System.out.printf("Hit filenot found exception %s", fe.getMessage());
        }
        catch(IOException ie)
        {
            System.out.printf("Hit IO exception %s", ie.getMessage());

        }

    }
}

  }

次のコマンドを発行してこれを実行できます: java -jar SimpleWriter.jar my_test_dir 500000

これは単純なコードだと思っていましたが、このコードが最大 14G のメモリを使用していることに気付きました。free -m を使用してメモリをチェックすると、15G メモリ VM の空きメモリが 70 MB しか残らないようになるまで、空きメモリが減少し続けたからです。これを Eclipse を使用してコンパイルし、これを JDK 1.6 に対してコンパイルし、次に JDK1.7 に対してコンパイルしました。結果は同じです。面白いことに、fs.write() をコメントアウトして、ストリームを開いたり閉じたりすると、ある時点でメモリが安定しました。fs.write() を元に戻すと、メモリ割り当てが暴走します。500K 40KB のファイルは約 20G です。Java のストリーム ライターは、操作中にバッファの割り当てを解除しないようです。

私はかつて、Java GC にはクリーンアップする時間がないと思っていました。しかし、すべてのファイルのファイル ストリームを閉じたので、これは意味がありません。コードを C# に転送し、Windows で実行すると、CentOS のように 14G を使用せずに、特定の時点でメモリが安定した 500K 40KB のファイルを生成する同じコードが生成されます。少なくとも C# の動作は私が予想していたものですが、Java がこのように動作するとは信じられませんでした。Java経験のある同僚に聞いてみました。彼らはコードに問題は見当たりませんでしたが、なぜこれが起こったのか説明できませんでした。そして彼らは、500K のファイルをループで止めずに作成しようとした人は誰もいないことを認めています。

私もオンラインで検索しましたが、注意を払う必要があるのはストリームを閉じることだけだと誰もが言っています。

誰が何が間違っているのかを理解するのを手伝ってくれますか?

誰でもこれを試して、あなたが見たものを教えてもらえますか?

ところで、このコミュニティの何人かが Windows でコードを試してみましたが、問題なく動作したようです。Windowsでは試していません。人々がJavaを使用する場所だと思ったので、Linuxでのみ試しました。そのため、この問題は Linux で発生したようです)。

JVMヒープを制限するために次のことも行いましたが、効果はありません java -Xmx2048m -jar SimpleWriter.jar my_test_dir 500000

4

2 に答える 2

0

[編集 2:元の回答は、この投稿の最後にイタリック体で残されています]

コメントで説明した後、Windows マシン (Java 1.6) でコードを実行しました。これが私の調査結果です (数値は VisualVM から、タスク マネージャーから見た OS メモリからのものです)。

  • 40K サイズ、500K ファイルへの書き込み (JVM へのパラメータなし) の例: 使用ヒープ: ~4M、合計ヒープ: 16M、OS メモリ: ~16M

  • 40M サイズの例で、500 個のファイルに書き込みます (JVM のパラメーター -Xms128m -Xmx512m。パラメーターがないと、StringBuilder の作成時に OutOfMemory エラーが発生します): 使用ヒープ: ~265M、ヒープ サイズ: ~365M、OS メモリ: ~365M

特に 2 番目の例から、私の元の説明が依然として有効であることがわかります。はい、第 1 世代の空間 (短命のオブジェクト)byte[]に存在するため、ほとんどのメモリが解放されると予想する人もいるでしょうが、これは a) すぐには発生せず、b) GC が開始することを決定したとき (実際には私の環境では発生します)BufferedOutputStreamケース)はい、メモリをクリアしようとしますが、必ずしもすべてではなく、必要なだけメモリをクリアできます。GC は、信頼できる保証を提供しません。

したがって、一般的に言えば、JVM には、快適に感じるだけのメモリを割り当てる必要があります。特別な機能のためにメモリを低く抑える必要がある場合は、元の回答で下に示したコード例のように戦略を試す必要があります。つまり、これらのbyte[]オブジェクトをすべて作成しないでください。

CentOS の場合、JVM の動作がおかしいようです。おそらく、バグのある実装や悪い実装について話すことができます。リーク/バグとして分類するには-Xmx、ヒープを制限するために使用する必要があります。また、一度にすべてのバイトを書き込むだけなので、 Peter Lawrey がBufferedOutputStream(小さなファイルの場合) をまったく作成しないように提案したことを試してください。

それでもメモリ制限を超えている場合は、リークが発生しており、おそらくバグを報告する必要があります。(ただし、まだ文句を言う可能性があり、将来的に最適化される可能性があります)。


[編集 1: 以下の回答は、OP のコードが書き込み操作と同じ数の読み取り操作を実行したと仮定したため、メモリ使用量は正当でした。OPはこれが当てはまらないことを明確にしたため、彼の質問には答えていません

「...私の15GメモリVM...」JVMに多くのメモリを与える場合、なぜGCを実行しようとするのでしょうか? JVM に関する限り、システムからできるだけ多くのメモリを取得し、GC を実行することが適切であると判断した場合にのみ GC を実行することが許可されています。を実行するたびにBufferedOutputStream、デフォルトで 8K サイズのバッファが割り当てられます。JVM は、必要な場合にのみそのメモリを再利用しようとします。これは予期される動作です。システムの観点と JVM の観点から見た空きメモリを混同しないでください。システムに関する限り、メモリーは割り当てられており、JVM がシャットダウンすると解放されます。JVMに関する限り、byte[]割り当てられたすべての配列はBufferedOutputStreamこれは「空き」メモリであり、必要に応じて再利用されます。何らかの理由でこの動作が望ましくない場合は、次のことを試すことができます: クラスを拡張しBufferedOutputStream(クラスを作成するReusableBufferedOutputStreamなど)、新しいメソッドを追加しreUseWithStream(OutputStream os)ます。次に、このメソッドは internal をクリアしbyte[]、以前のストリームをフラッシュして閉じ、使用されている変数をリセットして、新しいストリームを設定します。コードは次のようになります。

// intialize once
ReusableBufferedOutputStream fs = new ReusableBufferedOutputStream();
for (int i=0; i < fileCount; i ++)
{
    String path = String.format("%s%sTestFile_%d.txt", dir, File.separator, i);

    //set the new stream to be buffered and read
    fs.reUseWithStream(new FileOutputStream(path));
    fs.write(this._buf, 0, this._buf.length); // this._buf was allocated once, 40K long contain text
}
fs.close();  // Close the stream after we are done

上記のアプローチを使用すると、多数のbyte[]. ただし、予想される動作に問題は見られません。また、「メモリが多すぎることがわかりました」以外の問題についても言及していません。結局、あなたはそれを使用するように構成しました。]

于 2013-07-21T08:14:03.570 に答える