Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 1. Классический Java I/O (java.io)
BufferedWriter – запись строк. Буферизация и путь данных в памяти JVM
Ключевое отличие от
Внутреннее устройство
Поле
Механика write(String) и путь данных
Накопление в буфере
Путь данных при
Никаких системных вызовов, никакого кодирования. Пять символов копируются в
Сброс буфера: flushBuffer()
Путь данных при заполнении буфера:
Ключевой момент:
Метод newLine(): платформенная независимость
#Java #для_новичков #beginner #IO #NIO #BufferedWriter
Глава 1. Классический Java I/O (java.io)
BufferedWriter – запись строк. Буферизация и путь данных в памяти JVM
BufferedWriter — декоратор над Writer, добавляющий внутреннюю буферизацию символьного вывода. Он решает проблему, зеркальную BufferedReader: накладные расходы системных вызовов при частых мелких операциях записи. Вместо немедленной отправки каждого символа или строки в underlying Writer, BufferedWriter накапливает данные в массиве char[] в heap и сбрасывает их пачками.Ключевое отличие от
BufferedOutputStream: BufferedWriter работает с символами char (16 бит), а не байтами. Это означает, что его внутренний буфер хранит char[], а преобразование в байты происходит позже — в underlying OutputStreamWriter, который кодирует символы в UTF-8 или другую кодировку.Внутреннее устройство
// Упрощенная структура из OpenJDK
public class BufferedWriter extends Writer {
// Внутренний буфер символов в heap JVM
private char[] cb;
// Текущая позиция записи в буфере
private int nChars;
// Ссылка на underlying Writer
private Writer out;
// Разделитель строки для newLine()
private String lineSeparator;
// Конструкторы
public BufferedWriter(Writer out) {
this(out, defaultCharBufferSize); // 8192 по умолчанию
}
public BufferedWriter(Writer out, int sz) {
super(out);
if (sz <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
this.out = out;
cb = new char[sz]; // Выделение в heap: 8192 * 2 = 16384 байт
nChars = 0;
lineSeparator = System.lineSeparator(); // Платформенный разделитель
}
}
Поле
cb — массив char[] размером 8192 по умолчанию, что занимает 16 KB в heap. lineSeparator инициализируется значением System.lineSeparator(), которое равно \n на Unix/Linux/macOS и \r\n на Windows.Механика write(String) и путь данных
Накопление в буфере
public void write(String s, int off, int len) throws IOException {
// Если строка не влезает в оставшийся буфер — сброс
if (len >= cb.length - nChars) {
flushBuffer(); // Сброс накопленного в underlying Writer
}
if (len >= cb.length) {
// Строка больше буфера — запись напрямую, минуя буфер
writeDirect(s, off, len);
} else {
// Копирование строки в буфер
s.getChars(off, off + len, cb, nChars); // Быстрое копирование из String
nChars += len;
}
}Путь данных при
write("Hello"):[Java: bw.write("Hello")]
-> [BufferedWriter: len=5 < cb.length - nChars?]
-> [s.getChars(0, 5, cb, nChars)] // Копирование char[] из String в buf
-> [System.arraycopy: копирование 5 char в cb]
-> [nChars += 5]
-> [Возврат]Никаких системных вызовов, никакого кодирования. Пять символов копируются в
cb через System.arraycopy — нативный метод, оптимизированный JVM.Сброс буфера: flushBuffer()
private void flushBuffer() throws IOException {
if (nChars > 0) {
out.write(cb, 0, nChars); // Запись в underlying Writer
nChars = 0; // Сброс позиции
}
}Путь данных при заполнении буфера:
[BufferedWriter: flushBuffer()]
-> [OutputStreamWriter.write(cb, 0, nChars)]
-> [StreamEncoder: кодирование char[] -> байты UTF-8]
-> [ByteBuffer: накопление закодированных байт]
-> [FileOutputStream.write(byte[]): системный вызов]
-> [Нативный буфер userspace]
-> [Ядро ОС: syscall write]
-> [Page Cache: dirty pages]
Ключевой момент:
BufferedWriter не знает о кодировке. Он накапливает char[], а кодирование в байты происходит в OutputStreamWriter. Это означает, что буфер BufferedWriter содержит символы, а буфер StreamEncoder — байты. Два уровня буферизации создают двойное копирование при сбросе.Метод newLine(): платформенная независимость
public void newLine() throws IOException {
write(lineSeparator); // Запись \n или \r\n из поля lineSeparator
}
newLine() записывает разделитель строки, определенный при конструировании. Это позволяет генерировать файлы с правильными разделителями для целевой платформы, независимо от платформы выполнения.// На Unix: запись \n
// На Windows: запись \r\n
// Результат одинаков для кода, различается для файла
#Java #для_новичков #beginner #IO #NIO #BufferedWriter
👍4
Путь данных в памяти: детальный разбор
Сценарий: запись списка строк
Память в процессе:
Никаких новых аллокаций в цикле, кроме возможных расширений
Сценарий: буфер заполнен
Роль garbage collector
Жизненный цикл буферов
Анализ памяти:
Общий пик памяти: ~72 KB на буферы + строки. Для больших файлов это незначительно.
Проблема: очень длинные строки
При превышении размера буфера
Практический пример: запись списка строк в файл
Сравнение с небуферизованной записью
С
#Java #для_новичков #beginner #IO #NIO #BufferedWriter
Сценарий: запись списка строк
public void writeLines(String path, List<String> lines) throws IOException {
// cb[8192] создается в heap при конструировании BufferedWriter
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(path), StandardCharsets.UTF_8))) {
for (String line : lines) {
writer.write(line); // Копирование в cb
writer.newLine(); // Копирование \n или \r\n в cb
// Системный вызов отсутствует, пока cb не заполнен
}
// Явный flush для гарантии сброса
writer.flush();
} // close() с неявным flush()
}Память в процессе:
cb[8192] — буфер BufferedWriter, 16 KB heapByteBuffer внутри StreamEncoder — буфер для кодирования, ~24 KB heapString из lines — удерживаются списком, не создаются зановоНикаких новых аллокаций в цикле, кроме возможных расширений
ByteBufferСценарий: буфер заполнен
// Допустим, cb заполнен на 8188 символов
// Запись строки "Hello" (5 символов + 1 \n = 6)
writer.write("Hello"); // 8188 + 5 = 8193 > 8192 — сброс!
writer.newLine(); // \n записывается в пустой cb
[write("Hello")]
-> [len=5 >= cb.length - nChars? 5 >= 4? да]
-> [flushBuffer()]
-> [out.write(cb, 0, 8188)] // Сброс 8188 символов в OutputStreamWriter
-> [StreamEncoder: кодирование 8188 char в байты UTF-8]
-> [FileOutputStream: системный вызов]
-> [nChars = 0]
-> [s.getChars(0, 5, cb, 0)] // "Hello" в cb[0..4]
-> [nChars = 5]
[newLine()]
-> [write("\n")] // или "\r\n"
-> [s.getChars(0, 1, cb, 5)] // \n в cb[5]
-> [nChars = 6]Роль garbage collector
Жизненный цикл буферов
public void processAndWrite(String inputPath, String outputPath) throws IOException {
// Все объекты создаются в heap
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(inputPath), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(outputPath), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
String processed = processLine(line); // Новая String в heap
writer.write(processed); // Копирование в cb
writer.newLine();
// processed становится мусором, если не сохранена
}
} // Все потоки закрыты, все буферы освобождены
// reader.cb, writer.cb, ByteBuffer'ы StreamEncoder — недостижимы
// GC собирает все при следующей minor collection
}Анализ памяти:
reader.cb[8192] — 16 KB, освобожден при закрытииwriter.cb[8192] — 16 KB, освобожден при закрытииByteBuffer декодера — ~16 KB, освобожденByteBuffer кодера — ~24 KB, освобожденline — собирается GC каждую итерациюprocessed — собирается GC, если не сохраненаОбщий пик памяти: ~72 KB на буферы + строки. Для больших файлов это незначительно.
Проблема: очень длинные строки
// Строка длиной 100 000 символов
String hugeLine = generateHugeLine();
// write() обнаруживает, что len >= cb.length
// Прямая запись, минуя буфер
writer.write(hugeLine);
При превышении размера буфера
BufferedWriter не копирует строку в cb, а передает её напрямую в underlying Writer. Это экономит память (не дублирует строку в буфере), но лишает преимущества буферизации — системный вызов выполняется немедленно.Практический пример: запись списка строк в файл
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class LineWriter {
public void writeLines(String path, List<String> lines) throws IOException {
// Путь данных:
// String -> char[] -> cb[BufferedWriter] -> ByteBuffer[StreamEncoder] -> FileOutputStream -> page cache -> диск
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(path), StandardCharsets.UTF_8))) {
for (String line : lines) {
writer.write(line); // Копирование в cb
writer.newLine(); // Платформенный разделитель
}
// Явный flush перед закрытием
writer.flush();
} // close() с неявным flush()
// Все буферы в heap освобождены, GC собирает
}
// Альтернатива: запись с заголовком и подвалом
public void writeReport(String path, String header,
List<String> data, String footer) throws IOException {
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(path), StandardCharsets.UTF_8))) {
writer.write(header);
writer.newLine();
writer.newLine(); // Пустая строка
for (String line : data) {
writer.write(line);
writer.newLine();
}
writer.newLine(); // Пустая строка перед подвалом
writer.write(footer);
writer.newLine();
}
}
}
Сравнение с небуферизованной записью
// Антипаттерн: запись без буферизации
public void writeUnbuffered(String path, List<String> lines) throws IOException {
try (OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream(path), StandardCharsets.UTF_8)) {
for (String line : lines) {
writer.write(line); // Каждый символ = системный вызов!
writer.write('\n');
}
}
}
OutputStreamWriter без BufferedWriter кодирует и записывает каждую порцию данных немедленно. Для строки "Hello" это означает: кодирование 5 символов в 5 байт UTF-8, системный вызов write(fd, buf, 5). Для 1 000 000 строк — 2 000 000 системных вызовов.С
BufferedWriter: накопление в cb, сброс при заполнении. Для средней строки 80 символов + 1 \n = 81 символ. Буфер 8192 вмещает ~100 строк. Системных вызовов: 1 000 000 / 100 = 10 000. Ускорение в 200 раз.#Java #для_новичков #beginner #IO #NIO #BufferedWriter
👍4