«1. Обзор

Строки в Java внутренне представлены символом char[], содержащим символы строки. И каждый char состоит из 2 байтов, потому что Java внутри использует UTF-16.

Например, если строка содержит слово на английском языке, первые 8 битов будут равны 0 для каждого символа, поскольку символ ASCII может быть представлен одним байтом.

Для представления многих символов требуется 16 бит, но статистически большинству из них требуется всего 8 бит — представление символов LATIN-1. Таким образом, есть возможность улучшить потребление памяти и производительность.

Что также важно, так это то, что строки обычно обычно занимают большую часть пространства кучи JVM. И из-за того, как они хранятся в JVM, в большинстве случаев экземпляр String может занимать вдвое больше места, чем ему действительно нужно.

В этой статье мы обсудим опцию Compressed String, представленную в JDK6, и новую компактную строку, недавно представленную в JDK9. Оба они были разработаны для оптимизации потребления памяти строками в JMV.

2. Сжатая строка — Java 6

В обновлении JDK 6 21 Performance Release появилась новая опция виртуальной машины:

-XX:+UseCompressedStrings

Когда эта опция включена, строки вместо этого сохраняются как byte[]. of char[] — таким образом, экономя много памяти. Однако в конечном итоге этот параметр был удален в JDK 7, главным образом потому, что он имел некоторые непредвиденные последствия для производительности.

3. Компактная строка — Java 9

Java 9 вернула концепцию компактных строк.

Это означает, что всякий раз, когда мы создаем строку, если все символы строки могут быть представлены с использованием байтового представления LATIN-1, внутри будет использоваться массив байтов, так что один байт дается для одного символа.

В других случаях, если для представления какого-либо символа требуется более 8 бит, все символы хранятся с использованием двух байтов для каждого — представление UTF-16.

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

Теперь вопрос: как будут работать все операции со строками? Как он будет различать представления LATIN-1 и UTF-16?

Ну, чтобы решить эту проблему, во внутреннюю реализацию String внесено еще одно изменение. У нас есть окончательный полевой кодировщик, который сохраняет эту информацию.

3.1. Реализация String в Java 9

До сих пор String хранился как 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, готова и доступна , параметр VM CompactString включен по умолчанию. Чтобы отключить его, мы можем использовать:

+XX:-CompactStrings

3.2. Как работает кодер

В реализации класса String в Java 9 длина вычисляется как:

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[], а не от массива байтов. Из-за этого приходилось много распаковывать, что сказывалось на производительности.

Принимая во внимание, что в случае Compact String сохранение дополнительного поля «coder» также может увеличить накладные расходы. Для снижения стоимости кодера и распаковки байтов в символы (в случае представления 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 миллионов строк, а затем добавляем их наивным образом. Когда мы запускаем этот код (компактные строки включены по умолчанию), мы получаем вывод:

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 — путем хранения строк с эффективным использованием памяти.

Как всегда, весь код доступен на Github.