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)

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

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

Ключевое архитектурное решение — разделение ответственности. FileInputStream отвечает за взаимодействие с ОС и файловой системой. BufferedInputStream отвечает за оптимизацию паттерна доступа: замену множества мелких запросов на несколько крупных. Это классический применение паттерна декоратора в Java I/O.

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

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

// Текущая позиция чтения в буфере
protected int pos;

// Позиция маркера для mark/reset, -1 если не установлен
protected int markpos = -1;

// Максимальное количество байт, читаемых после mark
protected int marklimit;

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

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

Поля buf, count, pos образуют кольцевой буфер (ring buffer), хотя в стандартной реализации используется линейный массив с сдвигом при исчерпании. При вызове read() BufferedInputStream сначала проверяет pos < count. Если условие истинно — байт возвращается из buf[pos++] без системного вызова. Если ложно — выполняется fill(), который читает новый блок данных из underlying потока.


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

Метод fill() — сердце буферизации. Он выполняется, когда внутренний буфер исчерпан:
// Упрощенная логика fill()
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();

if (markpos < 0) {
// Маркер не установлен — читаем с начала буфера
pos = 0;
} else if (pos >= buffer.length) {
// Позиция достигла конца буфера
if (markpos > 0) {
// Сдвигаем данные от markpos к началу
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
// Буфер превысил лимит маркера — сбрасываем
markpos = -1;
pos = 0;
} else {
// Расширяем буфер
int nsz = pos * 2;
if (nsz > marklimit) nsz = marklimit;
byte[] nbuf = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
buf = nbuf;
buffer = nbuf;
markpos = 0;
}
}

count = pos;
// Системный вызов: чтение из underlying потока
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0) count = n + pos;
}


Путь данных при fill():
Проверка состояния буфера. Определение, можно ли читать с начала или требуется сдвиг/расширение.
Системный вызов read(). getInIfOpen().read(buffer, pos, len) — один вызов для чтения до buffer.length - pos байт.
Копирование в heap. Данные из нативного буфера через JNI попадают в buf в heap.
Обновление счетчиков. count устанавливается в количество фактически прочитанных байт.


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

Сценарий: первое чтение после создания
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("data.bin"), 8192)) {

int firstByte = bis.read(); // Инициирует fill()
}


Путь данных для первого read():
[Java: bis.read()]
-> [BufferedInputStream: pos=0, count=0, буфер пуст]
-> [fill()]
-> [FileInputStream.read(buf, 0, 8192)]
-> [JNI: нативный readBytes]
-> [Ядро ОС: syscall read(fd, native_buf, 8192)]
-> [Page Cache: поиск/загрузка страниц]
-> [Копирование: 8192 байт из page cache в native_buf]
-> [Возврат в userspace]
-> [JNI: копирование 8192 байт в buf[8192] в heap]
-> [BufferedInputStream: count=8192, pos=0]
-> [Возврат buf[0] как int]


Массив buf[8192] создан при конструировании BufferedInputStream и размещен в heap. Для размера 8192 байт это обычный объект, не humongous. Он размещается в Eden space young generation.


Сценарий: последующие чтения из буфера
int secondByte = bis.read(); // pos=1, count=8192, системный вызов отсутствует
int thirdByte = bis.read(); // pos=2, count=8192, системный вызов отсутствует


[Java: bis.read()]
-> [BufferedInputStream: pos < count? 1 < 8192 — да]
-> [Возврат buf[1] как int, pos++]

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


Сценарий: буфер исчерпан
// После 8192 вызовов read()
int byte8193 = bis.read(); // pos=8192, count=8192, буфер пуст


[Java: bis.read()]
-> [BufferedInputStream: pos < count? 8192 < 8192 — нет]
-> [fill()]
-> [System.arraycopy при необходимости]
-> [FileInputStream.read(buf, 0, 8192)]
-> [Новый системный вызов]
-> [count=новое_значение, pos=0]
-> [Возврат buf[0]]



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

Жизненный цикл буфера
public void processFile(String path) throws IOException {
// buf создается в Eden space при конструировании BufferedInputStream
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(path))) {

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

} // close() вызывается автоматически
// buf становится недостижимым
// GC собирает buf и bis при следующей minor collection
}

Буфер buf — это долгоживущий объект относительно метода. Он создается при входе в try и живет до выхода. Если метод вызывается часто, буфер может быть повышен в survivor space или old generation при выживании нескольких minor GC. Это нормально — буфер небольшой (8KB) и не создает проблем с памятью.

Проблема: большой буфер и humongous objects
// Потенциально проблематично: 1MB буфер
BufferedInputStream bis = new BufferedInputStream(fis, 1024 * 1024);

При размере буфера 1MB и размере G1-региона 1MB массив становится humongous object. Он размещается в специальных humongous-регионах old generation и не перемещается при minor GC. Это увеличивает фрагментацию heap и может вызвать преждевременный full GC.

Рекомендация: использовать стандартный размер 8KB или увеличивать до 64KB для последовательного чтения больших файлов. Размеры более 256KB редко дают выигрыш из-за размера страницы ОС (4KB) и размера блока файловой системы (4KB).

Проблема: утечка ссылки на поток
// Антипаттерн: утечка ссылки
private InputStream leakedStream;

public void open(String path) throws IOException {
leakedStream = new BufferedInputStream(new FileInputStream(path));
// Если исключение позже — поток не закрыт, buf не освобожден
}

Если ссылка на BufferedInputStream сохраняется в поле класса, GC не может освободить буфер даже после исчерпания потока. Буфер остается в памяти до тех пор, пока объект BufferedInputStream достижим. При многократном открытии файлов без закрытия это приводит к исчерпанию heap.


mark() и reset(): семантика и память

BufferedInputStream поддерживает маркировку позиции для последующего возврата. Это требует хранения данных в буфере между mark() и reset():
bis.mark(1024); // Запомнить позицию, лимит 1024 байт вперед
// Чтение до 1024 байт
bis.reset(); // Вернуться к отмеченной позиции



Если количество прочитанных байт после mark() превышает marklimit, маркер инвалидируется. Если данные влезают в текущий буфер — reset() работает без системных вызовов. Если требуется больше данных, чем вмещает буфер — fill() выполняет System.arraycopy для сдвига или расширяет буфер.

Расширение буфера при активном маркере:
// Из fill(): буфер расширяется до marklimit
byte[] nbuf = new byte[nsz]; // Новый массив в heap
System.arraycopy(buffer, 0, nbuf, 0, pos); // Копирование старых данных
buf = nbuf; // Старая ссылка на массив теряется, GC собирает старый buf

Это единственный сценарий, где BufferedInputStream создает новые массивы в процессе работы. При отсутствии mark()/reset() буфер остается неизменным.


Практический пример: сравнение производительности
public class BufferedPerformanceDemo {

private static final int FILE_SIZE = 10 * 1024 * 1024; // 10 MB

public static void main(String[] args) throws IOException {
// Создание тестового файла
createTestFile("test.bin", FILE_SIZE);

// Тест 1: FileInputStream без буферизации
measure("FileInputStream raw", () -> {
try (FileInputStream fis = new FileInputStream("test.bin")) {
while (fis.read() != -1) {} // Побайтово
}
});

// Тест 2: FileInputStream с внешним буфером
measure("FileInputStream + byte[]", () -> {
try (FileInputStream fis = new FileInputStream("test.bin")) {
byte[] buf = new byte[8192];
while (fis.read(buf) != -1) {}
}
});

// Тест 3: BufferedInputStream (внутренняя буферизация)
measure("BufferedInputStream", () -> {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("test.bin"))) {
while (bis.read() != -1) {} // Побайтово, но буферизовано
}
});
}

private static void measure(String name, Runnable task) {
long start = System.nanoTime();
task.run();
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.printf("%s: %d ms%n", name, ms);
}

private static void createTestFile(String path, int size) throws IOException {
byte[] data = new byte[size];
new Random().nextBytes(data);
try (FileOutputStream fos = new FileOutputStream(path)) {
fos.write(data);
}
}
}


Ожидаемые результаты на SSD:
FileInputStream raw: 15 000-30 000 ms
FileInputStream + byte[]: 30-50 ms
BufferedInputStream: 30-50 ms
BufferedInputStream с побайтовым read() достигает той же производительности, что и FileInputStream с внешним массивом, но с более чистым API. Внешний буфер требует ручного управления размером и границами, BufferedInputStream инкапсулирует эту сложность.


#Java #для_новичков #beginner #IO #NIO #BufferedInputStream
👍4
Что выведет код?

import java.io.*;

public class Task110626 {
public static void main(String[] args) throws IOException {
byte[] data = {10, 20, 30, 40, 50};
BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data));
bis.mark(2);
System.out.print(bis.read());
System.out.print(bis.read());
System.out.print(bis.read());
bis.reset();
System.out.print(bis.read());
}
}


#Tasks
👍2
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Что такое Object.finalize() и как его переопределить? 🤓

Ответ:

Метод finalize() был способом выполнить "последнюю волю" объекта перед сборкой мусора.

Чтобы его переопределить, достаточно было написать protected void finalize() throws Throwable { try { ... } finally { super.finalize(); } }.

Проблемы:
JVM мог не вызвать finalize() для всех объектов перед завершением;
вызывался в отдельном потоке финализации (низкий приоритет);
имел непредсказуемое время выполнения.

Поэтому его использование считалось антипаттерном много лет.

Начиная с Java 18, его удалили.



#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
История технологии сегодня — 12 июня

День Росси́и (до 2002 года — День принятия Декларации о государственном суверенитете Российской Федерации) — государственный праздник Российской Федерации. Отмечается 12 июня — в день принятия в 1990 году Декларации о государственном суверенитете РСФСР. Установлен в 1992 году (нерабочий день с 1991 года).


ℹ️ Кто родился в этот день

Владимир Александрович Магницкий (30 мая [12 июня] 1915, Пенза — 16 октября 2005, Москва) — советский учёный-геофизик, академик АН CCCP (1979), основатель советской школы физики земных недр. Заложил основы отечественной школы физики земных недр, проанализировал результаты различных направлений с общих позиций и новые физические методы в исследовании внутреннего строения Земли. Для определения температурного распределения в недрах Земли предложил использовать метод реперных точек, а при изучении процессов формирования месторождений полезных ископаемых — теорию зонной плавки; при анализе состава нижней мантии предложил гипотезу о распаде вещества на окислы. При построении модели Земли ввёл методы, основанные на использовании уравнений состояния вещества при высоких давлениях.


🌐 Знаковые события

1967 — запущен космический аппарат Венера-4, впервые в мире передавший данные об атмосфере другой планеты.


#Biography #Birth_Date #Events #12июня
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
[Совет по Java #049]

Тема: System.gc() не гарантирует запуск сборщика мусора.

Проблема: Вызов System.gc() (или Runtime.getRuntime().gc()) является запросом к JVM на выполнение сборки мусора, но спецификация Java не требует, чтобы JVM немедленно или вообще запускала GC.

Современные JVM имеют сложные эвристики для управления памятью, и явный вызов может быть проигнорирован (например, при использовании G1 GC с параметром -XX:+DisableExplicitGC). Более того, принудительный запуск Full GC может вызвать длительные паузы ("stop-the-world"), снижая производительность приложения.

Разработчики часто вызывают System.gc() в ошибочной надежде "почистить память" или избежать OutOfMemoryError, но это может привести к обратному эффекту: сборка мусора запускается в неподходящий момент, перемещая объекты между поколениями и увеличивая фрагментацию.

Решение: Никогда не вызывайте System.gc() в production-коде.

Доверьте управление памятью JVM. Настройте параметры GC (размеры куч, поколения, алгоритм) под ваше приложение. Для профилирования используйте инструменты мониторинга (JConsole, VisualVM, GC logs). Если вам нужно принудительно запустить GC для тестирования или бенчмаркинга, используйте флаг -XX:+ExplicitGCInvokesConcurrent или вызывайте System.gc() в изолированной среде с отключенным DisableExplicitGC.

public class SystemGcExample {

//Антипаттерн: вызов System.gc()
public static void badPractice() {
// ... после обработки больших данных
System.gc(); // Бесполезно и вредно!
// JVM может проигнорировать, а если запустит — вызовет паузу
}

//Решение: довериться JVM
public static void goodPractice() {
// Просто работаем, JVM сама решит, когда собрать мусор
byte[] data = new byte[1024 * 1024];
// данные становятся недоступными
data = null;
// JVM соберет их при необходимости
}

// Для отладки: мониторинг GC через MXBean
public static void printGcStats() {
for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
System.out.println(bean.getName() + ": " + bean.getCollectionCount() + " collections");
}
}

// Единственное разумное использование: сигнал в тестах или бенчмарках
public static void onlyForBenchmark() {
// При бенчмаркинге можно вызвать несколько раз для стабилизации состояния
for (int i = 0; i < 3; i++) {
System.gc();
Thread.sleep(100);
}
}

public static void main(String[] args) {
goodPractice();
System.out.println("GC вызов не гарантирован, не полагайтесь на него");
}
}


Объяснение:
 Современные JVM используют сборщики мусора, которые адаптивно оптимизируют паузы и пропускную способность.

Явный вызов System.gc() нарушает эту адаптивность.

На HotSpot JVM по умолчанию System.gc() вызывает Full GC, очищающий все поколения, что особенно дорого для приложений с большой кучей. С помощью флага -XX:+DisableExplicitGC можно полностью отключить реакцию на явные вызовы, что многие production-системы и делают. Если вам кажется, что памяти не хватает, лучше анализировать утечки через heap dump, а не полагаться на принудительный GC.

#Java #советы
👍4
Что выведет код?

public class Task120626 {
static boolean finalized = false;

@Override
protected void finalize() {
finalized = true;
}

public static void main(String[] args) {
new Task120626();
System.gc();
System.out.println(finalized);
}
}


#Tasks
👍2
Варианты ответа:
Anonymous Quiz
40%
true
53%
false
7%
null
0%
Ошибка компиляции
👍2
Что такое ReferenceQueue? 🤓

Ответ:

ReferenceQueue — это очередь, в которую помещаются объекты-ссылки (Soft, Weak, Phantom) после того, как объекты, на которые они указывали, становятся недостижимыми (т.е. удаляются GC).

Это позволяет программе узнать, какие объекты были собраны, и провести post-processing (например, удалить ключи из кэша).

Используется в WeakHashMap внутри. Работает так: создаётся ReferenceQueue, затем при создании ссылки передаётся в конструктор.

После очистки объекта GC добавляет ссылку в очередь. Можно вызывать poll() или remove().



#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
С 30.05 по 12.06
Предыдущий пост(с 23.05 по 29.05)

Запись встреч/видео:
14. Transactional Outbox: как гарантировать публикацию события

Обучающие статьи:

Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 1. Классический Java I/O (java.io (https://java.io/))

Байтовые потоки: абстрактные классы InputStream и OutputStream
FileInputStream – чтение байтов из файла. Путь данных в памяти JVM
FileOutputStream – запись байтов в файл. Путь данных от heap к диску
Проблема производительности побайтового ввода/вывода. Идея буфера
BufferedInputStream – буферизованное чтение. Путь данных через память JVM

[Совет по Java #045]
Тема: volatile не гарантирует атомарность.

[Совет по Java #046]
Тема: java.lang.Enum не может наследовать другой класс, потому что уже наследует Enum

[Совет по Java #047]
Тема: clone() — опасный метод с неясной семантикой.

[Совет по Java #048]
Тема: finalize() — устаревший и непредсказуемый.

[Совет по Java #049]
Тема: System.gc() не гарантирует запуск сборщика мусора.


Полезные статьи и видео:
Ты не хочешь стать программистом.

Как и всегда, задачи можно найти под тегом - #Tasks, вопросы с собеседований - #собеседование
👍2
Forwarded from ChatRoom (Java for Beginner) (Первожрец Java)
Я вот сижу продумываю систему оценки статей на deforge. Есть идеи? Как по вашему как это красиво реализовать? Чем хороши или плохи системы оценок и комментариев на Reddit StackOverflow или Хабр?
👍1
Как вам вариант? (сгенерировано)
🔥5
История технологии сегодня — 14 июня

ℹ️ Кто родился в этот день

Андре́й Андре́евич Ма́рков (2 (14) июня 1856, Рязань — 20 июля 1922, Петроград) — русский математик, академик, внёсший большой вклад в теорию вероятностейматематический анализ и теорию чисел. А. А. Марков является первооткрывателем обширного класса стохастических процессов с дискретной и непрерывной временно́й компонентой, названных его именем. Марковские процессы можно описать так: следующее состояние процесса зависит вероятностно только от текущего состояния. В то время, когда эта теория была построена, она считалась абстрактной, однако в настоящее время практические применения данной теории чрезвычайно многочисленны. Теория цепей Маркова выросла в огромную и весьма важную область научных исследований — теорию марковских случайных процессов, которая в свою очередь представляет основу общей теории стохастических процессов (см. также: Неравенство Маркова). А. А. Марков существенно продвинул классические исследования предшественников, касающиеся закона больших чисел и центральной предельной теоремы теории вероятностей, а также распространил их и на цепи Маркова.

Кири́лл Я́ковлевич Кондра́тьев (14 июня 1920, Рыбинск — 1 мая 2006, Санкт-Петербург)советский и российский геофизик. Основные труды относятся к исследованиям в области физики атмосферы, спутниковой метеорологии, атмосферной оптикеактинометрии, проблемам глобальной экологии и глобальным изменениям. Автор первой в мире монографии о спутниковой метеорологии (1963), серии монографий о дистанционном зондировании атмосферы и подстилающей поверхности, проблеме радиационного баланса Земли, фундаментальных основах природноресурсных космических исследований, сравнительном планетоведении. Впервые руководил экологическими исследованиями, проведёнными космонавтами из космоса. Соавтор научного открытия «Явление вертикально-лучевой структуры дневного излучения верхней атмосферы Земли».

Фёдор Васи́льевич То́карев (2 (14) июня 1871, станица МечётинскаяЧеркасский округОбласть Войска ДонскогоРоссийская империя — 7 июня 1968МоскваСССР) — русский и советский конструктор стрелкового оружия. Наиболее известные разработки: пистолет ТТ и винтовка СВТ-38/40.


🌐 Знаковые события

1951 — в Вашингтоне поступил в продажу первый компьютер, созданный в США — UNIVAC I.


#Biography #Birth_Date #Events #14июня
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
История технологии сегодня — 15 июня

ℹ️ Кто родился в этот день

Евгений Викторович Колба́сьев (3 (15) июня 1862, Одесса, Российская империя — 20 ноября 1918, Инкерман, Крым) — русский изобретатель в области военно-морского дела, преподаватель Кронштадтской водолазной школы, капитан 1-го ранга. Автор оригинальной конструкции плавающей мины и нескольких проектов подводных лодок, в том числе диверсионной подводной лодки «Матрос Пётр Кошка», созданной в 1901 году. Колбасьев заметно опередил время, первыми создав секционный метод строительства подводных лодок. В разобранном виде подлодка помещалась в обычном железнодорожном вагоне, а процесс сборки занимал до 6 часов.

Поль Корню́ (фр. Paul Cornu; 15 июня 1881 года, Гло-ля-Феррьер — 6 июня 1944 года, Лизьё) — французский механик-изобретатель и авиатор; пионер вертолётостроения; первый в мире человек, поднявшийся в воздух на вертолёте (3 (13) ноября 1907).

🌐 Знаковые события

1844 — Чарльз Гудьир запатентовал способ вулканизации резины.

1869 — Джон Хайат в Олбани (штат Нью-Йорк) запатентовал целлулоид.

2004 — Обнаружен первый сетевой вирус для мобильных телефонов — Cabir.


#Biography #Birth_Date #Events #15июня
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Please open Telegram to view this post
VIEW IN 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