Java for Beginner
870 subscribers
1.01K photos
275 videos
14 files
1.68K 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)

FileOutputStream – запись байтов в файл. Путь данных от heap к диску

FileOutputStream — конкретная реализация OutputStream, предназначенная для записи байтов в файл файловой системы. Он представляет собой низкоуровневый мост между Java-кодом и операционной системой: каждый вызов write() транслируется в системный вызов write() ядра ОС, который взаимодействует с файловой системой, страничным кэшем и, в конечном итоге, с блочным устройством хранения.

Как и FileInputStream, FileOutputStream не выполняет внутреннюю буферизацию. Каждый вызов write(int) напрямую обращается к ОС, что делает посимвольную запись крайне неэффективной. Однако именно эта прямота делает FileOutputStream полезным как underlying поток для буферизующих оберток (BufferedOutputStream) или как инструмент для сценариев, где требуется точный контроль над моментом физической записи.


Конструкторы и режимы открытия файла

FileOutputStream предоставляет несколько конструкторов, различающихся способом указания файла и режимом открытия:
// Перезапись: если файл существует, содержимое уничтожается
public FileOutputStream(String name) throws FileNotFoundException

// Режим добавления: запись начинается с конца существующего файла
public FileOutputStream(String name, boolean append) throws FileNotFoundException

// Аналогичные конструкторы с File вместо String
public FileOutputStream(File file) throws FileNotFoundException
public FileOutputStream(File file, boolean append) throws FileNotFoundException


Режим перезаписи (append = false)

При открытии в режиме перезаписи конструктор выполняет следующую последовательность:
Валидация пути. Проверка на null, проверка родительской директории на существование и возможность создания файла.
Создание или усечение файла. Системный вызов open() с флагами O_WRONLY | O_CREAT | O_TRUNC. Если файл существует, его содержимое уничтожается, размер сбрасывается в ноль. Если файл не существует, создается новый пустой файл.
Установка позиции записи. Указатель файла устанавливается в начало — позицию 0.
Создание объекта. Выделение FileOutputStream в heap JVM, инициализация полей, включая FileDescriptor с файловым дескриптором ОС.

Этот режим опасен при случайном использовании: открытие существующего файла на запись без append = true безвозвратно удаляет его содержимое. В production-коде рекомендуется явно указывать режим, даже если требуется перезапись, для самодокументирования намерений.


Режим добавления (append = true)

Режим добавления открывает файл с флагами O_WRONLY | O_CREAT | O_APPEND. Ключевое отличие: флаг O_APPEND гарантирует атомарное позиционирование указателя в конец файла перед каждой операцией записи.

Это критично для сценариев, где несколько процессов или потоков пишут в один файл параллельно: каждая запись добавляется в конец без риска перезаписи данных другого писателя.
// Безопасное добавление в лог-файл из нескольких потоков
public void appendToLog(String message) throws IOException {
byte[] data = message.getBytes(StandardCharsets.UTF_8);
try (FileOutputStream fos = new FileOutputStream("app.log", true)) {
fos.write(data);
fos.write('\n');
}
}

Однако FileOutputStream с append = true не обеспечивает потокобезопасность на уровне Java: два потока, одновременно вызывающие write(), могут перемешать байты внутри одного вызова, так как write(byte[]) не атомарен. Для потокобезопасной записи требуется внешняя синхронизация или использование java.nio.channels.FileChannel с блокировками.


#Java #для_новичков #beginner #IO #NIO #FileOutputStream
👍4
Путь данных от heap до диска

Запись через FileOutputStream проходит через те же уровни памяти, что и чтение, но в обратном направлении:

Уровень 1: Массив в heap JVM

Java-код подготавливает данные в массиве byte[], расположенном в heap. Это обычный объект, управляемый garbage collector.
String text = "Hello, World!";
byte[] data = text.getBytes(StandardCharsets.UTF_8);
// data — массив в heap, содержит байты UTF-8: 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21


Уровень 2: Системный вызов write()

Вызов fos.write(data) инициирует цепочку: нативный метод writeBytes() в FileOutputStream.c вызывает IO_Write, который оборачивает write(fd, buf, len).
// Путь данных для write(byte[]):
// 1. byte[] в heap JVM
// 2. Копирование в нативный буфер (JNI transition)
// 3. Системный вызов write() — данные попадают в page cache ядра
// 4. Ядро помечает страницы как dirty (грязные)
// 5. Асинхронная запись dirty-страниц на диск фоновыми потоками ядра


Уровень 3: Страничный кэш и dirty pages

Данные не записываются на диск немедленно. Ядро ОС помещает их в страничный кэш, помечая соответствующие страницы как dirty (измененные). Фоновые потоки ядра (pdflush на Linux, buffer cache flush на других системах) периодически синхронизируют dirty pages с блочным устройством. Это называется write-back caching и является стандартной стратегией для файловых систем.

Параметр vm.dirty_ratio в Linux определяет, при каком проценте занятой памяти dirty pages начинается агрессивная запись на диск. Параметр vm.dirty_expire_centisecs — через какое время dirty page считается устаревшей и подлежит сбросу.

Уровень 4: Физическая запись на диск

Контроллер диска (SSD или HDD) получает команды записи от драйвера файловой системы. Для SSD контроллер выполняет wear leveling, распределяя записи равномерно по ячейкам флеш-памяти для продления срока службы. Для HDD головки позиционируются над нужными дорожками. Этот уровень полностью вне контроля JVM и ОС — он управляется прошивкой устройства.


#Java #для_новичков #beginner #IO #NIO #FileOutputStream
👍4
Роль flush() и fsync()

Метод flush() в FileOutputStream вызывает fd.sync() или аналог, который выполняет системный вызов fsync() на POSIX. Этот вызов гарантирует, что все dirty pages данного файла сброшены на физический носитель, и данные переживут сбой питания.
try (FileOutputStream fos = new FileOutputStream("critical.dat")) {
fos.write(transactionData);
fos.flush(); // Сброс буферов JVM в ОС
fos.getFD().sync(); // fsync — гарантия физической записи
} // close() неявно вызывает flush(), но не fsync()


Важное различие:
flush() — сброс буферов JVM в page cache ОС. Данные еще не на диске, но видны другим процессам.
fsync() (через FileDescriptor.sync()) — принудительная запись dirty pages на физический носитель. Дорогостоящая операция, блокирующая поток до завершения I/O.

В большинстве сценариев flush() достаточно. fsync() применяется только для критичных данных: финансовые транзакции, контрольные точки баз данных, состояния консенсуса в распределенных системах.

Пример: запись строки как массива байтов
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class FileWriteExample {

public void writeStringToFile(String path, String content) throws IOException {
// Преобразование строки в байты UTF-8
// Создается новый массив byte[] в heap Eden space
byte[] data = content.getBytes(StandardCharsets.UTF_8);

// FileOutputStream создается в heap
// Нативный fd выделяется ОС, файл открывается или создается
try (FileOutputStream fos = new FileOutputStream(path)) {

// write(byte[]) — копирование из heap в page cache через нативный буфер
fos.write(data);

// Явный flush — гарантия сброса в ОС
// Без flush данные могут оставаться в нативных буферах JVM
fos.flush();

} // close() вызывается автоматически
// 1. flush() — сброс остатков в ОС
// 2. Закрытие файлового дескриптора
// 3. Объект fos становится недостижимым для GC

// Массив data становится недостижимым после выхода из метода
// GC собирает data и fos при следующей minor collection
}

// Режим добавления
public void appendToFile(String path, String content) throws IOException {
byte[] data = (content + System.lineSeparator()).getBytes(StandardCharsets.UTF_8);

// append = true — открытие с O_APPEND, указатель в конце файла
try (FileOutputStream fos = new FileOutputStream(path, true)) {
fos.write(data);
fos.flush();
}
}
}


#Java #для_новичков #beginner #IO #NIO #FileOutputStream
👍4
Роль garbage collector при записи

Жизненный цикл объектов
public void processAndWrite(String outputPath, List<String> records) throws IOException {
// records — ссылка в стеке, объект List в heap, элементы — объекты String в heap

try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(outputPath), 8192)) {

for (String record : records) {
// Каждая итерация:
// 1. getBytes() создает новый byte[] в heap Eden
// 2. write() копирует данные в buf BufferedOutputStream (heap)
// 3. При заполнении buf — сброс в FileOutputStream -> ОС
byte[] bytes = record.getBytes(StandardCharsets.UTF_8);
bos.write(bytes);
bos.write('\n');
// bytes становится недостижимым после итерации
// GC собирает bytes в следующей minor collection
}

// bos.flush() сбрасывает остаток buf в ОС

} // bos и underlying fos закрыты

// records все еще достижим, если используется дальше
}

В этом прице каждая итерация создает временный массив bytes. Эти массивы короткоживущие и собираются minor GC в young generation. Если records содержит миллионы элементов, это создает значительное давление на GC, хотя сами массивы малы.


Оптимизация: переиспользование буфера

Для устранения аллокаций в цикле можно использовать переиспользуемый ByteArrayOutputStream или CharsetEncoder с переиспользуемым ByteBuffer:
public void optimizedWrite(String outputPath, List<String> records) throws IOException {
// Переиспользуемый буфер в heap, создается один раз
ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);

try (FileOutputStream fos = new FileOutputStream(outputPath);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {

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

byte[] bytes = record.getBytes(StandardCharsets.UTF_8);
baos.write(bytes);
baos.write('\n');

// Запись накопленных данных
bos.write(baos.toByteArray(), 0, baos.size());
}
}
}

Однако этот подход усложняет код без значительного выигрыша для типичных сценариев. JVM эффективно обрабатывает короткоживущие объекты в Eden через TLAB (Thread Local Allocation Buffer), и minor GC для young generation быстр.


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