Компактные строки в Java 9

https://dzone.com/articles/compact-strings-in-java-9
  • Перевод
И снова здрасьте! Мы открыли очередной набор в, теперь уже девятую, группу «Разработчик Java» (а десятая группа в планах, внезапно, стоит на 31.12) и подготовили для вас интересные материалы и открытый урок.

Так что поехали.

Хотите уменьшить количество памяти используемой вашим Java-приложением? Посмотрите, как можно улучшить производительность с помощью компактных строк, доступных в Java 9.

Одним из решений по улучшению производительности, представленных в JVM (Oracle HotSpot, если быть точным), в качестве части Java SE 9, оказались компактные строки (compact strings). Их задача заключается в уменьшении размера String-объектов, что позволяет уменьшить общий объем (футпринт) памяти потребляемой приложением. В результате, это может уменьшить количество времени, затрачиваемого на сбор мусора.



В основе функции лежит наблюдение, что многим String-объектам не требуется 2 байта для кодирования каждого символа, так как большинство приложений использует только символы Latin-1. Следовательно, вместо такого:

/** это значение используется для хранения символа */
private final char value[];

В java.lang.String теперь есть это:

private final byte[] value;
/**
 * идентификатор кодировки используется для кодирования байтов в 
 * {@code value}. В этой имплементации поддерживаются следующие значения: 
 *
 * LATIN1
 * UTF16
 *
 * @implNote Виртуальная машина доверяет этому полю. Оно подлежит постоянному
 * “сворачиванию”, если инстанс String - константа. Перезапись этого 
 * поля после конструирования может вызвать проблемы. 
 */
private final byte coder;

Другими словами, эта функция заменяет значение в массиве char (где каждый элемент использует 2 байта) байтовым массивом с дополнительным байтом для определения кодировки (Latin-1 или UTF-16). Это значит, что в большинстве приложений, использующих только символы Latin-1, будет применяться лишь половина кучи. Пользователь не заметит отличий, но связанные API, например, StringBuilder, автоматически этим воспользуются.

Чтобы показаться это изменение с точки зрения размера String-объекта, я воспользуюсь Java Object Layout — простой утилитой для визуализации структуры объекта в куче. С этой точки зрения, нас интересует футпринт массива (хранящегося в переменной value выше), а не просто ссылка (ссылка байтового массива, как и ссылка массива символов, использует 4 байта). Код ниже выводит информацию при помощи JOL GraphLayout:

public class JOLSample {
    public static void main(String[] args) {
        System.out.println(GraphLayout.parseInstance("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz").toFootprint());
    }
}

Запуск кода выше в Java 8, а затем в Java 9 показывает разницу:

$java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
$java -cp lib\jol-cli-0.9-full.jar;. test.JOLSample
java.lang.String@4554617cd footprint:
     COUNT       AVG       SUM   DESCRIPTION
         1       432       432   [C
         1        24        24   java.lang.String
         2                 456   (total)
...
$java -version
java version "9"
Java(TM) SE Runtime Environment (build 9+181)
Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode)
$java -cp lib\jol-cli-0.9-full.jar;. test.JOLSample
java.lang.String@73035e27d footprint:
     COUNT       AVG       SUM   DESCRIPTION
         1       224       224   [B
         1        24        24   java.lang.String
         2                 248   (total)

Игнорируя 24-байтный размер внутренних составляющих java.lang.String (заголовок плюс ссылки), мы видим, что благодаря компактности размер уменьшился почти в два раза.
Если мы заменим строку выше на другую, использующую символы UTF-16, например \u0780, а затем перезапустим код выше, то и Java 8, и Java 9 покажут одинаковый футпринт, так как компактность больше не будет использоваться.

Эту функцию можно отключить, передав параметр -XX:-CompactStrings команде java.

Как всегда ждём ваши комментарии и вопросы тут, а так же приглашаем на открытый урок.

Отус

206,00

Профессиональные онлайн-курсы для разработчиков

Поделиться публикацией
Комментарии 33
    0
    Тезисно:
    — Для использования компактных строк от программиста не требуется ничего, кроме запуска приложения под джавой 9.
    — Для русского языка компактные строки не дадут ни байта выигрыша.

    М-да…

    Скажите, я правильно понял: внутри компактные строки хранят utf-8? А каков проигрыш в производительности при использовании строк с кириллицей — ведь их придется кодировать-декодировать при любых манипуляциях? А каков будет «выигрыш» при использовании, скажем, строк индийских символов или китайских иероглифов, где один символ может кодироваться в urf-8 тремя, четырьмя, а то и пятью байтами?
      –2
      с чего вдруг взялась кодировка utf-8? мне вот по душе koi8r
        –1
        Ай, моя плохой, не заметил, что там utf-16:
        Другими словами, эта функция заменяет значение в массиве char (где каждый элемент использует 2 байта) байтовым массивом с дополнительным байтом для определения кодировки (Latin-1 или UTF-16).

        Вопрос снимается, голова пеплом посыпается.
        Остается не очень понятно, зачем под битовый флажок отвели целый байт, ну да ладно.
          +1
          Надеюсь не сильно перевру если опишу историю кратко — JEP делал не безызвестный Алексей Шипилёв — и что согласно профилям приложений который активно работали с не latin-1 строками (имена не назывались, но представьте себе региональные социальные сети) — и даже в их случае количество latin-1 строк было достаточно высокое (не 50%, но и не 10%). Среди прочего это и внутри vm вещи — имена классов и т.п.

          Почему не бит — вопрос в том как его хранить с кем его есть — опять же — бенчмарки показали, что так (с флагом) будет лучше.
            +4
            Остается не очень понятно, зачем под битовый флажок отвели целый байт, ну да ладно.
            А как вы себе представляете размещение отдельного бита в памяти?
              –4
              Отдельный бит можно запихать в уже существующее поле флагов, если таковое есть. Если нет — то остается еще вариант разместить флаг в младшем бите какого-нибудь адреса или ссылки. Адреса, выделяемые менеджером памяти, как правило, выровнены на границу слова, а то и 16 байт — т.е. младшие один-два-три бита в адресе всегда нулевые. Этим можно воспользоваться.
                0
                Отдельный бит можно запихать в уже существующее поле флагов, если таковое есть.

                Можно, посмотрели, это всё не взлетело. Кроме того, добавление байтового поля реально не увеличило затрат памяти из-за уже существующего до этого выравнивания. Подробности можно увидеть по ссылка в этом комментарии.

          0
          Оффтопик.

          один символ может кодироваться в urf-8 тремя, четырьмя, а то и пятью байтами


          Разве где-то сейчас используется UTF-8 с символами больше 4 байт?

          Вроде в RFC 3629, который является официальным стандартом, указано, что «In UTF-8, characters from the U+0000..U+10FFFF range are encoded using sequences of 1 to 4 octets». И символы за пределами этого range запрещены («Restricted the range of characters to 0000-10FFFF»).

          Но в предыдущей (давно устаревшей) версии стандарта (rfc2279) были возможны символы, требующие до 6 байт.

          Или я что-то упускаю?
            0
            Нет, не упускаете.
            Это у меня в голове сведения устаревшие.
          –2
          Господи, почему в 2к18 ещё не все переехали на UTF-8?
            0

            Нет, и не переедут. Это поломает существующее API

              0
              Думаю имелся ввиду Юникод вообщем, а не конкретно UTF-8.
                +4
                На «Юникод вообщем» Java переехала с рождения.
              +6
              А должны? Потому что UTF-8 очень неудобен в качестве внутреннего представления строк в программе. Это транспортный формат — в нем удобно сохранять данные на диск, передавать по сети и т.д., но в качестве внутреннего формата строк он не подходит.
                +2
                А что подходит?
                UTF-16 со своими суррогатными парами даёт ложную надежду, но всё равно не позволяет бегать по индексу. Для того, чтобы обращаться по индексу к строке с эмодзи нужна отдельная функция. Строка — не массив, в любой кодировке.
                  0

                  И utf-16 не подходит для строк с эмоджи, да. И непонятно зачем его заменять на utf-8 который не подойдёт для ещё большего множества символов и вдобавок ещё и сломает вообще всю совместимость.

                    +2

                    Utf8 совместим с ascii, utf16 нет.
                    Для ascii строк он в два раза выгоднее, чем utf16.
                    У utf8 нет проблемы с BE/LE.
                    Оба не позволяют индексировать строку, в отличие от utf32.
                    Так что не все так однозначно.

                      +2
                      Забыли упомянуть о том, что UTF-8 совместим с null-terminated представлением строк (к примеру, в C), а остальные нет
                        0
                        Суть в том, что UTF-32 тоже не позволяет индексировать строку. Ну или я не понимаю, как индексировать эмодзи с флагом цвета кожи и пола; как индексировать флаг Англии (который на каких-то системах — 7 символов, а на каких-то распарсился в 1 флаг). В итоге часто встречаю в современных мессенджерах обрезку посередине, что не красиво, но всем пофигу, т.к. верят в непогрешимость UTF-16 (который ворвался в стандарты как UCS-2).
                        P.S. хабр вообще порезал сообщение, встретив символ этого флага (ну или firefox плохо закодировал): emojipedia.org/flag-for-england
                          0

                          Максимальное значение одного символа — 0x10FFFF. Это около 21 бит. Следовательно в кодировке utf32 каждый символ занимает вмещается в 4 байта, и индексация вполне работает.

                            –1
                            Есть еще суррогатные пары. Это когда, скажем, буква Ё передается двумя символами: модифицирующий символ «две точки над буквой» и собственно сама буква Е. Даже в русском языке суррогатными парами можно выразить аж две буквы (Ё и Й), а есть языки, где вообще половина символов может быть суррогатами.
                              0
                              Это называется «составные символы», а не «суррогатные пары».
                    +1
                    –1
                    Я полагаю вы ошиблись и имели ввиду UTF-*, а не конкретно UTF-8?
                    Так как для разных задач используются разные UTF.

                    • UTF-8 — выгодно хранить текст. Совместима с ascii, нормально работает в старых программах и компиляторах как нуль-терминальная строка.
                    • UTF-16 — удобно использовать в программировании, так как размер массива, как правило, равен количеству символов, по крайней мере для латинского/кириллического алфавита и спецсимволов.
                    • UTF-32 — можно использовать как массив символов, удобно в работе с обработкой текста (поиском и т.д).


                      –1
                      Вот поиск как раз одинаково хорошо работает в любой кодировке…
                        0
                        Павел, я не написал, что поиск по другим кодировкам как то плохо работает. Писать софт для обработки текста (в том числе и поиска) удобнее/проще, для кодировки где элемент массива это всегда отдельный символ.
                      –1
                      Господи, почему в 2к18 ещё не все переехали на UTF-8?

                      В Джаве есть класс String. Его методы используются в большом количестве софта. В частности используется метод charAt(int index). В частности он используется внутри циклов. Если перейти на utf-8 (что в общем прекрасная идея) — алгоритмическая сложность charAt станет линейной, а сложность методов, в которых есть циклы — квадратичной. На это пойти никак нельзя.

                        –1
                        Суть данного треда как раз в том, что сложность charAt константна только для 1-байтных кодировок и UCS-* (который использовался, когда зарождались Java и WinNT), но сейчас они все [втихую] переехали на UTF-*, а Unicode ввёл emoji. И уже у всех charAt должен быть линейным, а у кого не так, тот заблуждается.
                          0
                          Суть данного треда как раз в том, что сложность charAt константна только для 1-байтных кодировок

                          А суть моего комментария в том, что charAt возвращает не codePoint а значение типа char, которое берётся прямиком из массива типа char, который находится под капотом у строки )). И куча народу этим пользуется. И ломать код этих людей нельзя, хотя он, возможно и не вполне корректный.

                      +6
                      Объем статьи, конечно, поражает. Для тех кто хочет узнать об этом чуть больше, есть прекрасный доклад от Алексея Шипилёва.
                        –4
                        Странно, что раньше не ввели. Жду, когда будет перегрузка операторов, constexpr, setter/getter, checked arithmetics, function purity level и тд
                          0
                          Получается, что если в «схлопнутую» строку добавить utf8, то размер новой строки = 2 размера старой + новый символ?
                            0

                            Если добавить любой символ, который не входит в latin1.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое