И снова здрасьте! Мы открыли очередной набор в, теперь уже девятую, группу «Разработчик 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.

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

Комментарии (33)


  1. berez
    16.10.2018 17:37
    +1

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

    М-да…

    Скажите, я правильно понял: внутри компактные строки хранят utf-8? А каков проигрыш в производительности при использовании строк с кириллицей — ведь их придется кодировать-декодировать при любых манипуляциях? А каков будет «выигрыш» при использовании, скажем, строк индийских символов или китайских иероглифов, где один символ может кодироваться в urf-8 тремя, четырьмя, а то и пятью байтами?


    1. vladimir_dolzhenko
      16.10.2018 18:08

      с чего вдруг взялась кодировка utf-8? мне вот по душе koi8r


      1. berez
        16.10.2018 18:12

        Ай, моя плохой, не заметил, что там utf-16:

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

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


        1. vladimir_dolzhenko
          16.10.2018 18:27

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

          Почему не бит — вопрос в том как его хранить с кем его есть — опять же — бенчмарки показали, что так (с флагом) будет лучше.


        1. horror_x
          16.10.2018 19:17
          +2

          Остается не очень понятно, зачем под битовый флажок отвели целый байт, ну да ладно.
          А как вы себе представляете размещение отдельного бита в памяти?


          1. berez
            17.10.2018 08:57
            -1

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


            1. poxvuibr
              17.10.2018 13:29

              Отдельный бит можно запихать в уже существующее поле флагов, если таковое есть.

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


    1. Perlovich
      16.10.2018 18:28

      Оффтопик.

      один символ может кодироваться в 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 байт.

      Или я что-то упускаю?


      1. berez
        16.10.2018 18:41

        Нет, не упускаете.
        Это у меня в голове сведения устаревшие.


  1. agmt
    16.10.2018 18:20

    Господи, почему в 2к18 ещё не все переехали на UTF-8?


    1. mayorovp
      16.10.2018 18:31

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


      1. brzsmg
        17.10.2018 12:39

        Думаю имелся ввиду Юникод вообщем, а не конкретно UTF-8.


        1. mayorovp
          17.10.2018 12:52

          На «Юникод вообщем» Java переехала с рождения.


    1. balexa
      16.10.2018 19:44
      +1

      А должны? Потому что UTF-8 очень неудобен в качестве внутреннего представления строк в программе. Это транспортный формат — в нем удобно сохранять данные на диск, передавать по сети и т.д., но в качестве внутреннего формата строк он не подходит.


      1. agmt
        16.10.2018 19:52
        +1

        А что подходит?
        UTF-16 со своими суррогатными парами даёт ложную надежду, но всё равно не позволяет бегать по индексу. Для того, чтобы обращаться по индексу к строке с эмодзи нужна отдельная функция. Строка — не массив, в любой кодировке.


        1. balexa
          16.10.2018 22:06

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


          1. MikailBag
            16.10.2018 23:42

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


            1. S-trace
              17.10.2018 00:31

              Забыли упомянуть о том, что UTF-8 совместим с null-terminated представлением строк (к примеру, в C), а остальные нет


            1. agmt
              17.10.2018 12:46

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


              1. MikailBag
                17.10.2018 23:38

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


                1. berez
                  18.10.2018 12:16
                  -1

                  Есть еще суррогатные пары. Это когда, скажем, буква Ё передается двумя символами: модифицирующий символ «две точки над буквой» и собственно сама буква Е. Даже в русском языке суррогатными парами можно выразить аж две буквы (Ё и Й), а есть языки, где вообще половина символов может быть суррогатами.


                  1. mayorovp
                    18.10.2018 13:10

                    Это называется «составные символы», а не «суррогатные пары».



    1. brzsmg
      17.10.2018 12:37
      -1

      Я полагаю вы ошиблись и имели ввиду UTF-*, а не конкретно UTF-8?
      Так как для разных задач используются разные UTF.

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



      1. mayorovp
        17.10.2018 12:57

        Вот поиск как раз одинаково хорошо работает в любой кодировке…


        1. brzsmg
          17.10.2018 15:20

          Павел, я не написал, что поиск по другим кодировкам как то плохо работает. Писать софт для обработки текста (в том числе и поиска) удобнее/проще, для кодировки где элемент массива это всегда отдельный символ.


    1. poxvuibr
      17.10.2018 13:16

      Господи, почему в 2к18 ещё не все переехали на UTF-8?

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


      1. agmt
        17.10.2018 13:32
        -1

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


        1. poxvuibr
          17.10.2018 13:45
          +1

          Суть данного треда как раз в том, что сложность charAt константна только для 1-байтных кодировок

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


  1. ris58h
    17.10.2018 10:50
    +1

    Объем статьи, конечно, поражает. Для тех кто хочет узнать об этом чуть больше, есть прекрасный доклад от Алексея Шипилёва.


  1. dim2r
    17.10.2018 12:31
    -1

    Странно, что раньше не ввели. Жду, когда будет перегрузка операторов, constexpr, setter/getter, checked arithmetics, function purity level и тд


  1. alex101andr
    17.10.2018 15:54

    Получается, что если в «схлопнутую» строку добавить utf8, то размер новой строки = 2 размера старой + новый символ?


    1. poxvuibr
      17.10.2018 16:10

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