W3docs

Java String Pool

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

Типичная Java-программа создаёт тысячи строк, и огромная их часть — те же самые символы, что и в других строках программы. Имена методов. Ключи конфигурации. Сообщения об ошибках. Подписи полей. JVM считает такую избыточность проблемой, достойной решения: она поддерживает специальную область памяти — пул строк (или таблицу интернирования строк) — и даёт каждому литералу из исходного кода общую запись в ней. Два литерала с одинаковым содержимым указывают на один и тот же объект.

Такое совместное использование имеет видимые последствия для сравнения по идентичности (==), расхода памяти и ряда тонких ошибок, связанных с intern(). Эта глава посвящена правилам.

Литералы попадают в пул, new String(...) — нет

Самый важный факт:

String a = "hello";
String b = "hello";
System.out.println(a == b);   // true — same pooled object

String c = new String("hello");
System.out.println(a == c);   // false — new String, fresh object

Каждый строковый литерал, который видит компилятор, добавляется в пул при первой загрузке. При последующих обращениях к тому же литералу — где угодно в программе, в любом классе — возвращается та же ссылка. Поэтому a и b — один и тот же объект.

new String("hello") принудительно создаёт новый объект в куче. Аргумент "hello" по-прежнему попадает в пул (потому что это литерал), однако конструктор копирует его в совершенно новый объект за пределами пула. Таким образом, c и a имеют одинаковое содержимое, но разную идентичность.

Именно поэтому в каждом учебнике по Java повторяют: «используйте equals, а не ==». Сравнение по идентичности случайно работает для простых литералов, но ломается, как только строка приходит из new, из парсера, из сети или из конкатенации, которую компилятор не свернул во время компиляции.

Что хранится в пуле

Пул пополняется двумя способами:

  1. Строковые литералы в исходном коде. Компилятор записывает каждый уникальный литерал как запись CONSTANT_String в константный пул класса; JVM превращает её в реальный объект String в пуле на куче при первом использовании класса.
  2. Явные вызовы intern(). Любую строку, на которую есть ссылка, можно добавить в пул вызовом s.intern(). Метод возвращает экземпляр из пула — одну и ту же ссылку для каждого вызывающего, кто интернирует строку с таким же содержимым.

Вычисленные строки — a + b, s.substring(...), результаты String.formatне добавляются в пул автоматически. Они живут там, куда их положил сборщик мусора, и имеют ту идентичность, которая у них оказалась.

String x = "java";
String y = "ja" + "va";          // compile-time constant — pooled, == x
String z = "ja" + new String("va"); // runtime computation — NOT pooled
System.out.println(x == y);      // true
System.out.println(x == z);      // false
System.out.println(x == z.intern()); // true — intern() returns the pooled instance

Второй случай — ловушка. y вычисляется из двух литералов, но компилятор сворачивает конкатенацию во время компиляции, поэтому результат — просто ещё один литерал в пуле. z включает new во время выполнения, компилятор не может его свернуть, и полученный объект живёт вне пула.

Метод intern()

String#intern() делает две вещи за один вызов:

  • Если строка с теми же символами уже есть в пуле, возвращает ссылку на неё.
  • Иначе добавляет строку в пул и возвращает её.

Второй вариант полезен, когда строки строятся во время выполнения из небольшого, но часто встречающегося набора значений — имён HTTP-заголовков, разбираемых из байтов, токенов лексера, имён столбцов из драйвера базы данных. Их интернирование сворачивает N отдельных объектов в один и позволяет использовать == в последующих сравнениях — если вы убедились, что игра стоит свеч.

String s1 = new String("status").intern();
String s2 = new String("status").intern();
System.out.println(s1 == s2);   // true — both refer to the pooled "status"

Подводный камень: каждый вызов intern() требует поиска по хеш-таблице, а интернированные строки живут в хеш-таблице фиксированного размера, которая не сжимается. Если интернировать неограниченный ввод (поисковые запросы пользователей, идентификаторы запросов), пул будет медленно заполняться строками, которые никогда не будут повторно использованы, — это утечка памяти в замедленном темпе. Интернируйте строки только тогда, когда (а) набор значений ограничен и (б) вы измерили проблему, достойную решения.

Внутреннее устройство пула (кратко)

Пул реализован как хеш-таблица внутри JVM. В HotSpot это StringTable с ёмкостью по умолчанию, которая со временем увеличивалась (в большинстве сборок сейчас 65 536 корзин). Её можно посмотреть из командной строки:

java -XX:+PrintStringTableStatistics MyApp

Для кода приложения реализация невидима: нельзя спросить «находится ли эта строка в пуле?» через публичный API, и это не нужно. Видимое поведение — == для одинаковых литералов и intern() для добавления вычисленных строк в пул.

Почему == всё равно неверен для строк

Пул может создать видимость, что == работает на тестовых входных данных:

String a = "hello";
String b = "hello";
if (a == b) { ... }   // happens to be true

Затем кто-то передаёт строку через BufferedReader.readLine() — и == незаметно возвращает false. Нужный контракт — «имеют ли они одинаковые символы?», и он записывается как a.equals(b). Пул — это оптимизация памяти, а не стратегия сравнения: никогда не полагайтесь на него для корректности.

Разбор примера

Пример ниже делает поведение пула наглядным. Каждый вызов printRef показывает системный хеш идентичности (однострочный способ понять, «какой это объект?»), чтобы было видно, где литералы совместно используют хранилище, а где вычисленные строки — нет.

java— editable, runs on the server

Сначала посмотрите на хеши идентичности: литералы и свёрнутая константа времени компиляции разделяют один хеш. У runtimeConcat и fresh — свои собственные. interned совпадает с литералом, потому что intern() вернул экземпляр из пула, а не объект, созданный через new. Результаты == прямо следуют из идентичностей; equals возвращает true для всех них, потому что по содержимому они действительно равны.

Что дальше

Пул существует потому, что String неизменяем — совместное использование одного объекта несколькими вызывающими безопасно только тогда, когда никто не может изменить его содержимое. Следующая глава развивает эту тему: почему была выбрана неизменяемость, что она даёт и какой компромисс она вынуждает принять. Продолжайте читать: неизменяемость строк в Java.

Практика

Практика
Две переменные `String` `a` и `b` были присвоены один и тот же литерал `'hi'` где-то в программе. Третья, `c`, была создана с помощью `new String('hi')`. Какой результат гарантирован?
Две переменные `String` `a` и `b` были присвоены один и тот же литерал `'hi'` где-то в программе. Третья, `c`, была создана с помощью `new String('hi')`. Какой результат гарантирован?
Was this page helpful?