CodeGym /Corsi /JAVA 25 SELF /Lavorare con file di grandi dimensioni: chunking, memory ...

Lavorare con file di grandi dimensioni: chunking, memory mapping

JAVA 25 SELF
Livello 41, Lezione 3
Disponibile

1. Chunking — lettura del file a blocchi

Come abbiamo già discusso nella lezione precedente, il chunking consente di lavorare con i file a blocchi, senza caricarli interamente in memoria. Questo è particolarmente importante quando si tratta di grandi volumi di dati. Se un file pesa 10 MB, di solito non ci sono problemi — lo si può caricare e gestire in qualsiasi modo. Ma cosa fare se il file arriva a 10 GB, la RAM è solo 8 GB, e in più il browser con decine di schede e l'IDE sono aperti? Il tentativo di leggere un file del genere per intero di solito finisce tragicamente: OutOfMemoryError, blocco del programma e lacrime dello sviluppatore.

Esempi reali di file così grandi si incontrano continuamente: i log dei server per un mese possono occupare decine di gigabyte, i grandi file CSV contengono milioni di righe, e video, archivi e dump di database sono ancora più grandi.

L'idea principale rimane la stessa: non cercare di «mangiare l'elefante tutto in una volta», ma lavorare a pezzetti. Proprio chunking consente di elaborare in modo sicuro ed efficiente tali dati, suddividendo il file in parti gestibili.

Ancora sul chunking

Chunk (pezzo, blocco) — è semplicemente una parte del file di dimensione definita. Invece di leggere tutto in una volta, leggiamo, per esempio, 4 MB (oppure 64 KB, oppure 1 MB — a seconda della situazione).

Principio:

  • Apriamo uno stream per la lettura del file.
  • Creiamo un buffer — un array di byte di dimensione fissa.
  • In un ciclo leggiamo dal file nel buffer finché non raggiungiamo la fine.
  • Ogni «pezzo» viene elaborato separatamente.

Esempio: copiare un file grande a blocchi

Supponiamo di avere un file enorme da copiare. Scriviamo un programma che lo faccia «in modo professionale».

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);
                // Si può aggiungere la stampa dell'avanzamento o l'elaborazione dei dati
            }
        }
        System.out.println("Copia completata!");
    }
}

Per lavorare con i file in Java si usano di solito gli stream standard FileInputStream e FileOutputStream. Una buona pratica è usare un buffer di circa 4 MB — sufficiente, sui dischi moderni, per lettura e scrittura efficienti. Nel ciclo il programma legge i pezzi del file e li scrive immediatamente nel nuovo file, senza provare a mantenere l'intero file in memoria.

Questo approccio consente di risparmiare memoria, evitare errori come OutOfMemoryError e lavorare con file praticamente di qualsiasi dimensione, anche 100 GB e oltre.

2. Chunking per l'elaborazione dei dati

Spesso l'obiettivo non è semplicemente copiare il file, ma, per esempio, trovare una determinata riga, contare le occorrenze, sostituire qualcosa ecc.

Esempio: ricerca di una riga in un grande file di testo

Se il file è di testo, è più comodo usare stream di caratteri e la lettura riga per riga:

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("Trovate " + count + " righe con ERROR");
    }
}

Perché funziona anche per file di molti gigabyte?

  • BufferedReader legge il file a blocchi (per impostazione predefinita buffer da 8 KB, ma si può impostare più grande).
  • In memoria in ogni momento è presente solo una singola riga.

Dimensione del buffer: quale scegliere?

Regola d'oro: un buffer troppo piccolo comporta molti accessi al disco, uno troppo grande spreca memoria.

  • Per HDD/SSD moderni funziona di solito bene un buffer tra 64 KB e 4 MB.
  • Per SSD molto veloci o su rete — si può aumentare (8–16 MB).
  • Per i file di testo — si può aumentare il buffer in BufferedReader.

Sperimentate! Misurate il tempo di esecuzione del programma con buffer diversi. A volte l'aumento del buffer dà un'accelerazione di 2–3 volte, a volte incide poco.

3. Memory-mapped files (mappatura del file in memoria)

Di cosa si tratta?

Il memory mapping è un modo per «mappare» un file direttamente nella memoria del processo tramite i meccanismi del sistema operativo. In Java si usa la classe MappedByteBuffer del package java.nio. Il file diventa come un grande array di byte con cui si può lavorare direttamente, senza lettura/scrittura esplicita di ogni pezzo.

Questo approccio è particolarmente utile per file molto grandi. Il sistema operativo carica da sé le parti necessarie del file in memoria, e voi potete accedere a qualsiasi punto del file come se fosse un normale array. I memory-mapped file offrono un'elevata velocità di accesso casuale. Per esempio, quando è necessario leggere rapidamente parti del file in posizioni diverse, senza caricarlo per intero.

Come appare nel codice?

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 — dimensione di un mapping

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

                // Leggiamo i dati dal buffer come da un array
                for (int i = 0; i < size; i++) {
                    byte b = buffer.get(i);
                    // Elaborazione del byte (ad esempio cerchiamo un valore specifico)
                }

                position += size;
            }
        }
        System.out.println("Lettura tramite memory mapping completata!");
    }
}

RandomAccessFile e FileChannel consentono di accedere al file a basso livello. La chiamata channel.map mappa un intervallo del file in memoria. L'accesso ai dati avviene tramite il buffer MappedByteBuffer.

Quali sono i vantaggi del memory mapping?

  • Molto veloce per l'accesso casuale a parti diverse del file.
  • Si può lavorare con file più grandi della RAM disponibile (il sistema operativo carica le pagine necessarie).
  • Usato in database moderni, indici, grandi log.

Quali sono gli svantaggi?

  • Non sempre adatto alla scrittura (soprattutto su file system di rete).
  • Limitazioni sulla dimensione del mapping (di solito fino a 2 GB per mapping su JVM a 32 bit).
  • Se si dimentica di chiudere il file, può rimanere bloccato (soprattutto su Windows).
  • Non tutte le operazioni sui file vengono accelerate: se occorre solo leggere sequenzialmente, un buffer normale spesso non è inferiore.

4. Esempi pratici

Esempio 1: ricerca di una sottostringa in un file grande tramite memory mapping

Supponiamo di avere un file da 10 GB e di voler trovare in esso una determinata sequenza di byte (per esempio la stringa "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("Trovato alla posizione " + (position + i));
                        // Si può interrompere la ricerca o proseguire
                    }
                }
                position += size;
            }
        }
    }
}

Attenzione:
Se la sottostringa può «spezzarsi» tra due chunk, è necessario prevedere una sovrapposizione tra i chunk pari alla lunghezza della sequenza cercata.

5. Dettagli utili

Quando usare il chunking e quando — il memory mapping?

  • Chunking — approccio universale per qualsiasi tipo di file (testo, binari, log, archivi). Funziona bene per l'elaborazione sequenziale.
  • Memory mapping — super efficiente per l'accesso casuale, il lavoro con grandi indici, database, ricerche rapide in file enormi.

Se non sapete cosa scegliere — iniziate dal chunking! Il memory mapping è potente, ma più «a basso livello», e richiede attenzione.

Raccomandazioni

  • Usate try-with-resources per la chiusura automatica di stream e canali.
  • Non aprite troppi file contemporaneamente: il sistema operativo ha limiti sul numero di descrittori aperti.
  • Non mappate porzioni troppo grandi — può portare a errori (soprattutto su JVM a 32 bit).
  • Per l'elaborazione parallela si può suddividere il file in chunk ed elaborarli in thread separati (ma è importante non saturare il disco e non superare la memoria disponibile).

6. Errori tipici nella gestione di file grandi

Errore n. 1: tentare di caricare l'intero file grande in memoria.
Un problema molto frequente — soprattutto per i principianti. Se il file è più grande di 1–2 GB, usate chunking o la lettura riga per riga, altrimenti il programma andrà in crash con OutOfMemoryError.

Errore n. 2: buffer troppo piccolo.
Un buffer da 512 byte non è un'ottimizzazione, ma un suicidio per le prestazioni. Usate buffer da 64 KB in su.

Errore n. 3: aver dimenticato di chiudere uno stream o un canale.
Il descrittore di file rimarrà pendente, il file non verrà eliminato o rilasciato fino al riavvio della JVM. Usate try-with-resources.

Errore n. 4: uso scorretto del memory mapping.
Se il file viene modificato da un altro processo durante il mapping, si possono ottenere dati non consistenti o un errore. Non usate il memory mapping per file che cambiano spesso.

Errore n. 5: non considerare la sovrapposizione dei chunk nella ricerca di sottostringhe.
Se la stringa cercata può trovarsi al confine tra due chunk, effettuate una sovrapposizione tra i chunk pari alla lunghezza di questa stringa.

1
Compito
JAVA 25 SELF, livello 41, lezione 3
Bloccato
Trasferimento di grandi volumi: Operazione "Camion Gigante" 🚚
Trasferimento di grandi volumi: Operazione "Camion Gigante" 🚚
1
Compito
JAVA 25 SELF, livello 41, lezione 3
Bloccato
Ricerca archeologica di rune nel monolite digitale 📜
Ricerca archeologica di rune nel monolite digitale 📜
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION