19

我是 Java 新手,正在阅读非常大的文件,需要一些帮助来理解问题并解决它。我们有一些遗留代码必须进行优化才能使其正常运行。文件大小可以从 10mb 到 10gb 不等。只有当文件开始超过 800mb 大小时才会出现问题。

InputStream inFileReader = channelSFtp.get(path); // file reading from ssh.
byte[] localbuffer = new byte[2048];
ByteArrayOutputStream bArrStream = new ByteArrayOutputStream();

int i = 0;
while (-1 != (i = inFileReader.read(buffer))) {
bArrStream.write(localbuffer, 0, i);
}

byte[] data = bArrStream.toByteArray();
inFileReader.close();
bos.close();

我们得到了错误

java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:2271)
    at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:113)
    at java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:93)
    at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:140)

任何帮助,将不胜感激?

4

14 回答 14

22

尝试使用java.nio.MappedByteBuffer

http://docs.oracle.com/javase/7/docs/api/java/nio/MappedByteBuffer.html

您可以将文件的内容映射到内存,而无需手动复制。高级操作系统提供内存映射,Java 具有利用该功能的 API。

如果我的理解是正确的,内存映射不会将文件的全部内容加载到内存中(意思是“根据需要部分加载和卸载”),所以我猜一个 10GB 的文件不会占用你的内存。

于 2013-08-29T11:17:20.960 回答
13

即使您可以增加 JVM 内存限制,但它是不必要的,并且分配像 10GB 这样的巨大内存来处理文件听起来有点过大和资源密集型。

目前您正在使用“ByteArrayOutputStream”,它保留一个内部存储器来保存数据。代码中的这一行不断将最后读取的 2KB 文件块附加到此缓冲区的末尾:

bArrStream.write(localbuffer, 0, i);

bArrStream 不断增长,最终您的内存不足。

相反,您应该重新组织算法并以流方式处理文件:

InputStream inFileReader = channelSFtp.get(path); // file reading from ssh.
byte[] localbuffer = new byte[2048];

int i = 0;
while (-1 != (i = inFileReader.read(buffer))) {
    //Deal with the current read 2KB file chunk here
}

inFileReader.close();
于 2015-07-31T09:58:05.963 回答
7

Java 虚拟机 (JVM) 以固定的内存上限运行,您可以对其进行修改:

java -Xmx1024m ....

例如,上面的选项 (-Xmx...) 将限制设置为 1024 兆字节。您可以根据需要进行修改(在您的机器、操作系统等范围内)。请注意,这与传统应用程序不同,传统应用程序会根据需要从操作系统分配越来越多的内存。

然而,更好的解决方案是重新设计您的应用程序,这样您就不需要一次将整个文件加载到内存中。这样您就不必调整 JVM,也不会强加巨大的内存占用。

于 2013-08-29T10:46:55.267 回答
5

您无法在内存中读取 10GB 文本文件。您必须先阅读 X MB,然后对其进行处理,然后再阅读下一个 X MB。

于 2013-08-29T10:47:48.210 回答
5

使用命令行选项 -Xmx 运行 Java,该选项设置堆的最大大小。

详情见这里。。

于 2015-08-04T08:02:49.947 回答
4

问题在于你正在做的事情。将整个文件读入内存始终是个坏主意。除非你有一些非常惊人的硬件,否则你真的无法用当前的技术将 10GB 的文件读入内存。找到一种方法来逐行、逐条记录、逐块处理它们,...

于 2013-08-29T10:54:08.237 回答
4

尝试使用大缓冲区读取大小可能为 10 mb 然后检查。

于 2013-08-29T10:52:07.790 回答
4

是否必须获取整个ByteArray()输出流?

byte[] data = bArrStream.toByteArray();

最好的方法是逐行读取并逐行写入。您可以使用BufferedReaderScanner读取大文件,如下所示。

import java.io.*;
import java.util.*;

public class FileReadExample {
  public static void main(String args[]) throws FileNotFoundException {
    File fileObj = new File(args[0]);

    long t1 = System.currentTimeMillis();
    try {
        // BufferedReader object for reading the file
        BufferedReader br = new BufferedReader(new FileReader(fileObj)); 
        // Reading each line of file using BufferedReader class
        String str;
        while ( (str = br.readLine()) != null) {
            System.out.println(str);
        }
    }catch(Exception err){
        err.printStackTrace();
    }
    long t2 = System.currentTimeMillis();
    System.out.println("Time taken for BufferedReader:"+(t2-t1));

    t1 = System.currentTimeMillis();
    try (
        // Scanner object for reading the file
        Scanner scnr = new Scanner(fileObj);) {
        // Reading each line of file using Scanner class
        while (scnr.hasNextLine()) {
            String strLine = scnr.nextLine();
            // print data on console
            System.out.println(strLine);
        }
    }
    t2 = System.currentTimeMillis();
    System.out.println("Time taken for scanner:"+(t2-t1));

  }
}

您可以在上面的示例中替换System.out为您的。ByteArrayOutputStream

请查看以下文章以获取更多详细信息:阅读大文件

看看相关的 SE 问题:

扫描仪与 BufferedReader

于 2015-07-30T12:43:40.840 回答
3

ByteArrayOutputStream写入内存缓冲区。如果这确实是您希望它工作的方式,那么您必须在输入的最大可能大小之后调整 JVM 堆的大小。此外,如果可能,您甚至可以在开始处理之前检查输入大小,以节省时间和资源。

另一种方法是流式解决方案,其中运行时使用的内存量是已知的(可能是可配置的,但在程序启动之前仍然已知),但它是否可行完全取决于您的应用程序的域(因为您不能使用内存缓冲区),如果您不能/不想更改它,可能还有其余代码的体系结构。

于 2013-08-29T10:51:23.253 回答
3

嗨,我假设您正在阅读大型 txt 文件并且数据是逐行设置的,请使用逐行读取方法。据我所知,您最多可以读取 6GB 可能更多。我强烈建议您尝试这种方法。

数据1 数据2 ...

// Open the file
 FileInputStream fstream = new FileInputStream("textfile.txt");
 BufferedReader br = new BufferedReader(new InputStreamReader(fstream));

  String strLine;

 //Read File Line By Line
 while ((strLine = br.readLine()) != null)   {
  // Print the content on the console
  System.out.println (strLine);
 }

 //Close the input stream
 br.close();

代码片段的引用

于 2015-08-04T04:42:59.173 回答
3

您应该按照以下答案中的说明增加堆大小:

在 Java 中增加堆大小

但请记住,Java 运行时和您的代码也会占用一些空间,因此请添加一些缓冲区以达到所需的最大值。

于 2015-08-04T19:51:30.570 回答
3

逐行迭代读取文件。这将显着减少内存消耗。或者,您可以使用

FileUtils.lineIterator(theFile, "UTF-8");

由 Apache Commons IO 提供。

FileInputStream inputStream = null;
Scanner sc = null;
try {
inputStream = new FileInputStream(path);
sc = new Scanner(inputStream, "UTF-8");
while (sc.hasNextLine()) {
    String line = sc.nextLine();
    // System.out.println(line);
}
// note that Scanner suppresses exceptions
if (sc.ioException() != null) {
    throw sc.ioException();
}
} finally {
if (inputStream != null) {
    inputStream.close();
}
if (sc != null) {
    sc.close();
}

}

于 2015-08-04T06:04:21.363 回答
2

简短的回答,

不做任何事情,您可以将电流限制提高 1.5 倍。这意味着,如果您能够处理 800MB,那么您可以处理 1200MB。这也意味着,如果通过一些技巧java -Xm ....可以移动到当前代码可以处理 7GB 的点,那么您的问题就解决了,因为 1.5 因子会将您带到 10.5GB,假设您的系统上有可用空间并且JVM可以得到它。

长答案:

该错误非常具有自我描述性。您达到了配置的实际内存限制。关于 JVM 的限制有很多猜测,我对此知之甚少,因为我找不到任何官方信息。然而,你会以某种方式受到诸如可用交换、内核地址空间使用、内存碎片等限制的限制。

现在发生的情况是,ByteArrayOutputStream如果您不提供任何大小(这是您的情况),则使用大小为 32 的默认缓冲区创建对象。每当您调用write对象上的方法时,都会启动一个内部机制。似乎与您的错误输出完美匹配的openjdk 实现版本 7u40-b43使用内部方法ensureCapacity来检查缓冲区是否有足够的空间来放置您要写入的字节。如果没有足够的空间,grow则调用另一个内部方法来增加缓冲区的大小。该方法定义了适当的大小并从类中grow调用该方法copyOfArrays做这项工作。缓冲区的适当大小是当前大小和保存所有内容(当前内容和要写入的新内容)所需的大小之间的最大值。copyOf类中的方法Arrays跟随链接)为新缓冲区分配空间,将旧缓冲区的内容复制到新缓冲区并将其返回给grow.

您的问题发生在为新缓冲区分配空间时,经过一些write,您到达了可用内存耗尽的地步:java.lang.OutOfMemoryError: Java heap space

如果我们查看细节,您正在阅读 2048 的块。所以

  • 您的第一次写入将缓冲区的大小从 32 增加到 2048
  • 你的第二个电话将加倍到 2*2048
  • 你的第三次调用将把它带到 2^2*2048,你必须在需要分配之前再写两次。
  • 然后 2^3*2048,在再次分配之前,您将有时间进行 4 次写入。
  • 在某些时候,您的缓冲区大小为 2^18*2048,即 2^19*1024 或 2^9*2^20 (512 MB)
  • 然后 2^19*2048 即 1024 MB 或 1 GB

您的描述中不清楚的是,您可以以某种方式读取高达 800MB 的内容,但无法超越。你必须向我解释。

我希望您的限制恰好是 2 的幂(或者如果我们使用 10 单位的幂,则接近)。在这方面,我希望您立即开始遇到以下问题:256MB、512MB、1GB、2GB 等。

当您达到该限制时,并不意味着您的内存不足,它只是意味着无法分配另一个两倍于您已有缓冲区大小的缓冲区。这种观察为您的工作提供了改进空间:找到可以分配的最大缓冲区大小,并通过调用适当的构造函数预先保留它

ByteArrayOutputStream bArrStream = new ByteArrayOutputStream(myMaxSize);

它的优点是减少了后台内存分配的开销,让您开心。通过这样做,您将能够达到您现在的限制 1.5。这仅仅是因为上次增加缓冲区时,它从当前大小的一半变为当前大小,并且在某些时候,您在内存中同时拥有当前缓冲区和旧缓冲区。但是您将无法超过现在限制的 3 倍。解释完全一样。

话虽如此,我没有任何神奇的建议来解决这个问题,除了按给定大小的块处理数据,一次一个块。另一个好方法是使用 Takahiko Kawasaki 的建议并使用MappedByteBuffer. 请记住,在任何情况下,您都需要至少 10 GB 的物理内存或交换内存才能加载 10 GB 的文件。

于 2015-08-04T21:01:40.730 回答
0

想了想,我决定再写第二个答案。我考虑了提出第二个答案的优点和缺点,优点是值得一试。所以就在这里。

ByteArrayOutputStream大多数建议的考虑因素都忘记了一个给定的事实:在 Java 中可以拥有的数组(包括 )大小有一个内置限制。该限制由int最大值决定,即 2^31 - 1(略小于 2Giga)。这意味着您最多只能读取 2 GB(-1 字节)并将其放入单个ByteArrayOutputStream. 如果 VM 需要更多控制,则数组大小的限制实际上可能更小。

我的建议是使用ArrayListofbyte[]而不是单个byte[]保存文件的全部内容。并且ByteArrayOutputStream在将其放入最终data数组之前删除不必要的放入步骤。这是基于您的原始代码的示例:

InputStream inFileReader = channelSFtp.get(path); // file reading from ssh.

// good habits are good, define a buffer size
final int BUF_SIZE = (int)(Math.pow(2,30)); //1GB, let's not go close to the limit

byte[] localbuffer = new byte[BUF_SIZE];

int i = 0;
while (-1 != (i = inFileReader.read(localbuffer))) {
    if(i<BUF_SIZE){
        data.add( Arrays.copyOf(localbuffer, i) )
        // No need to reallocate the reading buffer, we copied the data
    }else{
        data.add(localbuffer)
        // reallocate the reading buffer
        localbuffer = new byte[BUF_SIZE]
    }
}

inFileReader.close();
// Process your data, keep in mind that you have a list of buffers.
// So you need to loop over the list

简单地运行您的程序应该可以在具有足够物理内存或交换的 64 位系统上正常工作。现在,如果您想加快速度以帮助 VM 在开始时正确调整堆大小,请使用选项-Xms-Xmx. 例如,如果您希望 12GB 的堆能够处理 10GB 的文件,请使用java -Xms12288m -Xmx12288m YourApp

于 2015-08-05T14:57:40.800 回答