1. Обзор
Строки в Java внутренне представлены символом char [] , содержащим символы Строки . И каждый символ состоит из 2 байтов, потому что Java внутренне использует UTF-16.
Например, если Строка содержит слово на английском языке, начальные 8 бит будут равны 0 для каждого символа , поскольку символ ASCII может быть представлен одним байтом.
Для представления многих символов требуется 16 бит, но статистически большинству требуется только 8 бит — латинское представление 1 символа. Таким образом, есть возможности для повышения потребления памяти и производительности.
Что также важно, так это то, что String s обычно занимают большую часть пространства кучи JVM. И из-за того, как они хранятся в JVM, в большинстве случаев /Строка экземпляр может занимать двойное пространство , которое ему действительно нужно .
В этой статье мы обсудим опцию Сжатой строки, представленную в JDK 6, и новую компактную строку, недавно представленную в JDK9. Оба они были разработаны для оптимизации потребления памяти строками в JMV.
2. Сжатая строка – Java 6
В выпуске производительности JDK 6 с обновлением 21 появилась новая опция виртуальной машины:
-XX:+UseCompressedStrings
Когда эта опция включена, Строки сохраняются как байт [] вместо char []– , что позволяет сэкономить много памяти. Однако в конечном итоге эта опция была удалена в JDK 7, главным образом потому, что она имела некоторые непредвиденные последствия для производительности.
3. Компактная строка – Java 9
Java 9 привнесла концепцию компактных строк ba ck.
Это означает, что всякий раз, когда мы создаем Строку , если все символы Строки могут быть представлены с использованием представления byte — LATIN-1, внутри будет использоваться массив байтов , так что для одного символа задается один байт.
В других случаях, если для представления какого-либо символа требуется более 8 бит, все символы сохраняются с использованием двух байтов для каждого представления — UTF-16.
Так что в принципе, когда это возможно, он будет просто использовать один байт для каждого символа.
Теперь вопрос в том, как будут работать все операции String ? Как он будет различать представления LATIN-1 и UTF-16?
Что ж, для решения этой проблемы во внутреннюю реализацию Строка . У нас есть последнее поле кодер , который сохраняет эту информацию.
3.1. Реализация строк в Java 9
До сих пор строка | хранилась как символ[] :
private final char[] value;
Отныне это будет байт[]:
private final byte[] value;
Переменная кодер :
private final byte coder;
Где кодер может быть:
static final byte LATIN1 = 0; static final byte UTF16 = 1;
Большинство операций String теперь проверяют кодер и отправляют его в конкретную реализацию:
public int indexOf(int ch, int fromIndex) { return isLatin1() ? StringLatin1.indexOf(value, ch, fromIndex) : StringUTF16.indexOf(value, ch, fromIndex); } private boolean isLatin1() { return COMPACT_STRINGS && coder == LATIN1; }
Когда вся необходимая JVM информация готова и доступна, опция Компактная строка Виртуальная машина включена по умолчанию. Чтобы отключить его, мы можем использовать:
+XX:-CompactStrings
3.2. Как Работает кодер
В реализации класса Java 9 String длина вычисляется как:
public int length() { return value.length >> coder; }
Если Строка содержит только латинское-1, значение кодера будет равно 0, поэтому длина Строки будет такой же, как длина массива байтов.
В других случаях, если Строка находится в представлении UTF-16, значение кодера будет равно 1, и, следовательно, длина будет вдвое меньше размера фактического массива байтов.
Обратите внимание, что все изменения, внесенные в Compact Строка, находятся во внутренней реализации Строка класса и полностью прозрачны для разработчиков, использующих Строка .
4. Компактные строки по сравнению со Сжатыми строками
В случае JDK 6 Со сжатыми Строками основная проблема заключалась в том, что конструктор String принимал в качестве аргумента только char [] . В дополнение к этому многие операции String зависели от представления char [] , а не от массива байтов. Из-за этого пришлось много распаковывать, что сказалось на производительности.
В то время как в случае компактной строки сохранение дополнительного поля “кодер” также может увеличить накладные расходы. Чтобы снизить стоимость кодера и распаковки байта s в символ s (в случае представления UTF-16), некоторые методы интринсифицированы и код ASM, сгенерированный JIT-компилятором, также был улучшен.
Это изменение привело к некоторым противоречивым результатам. Латинский-1 indexOf(строка) вызывает встроенный метод, в то время как indexOf(символ) этого не делает. В случае UTF-16 оба этих метода вызывают внутренний метод. Эта проблема затрагивает только латинскую-1 Строку и будет исправлена в будущих выпусках.
Таким образом, компактные Строки лучше, чем сжатые Строки с точки зрения производительности.
Чтобы выяснить, сколько памяти сохраняется с помощью компактных строк , были проанализированы различные дампы кучи Java-приложений. И, хотя результаты сильно зависели от конкретных приложений, общие улучшения почти всегда были значительными.
4.1. Разница в производительности
Давайте рассмотрим очень простой пример разницы в производительности между включением и отключением компактных Строк:
long startTime = System.currentTimeMillis(); List strings = IntStream.rangeClosed(1, 10_000_000) .mapToObj(Integer::toString) .collect(toList()); long totalTime = System.currentTimeMillis() - startTime; System.out.println( "Generated " + strings.size() + " strings in " + totalTime + " ms."); startTime = System.currentTimeMillis(); String appended = (String) strings.stream() .limit(100_000) .reduce("", (l, r) -> l.toString() + r.toString()); totalTime = System.currentTimeMillis() - startTime; System.out.println("Created string of length " + appended.length() + " in " + totalTime + " ms.");
Здесь мы создаем 10 миллионов Строк s, а затем добавляем их наивным способом. Когда мы запускаем этот код (компактные строки включены по умолчанию), мы получаем вывод:
Generated 10000000 strings in 854 ms. Created string of length 488895 in 5130 ms.
Аналогично, если мы запустим его, отключив Компактные строки с помощью опции: -XX:-CompactStrings , вывод будет:
Generated 10000000 strings in 936 ms. Created string of length 488895 in 9727 ms.
Очевидно, что это тест на поверхностном уровне, и он не может быть очень репрезентативным – это всего лишь снимок того, что новая опция может сделать для повышения производительности в данном конкретном сценарии.
5. Заключение
В этом уроке мы рассмотрели попытки оптимизировать производительность и потребление памяти в JVM – путем эффективного хранения String s в памяти.
Как всегда, весь код доступен на Github .