Как работает ByteBuffer и почему прямые (байтовые) буферы теперь действительно полезны.
Во-первых, я немного удивлен, что это не общеизвестно, но терпите меня со мной
Прямые байтовые буферы выделяют адрес вне кучи Java.
Это крайне важно: все функции ОС (и родной C) могут использовать этот адрес без блокировки объекта в куче и копирования данных. Краткий пример копирования: для отправки любых данных через Socket.getOutputStream (). Write (byte []) нативный код должен «заблокировать» byte [], скопировать его вне кучи Java, а затем вызвать функцию ОС, например, отправить . Копирование выполняется либо в стеке (для меньшего байта []), либо через malloc / free для больших.
DatagramSockets ничем не отличаются, и они также копируют - за исключением того, что они ограничены 64 КБ и размещены в стеке, что может даже убить процесс, если стек потока не достаточно велик или имеет глубокую рекурсию.
примечание: блокировка не позволяет JVM / GC перемещать / перераспределять объект вокруг кучи
Итак, с введением NIO идея заключалась в том, чтобы избежать копирования и множества потоковых конвейеров / косвенных указаний. Часто существует 3-4 буферизованных типа потоков, прежде чем данные достигнут своего места назначения. (ууу Польша выравнивает (!) Красивым выстрелом)
Внедряя прямые буферы, java может напрямую связываться с нативным кодом C без какой-либо блокировки / копирования. Следовательно, функция sent
может взять адрес буфера, добавить позицию, и производительность будет практически такой же, как и у собственного C.
Это о прямом буфере.
Основная проблема с прямыми буферами - они дорогостоящи для выделения и дороги для освобождения и довольно громоздки в использовании, ничего подобного байту [].
Непрямой буфер не предлагает истинную сущность, которую делают прямые буферы - то есть прямой мост к нативной / ОС, вместо этого они легковесны и имеют одинаковый API - и даже больше, они могут wrap byte[]
и даже их резервный массив доступен для прямого манипулирования - что не любить? Ну, они должны быть скопированы!
Так как же Sun / Oracle обрабатывает непрямые буферы, так как OS / native не может использовать их - ну, наивно. Когда используется непрямой буфер, должна быть создана прямая противоположная часть. Реализация достаточно умна, чтобы использовать ThreadLocal
и кэшировать несколько прямых буферов с помощью SoftReference
*, чтобы избежать изрядных затрат на создание. Наивная часть возникает при их копировании - она пытается скопировать весь буфер (remaining()
) каждый раз.
Теперь представьте: 512 КБ непрямого буфера идет в буфер 64 КБ, буфер сокета не займет больше его размера. Таким образом, в первый раз 512 КБ будут скопированы из непрямого в поток-локальный-прямой, но только 64 КБ будут использованы. В следующий раз будет скопировано 512-64 КБ, но будет использовано только 64 КБ, а в третий раз будет скопировано 512-64 * 2 КБ, но будет использовано только 64 КБ, и так далее ... и это оптимистично, что всегда сокет буфер будет полностью пуст. Таким образом, вы копируете не только n
КБ, но и n
& times; n
& делить; m
(n
= 512, m
= 16 (среднее пространство, оставленное буфером сокета)).
Копирующая часть является общим / абстрактным путем ко всему непрямому буферу, поэтому реализация никогда не знает целевой емкости. Копирование кешей, а что нет, уменьшение пропускной способности памяти и т. Д.
* Примечание по кешированию SoftReference: это зависит от реализации GC и может варьироваться. GC от Sun использует свободную память кучи для определения срока службы SoftRefences, что приводит к некоторому неловкому поведению при их освобождении - приложению необходимо снова выделить ранее кэшированные объекты, то есть большее выделение (прямые байтовые буферы занимают незначительную часть в куче, поэтому по крайней мере, они не влияют на дополнительную очистку кэша, но влияют вместо этого)
Мое эмпирическое правило - объединенный прямой буфер размером с буфер чтения / записи сокета. ОС никогда не копирует больше, чем необходимо.
Этот микро-бенчмарк является в основном тестом пропускной способности памяти, ОС будет иметь файл полностью в кеше, поэтому в основном тестирует memcpy
Как только буферы заканчиваются из кэша L2, падение производительности должно быть заметно. Кроме того, выполнение такого эталонного теста приводит к увеличению и накоплению затрат на сбор ГХ. (rest()
не будет собирать мягкие ссылки на ByteBuffers)