CodeGym /Kurse /JAVA 25 SELF /Arbeiten mit großen Dateien: Chunking, Memory Mapping

Arbeiten mit großen Dateien: Chunking, Memory Mapping

JAVA 25 SELF
Level 41 , Lektion 3
Verfügbar

1. Chunking – Datei in Teilen lesen

Wie wir bereits in der vorherigen Vorlesung besprochen haben, ermöglicht Chunking, mit Dateien in Teilen zu arbeiten, ohne sie vollständig in den Speicher zu laden. Das ist besonders wichtig, wenn es um große Datenmengen geht. Ist eine Datei 10 MB groß, gibt es in der Regel kein Problem – man kann sie problemlos laden und auf jede beliebige Weise damit arbeiten. Aber was tun, wenn die Datei 10 GB erreicht, der Arbeitsspeicher nur 8 GB beträgt und außerdem ein Browser mit Dutzenden Tabs und eine IDE geöffnet sind? Der Versuch, eine solche Datei komplett zu lesen, endet meist tragisch: OutOfMemoryError, ein hängendes Programm und Tränen des Entwicklers.

Reale Beispiele für derart große Dateien gibt es ständig: Server-Logs für einen Monat können Dutzende Gigabyte belegen, große CSV-Dateien enthalten Millionen Zeilen, und Videos, Archive und Datenbank-Dumps sind noch größer.

Die Hauptidee bleibt dieselbe: nicht versuchen, „den Elefanten auf einmal zu essen“, sondern in Stücken arbeiten. Genau Chunking ermöglicht es, solche Daten sicher und effizient zu verarbeiten, indem die Datei in handhabbare Teile aufgeteilt wird.

Noch einmal zum Chunking

Chunk (Stück, Block) – das ist einfach ein Teil einer Datei mit einer bestimmten Größe. Anstatt alles auf einmal zu lesen, lesen wir zum Beispiel in 4 MB‑Stücken (oder 64 KB, oder 1 MB – je nach Situation).

Prinzip:

  • Wir öffnen einen Stream zum Lesen der Datei.
  • Wir erstellen einen Puffer – ein Byte-Array mit fester Größe.
  • In einer Schleife lesen wir aus der Datei in den Puffer, bis wir das Ende erreichen.
  • Jedes „Stück“ wird separat verarbeitet.

Beispiel: Große Datei stückweise kopieren

Angenommen, wir haben eine riesige Datei, die kopiert werden muss. Schreiben wir ein Programm, das das „wie die Profis“ macht.

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 MB

        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);
                // Fortschrittsausgabe oder Datenverarbeitung kann hinzugefügt werden
            }
        }
        System.out.println("Kopieren abgeschlossen!");
    }
}

Für die Arbeit mit Dateien in Java verwendet man üblicherweise die Standard-Streams FileInputStream und FileOutputStream. Als gute Praxis gilt ein Puffer von etwa 4 MB – das reicht bei modernen Laufwerken aus, damit Lesen und Schreiben effizient sind. In der Schleife liest das Programm Dateistücke und schreibt sie sofort in die neue Datei, ohne zu versuchen, die gesamte Datei im Speicher zu halten.

Dieser Ansatz spart Arbeitsspeicher, vermeidet Fehler wie OutOfMemoryError und ermöglicht die Arbeit mit Dateien praktisch jeder Größe – selbst 100 GB und mehr.

2. Chunking für die Datenverarbeitung

Oft geht es nicht nur darum, eine Datei zu kopieren, sondern zum Beispiel eine bestimmte Zeile zu finden, die Anzahl der Vorkommen zu zählen, etwas zu ersetzen usw.

Beispiel: Eine Zeichenkette in einer großen Textdatei suchen

Handelt es sich um eine Textdatei, sind Zeichenströme und zeilenweises Lesen bequemer:

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("Gefunden " + count + " Zeilen mit ERROR");
    }
}

Warum funktioniert das sogar bei Dateien im Gigabyte-Bereich?

  • BufferedReader liest die Datei in Stücken (Standardpuffer 8 KB, kann aber größer gesetzt werden).
  • Im Speicher befindet sich jeweils nur eine Zeile.

Puffergröße: Welche wählen?

Goldene Regel: Ein zu kleiner Puffer führt zu vielen Zugriffen auf das Laufwerk, ein zu großer verschwendet Arbeitsspeicher.

  • Für moderne HDDs/SSDs funktioniert meist ein Puffer von 64 KB – 4 MB gut.
  • Für Netzwerk- oder sehr schnelle SSDs – darf es mehr sein (8–16 MB).
  • Für Textdateien – kann der Puffer in BufferedReader erhöht werden.

Experimentieren Sie! Messen Sie die Laufzeit des Programms mit unterschiedlichen Puffern. Manchmal bringt eine größere Puffergröße eine Beschleunigung um den Faktor 2–3, manchmal hat sie fast keinen Einfluss.

3. Memory-mapped files (Dateiabbildung im Speicher)

Was ist das überhaupt?

Memory Mapping ist eine Methode, eine Datei mithilfe von Mechanismen des Betriebssystems direkt in den Speicher des Prozesses „einzublenden“. In Java wird dafür die Klasse MappedByteBuffer aus dem Paket java.nio verwendet. Die Datei wird quasi zu einem großen Byte-Array, mit dem man direkt arbeiten kann, ohne explizit jedes Stück zu lesen und zu schreiben.

Dieser Ansatz ist besonders nützlich für sehr große Dateien. Das Betriebssystem lädt die benötigten Teile der Datei selbst in den Speicher, und Sie können auf jede Stelle in der Datei zugreifen, als wäre es ein normales Array. Memory-mapped files bieten eine hohe Geschwindigkeit beim wahlfreien Zugriff. Zum Beispiel, wenn man schnell Dateistücke aus verschiedenen Bereichen lesen muss, ohne die Datei vollständig zu laden.

Wie sieht das im Code aus?

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 MB – Größe eines Mappings

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

                // Daten aus buffer lesen wie aus einem Array
                for (int i = 0; i < size; i++) {
                    byte b = buffer.get(i);
                    // Verarbeitung des Bytes (z. B. Suche nach einem bestimmten Wert)
                }

                position += size;
            }
        }
        System.out.println("Lesen per Memory Mapping abgeschlossen!");
    }
}

RandomAccessFile und FileChannel ermöglichen Low-Level-Zugriff auf die Datei. Der Aufruf channel.map bildet einen Bereich der Datei im Speicher ab. Der Zugriff auf die Daten erfolgt über den Puffer MappedByteBuffer.

Welche Vorteile hat Memory Mapping?

  • Sehr schnell für wahlfreien Zugriff auf verschiedene Teile der Datei.
  • Man kann mit Dateien arbeiten, die größer als der verfügbare Arbeitsspeicher sind (das Betriebssystem lädt die benötigten Seiten selbst nach).
  • Wird in modernen Datenbanken, Indizes und großen Logs eingesetzt.

Welche Nachteile gibt es?

  • Nicht immer geeignet für das Schreiben (insbesondere auf Netzwerkdateisystemen).
  • Größenbeschränkungen pro Mapping (in 32-Bit-JVMs meist bis 2 GB).
  • Wenn man vergisst, die Datei zu schließen, kann sie „blockiert“ bleiben (insbesondere unter Windows).
  • Nicht alle Dateioperationen werden schneller – wenn eine Datei nur sequentiell gelesen werden soll, ist ein normaler Puffer oft nicht unterlegen.

4. Praktische Beispiele

Beispiel 1: Unterzeichenkette in einer großen Datei per Memory Mapping suchen

Angenommen, wir haben eine 10 GB große Datei und möchten darin eine bestimmte Byte-Sequenz finden (z. B. die Zeichenkette "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 MB

            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("Gefunden an Position " + (position + i));
                        // Suche kann gestoppt oder fortgesetzt werden
                    }
                }
                position += size;
            }
        }
    }
}

Beachten Sie:
Wenn eine Untersequenz über zwei Chunks hinweg geteilt sein kann, müssen Sie eine Überlappung zwischen den Chunks in der Länge der gesuchten Sequenz vorsehen.

5. Nützliche Details

Wann Chunking und wann Memory Mapping verwenden?

  • Chunking – universeller Ansatz für beliebige Dateien (Text, Binärdateien, Logs, Archive). Funktioniert gut für sequentielle Verarbeitung.
  • Memory Mapping – hocheffizient für wahlfreien Zugriff, Arbeit mit großen Indizes und Datenbanken, schnelle Suchen in riesigen Dateien.

Wenn Sie unsicher sind, was Sie wählen sollen – beginnen Sie mit Chunking! Memory Mapping ist mächtig, aber ein eher „niedrigstufiges“ Werkzeug, das Sorgfalt erfordert.

Empfehlungen

  • Verwenden Sie try-with-resources für automatisches Schließen von Streams und Kanälen.
  • Öffnen Sie nicht zu viele Dateien gleichzeitig: Das Betriebssystem hat Limits für die Anzahl offener Deskriptoren.
  • Mappen Sie keine zu großen Bereiche – das kann zu Fehlern führen (insbesondere auf 32-Bit-JVMs).
  • Für parallele Verarbeitung kann man die Datei in Chunks aufteilen und sie in separaten Threads verarbeiten (wichtig ist dabei, das Laufwerk nicht zu überlasten und nicht den Speicher zu sprengen).

6. Typische Fehler beim Arbeiten mit großen Dateien

Fehler Nr. 1: Der Versuch, die gesamte große Datei in den Speicher zu laden.
Ein sehr häufiges Problem – insbesondere bei Anfängern. Wenn die Datei größer als 1–2 GB ist, verwenden Sie Chunking oder zeilenweises Lesen, sonst stürzt das Programm mit OutOfMemoryError ab.

Fehler Nr. 2: Ein zu kleiner Puffer.
Ein Puffer von 512 Byte ist keine Optimierung, sondern Performance‑Selbstmord. Verwenden Sie Puffer ab 64 KB und größer.

Fehler Nr. 3: Stream oder Channel nicht geschlossen.
Der Dateideskriptor bleibt offen hängen, die Datei lässt sich nicht löschen oder wird erst nach einem Neustart der JVM freigegeben. Verwenden Sie try-with-resources.

Fehler Nr. 4: Falscher Umgang mit Memory Mapping.
Wenn die Datei während des Mappings von einem anderen Prozess geändert wird, können inkonsistente Daten oder Fehler auftreten. Verwenden Sie Memory Mapping nicht für Dateien, die sich häufig ändern.

Fehler Nr. 5: Die Überlappung der Chunks bei der Suche nach Teilstrings wird nicht berücksichtigt.
Wenn die gesuchte Zeichenkette „am Übergang“ zwischen zwei Chunks liegen kann, überlappen Sie die Chunks unbedingt um die Länge dieser Zeichenkette.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION