«1. Обзор

Объект String является наиболее часто используемым классом в языке Java.

В этой быстрой статье мы рассмотрим Java String Pool — специальную область памяти, где строки хранятся JVM.

2. Интернирование строк

Благодаря неизменности строк в Java, JVM может оптимизировать объем памяти, выделяемой для них, сохраняя только одну копию каждой литеральной строки в пуле. Этот процесс называется интернированием.

Когда мы создаем переменную String и присваиваем ей значение, JVM ищет в пуле строку с равным значением.

Если он найден, компилятор Java просто вернет ссылку на свой адрес памяти, не выделяя дополнительной памяти.

Если он не найден, он будет добавлен в пул (интернирован) и его ссылка будет возвращена.

Давайте напишем небольшой тест, чтобы проверить это:

String constantString1 = "Baeldung";
String constantString2 = "Baeldung";
        
assertThat(constantString1)
  .isSameAs(constantString2);

3. Строки, выделенные с помощью конструктора

Когда мы создаем строку с помощью оператора new, компилятор Java создаст новый объект и сохранит его в пространство кучи, зарезервированное для JVM.

Каждая строка, созданная таким образом, будет указывать на другую область памяти со своим собственным адресом.

Давайте посмотрим, чем это отличается от предыдущего случая:

String constantString = "Baeldung";
String newString = new String("Baeldung");
 
assertThat(constantString).isNotSameAs(newString);

4. Строковый литерал против строкового объекта

Когда мы создаем объект String с помощью оператора new(), он всегда создает новый объект в куче памяти. С другой стороны, если мы создадим объект, используя синтаксис строкового литерала, например. «Baeldung», он может вернуть существующий объект из пула строк, если он уже существует. В противном случае он создаст новый объект String и поместит его в пул строк для повторного использования в будущем.

На высоком уровне оба являются объектами String, но основное различие заключается в том, что оператор new() всегда создает новый объект String. Кроме того, когда мы создаем строку, используя литерал, она интернируется.

Это станет намного яснее, если мы сравним два объекта String, созданные с помощью литерала String и оператора new:

String first = "Baeldung"; 
String second = "Baeldung"; 
System.out.println(first == second); // True

В этом примере объекты String будут иметь одну и ту же ссылку.

Далее создадим два разных объекта с помощью new и проверим, что у них разные ссылки:

String third = new String("Baeldung");
String fourth = new String("Baeldung"); 
System.out.println(third == fourth); // False

Аналогично, когда мы сравниваем литерал String с объектом String, созданным с помощью оператора new() с помощью оператора ==, он вернет false:

String fifth = "Baeldung";
String sixth = new String("Baeldung");
System.out.println(fifth == sixth); // False

В общем, мы должны использовать литеральную нотацию String, когда это возможно. Его легче читать, и он дает компилятору возможность оптимизировать наш код.

5. Интернирование вручную

Мы можем вручную интернировать строку в пуле строк Java, вызвав метод intern() для объекта, который мы хотим интернировать.

Интернирование строки вручную сохранит ее ссылку в пуле, и JVM вернет эту ссылку при необходимости.

Давайте создадим для этого тестовый пример:

String constantString = "interned Baeldung";
String newString = new String("interned Baeldung");

assertThat(constantString).isNotSameAs(newString);

String internedString = newString.intern();

assertThat(constantString)
  .isSameAs(internedString);

6. Сборка мусора

До Java 7 JVM помещала пул строк Java в пространство PermGen, которое имеет фиксированный размер — он может не расширяется во время выполнения и не подходит для сборки мусора.

Риск интернирования строк в PermGen (вместо кучи) заключается в том, что мы можем получить ошибку OutOfMemory от JVM, если интернируем слишком много строк.

Начиная с Java 7, пул строк Java хранится в пространстве кучи, которое является мусором, собираемым JVM. Преимущество этого подхода заключается в снижении риска ошибки OutOfMemory, поскольку строки, на которые нет ссылок, будут удалены из пула, тем самым освобождая память.

7. Производительность и оптимизация

В Java 6 единственная оптимизация, которую мы можем выполнить, — это увеличение пространства PermGen во время вызова программы с параметром JVM MaxPermSize:

-XX:MaxPermSize=1G

В Java 7 у нас есть более подробные варианты проверки и расширения/уменьшения размера пула. Давайте рассмотрим два варианта просмотра размера пула:

-XX:+PrintFlagsFinal
-XX:+PrintStringTableStatistics

-XX:StringTableSize=4901

Если мы хотим увеличить размер пула с точки зрения сегментов, мы можем использовать параметр JVM StringTableSize:

«

«До Java 7u40 размер пула по умолчанию составлял 1009 сегментов, но в более поздних версиях Java это значение претерпело некоторые изменения. Чтобы быть точным, размер пула по умолчанию от Java 7u40 до Java 11 был 60013, а теперь он увеличился до 65536.

Обратите внимание, что увеличение размера пула потребует больше памяти, но имеет то преимущество, что сокращает время, необходимое для вставки строк в Таблица.

8. Примечание о Java 9

До Java 8 строки были внутренне представлены как массив символов — char[], закодированный в UTF-16, так что каждый символ занимает два байта памяти.

В Java 9 реализовано новое представление, называемое компактными строками. Этот новый формат выберет подходящую кодировку между char[] и byte[] в зависимости от сохраненного содержимого.

Поскольку новое строковое представление будет использовать кодировку UTF-16 только при необходимости, объем памяти кучи будет значительно меньше, что, в свою очередь, вызовет меньшие накладные расходы сборщика мусора на JVM.

9. Заключение