Topjava – онлайн-школа по обучению программированию на самом популярном в мире языке Java

*Открытые вступительные занятия с выполнением домашнего задания
Компактные строки в Java 9
1. Обзор
Наверно, ни для кого не является секретом, что строки в Java представлены в виде массива символов char[]. При этом каждый символ в памяти занимает 2 байта (16 бит), т.к. Java использует кодировку UTF-16.

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

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

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

В этой статье мы обсудим опцию Compressed String (сжатая строка), представленную в JDK 6, и новую Compact String (компактную строку), появившуюся в JDK 9. Обе опции были разработаны для оптимизации потребления памяти строками в JVM.
2. Сжатие строк в Java 6
В JDK 6 в 21 обновлении была представлена новая опция для JVM:
-XX: + UseCompressedStrings
Когда эта опция включена, строки хранятся не как char[], а как byte[], что экономит много памяти. Однако эта опция была в конечном итоге удалена в JDK 7 из-за непредсказуемых последствий для производительности.
3. Компактные строки в Java 9
В Java 9 вернули концепцию компактных строк.

Это означает, что всякий раз, когда мы создаем строку символы которой могут быть представлены с использованием одного байта – в LATIN-1, то для хранения строк будет использоваться байтовый массив. Но, если какой-либо символ требует более 8 бит для своего представления, то каждый символы сроки будет занимать два байта (UTF-16).

Теперь вопрос — как будут работать все операции со строками? Как будут различаться кодировки строк?

Для решения этой проблемы было внесено ещё одно изменение во внутреннюю реализацию String. Теперь данный класс содержит поле private final byte coder, которое хранит эту информацию.

3.1 Реализация строк в Java 9
До сих пор строка хранилась, как массив символов char[]:
private final char[] value;
Теперь это массив байт byte[]:
private final byte[] value;
Идентификатор, отвечающий за кодировку coder:
private final byte coder;
При этом идентификатор поддерживает следующие значения:
static final byte LATIN1 = 0;
static final byte UTF16 = 1;
Большинство методов класса String теперь проверяют поле coder и в зависимости от его значения использую разную реализацию:
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;
}
Для отключения компактных строк существует опция:
+XX:-CompactStrings
3.2. Как работает coder
В реализации класса String в Java 9 длина вычисляется так:
public int length() {
    return value.length >> coder;
}
Если строка содержит только LATIN-1, значение кодировщика будет равно 0, поэтому длина строки будет равна длине байтового массива.

Если строка представлена в виде UTF-16, то значение кодировщика будет равно 1, и, следовательно, длина будет вдвое меньше размера фактического байтового массива.

4. Compact Strings vs. Compressed String
В Compressed Strings в JDK 6 основной проблемой было то, что конструктор String принимал в качестве аргумента только массив символов char[] не смотря на то, что многие операции со String зависят от представления char[], а не от байтового массива. Из-за этого приходилось производить распаковку, что сказывалось на производительности.

Обратите внимание, что в случае Compact String содержание дополнительного поля coder также может увеличить нагрузку. Чтобы снизить «стоимость» кодера и распаковку байтов в символы (в случае представления UTF-16), некоторые методы являются встроенными, и код ASM, сгенерированный компилятором JIT, также был улучшен.

Эти изменение привели к некоторым неожиданным результатам. LATIN-1 indexOf(String) вызывает встроенный метод, тогда как indexOf(char) — нет. В случае UTF-16 оба эти метода вызывают встроенный метод. Эта проблема касается только строки LATIN-1 и будет исправлена в будущих выпусках.

Таким образом, с точки зрения производительности, компактные строки Compact Strings лучше, чем сжатые строки Compressed Strings.

Чтобы узнать, сколько памяти сохранено с помощью Compact Strings, были проанализированы различные дампы кучи (heap) Java-приложений. И, хотя результаты сильно зависели от конкретных приложений, общие улучшения были почти всегда значительными.
4.1. Различия в производительности
Давайте рассмотрим очень простой пример, демонстрирующий разницу в производительности между включенным и отключенным Compact Strings:
package com.topjava;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class CompactStringTest {

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        List<String> strings = IntStream.rangeClosed(1, 100_000)
                .mapToObj(Integer::toString)
                .collect(Collectors.toList());

        long totalTime = System.currentTimeMillis() - startTime;
        System.out.println("Generated " + strings.size() + " strings in " + totalTime + " ms.");

        startTime = System.currentTimeMillis();
        String appended = strings.stream()
                .reduce("", (l, r) -> l + r);

        totalTime = System.currentTimeMillis() - startTime;
        System.out.println("Created string of length " + appended.length() + " in " + totalTime + " ms.");
    }
}
Когда мы запускаем этот код (Compact Strings включены по умолчанию), мы получаем вывод:
Generated 100000 strings in 15 ms.
Created string of length 488895 in 3464 ms.
Точно так же, если мы запустим, отключив Compact Strings с помощью опции:
-XX: -CompactStrings 
Получим:
Generated 100000 strings in 65 ms.
Created string of length 488895 in 7043 ms.
Понятно, что это тест поверхностен, и он не может быть очень репрезентативным — это всего лишь пример того, как новая опция может улучшить производительность в этом конкретном сценарии.
5. Заключение
В этой статье мы рассмотрели попытки оптимизировать производительность и потребление памяти в JVM путем хранения строк более эффективным способом.
Оригинальная статья Compact Strings in Java 9.