Java for Beginner
870 subscribers
1.01K photos
275 videos
14 files
1.69K links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

Наш YouTube канал - https://www.youtube.com/@Java_Beginner-Dev

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)

Глава 1. Классический Java I/O (java.io)

BufferedOutputStream – буферизованная запись. Путь данных от heap к диску

BufferedOutputStream — декоратор над OutputStream, добавляющий внутреннюю буферизацию к любому underlying потоку записи. Он решает зеркальную проблему по отношению к BufferedInputStream: накладные расходы системных вызовов при частых мелких операциях записи. Вместо немедленной отправки каждого байта в ОС, BufferedOutputStream накапливает данные в памяти и сбрасывает их пачками.

Ключевое отличие от чтения: запись буферизована по умолчанию, потому что ОС сама буферизует записи в page cache. Однако без BufferedOutputStream каждый вызов write(int) выполняет системный вызов, даже если данные попадают только в page cache. BufferedOutputStream устраняет эти избыточные вызовы.

Внутреннее устройство
// Упрощенная структура из OpenJDK
public class BufferedOutputStream extends FilterOutputStream {
// Внутренний буфер в heap JVM
protected byte[] buf;

// Количество байт, фактически записанных в буфер
protected int count;

// Конструкторы
public BufferedOutputStream(OutputStream out) {
this(out, 8192); // Размер по умолчанию
}

public BufferedOutputStream(OutputStream out, int size) {
super(out);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size]; // Выделение в heap
}
}

Поля buf и count образуют линейный буфер накопления. При вызове write(int) байт помещается в buf[count++]. Когда count достигает buf.length, выполняется flushBuffer() — сброс накопленных данных в underlying поток.


Механика write() и путь данных

Посимвольная запись
public void write(int b) throws IOException {
if (count >= buf.length) {
flushBuffer(); // Буфер полон — сброс
}
buf[count++] = (byte) b; // Запись в heap, системный вызов отсутствует
}


Путь данных для write(int) при неполном буфере:
[Java: bos.write(65)]
-> [BufferedOutputStream: count < buf.length?]
-> [buf[count++] = 65] // Прямая запись в массив heap
-> [Возврат]

Никаких системных вызовов, никакого JNI. Байт записывается в массив в heap напрямую JVM-байткодом. Это на порядки быстрее, чем посимвольная запись через FileOutputStream.


Сброс буфера: flushBuffer()
private void flushBuffer() throws IOException {
if (count > 0) {
// Системный вызов: запись накопленных данных
out.write(buf, 0, count); // out — underlying FileOutputStream
count = 0; // Сброс счетчика
}
}


Путь данных для flushBuffer():
[Java: flushBuffer()]
-> [FileOutputStream.write(buf, 0, count)]
-> [JNI: нативный writeBytes]
-> [Ядро ОС: syscall write(fd, native_buf, count)]
-> [Page Cache: копирование в dirty pages]
-> [Пометка страниц как dirty]
-> [Возврат в userspace]
-> [Возврат в Java]
-> [count = 0]

Данные попадают в page cache ядра, но не немедленно на диск. Ядро асинхронно сбрасывает dirty pages фоновыми потоками.


Роль flush() и принудительный сброс

flush() в BufferedOutputStream выполняет два действия:
Сброс внутреннего буфера. flushBuffer() — запись накопленных данных в underlying поток.
Проксирование flush. out.flush() — сброс буферов underlying потока.
public void flush() throws IOException {
flushBuffer(); // Сброс buf в FileOutputStream
out.flush(); // Проксирование вниз по цепочке
}


Важное различие между flush() и физической записью на диск:
flush() гарантирует, что данные покинули JVM и попали в page cache ОС. Другие процессы могут читать эти данные.
flush() не гарантирует физическую запись на диск. При сбое питания данные в page cache теряются.
FileDescriptor.sync() или FileChannel.force(true) выполняют fsync(), который гарантирует физическую запись.
// Пример: гарантия доставки для критичных данных
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("critical.dat"))) {

bos.write(transactionData);
bos.flush(); // Сброс в page cache

// Для финансовых данных — fsync
bos.out.getFD().sync(); // Физическая запись на диск
}



Путь данных в памяти: детальный разбор

Сценарий: накопление данных в буфере
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("output.bin"), 8192)) {

for (int i = 0; i < 10000; i++) {
bos.write(i); // Накопление в buf, системные вызовы отсутствуют
}
// После цикла: count=10000, buf содержит данные в heap
}


Память в процессе:
buf[8192] — массив в heap, создан при конструировании
count — примитив в объекте BufferedOutputStream
Данные накапливаются в buf, GC не вовлечен в цикл

Когда count достигает 8192, следующий write() инициирует flushBuffer():
[Java: bos.write(byte_8192)]
-> [count >= 8192? да]
-> [flushBuffer()]
-> [FileOutputStream.write(buf, 0, 8192)]
-> [syscall write: 8192 байт в page cache]
-> [count = 0]
-> [buf[0] = byte_8192]
-> [count = 1]


Сценарий: закрытие потока
} // try-with-resources вызывает close()


close()
в BufferedOutputStream:
public void close() throws IOException {
try (OutputStream os = out) {
flush(); // Неявный flush перед закрытием
} // out.close() вызывается автоматически
}


Путь данных при закрытии:
flush() — сброс остатка буфера в underlying поток
out.flush() — сброс в page cache ОС
out.close() — закрытие файлового дескриптора
Объекты bos, buf, out становятся недостижимыми
GC собирает их при следующей сборке


#Java #для_новичков #beginner #IO #NIO #BufferedOutputStream
👍4
Роль garbage collector

Жизненный цикл буфера при записи
public void writeRecords(String path, List<Record> records) throws IOException {
// buf создается в Eden space
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(path))) {

for (Record record : records) {
byte[] data = record.toBytes(); // Новый массив при каждой итерации!
bos.write(data); // Копирование в buf или flushBuffer()
// data становится мусором
}

bos.flush(); // Явный сброс остатка

} // close() с неявным flush()

// buf и все data[] собираются GC
}

В этом примере каждая итерация создает массив data. Эти массивы короткоживущие и нагружают GC. Оптимизация — переиспользование или прямая запись в BufferedOutputStream без промежуточных массивов.

Оптимизация: прямая запись без промежуточных массивов
public void writeRecordsOptimized(String path, List<Record> records) throws IOException {
// Переиспользуемый буфер для сериализации
ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);

try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(path))) {

for (Record record : records) {
baos.reset(); // Сброс без создания нового объекта

record.serialize(baos); // Запись в переиспользуемый baos

byte[] data = baos.toByteArray(); // Все еще создается массив
// Лучше: запись напрямую в bos, минуя baos
}
}
}



Идеальное решение — сериализация напрямую в BufferedOutputStream, без промежуточных массивов:
public void writeRecordsDirect(String path, List<Record> records) throws IOException {
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(path)))) {

for (Record record : records) {
dos.writeUTF(record.getId()); // Прямая запись в buf
dos.writeLong(record.getTimestamp()); // Без промежуточных массивов
dos.writeInt(record.getValue());
// Данные накапливаются в buf BufferedOutputStream
}

} // Автоматический flush() и close()
}

DataOutputStream пишет примитивы напрямую в underlying поток, который буферизован. Никаких временных массивов, минимальная нагрузка на GC.
Проблема: забытый flush() и потеря данных
java
// Антипаттерн: данные могут не попасть в файл
public void dangerousWrite(String path, String data) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(path));
bos.write(data.getBytes());
// flush() не вызван!
// close() не вызван!
// При выходе из метода: buf содержит данные, но не сброшен
// GC финализирует объект, close() вызовется через Cleaner, но с задержкой
}


В этом коде:
Данные остаются в buf в heap
Файловый дескриптор открыт, данные не записаны в ОС
Если приложение завершится до финализации — данные потеряны
Даже при финализации задержка непредсказуема

Правильный подход — try-with-resources, который гарантирует close() с неявным flush():
// Правильно: гарантированный flush и close
public void safeWrite(String path, String data) throws IOException {
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(path))) {
bos.write(data.getBytes());
// flush() вызовется неявно в close()
}
}


Практический пример: буферизованное копирование файла
public class BufferedFileCopy {

private static final int BUFFER_SIZE = 8192;

public static void copy(String sourcePath, String destPath) throws IOException {
// Буфер для чтения — внешний, создается в heap
byte[] buffer = new byte[BUFFER_SIZE];

// BufferedInputStream: buf[8192] в heap для накопления чтения
// BufferedOutputStream: buf[8192] в heap для накопления записи
// Итого: ~16KB в heap на время копирования

try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(sourcePath), BUFFER_SIZE);
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(destPath), BUFFER_SIZE)) {

int bytesRead;
// Цикл без аллокаций в heap — GC не вовлечен
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}

// Явный flush для гарантии сброса остатка
bos.flush();

} // Автоматический close() для обоих потоков

// buffer, bis.buf, bos.buf — все становятся недостижимым
// GC собирает при следующей minor collection
}

// Альтернатива: без внешнего буфера, используя внутренние буферы оберток
public static void copyOptimized(String sourcePath, String destPath) throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(sourcePath));
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(destPath))) {

int byteValue;
// Побайтово, но фактически буферизовано
// Внешний буфер не нужен — экономия 8KB в heap
while ((byteValue = bis.read()) != -1) {
bos.write(byteValue);
}
}
}
}


#Java #для_новичков #beginner #IO #NIO #BufferedOutputStream
👍5