仮想メモリ全体(テラバイト単位)よりも大きなファイルのメモリ マッピングを(純粋な Java で) エミュレートするオブジェクトが必要です。
/** interface to memory-mapped file emulator that can read/write to mapped file
* and return multiple views on it. */
public interface FileView {
/** returns single byte of mapped file at position (long index) */
byte getByte (long pos);
/** sets single byte of mapped file at position (long index) to a value */
void setByte (long pos, byte value);
/** returns ByteBuffer-like class exposing bytes of mapped file
* in specified range (from - to, long indices).
* Length of that buffer can be arbitrary limited (perhaps to 50MB) */
XxxByteBuffer getBuffer (long from, long to);
/** register new range of bytes that needs to be listened
* and in case of change, listener should get informed.
* RangeChangedListener takes two arguments in constructor: long from, long to */
void addRangeChangedListener(RangeChangedListener l);
}
私の質問 (6) は大きなヘッダーの下にあり、ここにコンテキスト情報が表示されます。
返された ByteBuffer のようなクラスの各インスタンスは、互いに一貫性を保つ必要があります (重複する領域に同じ内容が含まれる)。そのため、バッファの 1 つが変更された場合、他のバッファが重複している場合は適切に変更する必要があります。さらに、リッスンされた領域で変更が発生した場合、リスナーに通知する必要があります(バッファーの場合、同期とリッスンは単一のメカニズムである必要があります。プロキシ ビューの場合、同期に問題はありませんが、リスナーの問題は残ります)。この同期は「超高性能」である必要はありませんが、信頼できるものでなければなりません。オーバーラップするバッファーは多数あると思いますが、変更はそれほど多くありません。ほとんどの場合、「FileView」から小さなバッファー (2kb としましょう) を取得すると思います。
個々の書き込みはすべて、何らかの方法でメモリに格納する必要があります (バッファへの書き込みは別として)。ユーザーが「SAVE」をクリックするまで、ファイルへの直接書き込みは許可されません。もちろん、それは長く続くことはできません - メモリに 200MB 以上をキャッシュする必要はないと思います (変更の巨大なブロックは、ディスク上の一時ファイルにキャッシュできます)。これらの個々の変更は、マップされたファイル全体に広がる可能性があります。ファイル内の隣接する位置が変更された場合、変更を集約できます。
したがって、FileView は次のことを行う必要があります。
- 現在使用されているデータをメモリにバッファします。
- 個々の変更 (書き込み) をすべてキャッシュに保存します。
- 必要に応じて ByteBuffer のようなオブジェクトを公開し (ファイルから新しいデータを読み取るか、メモリから既存のデータを取得することにより)、それらに適切なキャッシュされた変更を適用します (変更が存在する場合)。
- ByteBuffer のようなオブジェクトが重なっている場合、これらのオブジェクトを互いに同期させます。
- メモリクリーンデータ(現在使用されておらず、変更されていない)から破棄(メモリを保持するため)
- 現在使用されていないときにメモリから「ダーティデータ」を安全に破棄します。これを行うには、変更が保存されていることを確認してから、クリーンであるかのようにデータを破棄する必要があります
- 現在使用されているメモリ量を最大と比較して監視し、メモリが不足している場合は、できるだけ多くを破棄してみてください。
2 つの大きなパフォーマンスの問題があります。
1. スタンドアロンのバッファとしてではなく、ビューとして新しいデータを取得する
次の呼び出しの後:
fileView.getBuffer(0,200000000);
fileView.getBuffer(0,200000789);
200MB チャンクのデータの 2 つのスタンドアロン コピーがあります。また、連続して呼び出すと、同じデータの次のコピーが生成されます。おそらく、fileView は新しいバッファではなく、既存の内部バッファのビューを返す必要がありますか? しかし、内部バッファを生成する方法は? 「パッチワーク」になります。fileView は、必要に応じて内部的にバッファーを作成し、何らかの形で接着およびパッチされたこれらの内部バッファーのビューを返します。以下を見てください - 既存の内部バッファは 4 回目の呼び出しで再利用 (パッチ) されます:
fileView.getBuffer(0,2); // {0,1,2}
fileView.getBuffer(4,5); // {4,5}
fileView.getBuffer(8,12); // {8,9,10,11,12}
fileView.getBuffer(0,16); // {0,1,2} {3} {4,5} {6,7} {8,9,10,11,12} {13,14,15,16}
2. ビューを破壊せずに内部データを取り除く (GC それら)
メモリが少ない場合は、次のことを行う必要があります。
- 変更されていないバッファーを GC します (後で HD から再度読み取ることができるため)。
- 変更 (書き込み) をキャッシュに保存し、GC で変更されたバッファーにも保存します。
このようにして、読み取りデータを完全に取り除くことができます (ただし、変更 (書き込み) は別のキャッシュに保持します)。しかし...
外部コンポーネントに返されるバッファ ビューがあります。これらのビューには内部バッファへの参照があるため、内部バッファを GC できません。そして、これらの外部ビューを破壊したくはありません - それらはそのままにしておくべきです - 内部データを取り除き、後で必要に応じて (プロキシのように) 埋め戻したいだけです。
.
上記の状況に対するアーキテクチャ的に優れたソリューションを探しています。そして、ここに私が正確に知る必要があるものがあります:
.
スタンドアロンのバッファとしてではなく、ビューとして新しいデータを取得する方法は? したい:
- 複数のバッファで同じデータの複数のスタンドアロン コピーを避けるため、ビューが必要です
- fileView.getBuffer の後 (long from, long to); ByteBuffer と機能的に似たものを返す
- 裸の ByteBuffers と比較してパフォーマンスをあまり損なわないでください たぶん、fileView の内部バッファーにプロキシ ビューを実装し、bytebuffer に似たインターフェイスを持つ真新しいクラスのインスタンスを返す必要がありますか?
ロードされているものとロードされていないものを追跡するのに適切なデータ構造はどれですか? TreeMap と、リーフ内にバッファが保持されたリーフの LinkedList を組み合わせたものでしょうか。
外部ビューを破壊せずに内部データ (GC) を取り除く方法は?
重複領域で返されたバッファ/ビューの内容を同期する (= 等しいことを保証する) 方法は? ビューの場合、すべてのビューが同じ基本構造を参照するため、この問題はなくなります。
個々の変更 (書き込み) を効率的に保存し、ファイルからデータをリロードするときに新しく返されたバッファ/ビューにそれらを再適用する方法は? すべての変更は 1 バイトの長さである場合もありますが、数メガバイトの長さになる場合もあります。
選択した範囲のバイトを適切なリスナーに効率的にマップする方法 (単一のリスナーは、定義された範囲のバイトの変更をリッスンします。バッファー全体ではありません)。
.
まだ読んでいる方のために、私が何をしているかについての追加の詳細を提供できます:)
私はキャッシングについてほとんど何も知らず、バッファについてはほとんど知りませんが...
内部にスクリプト エンジン (DSL) と GUI からデータへのバインディングを備えた 16 進エディターを作成しようとしています。目的は、データの分析と回復です。私が選んだ言語は Java + Groovy です。プロジェクトの一部は現在進行段階にあり、ByteBuffers、CharBuffers などに基づいています。
私のアプリケーションには、次のビジネス上の制約があります。
- アプリは非常に大きなファイル (HD のバックアップ ファイル) を開くことができます。
- ユーザーは、開いているファイルを表示し、スクロールして変更できます (16 進エディターのように)。
- スクリプト エンジンはファイルへのランダム アクセスが可能で、ファイルを処理 (読み取り/書き込み/バインド) できます。
- データはスクリプトによって GUI にバインドできます。つまり、バイト 100000045 から 100000048 を INTEGER として処理し、JTextField に表示できます。これらのバイトが変更されると、JTextField も変更されます。誰かがテキスト フィールドを変更すると、基になるデータ (ファイル内) が変更されます。
- [保存] をクリックするまで、ファイルはそのままである必要があります。
スクリプトは開かれたファイルのコンテキストで厳密に実行され、その主なタスクは (a) そのファイルの読み取りと書き込み (b) そのファイルに関する情報の出力 (c) ファイルの領域を GUI のフィールドにバインドすることです。
2 種類のニーズがあると思います (スクリプトの種類ですが、ややあいまいです)。
絶対スクリプト。ファイル全体と絶対アドレス指定を潜在的に必要とするスクリプト - 何かを探す、つまりパーティション テーブルを見つけてパーティションをチェックすることができます。ファイルを順次処理するとは思いません(ストリームのように)。代わりに、ファイル全体に散在するいくつかの場所にアクセスする必要があると思われますが、これらの場所は大きくはありません-約50MBと思われます.
相対スクリプト。一般的にローカルで動作するスクリプト - 一部のバイト ((ユーザーがマウスまたは他のスクリプトで直接ポイント)) を既知のレイアウトを持つデータのレコードとして解釈し、GUI でフィールドを動的に作成し、データをそのフィールドにバインドできます。
部分的に静的なレイアウトと一部のディスク データ構造を持つファイル タイプが多数あります。たとえば、NTFS ブート セクタは開始近くに静的レイアウトを持っています。
char[8] - "File system ID"
uint16 - "Bytes per sector"
uint8 - "Sectors per cluster"
uint16 - "Reserved sectors"
...
「静的」とは、ファイルからバイトバッファーにバイトを読み取り、最初の 8 バイトをファイルシステム ID を説明する文字として盲目的に解釈し、次の 2 バイトを unsigned int として解釈して、セクターあたりのバイト値などを示すことができることを意味します。したがって、テキスト フィールドを作成し、それらをラベルで記述し、内部にデータを表示できます。