编辑/警告:此解决方案存在潜在的陷阱,因为它大量使用,并且不清楚如何/何时发布相应的资源。请参阅此问答和JDK-4724038:(fs)将unmap方法添加到MappedByteBuffer。MappedByteBuffer
话虽如此,也请参阅本文的结尾
我会完全按照Nim的建议去做:
将其包装在一个类中,该类映射在“块”中,然后在编写时移动块。这样做的算法相当简单。只需选择一个对您正在写入的数据有意义的块大小。
事实上,我几年前就这样做了,只是挖掘了代码,它是这样的(在演示中剥离到最低限度,使用单一方法来写入数据):
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
public class SlidingFileWriterThingy {
private static final long WINDOW_SIZE = 8*1024*1024L;
private final RandomAccessFile file;
private final FileChannel channel;
private MappedByteBuffer buffer;
private long ioOffset;
private long mapOffset;
public SlidingFileWriterThingy(Path path) throws IOException {
file = new RandomAccessFile(path.toFile(), "rw");
channel = file.getChannel();
remap(0);
}
public void close() throws IOException {
file.close();
}
public void seek(long offset) {
ioOffset = offset;
}
public void writeBytes(byte[] data) throws IOException {
if (data.length > WINDOW_SIZE) {
throw new IOException("Data chunk too big, length=" + data.length + ", max=" + WINDOW_SIZE);
}
boolean dataChunkWontFit = ioOffset < mapOffset || ioOffset + data.length > mapOffset + WINDOW_SIZE;
if (dataChunkWontFit) {
remap(ioOffset);
}
int offsetWithinBuffer = (int)(ioOffset - mapOffset);
buffer.position(offsetWithinBuffer);
buffer.put(data, 0, data.length);
}
private void remap(long offset) throws IOException {
mapOffset = offset;
buffer = channel.map(FileChannel.MapMode.READ_WRITE, mapOffset, WINDOW_SIZE);
}
}
下面是一个测试代码段:
SlidingFileWriterThingy t = new SlidingFileWriterThingy(Paths.get("/tmp/hey.txt"));
t.writeBytes("Hello world\n".getBytes(StandardCharsets.UTF_8));
t.seek(1000);
t.writeBytes("Are we there yet?\n".getBytes(StandardCharsets.UTF_8));
t.seek(50_000_000);
t.writeBytes("No but seriously?\n".getBytes(StandardCharsets.UTF_8));
输出文件的外观:
$ hexdump -C /tmp/hey.txt
00000000 48 65 6c 6c 6f 20 77 6f 72 6c 64 0a 00 00 00 00 |Hello world.....|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
000003e0 00 00 00 00 00 00 00 00 41 72 65 20 77 65 20 74 |........Are we t|
000003f0 68 65 72 65 20 79 65 74 3f 0a 00 00 00 00 00 00 |here yet?.......|
00000400 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
02faf080 4e 6f 20 62 75 74 20 73 65 72 69 6f 75 73 6c 79 |No but seriously|
02faf090 3f 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |?...............|
02faf0a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
037af080
我希望我没有通过删除不必要的位和重命名来破坏一切......至少偏移量计算看起来是正确的(0x3e0 + 8 = 1000,0x02faf080 = 50000000)。
文件占用的块数(左列)以及另一个相同大小的非稀疏文件:
$ head -c 58388608 /dev/zero > /tmp/not_sparse.txt
$ ls -ls /tmp/*.txt
8 -rw-r--r-- 1 nug nug 58388608 Jul 19 00:50 /tmp/hey.txt
57024 -rw-r--r-- 1 nug nug 58388608 Jul 19 00:58 /tmp/not_sparse.txt
块的数量(和实际的“稀疏性”)将取决于操作系统和文件系统,以上是在Debian Buster上,ext4 - 稀疏文件在macOS的HFS+上不受支持,在Windows上,它们需要程序做一些我不知道的特定的事情,但这似乎并不容易,甚至从Java中也不可行,不确定。
我没有新的数字,但当时这种“滑动技术”非常快,正如您在上面看到的,它确实在文件中留下了漏洞。
你需要适应一些对你有意义的东西,添加你需要的所有方法,也许通过包装,任何适合你的方法。此外,在此状态下,它将根据需要扩展文件,但可能需要调整块。MappedByteBuffer
WINDOW_SIZE
writeThingy
writeBytes
WINDOW_SIZE
除非有很好的理由不这样做,否则最好使用这种单一机制保持简单,而不是维护复杂的双模系统。
关于脆弱性和内存消耗,我已经在Linux上运行了下面的压力测试,没有任何问题,在具有800GB RAM的计算机上,以及另一个具有1G RAM的非常适度的VM上。系统看起来非常健康,Java进程不使用任何大量的堆内存。
String path = "/tmp/data.txt";
SlidingFileWriterThingy w = new SlidingFileWriterThingy(Paths.get(path));
final long MAX = 5_000_000_000L;
while (true) {
long offset = 0;
while (offset < MAX) {
offset += Math.pow(Math.random(), 4) * 100_000_000;
if (offset > MAX/5 && offset < 2*MAX/5 || offset > 3*MAX/5 && offset < 4*MAX/5) {
// Keep 2 big "empty" bands in the sparse file
continue;
}
w.seek(offset);
w.writeBytes(("---" + new Date() + "---").getBytes(StandardCharsets.UTF_8));
}
w.seek(0);
System.out.println("---");
Scanner output = new Scanner(new ProcessBuilder("sh", "-c", "ls -ls " + path + "; free")
.redirectErrorStream(true).start().getInputStream());
while (output.hasNextLine()) {
System.out.println(output.nextLine());
}
Runtime r = Runtime.getRuntime();
long memoryUsage = (100 * (r.totalMemory() - r.freeMemory())) / r.totalMemory();
System.out.println("Mem usage: " + memoryUsage + "%");
Thread.sleep(1000);
}
所以,是的,这是经验性的,也许它只能在最近的Linux系统上正常工作,也许它只是运气好,因为特定的工作负载......但我开始认为这是某些系统和工作负载的有效解决方案,它可能很有用。