«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. Заключение