JavaRush /Курсы /JAVA 25 SELF /Работа с большими файлами: chunking, memory mapping

Работа с большими файлами: chunking, memory mapping

JAVA 25 SELF
41 уровень, 3 лекция
Открыта

1. Chunking — чтение файла по частям

Как мы уже обсуждали в предыдущей лекции, chunking позволяет работать с файлами по частям, не загружая их целиком в память. Это особенно важно, когда речь идёт о больших объёмах данных. Если файл весит 10 МБ, проблем обычно нет — его можно спокойно загрузить и работать с ним любым способом. Но что делать, если файл достигает 10 ГБ, а оперативной памяти всего 8 ГБ, да ещё и браузер с десятками вкладок и IDE открыты? Попытка прочитать такой файл целиком обычно заканчивается трагически: OutOfMemoryError, зависание программы и слёзы разработчика.

Реальные примеры таких больших файлов встречаются постоянно: логи серверов за месяц могут занимать десятки гигабайт, крупные CSV-файлы содержат миллионы строк, а видео, архивы и дампы баз данных ещё больше.

Главная идея остаётся прежней: не пытаться «съесть слона целиком», а работать по кусочкам. Именно chunking позволяет безопасно и эффективно обрабатывать такие данные, разделяя файл на управляемые части.

Ещё раз о chunking

Chunk (кусок, блок) — это просто часть файла определённого размера. Вместо того чтобы читать всё сразу, мы читаем, например, по 4 МБ (или по 64 КБ, или по 1 МБ — в зависимости от ситуации).

Принцип:

  • Открываем поток для чтения файла.
  • Создаём буфер — массив байтов фиксированного размера.
  • В цикле читаем из файла в буфер, пока не дойдём до конца.
  • Каждый «кусок» обрабатываем отдельно.

Пример: копирование большого файла по кускам

Допустим, у нас есть огромный файл, который нужно скопировать. Давайте напишем программу, которая делает это «по‑взрослому».

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BigFileCopy {
    public static void main(String[] args) throws IOException {
        String source = "bigfile.dat";
        String dest = "bigfile_copy.dat";
        int bufferSize = 4 * 1024 * 1024; // 4 МБ

        try (FileInputStream in = new FileInputStream(source);
             FileOutputStream out = new FileOutputStream(dest)) {

            byte[] buffer = new byte[bufferSize];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
                // Можно добавить вывод прогресса или обработку данных
            }
        }
        System.out.println("Копирование завершено!");
    }
}

Для работы с файлами в Java обычно используют стандартные потоки FileInputStream и FileOutputStream. Хорошей практикой считается использовать буфер размером около 4 МБ — этого достаточно для современных дисков, чтобы чтение и запись шли эффективно. В цикле программа читает куски файла и сразу записывает их в новый файл, не пытаясь держать весь файл в памяти.

Такой подход позволяет экономить оперативную память, избегать ошибок типа OutOfMemoryError и работать с файлами практически любого размера, даже если это 100 ГБ и больше.

2. Chunking для обработки данных

Часто задача — не просто скопировать файл, а, например, найти определённую строку, посчитать количество вхождений, заменить что-то и т.д.

Пример: поиск строки в большом текстовом файле

Если файл текстовый, удобнее использовать символьные потоки и построчное чтение:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BigFileSearch {
    public static void main(String[] args) throws IOException {
        String file = "biglog.txt";
        String keyword = "ERROR";
        int count = 0;

        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.contains(keyword)) {
                    count++;
                }
            }
        }
        System.out.println("Найдено " + count + " строк с ERROR");
    }
}

Почему это работает даже для гигабайтных файлов?

  • BufferedReader читает файл по кускам (по умолчанию буфер 8 КБ, но можно задать больше).
  • В памяти в каждый момент времени хранится только одна строка.

Buffer size: какой выбрать?

Золотое правило: слишком маленький буфер — много обращений к диску, слишком большой — зря расходуется память.

  • Для современных HDD/SSD обычно хорошо работает буфер 64 КБ – 4 МБ.
  • Для сетевых или очень быстрых SSD — можно больше (8–16 МБ).
  • Для текстовых файлов — можно увеличить буфер в BufferedReader.

Экспериментируйте! Измеряйте время работы программы с разными буферами. Иногда увеличение буфера даёт ускорение в 2–3 раза, иногда — почти не влияет.

3. Memory-mapped files (отображение файла в память)

Что это вообще такое?

Memory mapping — это способ «отобразить» файл прямо в память процесса с помощью механизмов операционной системы. В Java для этого используется класс MappedByteBuffer из пакета java.nio. Файл как будто становится большим массивом байтов, с которым можно работать напрямую, без явного чтения и записи каждого куска.

Такой подход особенно полезен для работы с очень большими файлами. Операционная система сама подгружает нужные части файла в память, а вы можете обращаться к любому месту в файле так, как если бы это был обычный массив. Memory-mapped files обеспечивают высокую скорость случайного доступа. Например, когда нужно быстро читать куски файла из разных мест, не загружая его полностью.

Как это выглядит в коде?

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MemoryMappedRead {
    public static void main(String[] args) throws Exception {
        String fileName = "bigfile.dat";
        try (RandomAccessFile file = new RandomAccessFile(fileName, "r");
             FileChannel channel = file.getChannel()) {

            long fileSize = channel.size();
            int chunkSize = 1024 * 1024 * 128; // 128 МБ — размер одного mapping

            long position = 0;
            while (position < fileSize) {
                long size = Math.min(chunkSize, fileSize - position);
                MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, position, size);

                // Читаем данные из buffer, как из массива
                for (int i = 0; i < size; i++) {
                    byte b = buffer.get(i);
                    // Обработка байта (например, ищем определённое значение)
                }

                position += size;
            }
        }
        System.out.println("Чтение через memory mapping завершено!");
    }
}

RandomAccessFile и FileChannel позволяют получить доступ к файлу на низком уровне. Вызов channel.map отображает участок файла в память. Доступ к данным — через буфер MappedByteBuffer.

В чём плюсы memory mapping?

  • Очень быстро для случайного доступа к разным частям файла.
  • Можно работать с файлами, которые больше доступной оперативной памяти (ОС сама подгружает нужные страницы).
  • Используется в современных базах данных, индексах, больших логах.

В чём минусы?

  • Не всегда подходит для записи (особенно на сетевых файловых системах).
  • Ограничения по размеру mapping (обычно до 2 ГБ на один mapping в 32-битных JVM).
  • Если забыть закрыть файл, может возникнуть «залипание» файла (особенно на Windows).
  • Не все операции с файлами ускоряются — если нужно просто последовательно читать файл, обычный буфер часто не уступает.

4. Практические примеры

Пример 1: Поиск подстроки в большом файле через memory mapping

Допустим, у нас есть файл на 10 ГБ, и мы хотим найти в нём определённую последовательность байтов (например, строку "SECRET").

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

public class MemoryMappedSearch {
    public static void main(String[] args) throws Exception {
        String fileName = "hugefile.bin";
        byte[] target = "SECRET".getBytes(StandardCharsets.UTF_8);

        try (RandomAccessFile file = new RandomAccessFile(fileName, "r");
             FileChannel channel = file.getChannel()) {

            long fileSize = channel.size();
            int chunkSize = 128 * 1024 * 1024; // 128 МБ

            long position = 0;
            while (position < fileSize) {
                long size = Math.min(chunkSize, fileSize - position);
                MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, position, size);

                for (int i = 0; i < size - target.length; i++) {
                    boolean found = true;
                    for (int j = 0; j < target.length; j++) {
                        if (buffer.get(i + j) != target[j]) {
                            found = false;
                            break;
                        }
                    }
                    if (found) {
                        System.out.println("Найдено в позиции " + (position + i));
                        // Можно остановить поиск или продолжить
                    }
                }
                position += size;
            }
        }
    }
}

Обратите внимание:
Если подстрока может «разорваться» между двумя чанками, нужно предусмотреть перекрытие между чанками на длину искомой последовательности.

5. Полезные нюансы

Когда использовать chunking, а когда — memory mapping?

  • Chunking — универсальный подход для любых файлов (текст, бинарные, логи, архивы). Хорошо работает для последовательной обработки.
  • Memory mapping — суперэффективен для случайного доступа, работы с большими индексами, базами данных, быстрых поисков по огромным файлам.

Если не знаете, что выбрать — начните с chunking! Memory mapping — мощное, но более «низкоуровневое» оружие, требующее аккуратности.

Рекомендации

  • Используйте try-with-resources для автоматического закрытия потоков и каналов.
  • Не открывайте слишком много файлов одновременно: у ОС есть лимиты на количество открытых дескрипторов.
  • Не делайте mapping на слишком большие куски — это может привести к ошибкам (особенно на 32-битных JVM).
  • Для параллельной обработки можно разбивать файл на чанки и обрабатывать их в отдельных потоках (но тут важно не «забить» диск и не выйти за пределы памяти).

6. Типичные ошибки при работе с большими файлами

Ошибка №1: попытка загрузить весь большой файл в память.
Очень частая проблема — особенно у новичков. Если файл больше 1–2 ГБ, используйте chunking или построчное чтение, иначе программа «упадёт» с OutOfMemoryError.

Ошибка №2: слишком маленький буфер.
Буфер по 512 байт — это не оптимизация, а медленное самоубийство для производительности. Используйте буферы от 64 КБ и выше.

Ошибка №3: забыли закрыть поток или канал.
Файловый дескриптор останется висеть, файл не удалится или не освободится до перезапуска JVM. Используйте try-with-resources.

Ошибка №4: неправильная работа с memory mapping.
Если файл изменяется другим процессом во время mapping, можно получить неконсистентные данные или ошибку. Не используйте memory mapping для файлов, которые часто меняются.

Ошибка №5: не учитывается перекрытие чанков при поиске подстрок.
Если искомая строка может оказаться «на стыке» двух чанков, обязательно делайте перекрытие на длину этой строки между чанками.

1
Задача
JAVA 25 SELF, 41 уровень, 3 лекция
Недоступна
Переброска массивных данных: Операция "Гигантский Грузовик" 🚚
Переброска массивных данных: Операция "Гигантский Грузовик" 🚚
1
Задача
JAVA 25 SELF, 41 уровень, 3 лекция
Недоступна
Археологический поиск рун в цифровом монолите 📜
Археологический поиск рун в цифровом монолите 📜
Комментарии (3)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Victor Уровень 50
25 сентября 2026
Как тут кто-то говорил - бессмысленная работа - хвосты циклам крутить - вместо того чтобы работать
Gans Electro Уровень 59
10 сентября 2026

Если подстрока может «разорваться» между двумя чанками,
нужно предусмотреть перекрытие между чанками на длину искомой последовательности.
Довольно интересная задача. Последний символ может оказаться на следующем. Или первый символ будет в этом чанке. Это два крайних случая. На нужен еще буфер 2 раза длиннее искомого слова, который запишет конец этого чанка (на 1 символ меньше чем искомое) и часть следующего (тоже на 1 символ меньше). Дополнительно ищем в этом буфере тоже:

S | ECRET
SE | CRET
SEC | RET
SECR | ET
SECRE | T
kasnil Уровень 38
10 февраля 2026
По MappedByteBuffer, очень показательный пример с описанием 1B Rows with the Memory API - JEP Cafe #25