我在磁盘中有一个40MB的文件,我需要使用字节数组将其“映射”到内存中。
起初,我认为将文件写入ByteArrayOutputStream是最好的方法,但我发现在复制操作期间的某个时刻需要大约160MB的堆空间。
如果不使用RAM文件大小的三倍,有人知道更好的方法吗?
更新:感谢您的回答。我注意到我可以减少内存消耗,稍微告诉ByteArrayOutputStream的初始大小比原始文件大小要大一些(使用我的代码强制重新分配的确切大小,检查原因)。
还有另一个高内存点:当我使用ByteArrayOutputStream.toByteArray返回byte []时。看看它的源代码,我可以看到它正在克隆数组:
public synchronized byte toByteArray()[] {
return Arrays.copyOf(buf, count);
}
我想我可以扩展ByteArrayOutputStream并重写此方法,以便直接返回原始数组。这里有没有潜在的危险,因为流和字节数组不会被多次使用?
答案 0 :(得分:13)
MappedByteBuffer
可能就是你要找的东西。
我很惊讶它需要这么多RAM来读取内存中的文件。您是否构建了具有适当容量的ByteArrayOutputStream
?如果还没有,则流可以在接近40 MB的末尾时分配一个新的字节数组,这意味着您将拥有一个39 MB的完整缓冲区和两倍大小的新缓冲区。然而,如果流具有适当的容量,则不会有任何重新分配(更快),也不会浪费内存。
答案 1 :(得分:10)
ByteArrayOutputStream
就可以了。当您拨打toByteArray
时,它仍会创建副本,但这只是临时。你真的介意短暂地记忆了吗?
或者,如果您已经知道要开始的大小,您可以创建一个字节数组,并重复从FileInputStream
读取到该缓冲区,直到您获得所有数据。
答案 2 :(得分:5)
如果你真的想将文件映射到内存中,那么FileChannel
是适当的机制。
如果您只想将文件读入一个简单的byte[]
(并且不需要对该数组进行更改以反映回文件),那么只需读入适当大小的byte[]
。来自正常FileInputStream
的1}}就足够了。
Guava已Files.toByteArray()
为您完成所有这些工作。
答案 3 :(得分:3)
有关ByteArrayOutputStream
的缓冲区增长行为的解释,请阅读this answer。
在回答您的问题时, 可以安全地扩展ByteArrayOutputStream
。在您的情况下,最好覆盖写入方法,以便最大额外分配限制为16MB。您不应覆盖toByteArray
以公开受保护的buf []成员。这是因为流不是缓冲区;流是具有位置指针和边界保护的缓冲区。因此,从类外部访问和潜在地操纵缓冲区是危险的。
答案 4 :(得分:2)
如果你有40 MB的数据,我没有看到任何理由为什么创建一个byte []需要超过40 MB。我假设您正在使用不断增长的ByteArrayOutputStream,它在完成时会创建一个byte []副本。
您可以尝试一次性读取旧文件。
File file =
DataInputStream is = new DataInputStream(FileInputStream(file));
byte[] bytes = new byte[(int) file.length()];
is.readFully(bytes);
is.close();
如果你可以直接使用ByteBuffer,那么使用MappedByteBuffer会更有效并且避免使用数据副本(或者使用堆很多),但是如果你必须使用byte []则不太可能有用。
答案 5 :(得分:2)
...但我发现在复制操作期间的某个时刻需要大约160MB的堆空间
我发现这非常令人惊讶......我怀疑你是在正确测量堆使用情况。
我们假设您的代码是这样的:
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("somefile"));
ByteArrayOutputStream baos = new ByteArrayOutputStream(); /* no hint !! */
int b;
while ((b = bis.read()) != -1) {
baos.write((byte) b);
}
byte[] stuff = baos.toByteArray();
现在,ByteArrayOutputStream管理其缓冲区的方式是分配一个初始大小,并在填充缓冲区时(至少)将缓冲区加倍。因此,在最坏的情况下,baos
可能使用高达80Mb的缓冲区来保存40Mb文件。
最后一步分配一个精确baos.size()
字节的新数组来保存缓冲区的内容。那是40Mb。因此,实际使用的峰值内存量应为120Mb。
那么这些额外的40Mb在哪里使用?我的猜测是它们不是,并且您实际上是在报告总堆大小,而不是可达对象占用的内存量。
那么解决方案是什么?
您可以使用内存映射缓冲区。
您可以在分配ByteArrayOutputStream
时给出尺寸提示; e.g。
ByteArrayOutputStream baos = ByteArrayOutputStream(file.size());
您可以完全省略ByteArrayOutputStream
并直接读入字节数组。
byte[] buffer = new byte[file.size()];
FileInputStream fis = new FileInputStream(file);
int nosRead = fis.read(buffer);
/* check that nosRead == buffer.length and repeat if necessary */
选项1和2在读取40Mb文件时应具有40Mb的峰值内存使用量;即没有浪费的空间。
如果您发布了代码并描述了测量内存使用情况的方法,那将会很有帮助。
我想我可以扩展ByteArrayOutputStream并重写此方法,以便直接返回原始数组。这里有没有潜在的危险,因为流和字节数组不会被多次使用?
潜在的危险是您的假设不正确,或者由于其他人在不知情的情况下修改您的代码而导致 错误...
答案 6 :(得分:2)
Google Guava ByteSource似乎是内存缓冲的不错选择。与ByteArrayOutputStream
或ByteArrayList
(来自Colt Library)的实现不同,它不会将数据合并到一个巨大的字节数组中,而是分别存储每个块。一个例子:
List<ByteSource> result = new ArrayList<>();
try (InputStream source = httpRequest.getInputStream()) {
byte[] cbuf = new byte[CHUNK_SIZE];
while (true) {
int read = source.read(cbuf);
if (read == -1) {
break;
} else {
result.add(ByteSource.wrap(Arrays.copyOf(cbuf, read)));
}
}
}
ByteSource body = ByteSource.concat(result);
ByteSource
可以在以后随时读作InputStream
:
InputStream data = body.openBufferedStream();
答案 7 :(得分:2)
我想我可以扩展ByteArrayOutputStream并重写此方法,以便直接返回原始数组。这里有没有潜在的危险,因为流和字节数组不会被多次使用?
您不应该更改现有方法的指定行为,但添加新方法完全没问题。这是一个实现:
/** Subclasses ByteArrayOutputStream to give access to the internal raw buffer. */
public class ByteArrayOutputStream2 extends java.io.ByteArrayOutputStream {
public ByteArrayOutputStream2() { super(); }
public ByteArrayOutputStream2(int size) { super(size); }
/** Returns the internal buffer of this ByteArrayOutputStream, without copying. */
public synchronized byte[] buf() {
return this.buf;
}
}
从任何 ByteArrayOutputStream获取缓冲区的另一种但是hackish方法是使用它的writeTo(OutputStream)
方法将缓冲区直接传递给提供的OutputStream这一事实:
/**
* Returns the internal raw buffer of a ByteArrayOutputStream, without copying.
*/
public static byte[] getBuffer(ByteArrayOutputStream bout) {
final byte[][] result = new byte[1][];
try {
bout.writeTo(new OutputStream() {
@Override
public void write(byte[] buf, int offset, int length) {
result[0] = buf;
}
@Override
public void write(int b) {}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
return result[0];
}
(这很有效,但我不确定它是否有用,因为Subteing ByteArrayOutputStream更简单。)
然而,从你的问题的其余部分来看,听起来你想要的只是文件完整内容的简单byte[]
。从Java 7开始,最简单,最快速的方法是调用Files.readAllBytes
。在Java 6及更低版本中,您可以使用DataInputStream.readFully
,如Peter Lawrey's answer中所示。无论哪种方式,您将获得一个以正确大小分配一次的数组,而不会重复重新分配ByteArrayOutputStream。
答案 8 :(得分:0)
...在阅读1GB文件时也有同样的观察结果:Oracle的ByteArrayOutputStream具有惰性内存管理。 字节数组由int索引,无论如何限制为2GB。如果不依赖第三方,您可能会觉得这很有用:
static public byte[] getBinFileContent(String aFile)
{
try
{
final int bufLen = 32768;
final long fs = new File(aFile).length();
final long maxInt = ((long) 1 << 31) - 1;
if (fs > maxInt)
{
System.err.println("file size out of range");
return null;
}
final byte[] res = new byte[(int) fs];
final byte[] buffer = new byte[bufLen];
final InputStream is = new FileInputStream(aFile);
int n;
int pos = 0;
while ((n = is.read(buffer)) > 0)
{
System.arraycopy(buffer, 0, res, pos, n);
pos += n;
}
is.close();
return res;
}
catch (final IOException e)
{
e.printStackTrace();
return null;
}
catch (final OutOfMemoryError e)
{
e.printStackTrace();
return null;
}
}