1. Обзор
Строки
в Java внутренне представлены char[]
, содержащим символы String
. И каждый char
состоит из 2 байтов, потому что Java внутри использует UTF-16.
Например, если строка
содержит слово на английском языке, все первые 8 битов будут равны 0 для каждого char
, поскольку символ ASCII может быть представлен одним байтом.
Для представления многих символов требуется 16 бит, но по статистике для большинства требуется всего 8 бит — представление символов LATIN-1. Таким образом, есть возможность улучшить потребление памяти и производительность.
Также важно то, что String
обычно обычно занимают большую часть пространства кучи JVM. И из-за того, как они хранятся в JVM, в большинстве случаев экземпляр String
может занимать вдвое больше места , чем ему действительно нужно .
В этой статье мы обсудим опцию Compressed String, представленную в JDK6, и новую компактную строку, недавно представленную в JDK9. Оба они были разработаны для оптимизации потребления памяти строками в JMV.
2. Сжатая строка
— Java 6
В обновлении JDK 6 21 Performance Release появилась новая опция виртуальной машины:
-XX:+UseCompressedStrings
Когда эта опция включена, строки
сохраняются как byte[]
вместо char[] —
таким образом экономится много памяти. Однако в конечном итоге этот параметр был удален в JDK 7, главным образом потому, что он имел некоторые непредвиденные последствия для производительности.
3. Компактная строка
— Java 9
Java 9 вернула концепцию компактных строк
.
Это означает, что всякий раз, когда мы создаем строку
, если все символы строки
могут быть представлены с использованием байтового представления LATIN-1, внутри будет использоваться массив байтов , так что один байт дается для одного символа.
В других случаях, если для представления какого-либо символа требуется более 8 бит, все символы хранятся с использованием двух байтов для каждого — представление UTF-16.
Так что в основном, когда это возможно, он будет использовать один байт для каждого символа.
Теперь вопрос: как будут работать все операции со строками
? Как он будет различать представления LATIN-1 и UTF-16?
Ну, чтобы решить эту проблему, во внутреннюю реализацию String
внесено еще одно изменение . У нас есть окончательный кодировщик
поля , который сохраняет эту информацию.
3.1. Строковая
реализация в Java 9
До сих пор строка
хранилась как char[]
:
private final char[] value;
Отныне это будет byte[]:
private final byte[] value;
Кодировщик
переменных :
private final byte coder;
Где может быть кодер :
static final byte LATIN1 = 0;
static final byte UTF16 = 1;
Большинство операций со строками
теперь проверяют кодировщик и направляют его в конкретную реализацию:
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, готова и доступна, опция CompactString
VM включена по умолчанию. Чтобы отключить его, мы можем использовать:
+XX:-CompactStrings
3.2. Как работает кодер
В реализации класса Java 9 String длина рассчитывается как:
public int length() {
return value.length >> coder;
}
Если строка
содержит только LATIN-1, значение кодера
будет равно 0, поэтому длина строки
будет такой же, как длина массива байтов.
В других случаях, если String
имеет представление UTF-16, значение coder
будет равно 1, и, следовательно, длина будет вдвое меньше размера фактического массива байтов.
Обратите внимание, что все изменения, сделанные для Compact String,
относятся к внутренней реализации класса String
и полностью прозрачны для разработчиков, использующих String
.
4. Компактные строки
против сжатых строк
В случае со сжатыми строками
JDK 6 основная проблема заключалась в том, что конструктор String
принимал в качестве аргумента только char[] .
В дополнение к этому, многие операции со строками
зависели от представления char[]
, а не от массива байтов. Из-за этого приходилось много распаковывать, что сказывалось на производительности.
Принимая во внимание, что в случае компактной строки
сохранение дополнительного поля «кодер» также может увеличить накладные расходы. Чтобы уменьшить стоимость кодера
и распаковки byte
в char
(в случае представления UTF-16), некоторые методы были встроены , а код ASM, сгенерированный JIT-компилятором, также был улучшен.
Это изменение привело к некоторым нелогичным результатам. LATIN-1 indexOf(String)
вызывает встроенный метод, а indexOf(char)
– нет. В случае UTF-16 оба эти метода вызывают встроенный метод. Эта проблема затрагивает только строку
LATIN-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 миллионов String
, а затем добавляем их наивным образом. Когда мы запускаем этот код (компактные строки включены по умолчанию), мы получаем вывод:
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
с эффективным использованием памяти.
Как всегда, весь код доступен на Github .