1. Обзор
Объект String
является наиболее часто используемым классом в языке Java.
В этой быстрой статье мы рассмотрим пул строк Java — специальную область памяти, в которой JVM хранит строки
.
2. Стажировка строк
Благодаря неизменности строк
в Java, JVM может оптимизировать объем выделяемой для них памяти, сохраняя в пуле только одну копию каждой литеральной строки
. Этот процесс называется интернированием
.
Когда мы создаем переменную String
и присваиваем ей значение, JVM ищет в пуле строку
с равным значением.
Если он найден, компилятор Java просто вернет ссылку на свой адрес памяти, не выделяя дополнительной памяти.
Если он не найден, он будет добавлен в пул (интернирован), и его ссылка будет возвращена.
Давайте напишем небольшой тест, чтобы проверить это:
String constantString1 = "ForEach";
String constantString2 = "ForEach";
assertThat(constantString1)
.isSameAs(constantString2);
3. Строки
, выделенные с помощью конструктора
Когда мы создаем String
с помощью оператора new
, компилятор Java создаст новый объект и сохранит его в пространстве кучи, зарезервированном для JVM.
Каждая строка
, созданная таким образом, будет указывать на другую область памяти со своим собственным адресом.
Давайте посмотрим, чем это отличается от предыдущего случая:
String constantString = "ForEach";
String newString = new String("ForEach");
assertThat(constantString).isNotSameAs(newString);
4. Строковый
литерал против строкового объекта
Когда мы создаем объект String
с помощью оператора new()
, он всегда создает новый объект в куче памяти. С другой стороны, если мы создадим объект, используя синтаксис строкового литерала, например, « ForEach
», он может вернуть существующий объект из пула строк, если он уже существует. В противном случае он создаст новый объект String и поместит его в пул строк для повторного использования в будущем.
На высоком уровне оба являются объектами String
, но основное различие заключается в том, что оператор new()
всегда создает новый объект String .
Кроме того, когда мы создаем строку
, используя литерал, она интернируется.
Это станет намного яснее, если мы сравним два объекта String
, созданные с использованием литерала String
и оператора new :
String first = "ForEach";
String second = "ForEach";
System.out.println(first == second); // True
В этом примере объекты String
будут иметь одну и ту же ссылку.
Далее создадим два разных объекта с помощью new
и проверим, что у них разные ссылки:
String third = new String("ForEach");
String fourth = new String("ForEach");
System.out.println(third == fourth); // False
Точно так же, когда мы сравниваем литерал String с объектом
String, созданным с помощью оператора
new()
с помощью оператора ==, он вернет false:
String fifth = "ForEach";
String sixth = new String("ForEach");
System.out.println(fifth == sixth); // False
В общем, мы должны использовать литеральную нотацию String
, когда это возможно . Его легче читать, и он дает компилятору возможность оптимизировать наш код.
5. Стажировка вручную
Мы можем вручную интернировать строку
в пуле строк Java, вызвав метод intern()
для объекта, который мы хотим интернировать.
Интернирование строки
вручную сохранит ее ссылку в пуле, и JVM вернет эту ссылку при необходимости.
Давайте создадим тестовый пример для этого:
String constantString = "interned ForEach";
String newString = new String("interned ForEach");
assertThat(constantString).isNotSameAs(newString);
String internedString = newString.intern();
assertThat(constantString)
.isSameAs(internedString);
6. Сбор мусора
До Java 7 JVM помещала Java String Pool в пространство PermGen
, которое имеет фиксированный размер — его нельзя расширить во время выполнения и нельзя использовать для сборки мусора .
Риск интернирования Strings
в PermGen
(вместо Heap
) заключается в том, что мы можем получить ошибку OutOfMemory
от JVM, если интернируем слишком много Strings
.
Начиная с Java 7, пул строк Java хранится в пространстве кучи
, которое является мусором , собираемым JVM .
Преимущество этого подхода заключается в снижении риска ошибки OutOfMemory
, поскольку строки
, на которые нет ссылок , будут удалены из пула, тем самым освобождая память.
7. Производительность и оптимизация
В Java 6 единственная оптимизация, которую мы можем выполнить, — это увеличение пространства PermGen
во время вызова программы с параметром JVM MaxPermSize
:
-XX:MaxPermSize=1G
В Java 7 у нас есть более подробные параметры для проверки и расширения/уменьшения размера пула. Рассмотрим два варианта просмотра размера пула:
-XX:+PrintFlagsFinal
-XX:+PrintStringTableStatistics
Если мы хотим увеличить размер пула с точки зрения сегментов, мы можем использовать параметр StringTableSize
JVM:
-XX:StringTableSize=4901
До Java 7u40 размер пула по умолчанию составлял 1009 сегментов, но в более поздних версиях Java это значение претерпело некоторые изменения. Если быть точным, размер пула по умолчанию от Java 7u40 до Java 11 составлял 60013, а теперь он увеличился до 65536.
Обратите внимание, что увеличение размера пула потребует больше памяти, но имеет то преимущество, что сокращает время, необходимое для вставки строк
в таблицу.
8. Примечание о Java 9
До Java 8 строки
были внутренне представлены как массив символов — char[]
, закодированный в UTF-16
, так что каждый символ использует два байта памяти.
В Java 9 предоставляется новое представление, называемое компактными строками.
Этот новый формат выберет подходящую кодировку между char[]
и byte[]
в зависимости от сохраненного содержимого.
Поскольку новое представление String
будет использовать кодировку UTF-16
только при необходимости, объем памяти кучи
будет значительно меньше, что, в свою очередь, приведет к меньшим накладным расходам сборщика мусора
на JVM.
9. Заключение
В этом руководстве мы показали, как JVM и компилятор Java оптимизируют выделение памяти для объектов String
через Java String Pool.
Все примеры кода, использованные в статье, доступны на GitHub .