«java.lang.OutOfMemoryError: пространство кучи Java» с большими двумерными массивами

Я сделал небольшую программу для разделения больших файлов на более мелкие с помощью Java.

Код следующий:

import java.io.*;
public class Main {

    final static int DIM = 6;
    final static int GB = 1024 * 1024 * 1024;
    public static void write(char[][] buffer, long currentFile) throws IOException {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter("out" + currentFile + ".json"))) {
            long wrote = 0;
            for (int i = 0; i < DIM; i++) {
                int firstNull = GB;
                for(int j = GB - 1; j > 0; j--){
                    if (buffer[i][j] == '\0'){
                        firstNull = j;
                    }
                }
                bw.write(buffer[i], 0, firstNull);
                wrote += firstNull;
            }
            bw.close();
            System.out.println("Wrote " + wrote + " bytes to <out" + currentFile + ".json>");
        }
    }

    public static void main(String[] args) {
        System.out.println("Starting");
        File file = new File("./twd.json");
        long start = System.currentTimeMillis();
        try {
            int bufPos = 0;
            int mapIdx = 0;
            char[][] buffer = new char[DIM][GB];
            long currentFile = 0;
            BufferedReader br = new BufferedReader(new FileReader(file));
            System.out.println("Config correct => Started");
            String line;
            while ((line = br.readLine()) != null){
                if (bufPos + line.length() >= GB){
                    if (mapIdx == DIM - 1){
                        write(buffer, currentFile);
                        buffer = null; // Otherwise it will trigger OutOfMemoryError
                        buffer = new char[DIM][GB];
                        bufPos = 0;
                        double progress = GB * (currentFile + 1.0) /  (file.length() / (double) DIM) * 100;
                        System.out.println("Status: " +  progress + "%");
                        currentFile++;
                        mapIdx = 0;
                    }else{
                        mapIdx++;
                        bufPos = 0;
                        System.out.println("Map idx to " + mapIdx);
                    }
                }
                int i;
                for(i = 0; i < line.length(); i++){
                    buffer[mapIdx][bufPos] = line.charAt(i);
                    bufPos++;
                }
                buffer[mapIdx][bufPos] = '\n';
                bufPos++;
            }
            write(buffer, currentFile);
            long end = System.currentTimeMillis();
            br.close();

            System.out.println("Diff: " + (end - start) / 1000 + "s");
        }catch (IOException e){
            System.out.println("IO Exception");
        }
    }
}

Проблема, с которой я столкнулся, заключается в следующем: использование памяти программой должно составлять DIM * ГБ * 2 (символ — 2 байта) ГБ. При DIM=5 все работает нормально. Если я попытаюсь увеличить его, он выдаст эту ошибку.

Я попытался передать дополнительный параметр -Xmx для увеличения максимального размера кучи. Но без всякой удачи. (Моя система имеет 48 ГБ ОЗУ, но даже попытка указать -Xmx(n)g, где n — безумно большое число (< 40), не работает.)

Заметил, что без каких-либо дополнительных параметров и при DIM=5 диспетчер задач показывает мне, что программа достигает использования 12ГБ ОЗУ.

Я что-то пропустил?


"java.lang.OutOfMemoryError: Java heap space" is thrown when I try to initialize the char[][] and DIM is greater or equals to 6.
I know the code can be more efficient and definitely simpler, but I was wondering what was causing the program to crash and why (I can't figure it out) rather than just making it work :)

Сказать, что символ имеет размер 2 байта, немного упрощенно. Сам массив имеет очень незначительные накладные расходы, но передача char[] в Writer (а именно Buffered > File > OutputStreamWriter > StreamEncoder) в конечном итоге обернет каждый char[] в CharBuffer и закодирует результат, так что определенно будет использовано больше. Что касается чтения, неудивительно, что ваше использование увеличилось. Что интересно, DIM=6 подвел вас больше, чем любое другое более высокое число. Гораздо более эффективным решением может быть запись файла по мере его чтения, вместо того, чтобы читать целые куски, как показано ниже.

Rogue 19.04.2024 21:10

По каким-то причинам я не понимаю этот код.

Antoniossss 19.04.2024 21:14

Зачем вам вообще нужен этот огромный буфер? Похоже, что все, что делает ваш код, — это разбивает файл на куски размером не более 6 ГБ, гарантируя при этом, что разделение происходит при разрыве строки. Это означает, что вы узнаете, можете ли вы написать строку, как только встретите конец этой строки. Если бы вы написали строку в это время, вам нужно было бы хранить в памяти только одну строку, что было бы гораздо более эффективно с точки зрения использования памяти и не вызывало бы никаких проблем со сборкой мусора...

meriton 19.04.2024 21:16

Пожалуйста, объясните или покажите, как именно вы указываете -Xmx. Максимальная куча по умолчанию, если вы не укажете -Xmx, составляет 1/4 системной оперативной памяти, а 1/4 от 48 ГБ составляет 12 ГБ, что с учетом накладных расходов и поломок достаточно для выделения 5Gx2B, но не 6Gx2B именно так, как вы испытываете.

dave_thompson_085 20.04.2024 01:04

Ваш код излишне сложен - например, часть, сканирующая каждый символ [] для поиска \0 - вы уже знаете длину, которую он содержит. Вам вообще не нужно выделять buffer, просто записывайте построчно в файл, пока не будет достигнут предел, а затем переключитесь на следующий файл.

DuncG 20.04.2024 10:17

@meriton за сравнительный анализ производительности. Я хотел проверить, происходит ли чтение всего сразу и запись всего быстрее, чем чтение и запись построчно. Мне было интересно, что вызвало эту ошибку, несмотря на то, что я выделил в кучу любой (буквально) максимальный объем памяти. <br/> @dave_thompson_085 java Main -Xmx32g из CMD Windows. Старался избегать использования IDE, чтобы облегчить понимание проблемы. Но даже из IntelliJ после того, как я установил ограничения кучи (правильно) для любого числа, он все равно выдает ту же ошибку с DIM = 6. Спасибо за подробности! :))

Enrypase 20.04.2024 18:32

@DuncG полностью согласен! Но суть вопроса в том, чтобы выяснить, почему это дает сбой, а не в том, как сделать его более эффективным :)

Enrypase 20.04.2024 18:34

@Rogue, спасибо за ответ! Извините, но я недостаточно описал ошибку. Программа вылетает, когда я пытаюсь инициализировать char[][]. В любом случае я очень ценю ваше объяснение! :)

Enrypase 20.04.2024 18:44

Для общей эффективности, если ваш код ожидает других системных процессов, то вы в значительной степени оптимальны, насколько это возможно. Если вам нужно «магическое число» байтов для чтения/записи, я бы выбрал размер блока файловой системы, в которую вы записываете (для обработки которого большинство прошивок жесткого диска будет хорошо оптимизировано, и любой файл будет по крайней мере, такой размер на диске). A BufferedWriter вполне может уже сделать это под капотом, отсюда и комментарий о том, что нужно просто писать строки, пока вы их читаете.

Rogue 22.04.2024 16:25
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
9
102
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я могу запустить вашу программу с DIM выше 6. Судя по вашим комментариям java Main -Xmx32g

Вам нужно поставить аргументы VM перед именем класса, иначе -Xmx32g — это аргумент, передаваемый Main.main(String ...args):

java -Xmx32g Main 

Еще одно предложение: если у вас все еще не работает, попробуйте более позднюю версию JDK. Я использовал JDK22 с DIM, установленным на 8, файлом размером 9 ГБ и java -Xmx20g Main - все работает нормально.

Есть более простые способы разделить большой файл без такого большого объема памяти, но, судя по вашему комментарию, это не часть вашего вопроса — надеюсь, это поможет.

Кстати, разделение не работает.

DuncG 20.04.2024 21:11

Привет! Это определенно решило мою проблему. Интересный факт: приведенному здесь коду требуется около 530 секунд для копирования файла размером 50 ГБ. А простое чтение одной строки и запись занимает около 360 секунд! Спасибо, что поделился! :)

Enrypase 22.04.2024 02:04

Другие вопросы по теме