Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 2. Современный NIO.2 (java.nio.file)
SeekableByteChannel — произвольный доступ к файлу. Символические и жёсткие ссылки
До этого момента мы работали с файлами через высокоуровневые абстракции: читали целиком, писали целиком, копировали, перемещали. Но файловая система — это в первую очередь последовательность байтов на диске, и иногда нужно читать или писать не с начала, а с произвольной позиции.
Для этого в NIO.2 существует интерфейс
Вторая часть посвящена ссылкам в файловой системе — механизму, который позволяет одним данным соответствовать несколько имён. Символические и жёсткие ссылки — фундаментальная концепция Unix-файловых систем, и их непонимание часто приводит к багам при обходе деревьев, подсчёте размеров и резервном копировании.
Часть 1. SeekableByteChannel
SeekableByteChannel — это интерфейс в пакете
Канал (channel) в Java NIO — это абстракция для соединения с сущностью, способной выполнять операции ввода-вывода: файл, сокет, пайп. Каналы работают с буферами (ByteBuffer), а не с потоками байтов. Это позволяет использовать прямой (direct) ввод-вывод, минуя промежуточное копирование в кучу JVM.
Интерфейс
Открытие канала: Files.newByteChannel()
Метод
StandardOpenOption — режимы открытия
READ — открыть для чтения. Файл должен существовать. Соответствует флагу
WRITE — открыть для записи. Файл должен существовать. Соответствует флагу
APPEND — все операции записи производятся в конец файла, независимо от текущей позиции. Соответствует флагу
CREATE — создать файл, если он не существует. Соответствует
CREATE_NEW — создать файл, но выбросить исключение, если он уже существует. Соответствует
TRUNCATE_EXISTING — если файл существует и открыт для записи, обрезать его размер до 0. Соответствует
DELETE_ON_CLOSE — удалить файл при закрытии канала. Полезно для временных файлов.
SYNC — каждая операция записи синхронизирует данные и метаданные с диском. Соответствует
DSYNC — синхронизирует только данные, не метаданные. Соответствует
SPARSE — создать разреженный файл (sparse file), если файловая система поддерживает.
Разреженный файл (sparse file) — это файл, в котором последовательности нулевых байтов не занимают место на диске. Файловая система хранит только информацию о "дырах" (holes), а не сами нули. При чтении таких областей возвращаются нулевые байты. Разреженные файлы экономят дисковое пространство при работе с большими файлами, большая часть которых пуста.
Durability (стойкость) — свойство системы хранения, гарантирующее, что записанные данные сохранятся даже при сбое питания. SYNC и DSYNC принудительно сбрасывают данные из кэша страниц ОС на физическое устройство хранения.
Чтение и запись с произвольной позиции
ByteBuffer — это класс в Java NIO, представляющий блок памяти, через который производится ввод-вывод. Буфер имеет три ключевых свойства: capacity (ёмкость), limit (предел) и position (текущая позиция). Метод flip() переключает буфер из режима записи (write) в режим чтения (read): устанавливает limit = position, position = 0.
#Java #для_новичков #beginner #IO #NIO #SeekableByteChannel
Глава 2. Современный NIO.2 (java.nio.file)
SeekableByteChannel — произвольный доступ к файлу. Символические и жёсткие ссылки
До этого момента мы работали с файлами через высокоуровневые абстракции: читали целиком, писали целиком, копировали, перемещали. Но файловая система — это в первую очередь последовательность байтов на диске, и иногда нужно читать или писать не с начала, а с произвольной позиции.
Для этого в NIO.2 существует интерфейс
SeekableByteChannel. Он объединяет возможности чтения, записи и позиционирования в единый канал, заменяя устаревший RandomAccessFile.Вторая часть посвящена ссылкам в файловой системе — механизму, который позволяет одним данным соответствовать несколько имён. Символические и жёсткие ссылки — фундаментальная концепция Unix-файловых систем, и их непонимание часто приводит к багам при обходе деревьев, подсчёте размеров и резервном копировании.
Часть 1. SeekableByteChannel
SeekableByteChannel — это интерфейс в пакете
java.nio.channels, расширяющий ByteChannel. ByteChannel в свою очередь объединяет ReadableByteChannel и WritableByteChannel, то есть поддерживает и чтение, и запись. Добавление приставки "Seekable" означает, что канал позволяет изменять текущую позицию чтения/записи — "перематывать" указатель в произвольное место файла.Канал (channel) в Java NIO — это абстракция для соединения с сущностью, способной выполнять операции ввода-вывода: файл, сокет, пайп. Каналы работают с буферами (ByteBuffer), а не с потоками байтов. Это позволяет использовать прямой (direct) ввод-вывод, минуя промежуточное копирование в кучу JVM.
Интерфейс
SeekableByteChannel декларирует три ключевых метода, отсутствующих в обычном ByteChannel:position() — возвращает текущую позицию в файле.position(long newPosition) — устанавливает новую позицию.size() — возвращает текущий размер файла в байтах.Открытие канала: Files.newByteChannel()
Метод
Files.newByteChannel(Path path, OpenOption... options) возвращает SeekableByteChannel для указанного пути. Это фабричный метод, который делегирует создание канала текущему FileSystemProvider.import java.nio.file.*;
import java.nio.channels.SeekableByteChannel;
import java.nio.ByteBuffer;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ChannelOpener {
public void openForRead(Path file) throws IOException {
// Открываем только для чтения
try (SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ)) {
System.out.println("Размер файла: " + channel.size());
System.out.println("Текущая позиция: " + channel.position());
}
}
public void openForWrite(Path file) throws IOException {
// Открываем для чтения и записи, создаём если нет, обрезаем если есть
try (SeekableByteChannel channel = Files.newByteChannel(file,
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING)) {
// Записываем данные
ByteBuffer buffer = ByteBuffer.wrap("Hello, World".getBytes(StandardCharsets.UTF_8));
channel.write(buffer);
}
}
}
StandardOpenOption — режимы открытия
StandardOpenOption — это enum, определяющий, как именно открывать или создавать файл. Каждая опция соответствует флагу системного вызова open() (POSIX):READ — открыть для чтения. Файл должен существовать. Соответствует флагу
O_RDONLY.WRITE — открыть для записи. Файл должен существовать. Соответствует флагу
O_WRONLY (в комбинации с READ даёт O_RDWR).APPEND — все операции записи производятся в конец файла, независимо от текущей позиции. Соответствует флагу
O_APPEND. Позиция после записи не изменяется (остаётся там, где была до записи).CREATE — создать файл, если он не существует. Соответствует
O_CREAT.CREATE_NEW — создать файл, но выбросить исключение, если он уже существует. Соответствует
O_CREAT | O_EXCL. Атомарная операция.TRUNCATE_EXISTING — если файл существует и открыт для записи, обрезать его размер до 0. Соответствует
O_TRUNC.DELETE_ON_CLOSE — удалить файл при закрытии канала. Полезно для временных файлов.
SYNC — каждая операция записи синхронизирует данные и метаданные с диском. Соответствует
O_SYNC. Сильно снижает производительность, но гарантирует durability.DSYNC — синхронизирует только данные, не метаданные. Соответствует
O_DSYNC.SPARSE — создать разреженный файл (sparse file), если файловая система поддерживает.
Разреженный файл (sparse file) — это файл, в котором последовательности нулевых байтов не занимают место на диске. Файловая система хранит только информацию о "дырах" (holes), а не сами нули. При чтении таких областей возвращаются нулевые байты. Разреженные файлы экономят дисковое пространство при работе с большими файлами, большая часть которых пуста.
Durability (стойкость) — свойство системы хранения, гарантирующее, что записанные данные сохранятся даже при сбое питания. SYNC и DSYNC принудительно сбрасывают данные из кэша страниц ОС на физическое устройство хранения.
Чтение и запись с произвольной позиции
public class RandomAccessExample {
public void readFromPosition(Path file, long position, int length) throws IOException {
try (SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ)) {
// Устанавливаем позицию
channel.position(position);
ByteBuffer buffer = ByteBuffer.allocate(length);
int bytesRead = channel.read(buffer);
buffer.flip(); // переключаем буфер из режима записи в режим чтения
byte[] data = new byte[bytesRead];
buffer.get(data);
System.out.println("Прочитано с позиции " + position + ": " + new String(data));
}
}
public void writeAtPosition(Path file, long position, String data) throws IOException {
try (SeekableByteChannel channel = Files.newByteChannel(file,
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
channel.position(position);
ByteBuffer buffer = ByteBuffer.wrap(data.getBytes(StandardCharsets.UTF_8));
channel.write(buffer);
// После записи позиция сдвинулась на количество записанных байт
System.out.println("Новая позиция: " + channel.position());
}
}
public void appendToFile(Path file, String data) throws IOException {
// Способ 1: через APPEND опцию
try (SeekableByteChannel channel = Files.newByteChannel(file,
StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
ByteBuffer buffer = ByteBuffer.wrap(data.getBytes(StandardCharsets.UTF_8));
channel.write(buffer);
}
// Способ 2: через position(size()) — ручное позиционирование в конец
try (SeekableByteChannel channel = Files.newByteChannel(file,
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
channel.position(channel.size()); // переходим в конец
ByteBuffer buffer = ByteBuffer.wrap(data.getBytes(StandardCharsets.UTF_8));
channel.write(buffer);
}
}
}ByteBuffer — это класс в Java NIO, представляющий блок памяти, через который производится ввод-вывод. Буфер имеет три ключевых свойства: capacity (ёмкость), limit (предел) и position (текущая позиция). Метод flip() переключает буфер из режима записи (write) в режим чтения (read): устанавливает limit = position, position = 0.
#Java #для_новичков #beginner #IO #NIO #SeekableByteChannel
👍4
Сравнение с RandomAccessFile
Класс
Однако у него есть архитектурные недостатки:
Не работает с Path:
Не поддерживает символические ссылки корректно:
Не интегрирован с NIO:
Менее гибкая модель опций:
Не поддерживает кастомные FileSystem:
FileChannel — это конкретная реализация
Часть 2. Символические и жёсткие ссылки
В файловых системах Unix имя файла — это не сам файл, а ссылка на структуру данных, хранящую метаданные и указатели на блоки. Эта структура называется inode. Директория — это просто список пар "имя — номер inode". Ссылка — это запись в этом списке.
inode (index node) — структура данных в файловой системе Unix, содержащая всё, кроме имени файла и его содержимого: тип файла, права доступа, владелец, группа, размер, временные метки, счётчик ссылок и указатели на блоки данных. Номер inode уникален в пределах файловой системы (раздела диска).
Жёсткая ссылка (hard link)
Жёсткая ссылка — это дополнительное имя для существующего файла, указывающее на тот же inode.
Все жёсткие ссылки на файл равноправны: нет "оригинала" и "копий". Каждая жёсткая ссылка увеличивает счётчик ссылок (link count) в inode на 1.
Когда файл удаляется через
Ограничения жёстких ссылок:
Нельзя создать на директорию (за исключением системных
Нельзя создать между разными файловыми системами (разделами диска), так как inode уникален только в пределах одной ФС.
Нельзя создать на несуществующий файл.
Символическая ссылка (symbolic link, symlink)
Символическая ссылка — это специальный файл, содержащий путь (строку) к другому файлу или директории. В отличие от жёсткой ссылки, символическая ссылка имеет свой собственный inode и не влияет на счётчик ссылок целевого файла. Она может указывать на несуществующий объект (висячая ссылка, dangling symlink), на директорию, на файл в другой файловой системе или даже на саму себя.
Висячая ссылка (dangling symlink) — символическая ссылка, целевой объект которой был удалён или перемещён. При попытке доступа к такой ссылке системный вызов вернёт ENOENT (No such file or directory).
Символические ссылки могут быть абсолютными (начинаются с /, указывают на фиксированный путь в файловой системе) или относительными (указывают относительно директории, в которой находится ссылка).
Относительные ссылки предпочтительны, так как сохраняют работоспособность при перемещении дерева.
#Java #для_новичков #beginner #IO #NIO #SeekableByteChannel
Класс
java.io.RandomAccessFile существует с Java 1.0 и предоставляет аналогичный функционал. Однако у него есть архитектурные недостатки:
Не работает с Path:
RandomAccessFile принимает только String или File, не Path. Для использования с NIO.2 требуется конвертация.Не поддерживает символические ссылки корректно:
RandomAccessFile всегда следует по ссылкам, нет опции NOFOLLOW_LINKS.Не интегрирован с NIO:
RandomAccessFile работает со своими методами read(), write(), не с ByteBuffer. Хотя с Java 1.4 у него появился метод getChannel(), возвращающий FileChannel.Менее гибкая модель опций:
RandomAccessFile имеет только два режима — "r" (read) и "rw" (read-write). Нет поддержки APPEND, CREATE_NEW, SYNC и других флагов.Не поддерживает кастомные FileSystem:
RandomAccessFile работает только с дефолтной файловой системой. SeekableByteChannel через FileSystemProvider может работать с ZIP, сетевыми ФС и т.д.// Старый способ (RandomAccessFile)
try (RandomAccessFile raf = new RandomAccessFile("data.bin", "rw")) {
raf.seek(100); // позиционирование
raf.write("data".getBytes());
}
// Новый способ (SeekableByteChannel)
try (SeekableByteChannel channel = Files.newByteChannel(Paths.get("data.bin"),
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
channel.position(100);
channel.write(ByteBuffer.wrap("data".getBytes()));
}
FileChannel — это конкретная реализация
SeekableByteChannel в Java, работающая с файлами локальной файловой системы. Он предоставляет дополнительные методы: map() (memory-mapped files), transferTo()/transferFrom() (zero-copy), lock() (файловые блокировки), force() (синхронизация с диском).Часть 2. Символические и жёсткие ссылки
В файловых системах Unix имя файла — это не сам файл, а ссылка на структуру данных, хранящую метаданные и указатели на блоки. Эта структура называется inode. Директория — это просто список пар "имя — номер inode". Ссылка — это запись в этом списке.
inode (index node) — структура данных в файловой системе Unix, содержащая всё, кроме имени файла и его содержимого: тип файла, права доступа, владелец, группа, размер, временные метки, счётчик ссылок и указатели на блоки данных. Номер inode уникален в пределах файловой системы (раздела диска).
Жёсткая ссылка (hard link)
Жёсткая ссылка — это дополнительное имя для существующего файла, указывающее на тот же inode.
Все жёсткие ссылки на файл равноправны: нет "оригинала" и "копий". Каждая жёсткая ссылка увеличивает счётчик ссылок (link count) в inode на 1.
Когда файл удаляется через
unlink() (системный вызов, лежащий в основе Files.delete()), ядро уменьшает счётчик ссылок. Если счётчик достигает 0 и нет открытых файловых дескрипторов на этот inode, блоки данных помечаются как свободные. Если счётчик 0, но дескрипторы открыты, файл становится "невидимым" в файловой системе, но продолжает существовать до закрытия всех дескрипторов.Ограничения жёстких ссылок:
Нельзя создать на директорию (за исключением системных
. и ..). Это предотвращает циклы в файловой системе.Нельзя создать между разными файловыми системами (разделами диска), так как inode уникален только в пределах одной ФС.
Нельзя создать на несуществующий файл.
import java.nio.file.*;
import java.io.IOException;
public class HardLinkManager {
public void createHardLink(Path existingFile, Path newLink) throws IOException {
// Проверяем, что исходный файл существует и на той же файловой системе
if (!Files.isRegularFile(existingFile)) {
throw new IllegalArgumentException("Исходный файл не существует или не является обычным файлом");
}
// createLink создаёт жёсткую ссылку
Files.createLink(newLink, existingFile);
// Проверяем: оба пути указывают на один inode
Object key1 = Files.readAttributes(existingFile, BasicFileAttributes.class).fileKey();
Object key2 = Files.readAttributes(newLink, BasicFileAttributes.class).fileKey();
if (key1 != null && key1.equals(key2)) {
System.out.println("Жёсткая ссылка создана. Оба пути указывают на inode " + key1);
}
}
}
Files.createLink(Path link, Path existing) выполняет системный вызов link() (POSIX), который создаёт новую запись в директории, указывающую на тот же inode, что и existing. Это атомарная операция на уровне файловой системы.Символическая ссылка (symbolic link, symlink)
Символическая ссылка — это специальный файл, содержащий путь (строку) к другому файлу или директории. В отличие от жёсткой ссылки, символическая ссылка имеет свой собственный inode и не влияет на счётчик ссылок целевого файла. Она может указывать на несуществующий объект (висячая ссылка, dangling symlink), на директорию, на файл в другой файловой системе или даже на саму себя.
Висячая ссылка (dangling symlink) — символическая ссылка, целевой объект которой был удалён или перемещён. При попытке доступа к такой ссылке системный вызов вернёт ENOENT (No such file or directory).
Символические ссылки могут быть абсолютными (начинаются с /, указывают на фиксированный путь в файловой системе) или относительными (указывают относительно директории, в которой находится ссылка).
Относительные ссылки предпочтительны, так как сохраняют работоспособность при перемещении дерева.
public class SymlinkManager {
public void createSymlink(Path link, Path target) throws IOException {
// Создаём символическую ссылку
// target может быть абсолютным или относительным
Files.createSymbolicLink(link, target);
}
public void createRelativeSymlink(Path linkDir, Path targetFile) throws IOException {
// Строим относительный путь от linkDir до targetFile
Path relativeTarget = linkDir.relativize(targetFile);
Path link = linkDir.resolve("shortcut");
Files.createSymbolicLink(link, relativeTarget);
}
public void inspectSymlink(Path link) throws IOException {
if (!Files.isSymbolicLink(link)) {
System.out.println("Это не символическая ссылка");
return;
}
// Читаем целевой путь, хранящийся в ссылке
Path target = Files.readSymbolicLink(link);
System.out.println("Ссылка указывает на: " + target);
// Проверяем, существует ли целевой объект
if (Files.exists(link)) {
System.out.println("Целевой объект существует");
// Читаем атрибуты целевого объекта (следуем по ссылке)
BasicFileAttributes attrs = Files.readAttributes(link, BasicFileAttributes.class);
System.out.println("Тип цели: " + (attrs.isDirectory() ? "директория" : "файл"));
} else {
System.out.println("Висячая ссылка — цель не существует");
}
// Читаем атрибуты самой ссылки (не следуем по ней)
BasicFileAttributes linkAttrs = Files.readAttributes(
link, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS
);
System.out.println("Размер самой ссылки: " + linkAttrs.size() + " байт (длина пути)");
}
}Files.readSymbolicLink(Path link) возвращает Path, содержащий строку, записанную в символической ссылке. Это не резолвит путь — просто читает содержимое ссылки. Размер символической ссылки (возвращаемый Files.size()) равен длине строки пути в байтах, а не размеру целевого файла.#Java #для_новичков #beginner #IO #NIO #SeekableByteChannel
👍4
Опасности символических ссылок при обходе дерева
Символические ссылки на директории создают риск бесконечного цикла при рекурсивном обходе. Если директория
В безопасном варианте с отслеживанием
Сравнение жёстких и символических ссылок
Жёсткая ссылка:
Указывает напрямую на inode.
Не может указывать на директорию (кроме системных).
Не может пересекать границы файловых систем.
Не может указывать на несуществующий файл.
Увеличивает счётчик ссылок в inode.
Не имеет собственного размера (размер равен размеру файла).
Удаление одной ссылки не влияет на другие.
Символическая ссылка:
Указывает на путь (строку), имеет свой inode.
Может указывать на директорию.
Может пересекать границы файловых систем.
Может указывать на несуществующий файл (висячая ссылка).
Не влияет на счётчик ссылок целевого файла.
Имеет размер, равный длине строки пути.
При удалении целевого файла становится висячей.
Путь байтов в памяти JVM при работе с SeekableByteChannel и ссылками
SeekableByteChannel: путь данных
При
Объект
JVM создаёт объект
Этот объект содержит:
Целочисленный файловый дескриптор (хранится как
Ссылку на
Флаги режима открытия.
Кэшированное значение позиции (для уменьшения системных вызовов
При вызове
Если кэшированная позиция уже 100, системный вызов не выполняется.
Иначе выполняется
При
Выполняется системный вызов
Если
Если
Direct ByteBuffer — это буфер, размещённый в нативной памяти (вне кучи JVM). Он создаётся через ByteBuffer.allocateDirect(size) и управляется JVM, но не подлежит обычной сборке мусора. Память освобождается, когда буфер становится недостижимым и Cleaner (специальный механизм JVM) вызывает free() для нативной памяти.
При
Данные из
Запись в кэш страниц — это операция в памяти, быстрая. Физическая запись на диск происходит позже, когда ядро решает сбросить "грязные" страницы (dirty pages) на устройство хранения.
Если открыт с
Объект
Создание ссылок: путь данных
При
Объекты
Выполняется системный вызов
Создаёт новый inode типа "символическая ссылка".
Записывает строку
Добавляет запись в родительскую директорию
Никаких новых объектов в куче JVM не создаётся. Метод возвращает
При
Выполняется системный вызов
Ядро находит inode существующего файла, увеличивает его счётчик ссылок на 1, и добавляет новую запись в директорию
Никаких объектов в куче не создаётся. Операция полностью атомарна на уровне файловой системы.
Работа GC с каналами и ссылками
Объекты
Они управляются стандартным механизмом GC:
Создаются в Eden (Young Generation).
При короткоживущем использовании (внутри метода с
Если канал открыт и сохранён в долгоживущем поле (например, пул соединений к файлам), объект переходит в Old Generation и живёт до Major GC.
Важно:
Cleaner — это механизм в JVM (класс java.lang.ref.Cleaner с Java 9, ранее sun.misc.Cleaner), который позволяет регистрировать callback-метод, вызываемый при сборке мусора определённого объекта. Используется для освобождения нативных ресурсов, ассоциированных с Java-объектами, например, нативной памяти DirectByteBuffer.
Однако Cleaner не гарантирует немедленное освобождение — нативная память может оставаться занятой до следующей сборки мусора. Поэтому для критичных ресурсов (файловые дескрипторы, каналы) всегда используется явное закрытие через
#Java #для_новичков #beginner #IO #NIO #SeekableByteChannel
Символические ссылки на директории создают риск бесконечного цикла при рекурсивном обходе. Если директория
A содержит символическую ссылку на свою родительскую директорию, обход зайдёт в родителя, снова найдёт A, снова зайдёт в ссылку — и так до переполнения стека или исчерпания ресурсов.public class SafeTreeWalker {
// ОПАСНО: может зациклиться, если есть symlink на предка
public void dangerousWalk(Path root) throws IOException {
try (Stream<Path> stream = Files.walk(root, FileVisitOption.FOLLOW_LINKS)) {
stream.forEach(System.out::println); // риск бесконечного цикла
}
}
// БЕЗОПАСНО: не следуем по символическим ссылкам
public void safeWalk(Path root) throws IOException {
try (Stream<Path> stream = Files.walk(root)) { // без FOLLOW_LINKS
stream.forEach(System.out::println);
}
}
// БЕЗОПАСНО: следуем по ссылкам, но отслеживаем посещённые inode
public void safeWalkWithCycleDetection(Path root) throws IOException {
Set<Object> visitedInodes = new HashSet<>();
Files.walkFileTree(root, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
Object key = attrs.fileKey();
if (key != null) {
if (visitedInodes.contains(key)) {
System.out.println("Обнаружен цикл через ссылку: " + file);
return FileVisitResult.SKIP_SIBLINGS;
}
visitedInodes.add(key);
}
System.out.println(file);
return FileVisitResult.CONTINUE;
}
}
);
}
}В безопасном варианте с отслеживанием
fileKey() (пара device_id + inode) мы можем обнаружить, что уже посещали этот inode, и пропустить его. Это защищает от циклов через жёсткие ссылки и символические ссылки на директории.Сравнение жёстких и символических ссылок
Жёсткая ссылка:
Указывает напрямую на inode.
Не может указывать на директорию (кроме системных).
Не может пересекать границы файловых систем.
Не может указывать на несуществующий файл.
Увеличивает счётчик ссылок в inode.
Не имеет собственного размера (размер равен размеру файла).
Удаление одной ссылки не влияет на другие.
Символическая ссылка:
Указывает на путь (строку), имеет свой inode.
Может указывать на директорию.
Может пересекать границы файловых систем.
Может указывать на несуществующий файл (висячая ссылка).
Не влияет на счётчик ссылок целевого файла.
Имеет размер, равный длине строки пути.
При удалении целевого файла становится висячей.
Путь байтов в памяти JVM при работе с SeekableByteChannel и ссылками
SeekableByteChannel: путь данных
При
Files.newByteChannel(path, StandardOpenOption.READ, StandardOpenOption.WRITE):Объект
Path в куче преобразуется в байты пути в нативной памяти. Выполняется системный вызов open() с флагами O_RDWR. Ядро ОС проверяет права доступа, находит или создаёт inode, открывает файловый дескриптор и возвращает его в пространство процесса.JVM создаёт объект
FileChannel (реализация SeekableByteChannel в OpenJDK) в куче (Heap), в Young Generation. Этот объект содержит:
Целочисленный файловый дескриптор (хранится как
int в поле объекта).Ссылку на
FileSystem.Флаги режима открытия.
Кэшированное значение позиции (для уменьшения системных вызовов
lseek).При вызове
channel.position(100):Если кэшированная позиция уже 100, системный вызов не выполняется.
Иначе выполняется
lseek(fd, 100, SEEK_SET) (POSIX), который перемещает указатель чтения-записи файлового дескриптора. Это операция только над метаданными дескриптора, данные на диске не читаются.При
channel.read(buffer):Выполняется системный вызов
readv() или pread() (если позиция задана явно). Данные читаются из кэша страниц ОС (page cache) в нативную память, а затем копируются в ByteBuffer.Если
ByteBuffer — direct (создан через ByteBuffer.allocateDirect()), данные копируются напрямую в нативную память буфера, минуя кучу JVM. Это zero-copy на уровне JVM: нет промежуточного массива byte[] в куче.Если
ByteBuffer — heap (создан через ByteBuffer.allocate()), JVM выделяет временный direct буфер в нативной памяти, копирует туда данные из ОС, а затем копирует из нативного буфера в массив byte[] в куче. Это лишнее копирование, но необходимо, так как системные вызовы работают только с нативной памятью.Direct ByteBuffer — это буфер, размещённый в нативной памяти (вне кучи JVM). Он создаётся через ByteBuffer.allocateDirect(size) и управляется JVM, но не подлежит обычной сборке мусора. Память освобождается, когда буфер становится недостижимым и Cleaner (специальный механизм JVM) вызывает free() для нативной памяти.
При
channel.write(buffer):Данные из
ByteBuffer копируются в кэш страниц ОС (для direct буфера — напрямую, для heap — через временный direct буфер).Запись в кэш страниц — это операция в памяти, быстрая. Физическая запись на диск происходит позже, когда ядро решает сбросить "грязные" страницы (dirty pages) на устройство хранения.
Если открыт с
SYNC или DSYNC, после записи выполняется fsync() или fdatasync(), который принудительно сбрасывает кэш на диск. Это блокирующая операция, зависящая от скорости диска.Объект
FileChannel остаётся в куче до закрытия. Если он локальная переменная в try-with-resources, он становится мусором после выхода из блока и собирается Minor GC. Файловый дескриптор закрывается через close(), а не через GC.Создание ссылок: путь данных
При
Files.createSymbolicLink(link, target):Объекты
link и target (оба Path) находятся в куче. Их строковые представления извлекаются.Выполняется системный вызов
symlink(targetPath, linkPath) (POSIX). Ядро ОС:Создаёт новый inode типа "символическая ссылка".
Записывает строку
targetPath в блоки данных этого inode.Добавляет запись в родительскую директорию
link, связывающую имя с новым inode.Никаких новых объектов в куче JVM не создаётся. Метод возвращает
Path link (тот же объект, что был передан).При
Files.createLink(newLink, existing):Выполняется системный вызов
link(existingPath, newLinkPath) (POSIX).Ядро находит inode существующего файла, увеличивает его счётчик ссылок на 1, и добавляет новую запись в директорию
newLink, указывающую на тот же inode.Никаких объектов в куче не создаётся. Операция полностью атомарна на уровне файловой системы.
Работа GC с каналами и ссылками
Объекты
SeekableByteChannel и Path при создании ссылок — это обычные объекты в куче. Они управляются стандартным механизмом GC:
Создаются в Eden (Young Generation).
При короткоживущем использовании (внутри метода с
try-with-resources) собираются при следующей Minor GC.Если канал открыт и сохранён в долгоживущем поле (например, пул соединений к файлам), объект переходит в Old Generation и живёт до Major GC.
Важно:
ByteBuffer.allocateDirect() создаёт буфер в нативной памяти. Эта память не входит в кучу и не управляется GC напрямую. Однако сам объект DirectByteBuffer (обёртка в куче) управляется GC. Когда обёртка собирается GC, внутренний механизм Cleaner вызывает нативный метод для освобождения нативной памяти.Cleaner — это механизм в JVM (класс java.lang.ref.Cleaner с Java 9, ранее sun.misc.Cleaner), который позволяет регистрировать callback-метод, вызываемый при сборке мусора определённого объекта. Используется для освобождения нативных ресурсов, ассоциированных с Java-объектами, например, нативной памяти DirectByteBuffer.
Однако Cleaner не гарантирует немедленное освобождение — нативная память может оставаться занятой до следующей сборки мусора. Поэтому для критичных ресурсов (файловые дескрипторы, каналы) всегда используется явное закрытие через
close(), а не полагание на GC и Cleaner.#Java #для_новичков #beginner #IO #NIO #SeekableByteChannel
👍4
Раскатил обновление https://devforge.ru/
Обновлений много:
📍 Добавлена логика мультичатовости. Теперь можно общаться как в Общем чате так и создавать личные, приглашая туда кого пожелаете.
📍 Добавлена возможность писать, редактировать и отправлять на проверку свои статьи. Статьи поддерживают язык схем - Mermaid. Пишете и отправляете на проверку. Если все хорошо статья будет опубликована.
📍 Авторизация организована через Authentik. Более малое количество времени на токены, но зато надежнее.
Кроме того куча мелких исправлений и новых багов ждут вас!😁
пысы: все предыдущие аккаунты удалены. не стесняйтесь регаться заново
Обновлений много:
Кроме того куча мелких исправлений и новых багов ждут вас!
пысы: все предыдущие аккаунты удалены. не стесняйтесь регаться заново
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3👍1
Что выведет код?
#Tasks
import java.nio.channels.SeekableByteChannel;
import java.nio.file.*;
import java.nio.*;
public class Task270726 {
public static void main(String[] args) throws Exception {
Path path = Paths.get("test.txt");
try (SeekableByteChannel ch = Files.newByteChannel(path,
StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ)) {
ch.write(ByteBuffer.wrap("Hello".getBytes()));
ch.position(10);
ch.write(ByteBuffer.wrap("World".getBytes()));
System.out.println(ch.size());
}
}
}
#Tasks
👍2
Что такое 🤓
Ответ:
"Hello %s".formatted("World") (Java 15) — это метод экземпляра String, который делает то же, что и статический String.format(), но в более удобном синтаксисе для цепочек вызовов.
Внутри он вызывает String.format(this, args).
Отличие только в синтаксисе: formatted() — метод объекта, format() — статический метод. Оба поддерживают спецификаторы (%s, %d, %n).
Это особенно удобно с текстовыми блоками: """ Name: %s """.formatted(name).
#собеседование
String.formatted() и чем отличается от String.format()? Ответ:
"Hello %s".formatted("World") (Java 15)
Внутри он вызывает String.format(this, args).
Отличие только в синтаксисе: formatted() — метод объекта, format() — статический метод. Оба поддерживают спецификаторы (%s, %d, %n).
Это особенно удобно с текстовыми блоками: """ Name: %s """.formatted(name).
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
Ребят, с сегодняшнего дня, основные статьи будут публиковаться и на https://devforge.ru/, но на день раньше!
Оцените удобство чтения и формата первой статьи на портале.🙂
Для написания рецензий и комментов придется зарегистрироваться🧑💻
Ссылка - тык✈️
Оцените удобство чтения и формата первой статьи на портале.
Для написания рецензий и комментов придется зарегистрироваться
Ссылка - тык
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
История технологии сегодня — 28 июля
ℹ️ Кто родился в этот день
Па́вел Алексе́евич Черенко́в (15 [28] июля 1904, Новая Чигла, Бобровский уезд, Воронежская губерния, Российская империя — 6 января 1990, Москва, СССР) — советский физик-ядерщик, нобелевский лауреат (1958), академик АН СССР (1970). Основные работы Черенкова посвящены физической оптике, ядерной физике, физике частиц высоких энергий. В 1934 году обнаружил специфическое голубое свечение прозрачных жидкостей при облучении быстрыми заряженными частицами. Показал отличие данного вида излучения от флуоресценции. В 1936 году установил основное его свойство — направленность излучения, образование светового конуса, ось которого совпадает с траекторией движения частицы. В 1958 году вместе с Таммом и Франком был награждён Нобелевской премией по физике «за открытие и истолкование эффекта Черенкова».
Никола́й Никола́евич Андре́ев (16 [28] июля 1880, село Курманы, Полтавская губерния, Российская империя — 31 декабря 1970, Москва, СССР) — российский и советский физик; специалист в области акустики. Дал строгую теорию распределения звука в движущихся средах. Осуществил исследования по теории распространения звука вдоль поглощаемых поверхностей, теории акустических фильтров и звуковых волн конечной амплитуды. Ряд работ связан с изучением спектра затухающих колебаний, с исследованием колебаний кристаллических и анизотропных сред, вопросов реверберации звука и звукоизоляции. Под его руководством в нашей стране были начаты исследования по нелинейной акустике, по распространению звука в слоистых средах, электромеханическим активным материалам. Создал школу в области физической и технической акустики.
Ро́берт Гук (англ. Robert Hooke; Роберт Хук, 18 (28) июля 1635 — 3 (14) марта 1703) — английский естествоиспытатель и изобретатель. Член Лондонского королевского общества (1663).
К числу открытий Гука принадлежат:
- открытие пропорциональности между упругими растяжениями, сжатиями и изгибами, и производящими их напряжениями (закон Гука),
- правильная формулировка закона всемирного тяготения (приоритет Гука оспаривался Ньютоном, но, по-видимому, не в части формулировки — сила тяготения обратно пропорциональна квадрату расстояния; кроме того, Ньютон утверждал о независимом и более раннем открытии этой формулы, которую, однако, до открытия Гуком никому не сообщал),
- открытие цветов тонких плёнок (то есть, в конечном итоге, явления интерференции света),
- идея о волнообразном распространении света (более или менее одновременно с Гюйгенсом), экспериментальное обоснование её открытой Гуком интерференцией света, волновая теория света,
- гипотеза о поперечном характере световых волн,
- открытия в акустике, например, демонстрация того, что высота звука определяется частотой колебаний,
- теоретическое положение о сущности теплоты как движения частиц тела,
- открытие постоянства температуры таяния льда и кипения воды,
- первое наблюдение живой клетки с помощью усовершенствованного им микроскопа; также женской яйцеклетки и мужских сперматозоидов. Гуку же принадлежит сам термин «клетка» (англ. cell).
Чарлз Хард Та́унс (англ. Charles Hard Townes; 28 июля 1915, Гринвилл, Южная Каролина — 27 января 2015, Окленд, Калифорния) — американский учёный, физик-ядерщик, лауреат Нобелевской премии по физике (1964). Основные труды Таунса посвящены радиоспектроскопии, квантовой электронике и её приложениям, нелинейной оптике, радиоастрономии. Независимо от А. М. Прохорова и Н. Г. Басова выдвинул идею нового принципа генерации и усиления электромагнитных волн и на его основе совместно с сотрудниками создал первый квантовый генератор — мазер на аммиаке (1954). В 1958 году совместно с А. Шавловым обосновали и запатентовали возможность создания оптического квантового генератора (лазера). В 1964 «за фундаментальные работы в области квантовой электроники, которые привели к созданию излучателей и усилителей на лазерно-мазерном принципе», Таунс совместно с Н. Г. Басовым и А. М. Прохоровым был удостоен Нобелевской премии по физике.
🌐 Знаковые события
1959 — В Англии впервые введены почтовые коды и установлены автоматические сортировочные машины.
#Biography #Birth_Date #Events #28июля
Па́вел Алексе́евич Черенко́в (15 [28] июля 1904, Новая Чигла, Бобровский уезд, Воронежская губерния, Российская империя — 6 января 1990, Москва, СССР) — советский физик-ядерщик, нобелевский лауреат (1958), академик АН СССР (1970). Основные работы Черенкова посвящены физической оптике, ядерной физике, физике частиц высоких энергий. В 1934 году обнаружил специфическое голубое свечение прозрачных жидкостей при облучении быстрыми заряженными частицами. Показал отличие данного вида излучения от флуоресценции. В 1936 году установил основное его свойство — направленность излучения, образование светового конуса, ось которого совпадает с траекторией движения частицы. В 1958 году вместе с Таммом и Франком был награждён Нобелевской премией по физике «за открытие и истолкование эффекта Черенкова».
Никола́й Никола́евич Андре́ев (16 [28] июля 1880, село Курманы, Полтавская губерния, Российская империя — 31 декабря 1970, Москва, СССР) — российский и советский физик; специалист в области акустики. Дал строгую теорию распределения звука в движущихся средах. Осуществил исследования по теории распространения звука вдоль поглощаемых поверхностей, теории акустических фильтров и звуковых волн конечной амплитуды. Ряд работ связан с изучением спектра затухающих колебаний, с исследованием колебаний кристаллических и анизотропных сред, вопросов реверберации звука и звукоизоляции. Под его руководством в нашей стране были начаты исследования по нелинейной акустике, по распространению звука в слоистых средах, электромеханическим активным материалам. Создал школу в области физической и технической акустики.
Ро́берт Гук (англ. Robert Hooke; Роберт Хук, 18 (28) июля 1635 — 3 (14) марта 1703) — английский естествоиспытатель и изобретатель. Член Лондонского королевского общества (1663).
К числу открытий Гука принадлежат:
- открытие пропорциональности между упругими растяжениями, сжатиями и изгибами, и производящими их напряжениями (закон Гука),
- правильная формулировка закона всемирного тяготения (приоритет Гука оспаривался Ньютоном, но, по-видимому, не в части формулировки — сила тяготения обратно пропорциональна квадрату расстояния; кроме того, Ньютон утверждал о независимом и более раннем открытии этой формулы, которую, однако, до открытия Гуком никому не сообщал),
- открытие цветов тонких плёнок (то есть, в конечном итоге, явления интерференции света),
- идея о волнообразном распространении света (более или менее одновременно с Гюйгенсом), экспериментальное обоснование её открытой Гуком интерференцией света, волновая теория света,
- гипотеза о поперечном характере световых волн,
- открытия в акустике, например, демонстрация того, что высота звука определяется частотой колебаний,
- теоретическое положение о сущности теплоты как движения частиц тела,
- открытие постоянства температуры таяния льда и кипения воды,
- первое наблюдение живой клетки с помощью усовершенствованного им микроскопа; также женской яйцеклетки и мужских сперматозоидов. Гуку же принадлежит сам термин «клетка» (англ. cell).
Чарлз Хард Та́унс (англ. Charles Hard Townes; 28 июля 1915, Гринвилл, Южная Каролина — 27 января 2015, Окленд, Калифорния) — американский учёный, физик-ядерщик, лауреат Нобелевской премии по физике (1964). Основные труды Таунса посвящены радиоспектроскопии, квантовой электронике и её приложениям, нелинейной оптике, радиоастрономии. Независимо от А. М. Прохорова и Н. Г. Басова выдвинул идею нового принципа генерации и усиления электромагнитных волн и на его основе совместно с сотрудниками создал первый квантовый генератор — мазер на аммиаке (1954). В 1958 году совместно с А. Шавловым обосновали и запатентовали возможность создания оптического квантового генератора (лазера). В 1964 «за фундаментальные работы в области квантовой электроники, которые привели к созданию излучателей и усилителей на лазерно-мазерном принципе», Таунс совместно с Н. Г. Басовым и А. М. Прохоровым был удостоен Нобелевской премии по физике.
1959 — В Англии впервые введены почтовые коды и установлены автоматические сортировочные машины.
#Biography #Birth_Date #Events #28июля
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
[Совет по Java #065]
Тема:
Проблема: Копирование массивов — часто выполняемая операция, особенно при работе с коллекциями, буферами и низкоуровневыми структурами.
Существует несколько способов скопировать массив: цикл
Эта проверка может быть неочевидна при работе с массивами объектов и ковариантными типами, что приводит к трудноуловимым ошибкам.
Решение: Для копирования массивов всегда используйте
Для простого копирования всего массива используйте
При копировании массивов объектов убедитесь, что исходный массив является подтипом целевого (например,
Объяснение:
При копировании массивов объектов
Поэтому при работе с обобщенными массивами или преобразованиями типов рекомендуется использовать
#Java #советы
Тема:
System.arraycopy — native метод, самый быстрый для копирования массивов, но проверяет типы во время выполнения.Проблема: Копирование массивов — часто выполняемая операция, особенно при работе с коллекциями, буферами и низкоуровневыми структурами.
Существует несколько способов скопировать массив: цикл
for, Arrays.copyOf, Arrays.copyOfRange, clone() и System.arraycopy. Многие разработчики используют циклы или высокоуровневые методы, забывая о самом производительном варианте.
System.arraycopy — это native метод, реализованный на уровне JVM и операционной системы, который использует прямое копирование памяти (обычно через memcpy в C). Он работает значительно быстрее, чем ручные циклы, особенно для больших массивов, так как оптимизирован под аппаратные возможности. Однако System.arraycopy проверяет совместимость типов во время выполнения и выбрасывает ArrayStoreException, если типы несовместимы. Эта проверка может быть неочевидна при работе с массивами объектов и ковариантными типами, что приводит к трудноуловимым ошибкам.
Решение: Для копирования массивов всегда используйте
System.arraycopy, если вам нужна максимальная производительность и вы контролируете типы. Для простого копирования всего массива используйте
Arrays.copyOf или clone(), которые внутри вызывают System.arraycopy. Для частичного копирования предпочтителен прямой вызов System.arraycopy. При копировании массивов объектов убедитесь, что исходный массив является подтипом целевого (например,
String[] можно скопировать в Object[]). Если типы несовместимы, будет выброшено исключение, поэтому используйте явные проверки или обобщения, чтобы гарантировать корректность. Избегайте ручных циклов для копирования больших объемов данных.import java.util.Arrays;
public class ArrayCopyPerformance {
public static void main(String[] args) {
int[] src = new int[1000000];
for (int i = 0; i < src.length; i++) src[i] = i;
//Медленно: ручной цикл
int[] dest1 = new int[src.length];
for (int i = 0; i < src.length; i++) {
dest1[i] = src[i];
}
//Быстро: System.arraycopy
int[] dest2 = new int[src.length];
System.arraycopy(src, 0, dest2, 0, src.length);
//Удобно: Arrays.copyOf (использует arraycopy внутри)
int[] dest3 = Arrays.copyOf(src, src.length);
// Частичное копирование
int[] destPartial = new int[100];
System.arraycopy(src, 500, destPartial, 0, 100); // копируем 100 элементов с позиции 500
// Опасность с объектами
String[] strings = {"a", "b", "c"};
Object[] objects = new Object[3];
System.arraycopy(strings, 0, objects, 0, 3); // OK: String[] -> Object[]
try {
// Ошибка: несовместимые типы
Integer[] ints = {1, 2, 3};
System.arraycopy(ints, 0, strings, 0, 3); // ArrayStoreException
} catch (ArrayStoreException e) {
System.err.println("Типы несовместимы: " + e);
}
// Для примитивов проверка осуществляется во время выполнения
// но совместимость гарантируется компилятором, если типы одинаковы
}
}
Объяснение:
System.arraycopy является intrinsic методом во многих JVM, то есть компилятор может заменить его вызов на высокооптимизированный машинный код без перехода в native-метод. Он копирует непрерывные участки памяти, используя инструкции процессора для быстрого перемещения блоков данных. Это делает его в несколько раз быстрее, чем поэлементное копирование в Java, особенно для массивов примитивов, где нет накладных расходов на упаковку. При копировании массивов объектов
System.arraycopy выполняет проверку типов: если исходный тип не является подтипом целевого, выбрасывается ArrayStoreException с указанием несовместимого элемента. Эта проверка происходит во время выполнения, что может быть неожиданным для разработчиков, привыкших к проверкам на этапе компиляции. Поэтому при работе с обобщенными массивами или преобразованиями типов рекомендуется использовать
Arrays.copyOf или явные проверки через instanceof.#Java #советы
👍4
Ребят! Продолжаю набор желающих обучиться Java до уровня мидла на Spring Framework.
Кроме того могу провести мок-собес или ревью любого вашего проекта с подробным фидбеком, для оценки уровня.
Пишите в лс @Oleborn
Кроме того могу провести мок-собес или ревью любого вашего проекта с подробным фидбеком, для оценки уровня.
Пишите в лс @Oleborn
👍2
Что выведет код?
#Tasks
public class Task280726 {
public static void main(String[] args) {
Object[] src = {"A", "B", 1};
String[] dest = new String[3];
System.arraycopy(src, 0, dest, 0, 3);
System.out.println(dest[0]);
}
}#Tasks
👍3
👍4
Что такое 🤓
Ответ:
Для создания неизменяемых карт с более чем 10 элементами в Java 9+ используется Map.ofEntries() с Map.entry().
Map.of() имеет перегрузки только до 10 пар. Map.ofEntries() принимает массив Map.Entry и подходит для больших карт.
Пример: Map.ofEntries( entry("a", 1), entry("b", 2), ... ).
Также можно использовать Map.copyOf() для создания неизменяемой копии существующей карты.
Все эти методы создают неизменяемые, сериализуемые структуры.
#собеседование
Map.ofEntries() и Map.entry()? Ответ:
Map.of() имеет перегрузки только до 10 пар. Map.ofEntries() принимает массив Map.Entry и подходит для больших карт.
Пример: Map.ofEntries( entry("a", 1), entry("b", 2), ... ).
Также можно использовать Map.copyOf() для создания неизменяемой копии существующей карты.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
История технологии сегодня — 29 июля
ℹ️ Кто родился в этот день
Михаи́л Кла́вдиевич Тихонра́вов (16 [29] июля 1900, Владимир, Российская империя — 4 марта 1974, Москва, СССР) — советский инженер и конструктор космической и ракетной техники, был сподвижником С. П. Королёва: возглавлял работы по созданию баллистических ракет на жидком топливе, разработал концепцию пакетной схемы (она легла в основу ракеты Р-7), участвовал в проектировании первых искусственных спутников Земли и пилотируемых кораблей.
Влади́мир Козьми́ч Зворы́кин (17 [29] июля 1888 или 30 июля 1889, Муром, Владимирская губерния, Российская империя — 29 июля 1982, Принстон, Нью-Джерси) — российский и американский инженер и изобретатель в области телевизионной техники. Зворыкину принадлежат более 120 американских патентов на различные изобретения. Он получил большое число различных наград. В частности, в 1967 году президент США Линдон Джонсон вручил ему Национальную научную медаль США за научные заслуги за 1966 год. В 1977 году избран в Национальный зал славы изобретателей.
Исидо́р А́йзек Ра́би (англ. Isidor Isaac Rabi; 29 июля 1898, Рыманов, Австро-Венгрия — 11 января 1988, Нью-Йорк, США) — американский физик-ядерщик, лауреат Нобелевской премии по физике в 1944 году «за резонансный метод измерений магнитных свойств атомных ядер», лауреат Премии «За мирный атом» 1967 года.
🌐 Знаковые события
1996 — Microsoft представила операционную систему Windows NT 4.0.
2015 — компания Microsoft представила свою новую операционную систему Windows 10.
ц
#Biography #Birth_Date #Events #29июля
Михаи́л Кла́вдиевич Тихонра́вов (16 [29] июля 1900, Владимир, Российская империя — 4 марта 1974, Москва, СССР) — советский инженер и конструктор космической и ракетной техники, был сподвижником С. П. Королёва: возглавлял работы по созданию баллистических ракет на жидком топливе, разработал концепцию пакетной схемы (она легла в основу ракеты Р-7), участвовал в проектировании первых искусственных спутников Земли и пилотируемых кораблей.
Влади́мир Козьми́ч Зворы́кин (17 [29] июля 1888 или 30 июля 1889, Муром, Владимирская губерния, Российская империя — 29 июля 1982, Принстон, Нью-Джерси) — российский и американский инженер и изобретатель в области телевизионной техники. Зворыкину принадлежат более 120 американских патентов на различные изобретения. Он получил большое число различных наград. В частности, в 1967 году президент США Линдон Джонсон вручил ему Национальную научную медаль США за научные заслуги за 1966 год. В 1977 году избран в Национальный зал славы изобретателей.
Исидо́р А́йзек Ра́би (англ. Isidor Isaac Rabi; 29 июля 1898, Рыманов, Австро-Венгрия — 11 января 1988, Нью-Йорк, США) — американский физик-ядерщик, лауреат Нобелевской премии по физике в 1944 году «за резонансный метод измерений магнитных свойств атомных ядер», лауреат Премии «За мирный атом» 1967 года.
1996 — Microsoft представила операционную систему Windows NT 4.0.
2015 — компания Microsoft представила свою новую операционную систему Windows 10.
ц
#Biography #Birth_Date #Events #29июля
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Раздел 11. Работа с файлами, I/O и сетью (NIO.2)
Глава 3. Сериализация и форматы обмена
Сериализация — это процесс преобразования структуры данных или объекта в формат, который можно сохранить на диск, передать по сети или поместить в кэш, а затем восстановить в исходное состояние.
В Java под сериализацией обычно понимают механизм Java Native Serialization — встроенный протокол, который превращает граф объектов в поток байтов. Однако в современной разработке этот механизм рассматривается как один из многих, и выбор формата сериализации — это архитектурное решение, влияющее на производительность, безопасность и эволюцию системы.
Граф объектов (object graph) — это совокупность объектов, связанных между собой ссылками. При сериализации Java обходит этот граф, начиная с корневого объекта, и записывает каждый объект ровно один раз, даже если на него есть несколько ссылок. Это предотвращает бесконечные циклы и дублирование данных.
Определение: сериализация как процесс
Сериализация — это преобразование объекта из его представления в памяти JVM (структуры в куче со ссылками на другие объекты) в линейный поток байтов. Этот поток можно записать в файл, отправить по сокету, сохранить в базе данных или закэшировать в памяти.
Маркерный интерфейс (marker interface) — это интерфейс в Java без методов, который служит меткой для JVM или библиотек. Serializable не требует реализации методов, но его наличие сигнализирует механизму сериализации, что класс разрешено преобразовывать в байты.
Ключевое слово transient — модификатор в Java, который указывает, что поле не должно быть сериализовано. При десериализации такое поле получает значение по умолчанию (null для объектов, 0 для чисел, false для boolean). Используется для чувствительных данных (пароли, ключи) или вычисляемых/кэшированных значений, которые можно восстановить после загрузки.
serialVersionUID — это статическое поле типа long, которое выступает идентификатором версии класса. При десериализации JVM сравнивает serialVersionUID в потоке байтов с serialVersionUID загруженного класса. Если они не совпадают, выбрасывается InvalidClassException. Это механизм защиты от несовместимых изменений структуры класса.
Десериализация: обратный процесс
Десериализация — это восстановление объекта из потока байтов. JVM читает байты, определяет класс объекта, создаёт экземпляр без вызова конструктора (через механизм allocation без инициализации) и заполняет поля значениями из потока.
Инвариант (invariant) — это условие, которое всегда истинно для объекта в корректном состоянии. Например, у класса
Важный нюанс: при десериализации не вызываются конструкторы класса и его суперклассов. Это означает, что логика инициализации, определённая в конструкторах, пропускается. Если класс зависит от конструкторской логики (например, создание внутренних структур), необходимо реализовать метод
#Java #для_новичков #beginner #IO #NIO #Serialize
Глава 3. Сериализация и форматы обмена
Сериализация — это процесс преобразования структуры данных или объекта в формат, который можно сохранить на диск, передать по сети или поместить в кэш, а затем восстановить в исходное состояние.
В Java под сериализацией обычно понимают механизм Java Native Serialization — встроенный протокол, который превращает граф объектов в поток байтов. Однако в современной разработке этот механизм рассматривается как один из многих, и выбор формата сериализации — это архитектурное решение, влияющее на производительность, безопасность и эволюцию системы.
Граф объектов (object graph) — это совокупность объектов, связанных между собой ссылками. При сериализации Java обходит этот граф, начиная с корневого объекта, и записывает каждый объект ровно один раз, даже если на него есть несколько ссылок. Это предотвращает бесконечные циклы и дублирование данных.
Определение: сериализация как процесс
Сериализация — это преобразование объекта из его представления в памяти JVM (структуры в куче со ссылками на другие объекты) в линейный поток байтов. Этот поток можно записать в файл, отправить по сокету, сохранить в базе данных или закэшировать в памяти.
import java.io.*;
public class NativeSerializationExample {
// Serializable — маркерный интерфейс, указывающий JVM, что класс может быть сериализован
public static class User implements Serializable {
// serialVersionUID — контрольная сумма версии класса для проверки совместимости
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String password; // transient — поле не сериализуется
public User(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", password='" + password + "'}";
}
}
public byte[] serialize(User user) throws IOException {
// ByteArrayOutputStream — поток, который пишет байты в массив в памяти
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
// writeObject обходит граф объектов и записывает его в поток
oos.writeObject(user);
oos.flush();
// toByteArray возвращает скопированный массив байтов
return baos.toByteArray();
}
}
public User deserialize(byte[] data) throws IOException, ClassNotFoundException {
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
// readObject восстанавливает граф объектов из байтов
return (User) ois.readObject();
}
}
}
Маркерный интерфейс (marker interface) — это интерфейс в Java без методов, который служит меткой для JVM или библиотек. Serializable не требует реализации методов, но его наличие сигнализирует механизму сериализации, что класс разрешено преобразовывать в байты.
Ключевое слово transient — модификатор в Java, который указывает, что поле не должно быть сериализовано. При десериализации такое поле получает значение по умолчанию (null для объектов, 0 для чисел, false для boolean). Используется для чувствительных данных (пароли, ключи) или вычисляемых/кэшированных значений, которые можно восстановить после загрузки.
serialVersionUID — это статическое поле типа long, которое выступает идентификатором версии класса. При десериализации JVM сравнивает serialVersionUID в потоке байтов с serialVersionUID загруженного класса. Если они не совпадают, выбрасывается InvalidClassException. Это механизм защиты от несовместимых изменений структуры класса.
Десериализация: обратный процесс
Десериализация — это восстановление объекта из потока байтов. JVM читает байты, определяет класс объекта, создаёт экземпляр без вызова конструктора (через механизм allocation без инициализации) и заполняет поля значениями из потока.
public class DeserializationFlow {
// При десериализации конструктор НЕ вызывается
// Вместо этого JVM использует механизм sun.misc.Unsafe.allocateInstance()
// или аналогичный внутренний API для создания "сырого" объекта
public void demonstrate(Path file) throws Exception {
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(Files.newInputStream(file)))) {
Object obj = ois.readObject();
// Если класс реализует readObject(), он вызывается после заполнения полей
// Это позволяет выполнить пост-инициализацию: восстановить transient-поля,
// проверить инварианты, зарегистрировать слушателей
}
}
}Инвариант (invariant) — это условие, которое всегда истинно для объекта в корректном состоянии. Например, у класса
DateRange инвариантом может быть start <= end. При десериализации, если поля заполняются напрямую из потока, инварианты могут быть нарушены, поэтому используется метод readObject() для валидации.Важный нюанс: при десериализации не вызываются конструкторы класса и его суперклассов. Это означает, что логика инициализации, определённая в конструкторах, пропускается. Если класс зависит от конструкторской логики (например, создание внутренних структур), необходимо реализовать метод
private void readObject(ObjectInputStream in), который JVM вызовет после заполнения полей.#Java #для_новичков #beginner #IO #NIO #Serialize
👍4
Сценарии использования сериализации
Сохранение состояния приложения
Приложения, работающие с долгоживущими сессиями или сложным состоянием, используют сериализацию для персистентности. Например, графический редактор сохраняет документ как сериализованный граф фигур, фильтров и настроек слоёв.
Передача по сети
В распределённых системах объекты сериализуются для передачи между узлами. Это фундамент RMI (Remote Method Invocation) и многих RPC-фреймворков.
RMI (Remote Method Invocation) — это механизм Java, позволяющий вызывать методы объектов, находящихся в другой JVM (возможно, на удалённой машине), так, будто они являются локальными. Параметры и возвращаемые значения методов автоматически сериализуются и передаются по сети.
Кэширование
Сериализованные объекты хранятся в распределённом кэше (Redis, Memcached) или локальном кэше на диске. При повторном запросе объект десериализуется, избегая дорогостоящих вычислений или обращений к базе данных.
RPC (Remote Procedure Call)
RPC-фреймворки (gRPC, Apache Thrift, Dubbo) используют сериализацию для кодирования запросов и ответов. Клиент вызывает метод, как будто он локальный, но фреймворк сериализует аргументы, отправляет их по сети, десериализует на сервере, вызывает реальный метод и возвращает результат обратным путём.
RPC (Remote Procedure Call) — это парадигма межпроцессного взаимодействия, при которой программа вызывает процедуру (метод) в другом адресном пространстве (другой JVM, другой сервер) без явного кодирования сетевых деталей. RPC абстрагирует сеть, делая удалённый вызов похожим на локальный.
#Java #для_новичков #beginner #IO #NIO #Serialize
Сохранение состояния приложения
Приложения, работающие с долгоживущими сессиями или сложным состоянием, используют сериализацию для персистентности. Например, графический редактор сохраняет документ как сериализованный граф фигур, фильтров и настроек слоёв.
public class SessionPersistence {
public static class AppState implements Serializable {
private static final long serialVersionUID = 1L;
private List<Document> openDocuments;
private Map<String, UserPreference> preferences;
private transient Connection dbConnection; // соединение не сохраняем
}
public void saveState(AppState state, Path file) throws IOException {
// Atomic write: сначала пишем во временный файл, потом атомарно перемещаем
Path temp = Files.createTempFile(file.getParent(), "state_", ".tmp");
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(Files.newOutputStream(temp)))) {
oos.writeObject(state);
}
Files.move(temp, file, StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
}
public AppState loadState(Path file) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(Files.newInputStream(file)))) {
AppState state = (AppState) ois.readObject();
// Восстанавливаем transient-поля
state.dbConnection = createNewConnection();
return state;
}
}
private Connection createNewConnection() {
// Логика создания соединения
return null;
}
}Передача по сети
В распределённых системах объекты сериализуются для передачи между узлами. Это фундамент RMI (Remote Method Invocation) и многих RPC-фреймворков.
RMI (Remote Method Invocation) — это механизм Java, позволяющий вызывать методы объектов, находящихся в другой JVM (возможно, на удалённой машине), так, будто они являются локальными. Параметры и возвращаемые значения методов автоматически сериализуются и передаются по сети.
// Интерфейс удалённого сервиса
public interface OrderService extends Remote {
Order createOrder(OrderRequest request) throws RemoteException;
}
// Реализация на сервере
public class OrderServiceImpl extends UnicastRemoteObject implements OrderService {
public Order createOrder(OrderRequest request) {
// OrderRequest сериализуется при передаче от клиента к серверу
// Order сериализуется при возврате от сервера к клиенту
return new Order(/* ... */);
}
}
Кэширование
Сериализованные объекты хранятся в распределённом кэше (Redis, Memcached) или локальном кэше на диске. При повторном запросе объект десериализуется, избегая дорогостоящих вычислений или обращений к базе данных.
public class CacheManager {
private final Map<String, byte[]> localCache = new ConcurrentHashMap<>();
public void put(String key, Serializable value) throws IOException {
byte[] bytes = serialize(value);
localCache.put(key, bytes);
}
public <T> T get(String key, Class<T> type)
throws IOException, ClassNotFoundException {
byte[] bytes = localCache.get(key);
if (bytes == null) return null;
return type.cast(deserialize(bytes));
}
private byte[] serialize(Serializable obj) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
return baos.toByteArray();
}
}
private Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
}RPC (Remote Procedure Call)
RPC-фреймворки (gRPC, Apache Thrift, Dubbo) используют сериализацию для кодирования запросов и ответов. Клиент вызывает метод, как будто он локальный, но фреймворк сериализует аргументы, отправляет их по сети, десериализует на сервере, вызывает реальный метод и возвращает результат обратным путём.
RPC (Remote Procedure Call) — это парадигма межпроцессного взаимодействия, при которой программа вызывает процедуру (метод) в другом адресном пространстве (другой JVM, другой сервер) без явного кодирования сетевых деталей. RPC абстрагирует сеть, делая удалённый вызов похожим на локальный.
#Java #для_новичков #beginner #IO #NIO #Serialize
👍4
Альтернативы Java Native Serialization
Java Native Serialization — не единственный и далеко не лучший выбор для большинства задач. Современные системы используют специализированные форматы, которые превосходят стандартный механизм по скорости, размеру данных и совместимости.
Текстовые форматы
JSON (JavaScript Object Notation) — текстовый формат, ставший де-факто стандартом для веб-API. Он человекочитаем, поддерживается всеми языками, но избыточен: ключи повторяются в каждом объекте, числа хранятся как строки, нет типизации.
XML (eXtensible Markup Language) — строго типизированный текстовый формат с поддержкой схем (XSD). Используется в enterprise-системах, SOAP-сервисах, конфигурациях. Избыточен по размеру и медленнее JSON в парсинге.
YAML (YAML Ain't Markup Language) — текстовый формат, ориентированный на читаемость человеком. Широко используется для конфигураций (Kubernetes, Docker Compose, Spring Boot
Бинарные форматы
Protocol Buffers (protobuf) — бинарный формат сериализации от Google. Основан на схемах (
Apache Avro — бинарный формат от Apache Hadoop ecosystem. Отличается от protobuf тем, что схема передаётся вместе с данными (или хранится в реестре схем, Schema Registry). Это делает Avro идеальным для потоковой обработки (Kafka), где потребители могут не знать заранее точную версию схемы. Avro использует схему для записи (writer schema) и схему для чтения (reader schema), разрешение совместимости происходит на лету.
Schema Registry — это сервис (например, Confluent Schema Registry), который хранит версии схем Avro/Protobuf/JSON Schema. Производители регистрируют схему и получают ID. Потребители по ID получают схему и десериализуют данные. Это централизованное управление эволюцией форматов.
Kryo — высокопроизводительная библиотека сериализации для Java. Не требует
FlatBuffers — бинарный формат от Google, оптимизированный для десериализации без парсинга. Данные хранятся в виде плоского буфера с прямым доступом к полям по смещению. Не требует выделения объектов при чтении — идеален для игр и embedded-систем с жёсткими ограничениями по памяти и GC.
Сравнение подходов
Java Native Serialization — это механизм, встроенный в JVM, но он имеет серьёзные недостатки: медленный (рефлексия, метаданные классов в потоке), объёмный (полные имена классов, дескрипторы полей), небезопасный (десериализация произвольных классов может выполнить вредоносный код) и жёстко привязан к Java. В современных микросервисных архитектурах его использование считается антипаттерном, за исключением специфических сценариев (RMI-наследие, кэширование сессий в Tomcat).
JSON доминирует в веб-интеграции благодаря читаемости и универсальности. Protobuf и Avro — выбор для высоконагруженных внутренних коммуникаций и потоковой обработки. Kryo — для纯 Java-систем, где нужна максимальная скорость сериализации в памяти.
#Java #для_новичков #beginner #IO #NIO #Serialize
Java Native Serialization — не единственный и далеко не лучший выбор для большинства задач. Современные системы используют специализированные форматы, которые превосходят стандартный механизм по скорости, размеру данных и совместимости.
Текстовые форматы
JSON (JavaScript Object Notation) — текстовый формат, ставший де-факто стандартом для веб-API. Он человекочитаем, поддерживается всеми языками, но избыточен: ключи повторяются в каждом объекте, числа хранятся как строки, нет типизации.
// Jackson — самая популярная библиотека для JSON в Java
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonExample {
private static final ObjectMapper mapper = new ObjectMapper();
public String toJson(User user) throws JsonProcessingException {
return mapper.writeValueAsString(user);
}
public User fromJson(String json) throws JsonProcessingException {
return mapper.readValue(json, User.class);
}
}
XML (eXtensible Markup Language) — строго типизированный текстовый формат с поддержкой схем (XSD). Используется в enterprise-системах, SOAP-сервисах, конфигурациях. Избыточен по размеру и медленнее JSON в парсинге.
YAML (YAML Ain't Markup Language) — текстовый формат, ориентированный на читаемость человеком. Широко используется для конфигураций (Kubernetes, Docker Compose, Spring Boot
application.yml). Поддерживает ссылки (anchors), что позволяет представлять циклические графы.Бинарные форматы
Protocol Buffers (protobuf) — бинарный формат сериализации от Google. Основан на схемах (
.proto файлы), строго типизирован, компактен, быстр. Каждое поле имеет номер (tag), и в потоке хранятся только заполненные поля. Поддерживает обратную и прямую совместимость: можно добавлять поля, не ломая старых клиентов.// user.proto
syntax = "proto3";
message User {
string name = 1;
int32 age = 2;
string email = 3;
}
// Сгенерированный Java-код из .proto
User user = User.newBuilder()
.setName("Alice")
.setAge(30)
.setEmail("alice@example.com")
.build();
byte[] bytes = user.toByteArray();
User parsed = User.parseFrom(bytes);
Apache Avro — бинарный формат от Apache Hadoop ecosystem. Отличается от protobuf тем, что схема передаётся вместе с данными (или хранится в реестре схем, Schema Registry). Это делает Avro идеальным для потоковой обработки (Kafka), где потребители могут не знать заранее точную версию схемы. Avro использует схему для записи (writer schema) и схему для чтения (reader schema), разрешение совместимости происходит на лету.
Schema Registry — это сервис (например, Confluent Schema Registry), который хранит версии схем Avro/Protobuf/JSON Schema. Производители регистрируют схему и получают ID. Потребители по ID получают схему и десериализуют данные. Это централизованное управление эволюцией форматов.
Kryo — высокопроизводительная библиотека сериализации для Java. Не требует
Serializable, работает напрямую с полями объектов через рефлексию. Значительно быстрее Java Native Serialization и компактнее. Используется в игровых движках, Apache Spark, Akka. Недостаток: привязан к Java, не кроссплатформенный.import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
public class KryoExample {
private final Kryo kryo = new Kryo();
public byte[] serialize(Object obj) {
Output output = new Output(1024, -1);
kryo.writeClassAndObject(output, obj);
return output.toBytes();
}
public Object deserialize(byte[] bytes) {
Input input = new Input(bytes);
return kryo.readClassAndObject(input);
}
}
FlatBuffers — бинарный формат от Google, оптимизированный для десериализации без парсинга. Данные хранятся в виде плоского буфера с прямым доступом к полям по смещению. Не требует выделения объектов при чтении — идеален для игр и embedded-систем с жёсткими ограничениями по памяти и GC.
Сравнение подходов
Java Native Serialization — это механизм, встроенный в JVM, но он имеет серьёзные недостатки: медленный (рефлексия, метаданные классов в потоке), объёмный (полные имена классов, дескрипторы полей), небезопасный (десериализация произвольных классов может выполнить вредоносный код) и жёстко привязан к Java. В современных микросервисных архитектурах его использование считается антипаттерном, за исключением специфических сценариев (RMI-наследие, кэширование сессий в Tomcat).
JSON доминирует в веб-интеграции благодаря читаемости и универсальности. Protobuf и Avro — выбор для высоконагруженных внутренних коммуникаций и потоковой обработки. Kryo — для纯 Java-систем, где нужна максимальная скорость сериализации в памяти.
#Java #для_новичков #beginner #IO #NIO #Serialize
👍4
Критерии выбора формата сериализации
Производительность
Производительность измеряется по трём метрикам: скорость сериализации (объект → байты), скорость десериализации (байты → объект) и размер полученного сообщения.
Java Native Serialization медленна из-за рефлексии и записи метаданных класса для каждого объекта. Protobuf и Kryo в 10–100 раз быстрее. JSON медленнее бинарных форматов из-за парсинга текста и boxing'а чисел.
Boxing — это автоматическая упаковка примитивных типов в объектные обёртки. В JSON число 42 парсится как строка, затем преобразуется в int, что требует дополнительных операций по сравнению с прямым чтением 4 байт из бинарного потока.
Читаемость
Текстовые форматы (JSON, XML, YAML) человекочитаемы, что упрощает отладку, логирование и ручное тестирование API. Бинарные форматы требуют специальных инструментов (hex-редакторов, декодеров protobuf) для анализа. В production-системах читаемость часто жертвуется ради производительности, но для конфигураций, API-спецификаций и логов предпочтительны текстовые форматы.
Версионность (эволюция схем)
Версионность — способность формата адаптироваться к изменениям структуры данных без поломки существующих клиентов и серверов.
Java Native Serialization требует ручного управления
Wire-совместимость (wire compatibility) — это свойство формата, при котором изменение схемы не нарушает формат бинарного потока. Например, в protobuf изменение int32 на int64 wire-совместимо, так как оба типа кодируются как varint, но семантически может привести к truncation на старых клиентах.
Безопасность
Безопасность — критичнейший критерий, особенно для десериализации данных из недоверенных источников.
Java Native Serialization имеет печальную репутацию: десериализация
CVE (Common Vulnerabilities and Exposures) — это стандартизированный каталог публично известных уязвимостей информационной безопасности. Каждой уязвимости присваивается уникальный идентификатор, например CVE-2015-4852 (уязвимость десериализации в Oracle WebLogic).
ObjectInputFilter — интерфейс, введённый в Java 9 (JEP 290), позволяющий задавать белые и чёрные списки классов для десериализации, ограничивать глубину графа объектов, количество ссылок и размер данных.
JSON безопаснее, так как парсер создаёт только простые структуры (строки, числа, массивы, объекты), не вызывая произвольных конструкторов Java. Однако и JSON имеет уязвимости: XXE (XML External Entity) при использовании небезопасных XML-парсеров, или DOS-атаки через Billion Laughs в YAML/XML.
XXE (XML External Entity) — тип атаки на XML-парсеры, при котором злоумышленник внедряет внешнюю сущность в XML-документ, заставляя парсер обращаться к внутренним файлам сервера или внешним ресурсам.
Protobuf и Avro безопасны по дизайну: они не поддерживают произвольную загрузку классов, работают только со схемой, строго типизированы и не позволяют встроить исполняемый код в сообщение.
Кроссплатформенность
Если система полиглотная (использует несколько языков программирования), Java Native Serialization и Kryo неприменимы — они привязаны к JVM. JSON, XML, protobuf, Avro, FlatBuffers имеют реализации на десятках языков и являются lingua franca для межсервисного взаимодействия.
Полиглотная система (polyglot system) — это система, построенная с использованием нескольких языков программирования. Например, backend на Java/Go, ML-сервис на Python, frontend на JavaScript. В таких системах формат обмена данных должен быть языконезависимым.
Размер данных
Бинарные форматы значительно компактнее текстовых. Например, объект с тремя полями в JSON может занимать 100 байт из-за повторяющихся ключей и скобок. Тот же объект в protobuf — 10–20 байт. Это критично для мобильных приложений, IoT-устройств и высоконагруженных сетевых протоколов.
#Java #для_новичков #beginner #IO #NIO #Serialize
Производительность
Производительность измеряется по трём метрикам: скорость сериализации (объект → байты), скорость десериализации (байты → объект) и размер полученного сообщения.
Java Native Serialization медленна из-за рефлексии и записи метаданных класса для каждого объекта. Protobuf и Kryo в 10–100 раз быстрее. JSON медленнее бинарных форматов из-за парсинга текста и boxing'а чисел.
Boxing — это автоматическая упаковка примитивных типов в объектные обёртки. В JSON число 42 парсится как строка, затем преобразуется в int, что требует дополнительных операций по сравнению с прямым чтением 4 байт из бинарного потока.
Читаемость
Текстовые форматы (JSON, XML, YAML) человекочитаемы, что упрощает отладку, логирование и ручное тестирование API. Бинарные форматы требуют специальных инструментов (hex-редакторов, декодеров protobuf) для анализа. В production-системах читаемость часто жертвуется ради производительности, но для конфигураций, API-спецификаций и логов предпочтительны текстовые форматы.
Версионность (эволюция схем)
Версионность — способность формата адаптироваться к изменениям структуры данных без поломки существующих клиентов и серверов.
Java Native Serialization требует ручного управления
serialVersionUID и ломается при изменении сигнатуры класса (удаление/переименование полей, изменение типов). Protobuf поддерживает добавление полей (старые клиенты игнорируют неизвестные теги), удаление полей (если они не обязательны), изменение типов в рамках wire-совместимости. Avro поддерживает схемы чтения и записи разных версий с автоматическим разрешением различий.Wire-совместимость (wire compatibility) — это свойство формата, при котором изменение схемы не нарушает формат бинарного потока. Например, в protobuf изменение int32 на int64 wire-совместимо, так как оба типа кодируются как varint, но семантически может привести к truncation на старых клиентах.
Безопасность
Безопасность — критичнейший критерий, особенно для десериализации данных из недоверенных источников.
Java Native Serialization имеет печальную репутацию: десериализация
ObjectInputStream.readObject() может выполнить произвольный код, если в classpath есть классы с уязвимыми конструкторами, методами readObject() или ObjectInputValidation. Это привело к множеству критических CVE (Common Vulnerabilities and Exposures). Начиная с Java 9, добавлен механизм JEP 290 — фильтрация входящих классов через ObjectInputFilter.CVE (Common Vulnerabilities and Exposures) — это стандартизированный каталог публично известных уязвимостей информационной безопасности. Каждой уязвимости присваивается уникальный идентификатор, например CVE-2015-4852 (уязвимость десериализации в Oracle WebLogic).
// JEP 290: фильтрация десериализации с Java 9
public void safeDeserialize(InputStream in) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(in);
// Разрешаем десериализовать только классы из доверенных пакетов
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"maxdepth=5;maxrefs=10000;com.myapp.*;!*"
);
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();
}
ObjectInputFilter — интерфейс, введённый в Java 9 (JEP 290), позволяющий задавать белые и чёрные списки классов для десериализации, ограничивать глубину графа объектов, количество ссылок и размер данных.
JSON безопаснее, так как парсер создаёт только простые структуры (строки, числа, массивы, объекты), не вызывая произвольных конструкторов Java. Однако и JSON имеет уязвимости: XXE (XML External Entity) при использовании небезопасных XML-парсеров, или DOS-атаки через Billion Laughs в YAML/XML.
XXE (XML External Entity) — тип атаки на XML-парсеры, при котором злоумышленник внедряет внешнюю сущность в XML-документ, заставляя парсер обращаться к внутренним файлам сервера или внешним ресурсам.
Protobuf и Avro безопасны по дизайну: они не поддерживают произвольную загрузку классов, работают только со схемой, строго типизированы и не позволяют встроить исполняемый код в сообщение.
Кроссплатформенность
Если система полиглотная (использует несколько языков программирования), Java Native Serialization и Kryo неприменимы — они привязаны к JVM. JSON, XML, protobuf, Avro, FlatBuffers имеют реализации на десятках языков и являются lingua franca для межсервисного взаимодействия.
Полиглотная система (polyglot system) — это система, построенная с использованием нескольких языков программирования. Например, backend на Java/Go, ML-сервис на Python, frontend на JavaScript. В таких системах формат обмена данных должен быть языконезависимым.
Размер данных
Бинарные форматы значительно компактнее текстовых. Например, объект с тремя полями в JSON может занимать 100 байт из-за повторяющихся ключей и скобок. Тот же объект в protobuf — 10–20 байт. Это критично для мобильных приложений, IoT-устройств и высоконагруженных сетевых протоколов.
#Java #для_новичков #beginner #IO #NIO #Serialize
👍4
Путь байтов в памяти JVM при сериализации
Сериализация: от объекта к байтам
Когда вы вызываете
Объект в куче. Объект
Дескриптор класса (полное имя класса,
Типы и значения полей (примитивы записываются как есть, объекты — рекурсивно)
Для каждого нового класса в иерархии — его дескриптор
Обход графа и отслеживание ссылок.
Буферизация.
ByteArrayOutputStream. Если сериализация идёт в память,
Возвращаемый массив.
Работа GC. Все временные объекты — внутренние буферы
ObjectStreamClass — это внутренний класс JVM, представляющий метаданные сериализуемого класса: имя,
Десериализация: от байтов к объекту
При
Байты в куче. Исходный массив байтов или поток находится в куче (если это
Чтение дескрипторов.
Создание объекта без конструктора. JVM выделяет память под объект в Young Generation (Eden) через механизм
Заполнение полей.
Восстановление графа. Если в потоке есть backreference (ссылка на ранее десериализованный объект),
Вызов readObject(). Если класс реализует
Работа GC. Вновь созданные объекты размещаются в Eden. Если десериализуемый граф большой (миллионы объектов), они могут быстро заполнить Young Generation и вызвать Minor GC в процессе десериализации. Если объекты сохраняются в долгоживущую структуру (коллекцию, кэш), они переходят в Old Generation.
Потоковая сериализация и GC
При сериализации в
Буфер
Буфер в куче переиспользуется — новые объекты не создаются при каждом сбросе. Это снижает давление на GC.
Если сериализация выполняется в цикле с высокой частотой (например, в RPC-сервере, обрабатывающем 10 000 запросов в секунду), объекты
Оптимизация: пул переиспользуемых потоков. В высоконагруженных системах
В этом примере
#Java #для_новичков #beginner #IO #NIO #Serialize
Сериализация: от объекта к байтам
Когда вы вызываете
ObjectOutputStream.writeObject(user):Объект в куче. Объект
user и все объекты, на которые он ссылается (глубокий граф), находятся в куче (Heap). ObjectOutputStream обходит граф, начиная с корневого объекта. Для каждого объекта он записывает:Дескриптор класса (полное имя класса,
serialVersionUID, флаги)Типы и значения полей (примитивы записываются как есть, объекты — рекурсивно)
Для каждого нового класса в иерархии — его дескриптор
Обход графа и отслеживание ссылок.
ObjectOutputStream поддерживает таблицу handle — целочисленных идентификаторов для уже записанных объектов. Если в графе есть циклические ссылки (A ссылается на B, B ссылается на A) или повторяющиеся ссылки, второе вхождение объекта записывается не полностью, а как backreference (handle ранее записанного объекта). Это предотвращает бесконечную рекурсию и дублирование.Буферизация.
ObjectOutputStream пишет данные во внутренний буфер byte[] (обычно 512 байт или больше). Этот буфер создаётся в Young Generation, в Eden. Когда буфер заполняется, он сбрасывается (flush) в нижележащий поток: ByteArrayOutputStream, FileOutputStream или SocketOutputStream.ByteArrayOutputStream. Если сериализация идёт в память,
ByteArrayOutputStream накапливает байты в динамически расширяющемся массиве. При превышении ёмкости создаётся новый массив вдвое большего размера, данные копируются, старый массив становится мусором. Это может породить несколько временных массивов в Young Generation, которые собираются Minor GC.Возвращаемый массив.
baos.toByteArray() создаёт новый массив точного размера и копирует в него данные. Исходный внутренний буфер ByteArrayOutputStream остаётся в куче до сборки мусора. Итоговый массив байтов — это объект в куче, который может быть передан по сети, сохранён в файл или закэширован.Работа GC. Все временные объекты — внутренние буферы
ObjectOutputStream, промежуточные массивы ByteArrayOutputStream, объекты ObjectStreamClass (метаданные классов) — создаются в Young Generation. При короткой сериализации они уничтожаются при следующей Minor GC. Если сериализация выполняется в цикле (например, сериализация 10 000 объектов для пакетной отправки), буферы могут пережить несколько циклов Minor GC и мигрировать в Survivor Space, а затем в Old Generation.ObjectStreamClass — это внутренний класс JVM, представляющий метаданные сериализуемого класса: имя,
serialVersionUID, поля, методы writeObject/readObject. Эти объекты кэшируются внутри ObjectOutputStream для повторного использования, но при сериализации множества разных классов они накапливаются.Десериализация: от байтов к объекту
При
ObjectInputStream.readObject():Байты в куче. Исходный массив байтов или поток находится в куче (если это
ByteArrayInputStream) или читается из нативного буфера (если это FileInputStream или SocketInputStream).Чтение дескрипторов.
ObjectInputStream читает заголовок потока (magic number, версия протокола), затем дескриптор класса. Он ищет класс в Metaspace через Class.forName(). Если класс не загружен, ClassLoader загружает его байткод в Metaspace.Создание объекта без конструктора. JVM выделяет память под объект в Young Generation (Eden) через механизм
sun.misc.Unsafe.allocateInstance() (или аналог в новых версиях). Это выделение памяти без вызова конструктора — объект создаётся "сырым", с полями в состоянии по умолчанию (null, 0, false).Заполнение полей.
ObjectInputStream читает значения полей из потока и записывает их напрямую в память объекта (через JNI или Unsafe). Для объектных полей — рекурсивная десериализация. Для transient-полей — значение не читается, остаётся по умолчанию.Восстановление графа. Если в потоке есть backreference (ссылка на ранее десериализованный объект),
ObjectInputStream подставляет ссылку на существующий объект из таблицы handles. Это восстанавливает точную топологию исходного графа, включая циклы.Вызов readObject(). Если класс реализует
private void readObject(ObjectInputStream in), JVM вызывает этот метод после заполнения полей. Это позволяет выполнить пост-инициализацию: восстановить transient-поля, вычислить производные значения, проверить инварианты.Работа GC. Вновь созданные объекты размещаются в Eden. Если десериализуемый граф большой (миллионы объектов), они могут быстро заполнить Young Generation и вызвать Minor GC в процессе десериализации. Если объекты сохраняются в долгоживущую структуру (коллекцию, кэш), они переходят в Old Generation.
Потоковая сериализация и GC
При сериализации в
FileOutputStream или SocketOutputStream:Буфер
ObjectOutputStream (в куче) периодически сбрасывается. Сброс вызывает системный вызов write() (POSIX), который копирует байты из кучи JVM в нативную память ядра (кэш страниц или сетевой буфер).Буфер в куче переиспользуется — новые объекты не создаются при каждом сбросе. Это снижает давление на GC.
Если сериализация выполняется в цикле с высокой частотой (например, в RPC-сервере, обрабатывающем 10 000 запросов в секунду), объекты
ObjectOutputStream и ObjectInputStream создаются и закрываются для каждого запроса. Каждый такой объект — это десятки килобайт в куче. При интенсивной нагрузке они быстро заполняют Eden и вызывают частые Minor GC.Оптимизация: пул переиспользуемых потоков. В высоконагруженных системах
ObjectOutputStream создаётся один раз и сбрасывается через reset() между запросами. Метод reset() очищает внутренние таблицы handles, позволяя сериализовать новый граф объектов без создания нового потока.public class PooledSerializer {
private final ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);
private final ObjectOutputStream oos;
public PooledSerializer() throws IOException {
this.oos = new ObjectOutputStream(baos);
}
public synchronized byte[] serialize(Object obj) throws IOException {
baos.reset(); // сбрасываем буфер, не создавая новый
oos.reset(); // сбрасываем таблицу handles
oos.writeObject(obj);
oos.flush();
return baos.toByteArray(); // копирует текущий буфер
}
}В этом примере
baos.reset() очищает счётчик байтов, но не освобождает внутренний массив. oos.reset() очищает таблицу handles, позволяя сериализовать новые объекты без утечки памяти. Это существенно снижает давление на GC в сценариях с высокой частотой сериализации.#Java #для_новичков #beginner #IO #NIO #Serialize
👍4
Что выведет код?
#Tasks
import java.io.*;
public class Task290726 {
public static void main(String[] args) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
MyObject290726 obj = new MyObject290726("Hello");
oos.writeObject(obj);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
MyObject290726 deserialized = (MyObject290726) ois.readObject();
ois.close();
System.out.println(deserialized.value);
}
static class MyObject290726 implements Serializable {
transient String value;
MyObject290726(String value) { this.value = value; }
}
}
#Tasks
👍3