Java for Beginner
871 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)

FileInputStream – чтение байтов из файла. Путь данных в памяти JVM

FileInputStream — конкретная реализация InputStream, предназначенная для чтения байтов из файловой системы. Она представляет собой низкоуровневый мост между Java-кодом и операционной системой: каждый вызов read() транслируется в системный вызов read() ядра ОС, который взаимодействует с файловой системой и блочным устройством хранения.

В отличие от BufferedInputStream, FileInputStream не выполняет внутреннюю буферизацию. Каждый вызов read() напрямую обращается к ОС, что делает посимвольное чтение через FileInputStream крайне неэффективным для больших файлов. Однако именно эта прямота делает FileInputStream полезным для сценариев, где требуется точный контроль над каждым системным вызовом: чтение с устройств с поблочным доступом, работа с pipe-файлами, или как underlying поток для буферизующих оберток.


Конструкторы и жизненный цикл файлового дескриптора


FileInputStream предоставляет два основных конструктора:
// Конструктор по строковому пути
public FileInputStream(String name) throws FileNotFoundException

// Конструктор по объекту File
public FileInputStream(File file) throws FileNotFoundException


Оба конструктора выполняют одну и ту же последовательность операций на уровне JVM и ОС:
Валидация параметра. Проверка на null, проверка существования файла через системный вызов stat() или аналог.
Открытие файла. Системный вызов open() создает файловый дескриптор (file descriptor) — целочисленный идентификатор, представляющий открытый файл в контексте процесса.
Создание объекта FileInputStream. Выделение объекта в heap JVM, инициализация полей, включая нативное поле fd типа FileDescriptor, которое хранит целочисленный дескриптор ОС.

Файловый дескриптор — это ограниченный ресурс операционной системы. Каждый процесс имеет лимит на количество открытых дескрипторов (обычно 1024 по умолчанию, расширяемый до миллионов через ulimit). Незакрытый FileInputStream удерживает дескриптор до вызова close() или финализации объекта GC, что создает риск исчерпания лимита при интенсивном открытии файлов без закрытия.


FileNotFoundException: семантика и обработка


Конструкторы FileInputStream объявляют throws FileNotFoundException — checked-исключение, которое компилятор обязывает обрабатывать.

Это исключение возникает в трех основных сценариях:
Файл не существует по указанному пути
Файл существует, но является директорией, а не обычным файлом
Файл существует, но приложение не имеет прав на чтение
public byte[] readFileContent(String path) {
try (FileInputStream fis = new FileInputStream(path)) {
return fis.readAllBytes();
} catch (FileNotFoundException e) {
// Файл не существует или недоступен — бизнес-решение
logger.warn("File not found: {}", path);
return new byte[0];
} catch (IOException e) {
// Другие I/O ошибки при чтении
logger.error("Failed to read file: {}", path, e);
throw new RuntimeException("File read error", e);
}
}

FileNotFoundException наследует IOException и является восстановимым сбоем в контексте файловых операций. Приложение может предпринять альтернативные действия: использовать значение по умолчанию, создать файл, запросить другой путь. В отличие от RuntimeException, checked-природа FileNotFoundException форсирует явную обработку в коде.


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

Уровень 1: Физическое чтение с носителя

Данные хранятся на блочном устройстве — SSD, HDD, NVMe — в секторах фиксированного размера (обычно 512 байт или 4KB). Когда ОС запрашивает чтение файла, драйвер устройства транслирует логические блоки файловой системы в физические адреса секторов. Для SSD контроллер выполняет wear leveling и garbage collection на уровне флеш-памяти, независимо от JVM.

Уровень 2: Страничный кэш ядра (Page Cache)

Операционная система поддерживает страничный кэш — область оперативной памяти ядра, где хранятся недавно прочитанные страницы файлов. Размер страницы обычно 4KB. При первом чтении файла данные загружаются с диска в page cache. При повторном чтении тех же данных ОС возвращает их из page cache без обращения к диску.
Page cache — это память ядра, недоступная напрямую из Java. JVM не управляет этой памятью и не видит её в heap. Однако интенсивное чтение больших файлов вытесняет другие страницы из кэша, что может повлиять на производительность всей системы.

Уровень 3: Системный вызов и нативный буфер

Когда FileInputStream.read() вызывается, JVM выполняет системный вызов read() через JNI (Java Native Interface). Ядро ОС копирует данные из page cache в нативный буфер в userspace — память процесса JVM за пределами heap. Это первое копирование данных.
// read() вызывает нативный метод, который выполняет системный вызов
public native int read() throws IOException;

Нативный метод read0() в FileInputStream.c из OpenJDK вызывает IO_Read на POSIX-системах, который оборачивает read(fd, buf, 1) для посимвольного чтения или read(fd, buf, len) для буферизованного.

Уровень 4: Копирование в heap JVM

После выполнения системного вызова данные копируются из нативного буфера в массив byte[] в heap JVM. Это второе копирование. Только после этого данные становятся доступны Java-коду.
// Путь данных для read():
// 1. Диск -> page cache (ядро)
// 2. page cache -> нативный буфер (userspace, вне heap)
// 3. нативный буфер -> byte[] в heap JVM
// 4. Java-код читает byte[0]

Для read(byte[] b) путь аналогичен, но шаг 3 копирует до b.length байт за одну операцию.

Пример: посимвольное чтение и нагрузка на память
public void readByteByByte(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path)) {
int byteValue;
// Каждая итерация: системный вызов + копирование в heap для 1 байта
while ((byteValue = fis.read()) != -1) {
System.out.printf("%02X ", byteValue);
}
}
}


В этом коде каждый вызов read() порождает полный путь данных для одного байта.

С точки зрения памяти:
Объект FileInputStream создается в heap и живет до выхода из try
Локальная переменная byteValue хранится в стеке потока
Никаких массивов в heap не создается при каждом вызове
Проблема не в GC, а в количестве системных вызовов
Однако при выводе в System.out каждый вызов printf создает объекты String и StringBuilder в heap для форматирования, что нагружает GC при больших файлах.


#Java #для_новичков #beginner #IO #NIO #FileInputStream
👍4
Буферизация и сокращение пути данных

BufferedInputStream добавляет промежуточный массив в heap, сокращая количество системных вызовов:
public void readBuffered(String path) throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path))) {
int byteValue;
// Первый read(): чтение 8192 байт в buf[8192] одним системным вызовом
// Последующие read(): возврат из buf без системных вызовов
while ((byteValue = bis.read()) != -1) {
System.out.printf("%02X ", byteValue);
}
}
}


Путь данных с BufferedInputStream:

Первый read(): диск -> page cache -> нативный буфер -> buf[8192] в heap -> возврат buf[0]
Второй read(): buf[1] из heap (системный вызов отсутствует, нативный буфер не задействован)
После исчерпания buf: повторение шага 1

Массив buf создается при конструировании BufferedInputStream и живет до закрытия потока. GC не вовлечен в работу цикла чтения, так как массив переиспользуется.


Роль garbage collector

Жизненный цикл объектов I/O
try (FileInputStream fis = new FileInputStream(path)) {
// fis — ссылка в стеке, объект FileInputStream в heap
// fis.fd — объект FileDescriptor в heap, хранит int fd (файловый дескриптор ОС)
// При использовании BufferedInputStream: bis.buf — byte[] в heap
}
// После выхода из try:
// 1. Вызывается fis.close() — закрывается файловый дескриптор ОС
// 2. Ссылка fis выходит из области видимости
// 3. Объект FileInputStream становится недостижимым
// 4. GC может собрать объект и связанные FileDescriptor при следующей сборке


Важно: FileInputStream реализует AutoCloseable, и try-with-resources гарантирует вызов close(). Если бы мы использовали старый подход без try-with-resources:
// Антипаттерн: утечка файлового дескриптора
FileInputStream fis = new FileInputStream(path);
// Если здесь возникнет исключение, close() не вызовется
fis.read();
fis.close();

В этом случае при исключении между read() и close() файловый дескриптор остается открытым. Объект FileInputStream все еще достижим через локальную переменную до выхода из метода, но если исключение пробрасывается наверх, метод завершается без закрытия. GC финализирует объект и закроет дескриптор, но финализация непредсказуема по времени — может пройти секунды или минуты до освобождения ресурса.


Финализация и Cleaner

В современных версиях Java FileInputStream использует java.lang.ref.Cleaner вместо устаревшего метода finalize(). Cleaner регистрирует cleanup-действие, которое выполняется, когда объект становится фантомно достижимым (phantom reachable) — то есть недостижимым, но еще не собранным GC.
// Упрощенная логика из OpenJDK
private final Cleanable cleanable;
private final FileCleanable cleanup;

public FileInputStream(File file) throws FileNotFoundException {
// ...
this.fd = new FileDescriptor();
this.cleanup = new FileCleanable(this.fd);
this.cleanable = Cleaner.create().register(this, cleanup);
}

FileCleanable — это Runnable, который вызывает close0(fd) — нативный метод закрытия дескриптора. Этот механизм гарантирует освобождение ресурса даже при забывчивости разработчика, но не заменяет явный вызов close(): Cleaner работает в отдельном потоке с низким приоритетом, и задержка может быть значительной.

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