在java中读取大型数据文件时会产生巨大的内存开销

时间:2017-01-03 18:56:24

标签: java memory

我正在深入学习神经网络开发,使用MNIST数据集进行测试。训练集由60,000个序列组成,每个序列具有784个双精度输入值。将这些数据从文件读入java中的数组的过程在某种程度上会产生大约4GB的内存开销,在整个程序运行期间仍会分配。这个开销是60000 * 784 * 8 = 376MB的补充,它是为双精度阵列本身分配的。看起来很可能发生了这种开销,因为java除了数值数组之外还存储了文件的完整副本,但这可能是扫描器的开销。

根据消息来源,将文件作为流读取可以避免将整个文件存储在内存中。但是,我仍然遇到流读取问题。我使用Java 8和Intellij 2016.2.4。这是流阅读代码:

FileInputStream inputStream = null;
Scanner fileScan = null;
String line;
String[] numbersAsStrings;

totalTrainingSequenceArray = new double[60000][784];

try {
    inputStream = new FileInputStream(m_sequenceFile);
    fileScan = new Scanner(inputStream, "UTF-8");
    int sequenceNum = 0;
    line = fileScan.nextLine();//Read and discard the first line.
    while (fileScan.hasNextLine()) {
        line = fileScan.nextLine();
        numbersAsStrings = line.split("\\s+"); //Split the line into an array of strings using any whitespace delimiter.
        for (int inputPosition = 0; inputPosition < m_numInputs; inputPosition++) {
            totalTrainingSequenceArray[sequenceNum][inputPosition] = Double.parseDouble(numbersAsStrings[inputPosition]);
        }
        sequenceNum++;
    }
    if (fileScan.ioException() != null) {//Handle fileScan exception
        throw fileScan.ioException();
    }
} catch (IOException e) {//Handle the inputstream exception
    e.printStackTrace();
} finally {
    if (inputStream != null)  {
        try {
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if (fileScan != null) {
        fileScan.close();
    }
}

我已经尝试在读取和调用System.gc()之后将流和扫描程序设置为null,但这没有做任何事情。这是扫描仪开销问题吗?在不产生大量永久开销的情况下读取这个大型数据文件最简单的方法是什么?感谢您的任何意见。

2 个答案:

答案 0 :(得分:2)

您的代码运行正常。完整GC后,实际将使用380MB的堆。

Java渴望分配内存以最大限度地减少GC开销,您可以使用-Xmx512m参数或使用其他GC来限制已分配内存的大小 - 例如-XX:+UseConcMarkSweepGC-XX:MaxHeapFreeRatio=40

答案 1 :(得分:1)

定义“开销”。 VM使用分配的堆来平衡垃圾收集时间和执行速度(可以使用一些螺丝来影响其决策)。

规范是允许堆填充直到达到gc阈值的VM,然后收集可以收集的任何垃圾,然后执行contine执行(这简化了很多)。这导致堆使用中的“锯齿”模式(逐渐填充,然后堆使用的突然下降)。对于以一定速率生成垃圾的代码,这是完全正常的。

你可以影响的点是“牙齿”可以建立多高(通过调整允许的堆和/或gc应该开启的时间)。垃圾创建的速度(堆使用量的大幅增加)取决于执行的代码,它可以在从零到可达到的最大分配率的任何范围内。

您的阅读代码属于创建大量小型垃圾对象的类型:扫描仪的线条,您将线条分割成的部分。如果您的堆足够大,则可以在不收集任何垃圾的情况下读取整个文件(很可能是4GB堆设置的情况)。

如果你把堆缩小,VM会更快地收集垃圾,减少内存使用量(同样你可以使用gc参数来强制收集所用堆的较小百分比)。

虽然期望代码仅使用您为数组计算的内存量运行,但这是不合理的。您在任务管理器中看到的只是VM使用的所有内存的累积。这包括堆栈,JRE,本机库和堆所需的任何资源。

堆外的内存可能会有很大差异,具体取决于程序使用的线程,文件和其他资源的数量。作为一个非常粗略的经验法则,JRE本身至少使用了20-50 MB,即使只是运行像“Hello world”这样简单的东西。

无论你只是调整堆大小还是微调gc参数,VM调整的问题在于,只要问题集发生变化就必须重做(例如,你可能会为当前文件使用-Xmx512m,但是你需要调整下一个文件的值。)

或者,您可以尝试减少创建的垃圾量,理想情况下为零。逐行读取扫描仪而不是扫描仪,您可以逐个字符地读取并使用状态机进行解析。这将大大减少垃圾创建,但使代码更加复杂

在许多情况下,最“高效”的解决方案只是不必担心关于内存使用情况 - 优化VM参数或代码所花费的时间可能会更有效地花费在专注于您的程序上取得进展。只要“开销”不妨碍你,为什么要这么麻烦?