Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 1. Классический Java I/O (java.io)
BufferedOutputStream – буферизованная запись. Путь данных от heap к диску
Ключевое отличие от чтения: запись буферизована по умолчанию, потому что ОС сама буферизует записи в page cache. Однако без
Внутреннее устройство
Поля
Механика write() и путь данных
Посимвольная запись
Путь данных для
Никаких системных вызовов, никакого JNI. Байт записывается в массив в heap напрямую JVM-байткодом. Это на порядки быстрее, чем посимвольная запись через
Сброс буфера: flushBuffer()
Путь данных для
Данные попадают в page cache ядра, но не немедленно на диск. Ядро асинхронно сбрасывает dirty pages фоновыми потоками.
Роль flush() и принудительный сброс
flush() в BufferedOutputStream выполняет два действия:
Сброс внутреннего буфера.
Проксирование flush.
Важное различие между flush() и физической записью на диск:
Путь данных в памяти: детальный разбор
Сценарий: накопление данных в буфере
Память в процессе:
Данные накапливаются в
Когда
Сценарий: закрытие потока
Путь данных при закрытии:
Объекты
GC собирает их при следующей сборке
#Java #для_новичков #beginner #IO #NIO #BufferedOutputStream
Глава 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
Жизненный цикл буфера при записи
В этом примере каждая итерация создает массив
Оптимизация: прямая запись без промежуточных массивов
Идеальное решение — сериализация напрямую в
Проблема: забытый flush() и потеря данных
java
В этом коде:
Данные остаются в
Файловый дескриптор открыт, данные не записаны в ОС
Если приложение завершится до финализации — данные потеряны
Даже при финализации задержка непредсказуема
Правильный подход —
Практический пример: буферизованное копирование файла
#Java #для_новичков #beginner #IO #NIO #BufferedOutputStream
Жизненный цикл буфера при записи
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