1. Chunking — faylı hissələr üzrə oxuma
Daha əvvəlki mühazirədə müzakirə etdiyimiz kimi, chunking fayllarla onları tam yaddaşa yükləmədən hissə-hissə işləməyə imkan verir. Bu, xüsusilə böyük həcmli verilənlərlə işləyərkən vacibdir. Faylın ölçüsü 10 MB-dirsə, adətən problem olmur — onu tam yükləyib istənilən üsulla işləmək mümkündür. Bəs fayl 10 GB həcmindədirsə, operativ yaddaş isə cəmi 8 GB-dır, üstəlik onlarla vərəqli brauzer və IDE açıqdır? Belə faylı bütövlükdə oxumaq cəhdi adətən pis nəticələnir: OutOfMemoryError, proqramın donması və developer üçün göz yaşları.
Belə böyük faylların real nümunələri daim rast gəlinir: server jurnalları (log-lar) bir ay üçün onlarla gigabayt tuta bilər, iri CSV fayllarında milyonlarla sətir olur, video, arxivlər və məlumat bazası dump-ları isə daha da böyükdür.
Əsas ideya dəyişmir: “filini bütöv udmağa” çalışmayın, parçalarla işləyin. Məhz chunking faylı idarəolunan hissələrə bölməklə belə verilənləri təhlükəsiz və effektiv emal etməyə imkan verir.
Chunking haqqında bir daha
Chunk (parça, blok) — faylın müəyyən ölçülü bir hissəsidir. Hər şeyi birdən oxumaqdansa, məsələn, 4 MB (və ya 64 KB, yaxud 1 MB — vəziyyətdən asılı olaraq) oxuyuruq.
Prinsip:
- Faylı oxumaq üçün axını açırıq.
- Sabit ölçülü bayt massivindən ibarət buffer yaradırıq.
- Sonuna qədər fayldan buffer-ə dövrədə oxuyuruq.
- Hər “parça” ayrıca emal olunur.
Nümunə: böyük faylı hissələrlə kopyalama
Tutaq ki, böyük bir faylı kopyalamaq lazımdır. Gəlin bunu “peşəkar şəkildə” edən proqram yazaq.
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);
// Proqresin çıxışını və ya verilənlərin işlənməsini əlavə etmək olar
}
}
System.out.println("Kopyalama başa çatdı!");
}
}
Java-da fayllarla işləmək üçün adətən standart FileInputStream və FileOutputStream axınlarından istifadə olunur. Yaxşı təcrübə kimi təxminən 4 MB ölçülü buffer istifadə etmək tövsiyə olunur — bu, müasir disklər üçün oxuma və yazmanın səmərəli getməsi üçün kifayətdir. Dövrədə proqram faylın hissələrini oxuyur və dərhal yeni fayla yazır, bütün faylı yaddaşda saxlamağa çalışmır.
Bu yanaşma operativ yaddaşa qənaət etməyə, OutOfMemoryError kimi xətalardan yayınmağa və hətta 100 GB və daha böyük fayllarla işləməyə imkan verir.
2. Məlumatların emalı üçün chunking
Çox vaxt vəzifə təkcə faylı kopyalamaq deyil, məsələn, müəyyən sətiri tapmaq, daxilolmaların sayını hesablamaq, nəyisə əvəzləmək və s. olur.
Nümunə: böyük mətn faylında sətir axtarışı
Fayl mətnlidirsə, simvol axınlarından və sətir-sətir oxumadan istifadə etmək daha rahatdır:
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("Tapıldı " + count + " ERROR olan sətir");
}
}
Bu niyə hətta giqabaytlarla fayllarda işləyir?
- BufferedReader faylı hissələrlə oxuyur (susmaya görə buffer 8 KB-dır, lakin daha böyüyünü təyin etmək olar).
- Hər an yaddaşda yalnız bir sətir saxlanılır.
Buffer size: hansını seçmək?
Qızıl qayda: buffer çox kiçikdirsə — diskə müraciətlər çox olacaq, çox böyükdürsə — yaddaş lazımsız yerə sərf olunacaq.
- Müasir HDD/SSD-lər üçün adətən 64 KB – 4 MB buffer yaxşı işləyir.
- Şəbəkə FS-ləri və ya çox sürətli SSD-lər üçün — daha böyük ola bilər (8–16 MB).
- Mətn faylları üçün — BufferedReader-də buffer-i artırmaq olar.
Eksperiment aparın! Proqramın iş vaxtını müxtəlif buffer-lərlə ölçün. Bəzən buffer-in artırılması 2–3 dəfə sürət verir, bəzən isə — demək olar ki, təsir etmir.
3. Memory-mapped files (faylın yaddaşa xəritələnməsi)
Bu nədir?
Memory mapping — faylı əməliyyat sisteminin mexanizmləri vasitəsilə birbaşa proses yaddaşına “xəritələmək” üsuludur. Java-da bunun üçün java.nio paketindən MappedByteBuffer sinfi istifadə olunur. Fayl sanki böyük bir bayt massivi olur və hər hissəni ayrıca oxuyub-yazmadan onunla birbaşa işləmək mümkündür.
Bu yanaşma xüsusilə çox böyük fayllarla işləyərkən faydalıdır. Əməliyyat sistemi faylın lazım olan hissələrini özü yaddaşa gətirir, siz isə fayldakı istənilən yerə sanki adi massiv kimi müraciət edirsiniz. Memory-mapped files təsadüfi giriş üçün yüksək sürət təmin edir. Məsələn, faylın müxtəlif yerlərindən parçaları tez oxumaq lazım olduqda, onu tam yükləmədən bunu edə bilərsiniz.
Bu kodda necə görünür?
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 — bir mapping-in ölçüsü
long position = 0;
while (position < fileSize) {
long size = Math.min(chunkSize, fileSize - position);
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, position, size);
// Məlumatları buffer-dən sanki massivdən oxuyuruq
for (int i = 0; i < size; i++) {
byte b = buffer.get(i);
// Baytın işlənməsi (məsələn, müəyyən dəyər axtarırıq)
}
position += size;
}
}
System.out.println("Memory mapping ilə oxuma başa çatdı!");
}
}
RandomAccessFile və FileChannel fayla aşağı səviyyəli giriş verir. channel.map çağırışı faylın bir hissəsini yaddaşa xəritələyir. Məlumatlara giriş — MappedByteBuffer buffer-i vasitəsilədir.
Memory mapping-in üstünlükləri nələrdir?
- Faylın müxtəlif hissələrinə təsadüfi giriş üçün çox sürətlidir.
- Mövcud operativ yaddaşdan böyük fayllarla işləmək olar (ƏS lazım olan səhifələri özü gətirir).
- Müasir məlumat bazalarında, indekslərdə, böyük log-larda istifadə olunur.
Mənfi cəhətlər nələrdir?
- Həmişə yazma üçün uyğun deyil (xüsusən şəbəkə fayl sistemlərində).
- Mapping ölçülərinə məhdudiyyətlər var (adətən 32-bit JVM-lərdə bir mapping 2 GB-a qədər).
- Faylı bağlamağı unutmaq faylın kilidlənməsinə səbəb ola bilər (xüsusilə Windows-da).
- Bütün fayl əməliyyatları sürətlənmir — sadəcə ardıcıl oxuma lazımdırsa, adi buffer çox vaxt geri qalmır.
4. Praktiki nümunələr
Nümunə 1: Memory mapping ilə böyük faylda alt sətir axtarışı
Tutaq ki, 10 GB-lıq faylınız var və orada müəyyən bayt ardıcıllığını (məsələn, "SECRET" sətirini) tapmaq istəyirsiniz.
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("Mövqedə " + (position + i) + " tapıldı");
// Axtarışı dayandırmaq və ya davam etdirmək olar
}
}
position += size;
}
}
}
}
Diqqət:
Əgər alt sətir iki chunk arasında “parçalanıb” qala bilərsə, mütləq axtarılan ardıcıllığın uzunluğu qədər chunk-lar arasında örtüşmə nəzərdə tutulmalıdır.
5. Faydalı incəliklər
Chunking nə vaxt, memory mapping nə vaxt istifadə edilməlidir?
- Chunking — hər cür fayl üçün (mətn, binar, log-lar, arxivlər) universal yanaşmadır. Ardıcıl emal üçün yaxşı işləyir.
- Memory mapping — təsadüfi giriş, böyük indekslər, məlumat bazaları, nəhəng fayllarda sürətli axtarışlar üçün super effektivdir.
Hansını seçəcəyinizi bilmirsinizsə — ilk olaraq chunking-dən başlayın! Memory mapping güclüdür, lakin daha “aşağı səviyyəli” yanaşmadır və diqqət tələb edir.
Tövsiyələr
- try-with-resources-dən istifadə edin — axınların və kanalların avtomatik bağlanması üçün.
- Eyni anda çox fayl açmayın: ƏS-də açıq deskriptorların sayı üçün limitlər var.
- Çox böyük hissələrə mapping etməyin — bu, xətalara səbəb ola bilər (xüsusilə 32-bit JVM-lərdə).
- Paralel emal üçün faylı chunk-lara bölüb onları ayrı thread-lərdə emal etmək olar (amma diski “doldurmamaq” və yaddaş limitlərini aşmamaq vacibdir).
6. Böyük fayllarla işləyərkən tipik səhvlər
Səhv № 1: böyük faylı tam yaddaşa yükləmək cəhdi.
Çox yayılmış problemdir — xüsusilə yeni başlayanlarda. Fayl 1–2 GB-dan böyükdürsə, chunking və ya sətir-sətir oxumadan istifadə edin, əks halda proqram OutOfMemoryError ilə çökəcək.
Səhv № 2: buffer çox kiçikdir.
512 baytlıq buffer — optimizasiya deyil, performans üçün yavaş “özünüməhvetmə”dir. 64 KB və yuxarı buffer-lərdən istifadə edin.
Səhv № 3: axını və ya kanalı bağlamağı unutmaq.
Fayl deskriptoru asılı qalacaq, fayl silinməyəcək və ya JVM yenidən başladılanadək azad olunmayacaq. try-with-resources-dən istifadə edin.
Səhv № 4: memory mapping ilə yanlış iş.
Əgər fayl mapping zamanı başqa proses tərəfindən dəyişdirilirsə, nekonsistent məlumatlar və ya xəta ala bilərsiniz. Tez-tez dəyişən fayllar üçün memory mapping istifadə etməyin.
Səhv № 5: alt sətirləri axtararkən chunk-ların örtüşməsini nəzərə almamaq.
Axtarılan sətir iki chunk-ın “sərhədində” qala bilərsə, mütləq həmin sətirin uzunluğu qədər chunk-lar arasında örtüşmə edin.
GO TO FULL VERSION