Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 1. Классический Java I/O (java.io)
Проблема производительности побайтового ввода/вывода. Идея буфера
Побайтовое чтение или запись через
Рассмотрим детальный путь одного вызова read():
Подготовка параметров в регистрах процессора. JVM помещает файловый дескриптор и адрес буфера в регистры.
Инструкция syscall или sysenter. Процессор переключается в привилегированный режим, сохраняя состояние userspace.
Обработка в ядре ОС. Ядро проверяет валидность дескриптора, ищет страницу в page cache, при промахе (page fault) инициирует чтение с диска.
Копирование данных. Ядро копирует данные из page cache в нативный буфер userspace.
Возврат в userspace. Восстановление состояния регистров, возврат управления JVM.
Копирование в heap. JNI-код копирует данные из нативного буфера в массив
Для одного байта этот цикл избыточен в 1000 раз. Файл размером 1 MB требует 1 048 576 таких циклов. При стоимости 500 нс на вызов общее время составляет 524 мс только на системные вызовы, без учета фактического чтения данных.
Демонстрация: копирование 1 MB побайтово
Ожидаемый результат на современном оборудовании: 500-2000 мс для 1 MB. Причина не в скорости диска (современные SSD читают 1 MB за 2-5 мс), а в накладных расходах системных вызовов.
Путь данных в памяти при побайтовом чтении
Каждый вызов
Критический недостаток: page cache работает со страницами 4KB. Чтение одного байта из файла загружает целую страницу в page cache. При последовательном побайтовом чтении каждый следующий байт попадает в уже загруженную страницу, но системный вызов все равно выполняется. Page cache эффективен, но syscall-ов слишком много.
#Java #для_новичков #beginner #IO #NIO #BufferedInputStream
Глава 1. Классический Java I/O (java.io)
Проблема производительности побайтового ввода/вывода. Идея буфера
Побайтовое чтение или запись через
InputStream.read() и OutputStream.write(int) кажутся простыми и интуитивными, но скрывают фундаментальную проблему: каждый вызов инициирует полный цикл системного вызова операционной системы. Этот цикл включает переключение контекста процессора из userspace в kernelspace, выполнение кода ядра, и обратное переключение. Стоимость этого цикла на современных процессорах составляет 100-1000 наносекунд, что на порядки превышает стоимость чтения байта из оперативной памяти.Рассмотрим детальный путь одного вызова read():
Подготовка параметров в регистрах процессора. JVM помещает файловый дескриптор и адрес буфера в регистры.
Инструкция syscall или sysenter. Процессор переключается в привилегированный режим, сохраняя состояние userspace.
Обработка в ядре ОС. Ядро проверяет валидность дескриптора, ищет страницу в page cache, при промахе (page fault) инициирует чтение с диска.
Копирование данных. Ядро копирует данные из page cache в нативный буфер userspace.
Возврат в userspace. Восстановление состояния регистров, возврат управления JVM.
Копирование в heap. JNI-код копирует данные из нативного буфера в массив
byte[] в heap.Для одного байта этот цикл избыточен в 1000 раз. Файл размером 1 MB требует 1 048 576 таких циклов. При стоимости 500 нс на вызов общее время составляет 524 мс только на системные вызовы, без учета фактического чтения данных.
Демонстрация: копирование 1 MB побайтово
public class ByteByByteCopy {
public static void copyByteByByte(String source, String dest) throws IOException {
long start = System.nanoTime();
int totalBytes = 0;
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(dest)) {
int byteValue;
// Каждая итерация = полный цикл системного вызова
while ((byteValue = fis.read()) != -1) {
fos.write(byteValue); // Еще один системный вызов на запись
totalBytes++;
}
}
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.printf("Copied %d bytes in %d ms (byte-by-byte)%n", totalBytes, duration);
}
public static void main(String[] args) throws IOException {
// Создание тестового файла 1 MB
byte[] oneMB = new byte[1024 * 1024];
new Random().nextBytes(oneMB);
try (FileOutputStream fos = new FileOutputStream("source.bin")) {
fos.write(oneMB);
}
// Побайтовое копирование
copyByteByByte("source.bin", "dest_byte_by_byte.bin");
}
}Ожидаемый результат на современном оборудовании: 500-2000 мс для 1 MB. Причина не в скорости диска (современные SSD читают 1 MB за 2-5 мс), а в накладных расходах системных вызовов.
Путь данных в памяти при побайтовом чтении
Каждый вызов
read() создает уникальный путь через память:Поток выполнения:
[Java-код: read()]
-> [JVM: нативный метод read0()]
-> [JNI: переход в нативный код]
-> [Ядро ОС: syscall read(fd, buf, 1)]
-> [Page Cache: поиск страницы 4KB]
-> [При промахе: чтение с диска в page cache]
-> [Копирование: 1 байт из page cache в нативный буфер]
-> [Возврат в userspace]
-> [JNI: копирование 1 байта в heap]
-> [Java: возврат int]
Критический недостаток: page cache работает со страницами 4KB. Чтение одного байта из файла загружает целую страницу в page cache. При последовательном побайтовом чтении каждый следующий байт попадает в уже загруженную страницу, но системный вызов все равно выполняется. Page cache эффективен, но syscall-ов слишком много.
#Java #для_новичков #beginner #IO #NIO #BufferedInputStream
👍4
Идея буфера: чтение и запись блоками
Буферизация решает проблему, заменяя множество мелких системных вызовов на один крупный. Вместо чтения одного байта за раз, приложение запрашивает блок данных — обычно 4KB, 8KB или 64KB — одним системным вызовом, а затем извлекает байты из блока в памяти без обращения к ОС.
Математика выигрыша
Для файла 1 MB:
Побайтово: 1 048 576 системных вызовов чтения + 1 048 576 вызовов записи = 2 097 152 syscall
Буфер 4KB: 256 системных вызовов чтения + 256 вызовов записи = 512 syscall
Сокращение: в 4096 раз
Ожидаемые результаты для 1 MB файла на SSD:
128 bytes: ~50 ms
512 bytes: ~15 ms
4096 bytes: ~5 ms
8192 bytes: ~4 ms
65536 bytes: ~4 ms
Выигрыш снижается после 8KB, так как это типичный размер страницы ОС и размер блока файловой системы. Большие буферы не дают дополнительного выигрыша, но увеличивают потребление памяти в heap.
Путь данных в памяти при буферизованном чтении
Роль garbage collector и памяти
Буфер в Eden space
Массив
Если метод вызывается в цикле обработки множества файлов, буфер может быть повышен до survivor space или даже old generation при частом выделении других объектов.
#Java #для_новичков #beginner #IO #NIO #BufferedInputStream
Буферизация решает проблему, заменяя множество мелких системных вызовов на один крупный. Вместо чтения одного байта за раз, приложение запрашивает блок данных — обычно 4KB, 8KB или 64KB — одним системным вызовом, а затем извлекает байты из блока в памяти без обращения к ОС.
Математика выигрыша
Для файла 1 MB:
Побайтово: 1 048 576 системных вызовов чтения + 1 048 576 вызовов записи = 2 097 152 syscall
Буфер 4KB: 256 системных вызовов чтения + 256 вызовов записи = 512 syscall
Сокращение: в 4096 раз
public class BufferedCopy {
public static void copyWithBuffer(String source, String dest, int bufferSize)
throws IOException {
long start = System.nanoTime();
int totalBytes = 0;
// Буфер создается в heap Eden space
// Для bufferSize = 4096: 4KB массив, быстро собирается GC если не переиспользуется
byte[] buffer = new byte[bufferSize];
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(dest)) {
int bytesRead;
// Каждая итерация = один системный вызов на чтение блока
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead); // Один системный вызов на запись блока
totalBytes += bytesRead;
}
}
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.printf("Copied %d bytes in %d ms (buffer=%d bytes)%n",
totalBytes, duration, bufferSize);
}
public static void main(String[] args) throws IOException {
// Тест с разными размерами буфера
int[] sizes = {128, 512, 1024, 4096, 8192, 65536};
for (int size : sizes) {
copyWithBuffer("source.bin", "dest_" + size + ".bin", size);
}
}
}Ожидаемые результаты для 1 MB файла на SSD:
128 bytes: ~50 ms
512 bytes: ~15 ms
4096 bytes: ~5 ms
8192 bytes: ~4 ms
65536 bytes: ~4 ms
Выигрыш снижается после 8KB, так как это типичный размер страницы ОС и размер блока файловой системы. Большие буферы не дают дополнительного выигрыша, но увеличивают потребление памяти в heap.
Путь данных в памяти при буферизованном чтении
Первая итерация read(buffer[4096]):
[Java-код: read(buffer)]
-> [JVM: нативный метод readBytes()]
-> [JNI: переход в нативный код]
-> [Ядро ОС: syscall read(fd, buf, 4096)]
-> [Page Cache: поиск страниц, загрузка при необходимости]
-> [Копирование: 4096 байт из page cache в нативный буфер]
-> [Возврат в userspace]
-> [JNI: копирование 4096 байт в buffer[4096] в heap]
-> [Java: возврат 4096]
Вторая итерация read(buffer[4096]):
[Java-код: read(buffer)]
-> [JVM: нативный метод readBytes()]
-> [JNI: переход в нативный код]
-> [Ядро ОС: syscall read(fd, buf, 4096)]
-> [Page Cache: страницы уже в кэше]
-> [Копирование: 4096 байт]
-> [Возврат]
-> [JNI: копирование 4096 байт в buffer в heap]
-> [Java: возврат 4096]
// Всего 256 итераций для 1 MB вместо 1 048 576
Роль garbage collector и памяти
Буфер в Eden space
Массив
byte[], созданный как буфер, размещается в Eden space young generation. Если метод copyWithBuffer вызывается редко, буфер собирается при следующем minor GC. Если метод вызывается в цикле обработки множества файлов, буфер может быть повышен до survivor space или даже old generation при частом выделении других объектов.
// Антипаттерн: создание буфера внутри цикла
for (String file : files) {
byte[] buffer = new byte[8192]; // Создание при каждой итерации
copyFile(file, buffer);
// buffer становится мусором, GC собирает его
}
#Java #для_новичков #beginner #IO #NIO #BufferedInputStream
👍4
Этот код создает множество короткоживущих массивов, нагружая GC. Правильный подход — переиспользование:
ThreadLocal для многопоточности
В многопоточном приложении совместное использование одного буфера требует синхронизации.
Память: N потоков * 8KB = N * 8KB в heap на протяжении жизни потоков. Это компромисс между памятью и производительностью. Буферы в
Humongous objects и G1 GC
Буферы размером более половины размера G1-региона (обычно > 512KB для региона 1MB) считаются humongous objects. Они размещаются в специальных humongous-регионах old generation и не перемещаются при minor GC.
Это увеличивает фрагментацию heap и может вызвать преждевременный full GC.
BufferedInputStream: встроенная буферизация
Java предоставляет готовую обертку
При чтении
Путь данных с
Первый
Второй
8193-й
Массив
#Java #для_новичков #beginner #IO #NIO #BufferedInputStream
// Паттерн: переиспользование буфера
byte[] buffer = new byte[8192]; // Один буфер на все итерации
for (String file : files) {
copyFile(file, buffer); // Передача по ссылке, без создания
}
// Буфер живет до выхода из метода, собирается одним GC
ThreadLocal для многопоточности
В многопоточном приложении совместное использование одного буфера требует синхронизации.
ThreadLocal предоставляет каждому потоку свой буфер, устраняя contention:public class ThreadLocalBuffer {
// Каждый поток получает свой буфер 8KB
private static final ThreadLocal<byte[]> BUFFER =
ThreadLocal.withInitial(() -> new byte[8192]);
public void copyFileThreadSafe(String source, String dest) throws IOException {
byte[] buffer = BUFFER.get(); // Буфер потока, без синхронизации
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(dest)) {
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
// Буфер остается в ThreadLocal, не собирается GC между вызовами
}
}Память: N потоков * 8KB = N * 8KB в heap на протяжении жизни потоков. Это компромисс между памятью и производительностью. Буферы в
ThreadLocal живут долго и могут быть повышены в old generation, что увеличивает размер heap, но устраняет аллокации в hot path.Humongous objects и G1 GC
Буферы размером более половины размера G1-региона (обычно > 512KB для региона 1MB) считаются humongous objects. Они размещаются в специальных humongous-регионах old generation и не перемещаются при minor GC.
Это увеличивает фрагментацию heap и может вызвать преждевременный full GC.
// Потенциально проблематично: 1MB буфер как humongous object
byte[] largeBuffer = new byte[1024 * 1024];
// Предпочтительнее: несколько итераций с 8KB буфером
byte[] smallBuffer = new byte[8192];
for (int i = 0; i < 128; i++) { // 128 * 8KB = 1MB
fis.read(smallBuffer);
// Обработка
}
BufferedInputStream: встроенная буферизация
Java предоставляет готовую обертку
BufferedInputStream, которая инкапсулирует логику буферизации: // Внутреннее устройство BufferedInputStream
public class BufferedInputStream extends FilterInputStream {
protected volatile byte[] buf; // Буфер в heap
protected int count; // Количество доступных байт
protected int pos; // Текущая позиция чтения
protected int markpos = -1; // Позиция маркера
// Конструктор по умолчанию: buf = new byte[8192]
public BufferedInputStream(InputStream in) {
this(in, 8192);
}
}
При чтении
BufferedInputStream сначала проверяет, есть ли данные в buf. Если есть — возвращает байт из буфера без системного вызова. Если буфер пуст — выполняет fill(), который читает до buf.length байт из underlying потока одним системным вызовом.// Использование: прозрачная буферизация
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("source.bin"))) {
int byteValue;
// Чтение выглядит побайтовым, но фактически буферизовано
while ((byteValue = bis.read()) != -1) {
processByte(byteValue);
}
}
Путь данных с
BufferedInputStream:Первый
read(): fill() читает 8192 байт в buf (heap), возвращает buf[0]Второй
read(): возвращает buf[1] из heap, системный вызов отсутствует8193-й
read(): буфер пуст, fill() выполняет новый системный вызовМассив
buf создается при конструировании и живет до закрытия потока. GC не вовлечен в цикл чтения.#Java #для_новичков #beginner #IO #NIO #BufferedInputStream
👍4
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 1. Классический Java I/O (java.io)
BufferedInputStream – буферизованное чтение. Путь данных через память JVM
Ключевое архитектурное решение — разделение ответственности.
Внутреннее устройство
Поля
Механика fill() и путь данных
Метод
Путь данных при
Проверка состояния буфера. Определение, можно ли читать с начала или требуется сдвиг/расширение.
Системный вызов read().
Копирование в heap. Данные из нативного буфера через JNI попадают в
Обновление счетчиков.
Путь данных в памяти: детальный разбор
Сценарий: первое чтение после создания
Путь данных для первого
Массив
Сценарий: последующие чтения из буфера
Никаких системных вызовов, никакого JNI, никакого обращения к ядру. Данные извлекаются из массива в heap напрямую JVM-байткодом. Это на порядки быстрее, чем побайтовое чтение через
Сценарий: буфер исчерпан
#Java #для_новичков #beginner #IO #NIO #BufferedInputStream
Глава 1. Классический Java I/O (java.io)
BufferedInputStream – буферизованное чтение. Путь данных через память JVM
BufferedInputStream — декоратор над InputStream, добавляющий внутреннюю буферизацию к любому underlying потоку. Он не является самостоятельным источником данных, а служит промежуточным слоем, который накапливает данные в памяти и выдает их порциями при запросе. Это устраняет главный недостаток побайтового чтения: накладные расходы системных вызовов.Ключевое архитектурное решение — разделение ответственности.
FileInputStream отвечает за взаимодействие с ОС и файловой системой. BufferedInputStream отвечает за оптимизацию паттерна доступа: замену множества мелких запросов на несколько крупных. Это классический применение паттерна декоратора в Java I/O.Внутреннее устройство
// Упрощенная структура из OpenJDK
public class BufferedInputStream extends FilterInputStream {
// Внутренний буфер в heap JVM
protected volatile byte[] buf;
// Количество байт, фактически доступных в буфере
protected int count;
// Текущая позиция чтения в буфере
protected int pos;
// Позиция маркера для mark/reset, -1 если не установлен
protected int markpos = -1;
// Максимальное количество байт, читаемых после mark
protected int marklimit;
// Конструкторы
public BufferedInputStream(InputStream in) {
this(in, 8192); // Размер по умолчанию
}
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size]; // Выделение в heap
}
}
Поля
buf, count, pos образуют кольцевой буфер (ring buffer), хотя в стандартной реализации используется линейный массив с сдвигом при исчерпании. При вызове read() BufferedInputStream сначала проверяет pos < count. Если условие истинно — байт возвращается из buf[pos++] без системного вызова. Если ложно — выполняется fill(), который читает новый блок данных из underlying потока.Механика fill() и путь данных
Метод
fill() — сердце буферизации. Он выполняется, когда внутренний буфер исчерпан:// Упрощенная логика fill()
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos < 0) {
// Маркер не установлен — читаем с начала буфера
pos = 0;
} else if (pos >= buffer.length) {
// Позиция достигла конца буфера
if (markpos > 0) {
// Сдвигаем данные от markpos к началу
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
// Буфер превысил лимит маркера — сбрасываем
markpos = -1;
pos = 0;
} else {
// Расширяем буфер
int nsz = pos * 2;
if (nsz > marklimit) nsz = marklimit;
byte[] nbuf = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
buf = nbuf;
buffer = nbuf;
markpos = 0;
}
}
count = pos;
// Системный вызов: чтение из underlying потока
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0) count = n + pos;
}
Путь данных при
fill():Проверка состояния буфера. Определение, можно ли читать с начала или требуется сдвиг/расширение.
Системный вызов read().
getInIfOpen().read(buffer, pos, len) — один вызов для чтения до buffer.length - pos байт.Копирование в heap. Данные из нативного буфера через JNI попадают в
buf в heap.Обновление счетчиков.
count устанавливается в количество фактически прочитанных байт.Путь данных в памяти: детальный разбор
Сценарий: первое чтение после создания
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("data.bin"), 8192)) {
int firstByte = bis.read(); // Инициирует fill()
}
Путь данных для первого
read():[Java: bis.read()]
-> [BufferedInputStream: pos=0, count=0, буфер пуст]
-> [fill()]
-> [FileInputStream.read(buf, 0, 8192)]
-> [JNI: нативный readBytes]
-> [Ядро ОС: syscall read(fd, native_buf, 8192)]
-> [Page Cache: поиск/загрузка страниц]
-> [Копирование: 8192 байт из page cache в native_buf]
-> [Возврат в userspace]
-> [JNI: копирование 8192 байт в buf[8192] в heap]
-> [BufferedInputStream: count=8192, pos=0]
-> [Возврат buf[0] как int]
Массив
buf[8192] создан при конструировании BufferedInputStream и размещен в heap. Для размера 8192 байт это обычный объект, не humongous. Он размещается в Eden space young generation.Сценарий: последующие чтения из буфера
int secondByte = bis.read(); // pos=1, count=8192, системный вызов отсутствует
int thirdByte = bis.read(); // pos=2, count=8192, системный вызов отсутствует
[Java: bis.read()]
-> [BufferedInputStream: pos < count? 1 < 8192 — да]
-> [Возврат buf[1] как int, pos++]
Никаких системных вызовов, никакого JNI, никакого обращения к ядру. Данные извлекаются из массива в heap напрямую JVM-байткодом. Это на порядки быстрее, чем побайтовое чтение через
FileInputStream.Сценарий: буфер исчерпан
// После 8192 вызовов read()
int byte8193 = bis.read(); // pos=8192, count=8192, буфер пуст
[Java: bis.read()]
-> [BufferedInputStream: pos < count? 8192 < 8192 — нет]
-> [fill()]
-> [System.arraycopy при необходимости]
-> [FileInputStream.read(buf, 0, 8192)]
-> [Новый системный вызов]
-> [count=новое_значение, pos=0]
-> [Возврат buf[0]]
#Java #для_новичков #beginner #IO #NIO #BufferedInputStream
👍4
Роль garbage collector
Жизненный цикл буфера
Буфер
Проблема: большой буфер и humongous objects
При размере буфера 1MB и размере G1-региона 1MB массив становится humongous object. Он размещается в специальных humongous-регионах old generation и не перемещается при minor GC. Это увеличивает фрагментацию heap и может вызвать преждевременный full GC.
Рекомендация: использовать стандартный размер 8KB или увеличивать до 64KB для последовательного чтения больших файлов. Размеры более 256KB редко дают выигрыш из-за размера страницы ОС (4KB) и размера блока файловой системы (4KB).
Проблема: утечка ссылки на поток
Если ссылка на
mark() и reset(): семантика и память
Если количество прочитанных байт после
Расширение буфера при активном маркере:
Это единственный сценарий, где
Практический пример: сравнение производительности
Ожидаемые результаты на SSD:
#Java #для_новичков #beginner #IO #NIO #BufferedInputStream
Жизненный цикл буфера
public void processFile(String path) throws IOException {
// buf создается в Eden space при конструировании BufferedInputStream
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(path))) {
int b;
// Цикл без аллокаций в heap — GC не вовлечен
while ((b = bis.read()) != -1) {
processByte(b);
}
} // close() вызывается автоматически
// buf становится недостижимым
// GC собирает buf и bis при следующей minor collection
}Буфер
buf — это долгоживущий объект относительно метода. Он создается при входе в try и живет до выхода. Если метод вызывается часто, буфер может быть повышен в survivor space или old generation при выживании нескольких minor GC. Это нормально — буфер небольшой (8KB) и не создает проблем с памятью.Проблема: большой буфер и humongous objects
// Потенциально проблематично: 1MB буфер
BufferedInputStream bis = new BufferedInputStream(fis, 1024 * 1024);
При размере буфера 1MB и размере G1-региона 1MB массив становится humongous object. Он размещается в специальных humongous-регионах old generation и не перемещается при minor GC. Это увеличивает фрагментацию heap и может вызвать преждевременный full GC.
Рекомендация: использовать стандартный размер 8KB или увеличивать до 64KB для последовательного чтения больших файлов. Размеры более 256KB редко дают выигрыш из-за размера страницы ОС (4KB) и размера блока файловой системы (4KB).
Проблема: утечка ссылки на поток
// Антипаттерн: утечка ссылки
private InputStream leakedStream;
public void open(String path) throws IOException {
leakedStream = new BufferedInputStream(new FileInputStream(path));
// Если исключение позже — поток не закрыт, buf не освобожден
}
Если ссылка на
BufferedInputStream сохраняется в поле класса, GC не может освободить буфер даже после исчерпания потока. Буфер остается в памяти до тех пор, пока объект BufferedInputStream достижим. При многократном открытии файлов без закрытия это приводит к исчерпанию heap.mark() и reset(): семантика и память
BufferedInputStream поддерживает маркировку позиции для последующего возврата. Это требует хранения данных в буфере между mark() и reset():bis.mark(1024); // Запомнить позицию, лимит 1024 байт вперед
// Чтение до 1024 байт
bis.reset(); // Вернуться к отмеченной позиции
Если количество прочитанных байт после
mark() превышает marklimit, маркер инвалидируется. Если данные влезают в текущий буфер — reset() работает без системных вызовов. Если требуется больше данных, чем вмещает буфер — fill() выполняет System.arraycopy для сдвига или расширяет буфер.Расширение буфера при активном маркере:
// Из fill(): буфер расширяется до marklimit
byte[] nbuf = new byte[nsz]; // Новый массив в heap
System.arraycopy(buffer, 0, nbuf, 0, pos); // Копирование старых данных
buf = nbuf; // Старая ссылка на массив теряется, GC собирает старый buf
Это единственный сценарий, где
BufferedInputStream создает новые массивы в процессе работы. При отсутствии mark()/reset() буфер остается неизменным.Практический пример: сравнение производительности
public class BufferedPerformanceDemo {
private static final int FILE_SIZE = 10 * 1024 * 1024; // 10 MB
public static void main(String[] args) throws IOException {
// Создание тестового файла
createTestFile("test.bin", FILE_SIZE);
// Тест 1: FileInputStream без буферизации
measure("FileInputStream raw", () -> {
try (FileInputStream fis = new FileInputStream("test.bin")) {
while (fis.read() != -1) {} // Побайтово
}
});
// Тест 2: FileInputStream с внешним буфером
measure("FileInputStream + byte[]", () -> {
try (FileInputStream fis = new FileInputStream("test.bin")) {
byte[] buf = new byte[8192];
while (fis.read(buf) != -1) {}
}
});
// Тест 3: BufferedInputStream (внутренняя буферизация)
measure("BufferedInputStream", () -> {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("test.bin"))) {
while (bis.read() != -1) {} // Побайтово, но буферизовано
}
});
}
private static void measure(String name, Runnable task) {
long start = System.nanoTime();
task.run();
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.printf("%s: %d ms%n", name, ms);
}
private static void createTestFile(String path, int size) throws IOException {
byte[] data = new byte[size];
new Random().nextBytes(data);
try (FileOutputStream fos = new FileOutputStream(path)) {
fos.write(data);
}
}
}Ожидаемые результаты на SSD:
FileInputStream raw: 15 000-30 000 msFileInputStream + byte[]: 30-50 msBufferedInputStream: 30-50 msBufferedInputStream с побайтовым read() достигает той же производительности, что и FileInputStream с внешним массивом, но с более чистым API. Внешний буфер требует ручного управления размером и границами, BufferedInputStream инкапсулирует эту сложность.#Java #для_новичков #beginner #IO #NIO #BufferedInputStream
👍4