Компактные строки - одна из самых веских причин отказаться от Java 8. И вот почему.

Вступление

Согласно некоторым опросам, например от JetBrains, версия 8 Java в настоящее время наиболее часто используется разработчиками во всем мире, несмотря на то, что она была выпущена в 2014 году.

То, что вы читаете, является первой из серии статей под названием «Выход за рамки Java 8», основанных на содержании моей книги «Java для пришельцев». Эти статьи проведут читателя шаг за шагом к изучению наиболее важных функций, представленных, начиная с версии 9. Цель состоит в том, чтобы ознакомить читателя с тем, насколько важно перейти от Java 8, объясняя огромные преимущества предложений последних версий языка Java.

В этой статье мы поговорим о компактных строках, механизме, представленном в Java 9, который является одной из наиболее веских причин отказаться от Java 8 и перейти на одну из самых последних версий.

Осторожно, спойлеры

Класс String согласно статистике наиболее часто используется в программировании на Java. Поэтому кажется важным спросить себя, насколько эффективны объекты этого класса. Хорошая новость заключается в том, что, начиная с Java 9, эти объекты работают значительно лучше, чем в предыдущей версии. Причем это преимущество получается практически без усилий, то есть достаточно будет запустить нашу программу с JVM версии 9 (или выше), не принимая никаких действий в отношении нашего кода. Итак, давайте разберемся, что такое компактные строки и как их использовать.

За кулисами

Рисунок 1. Расположение файла src.zip в папке установки JDK версии 8.

До Java 8 в классе использовался массив символов для хранения составляющих строку символов. В этом можно было убедиться, прочитав исходный код класса String. Для этого просто найдите файл String.java в файле src.zip, расположенном в папке установки JDK версии 8. 

Этот файл содержит все исходные файлы стандартной библиотеки Java.

Итак, распаковав его, мы можем найти источник класса String.java в пути java/lang (на самом деле String класс содержится в пакете java.lang). Если мы откроем этот файл с помощью любого редактора, мы сможем убедиться, что String класс объявлен следующим образом (мы удалили некоторые комментарии и другие элементы, бесполезные для нашего обсуждения):

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** Значение используется для хранения символов. */
    private final char value[];
    // оставшаяся часть кода опущена               

Таким образом, до Java 8 существование массива символов значения означало, что для каждого символа строки выделялось 16 бит (2 байта) памяти.

Фактически, в большинстве приложений мы используем символы, которые могут храниться всего в 8 битах (1 байт). Итак, чтобы добиться большей производительности с точки зрения скорости и использования памяти в наших программах, в Java 9 реализация String класса была пересмотрена и теперь поддерживается массивом байтов, а не char массивом. Ниже приводится начальная часть объявления класса String в версии 15 Java, из которой удалены неинтересные элементы:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** Значение используется для хранения символов. */
    private final byte[] value;

    /**
     * Идентификатор кодировки, используемой для кодирования байтов
     * {@code value}.
     */
    private final byte coder;

Рисунок 2. Расположение файла src.zip в папке установки JDK версии 15.

В JDK 9 файл src.zip был перемещен в каталог lib, а пакеты были включены в папки, представляющие модули. Итак, исходник String.java теперь находится в папках java.base/java/lang. Фактически, java.base  - это имя модуля, содержащего пакет java.lang.

Однако часто нужно использовать менее распространенные символы, которые необходимо хранить в 16 битах (2 байта). Фактически, внутри String класса был реализован механизм, основанный на переменном кодировщике, который заботится о выделении правильного количества байтов для каждого символа. Этот механизм известен как компактные строки, и начиная с версии 9 Java это метод, используемый JVM по умолчанию. Программно ничего не меняется, мы будем использовать строки, как обычно. Однако приложения Java будут работать лучше.

Неужели мы собираемся использовать половину памяти для строк?

Хотя мы заметили, что сегодня String класс поддерживается byte массивом, а не char массивом, как в версии 8, к сожалению, в Java невозможно определить априори, сколько памяти будет использовать программа. Фактически, он автоматически управляется сложными механизмами сборщика мусора, и при каждом выполнении наша программа может использовать очень разные объемы памяти. Более того, в Java нет способа точно знать, сколько памяти используется для определенного объекта в любой момент времени, как это возможно в других языках. 

С помощью стратегии, основанной на интерфейсе Instrumentation пакета java.lang.instrument, можно получить приблизительный размер объекта, но это не относится к строкам, которые, будучи неизменяемыми объектами, размещаются в памяти иным образом, чем другие объекты. Таким образом, даже если кажется, что механизм компактных строк подразумевает экономию памяти, это не является ни достоверным, ни доказуемым. Итак, давайте посмотрим, в чем преимущество использования JDK версии 9 или выше на примере кода.

Пример

Рассмотрим следующий пример:

public class CompactStringsDemo {

    public static void main(String[] args) {
        long initialTime = System.currentTimeMillis();
        long limit = 100_000;
        String s ="";
        for (int i = 0; i < limit; i++) {
            s += limit;
        }
        long totalTime = System.currentTimeMillis() - initialTime;
        System.out.println("Создано "+ limit +" строк за "+ totalTime +
                               " миллисекунд");
    }
}

В этом классе создается 100 000 строк (которые содержат самые первые 100 000 чисел), которые конкатенируются. Кроме того, вычисляется и печатается время в миллисекундах, необходимое для создания этих экземпляров и их конкатенации.

Давайте попробуем запустить это приложение 5 раз, используя JDK версии 15.1, и проанализируем результаты:

java CompactStringsDemo
Created 100000 strings in 3539 milliseconds

java CompactStringsDemo
Created 100000 strings in 3548 milliseconds

java CompactStringsDemo
Created 100000 strings in 3564 milliseconds

java CompactStringsDemo
Created 100000 strings in 3561 milliseconds

java CompactStringsDemo
Created 100000 strings in 3609 milliseconds

Можно заметить, что при каждом запуске скорость приложения почти постоянна и составляет около 3,5 секунд.

Итак, давайте попробуем отключить компактные строки, используя опцию -XX:-CompactStrings, и запустить одно и то же приложение 5 раз, а затем снова проанализируем результаты:

java -XX:-CompactStrings CompactStringsDemo
Created 100000 strings in 8731 milliseconds

java -XX:-CompactStrings CompactStringsDemo
Created 100000 strings in 8263 milliseconds

java -XX:-CompactStrings CompactStringsDemo
Created 100000 strings in 8547 milliseconds

java -XX:-CompactStrings CompactStringsDemo
Created 100000 strings in 8602 milliseconds

java -XX:-CompactStrings CompactStringsDemo
Created 100000 strings in 8353 milliseconds

Опять же, производительность с точки зрения скорости почти постоянна, но намного хуже, чем при использовании компактных строк. Фактически, средняя скорость выполнения этого приложения без компактных строк составляет около 8,5 секунд, в то время как при использовании компактных строк средняя скорость составляет всего около 3,5 секунд. Значительное преимущество, которое позволяет сэкономить нам почти 60% времени.

Если мы даже перекомпилируем и перезапустим программу напрямую с последней сборкой Java 8 (JDK 1.8.0_261), преимущества станут еще более очевидными:

"C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo
Created 100000 strings in 31113  milliseconds

"C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo
Created 100000 strings in 30376  milliseconds

"C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo
Created 100000 strings in 32868  milliseconds

"C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo
Created 100000 strings in 32508  milliseconds

"C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo
Created 100000 strings in 35328  milliseconds

Ухудшение производительности на этот раз еще более очевидно: с JDK 15 и компактными строками производительность приложения была почти в 10 раз лучше! Конечно, это не означает, что все программы будут иметь такие значительные улучшения, потому что наш пример был основан исключительно на создании экземпляров строк и их конкатенации.

Что касается экономии использования памяти, хотя и вероятной, как мы уже сказали, это трудно доказать, поскольку сборщик мусора выполняет сложную работу в зависимости от текущей ситуации.

Выводы

В этой статье мы увидели первую вескую причину для перехода с Java 8. Компактные строки, представленные начиная с версии 9, позволяют нашим программам быть более эффективными при использовании строк. Поскольку класс String является статистически наиболее часто используемым классом в программах Java, мы можем сделать вывод, что простое использование JDK с версией выше 8 гарантирует более высокую скорость выполнения наших приложений. Мы также обнаружили, что JDK 15 без использования компактных строк по-прежнему гарантирует значительно более высокую производительность, чем последняя сборка JDK 8.

Таким образом, обновление JDK является первым шагом к повышению скорости выполнения наших приложений.

Заметки автора

Даже если игнорировать повышенную безопасность, предлагаемую последними версиями JDK, есть множество причин, чтобы улучшить свои знания Java или, по крайней мере, ваши собственные установки среды выполнения Java. Моя книга «Java для инопланетян» послужившая источником для серии «Выход за рамки Java 8», позволит углубиться в рассматриваемые темы и получить необходимые знания. Для получения дополнительной информации посетите https://www.javaforaliens.com.