Что такое интернирование Java String?

234

Что такое String Interning в Java, когда я должен его использовать и почему ?

saplingPro
источник
2
если String a = new String("abc"); String b = new String("abc"); тогдаa.intern() == b.intern()
Асанка Сиривардена
Зависит String.intern()от того ClassLoader, что означает, что разные загрузчики классов создают "разные"String , вызывая разные intern?
Алик Эльзин-килака
1
@ AlikElzin-kilaka нет, загрузчики классов не имеют никакого отношения к интернированию строк. В следующий раз, когда у вас возникнет вопрос, пожалуйста, откройте новый вопрос а не размещайте его как комментарий к другому вопросу.
Хольгер

Ответы:

233

http://docs.oracle.com/javase/7/docs/api/java/lang/String.html#intern ()

По сути, выполнение String.intern () для ряда строк гарантирует, что все строки, имеющие одинаковое содержимое, совместно используют одну и ту же память. Таким образом, если у вас есть список имен, где «Джон» появляется 1000 раз, при интернировании вы гарантируете, что только один «Джон» фактически выделен памяти.

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


Подробнее об ограничениях памяти при использовании intern ()

С одной стороны, это правда, что вы можете удалить дубликаты строк, усвоив их. Проблема заключается в том, что интернализованные строки отправляются в постоянную генерацию, которая является областью JVM, зарезервированной для не пользовательских объектов, таких как классы, методы и другие внутренние объекты JVM. Размер этой области ограничен и обычно намного меньше, чем куча. Вызов intern () для String приводит к его перемещению из кучи в постоянное поколение, и вы рискуете исчерпать пространство PermGen.

- От: http://www.codeinstructions.com/2009/01/busting-javalangstringintern-myths.html


С JDK 7 (я имею в виду в HotSpot) что-то изменилось.

В JDK 7 интернированные строки больше не выделяются в постоянном поколении кучи Java, а вместо этого выделяются в основной части кучи Java (известной как молодое и старое поколения) вместе с другими объектами, созданными приложением , Это изменение приведет к увеличению объема данных, находящихся в основной куче Java, и уменьшению объема данных в постоянной генерации, что может потребовать корректировки размеров кучи. Большинство приложений увидят только относительно небольшие различия в использовании кучи из-за этого изменения, но более крупные приложения, которые загружают много классов или интенсивно используют метод String.intern (), увидят более существенные различия.

-- Из Java SE 7 Возможности и улучшения

Обновление: Interned строки хранятся в основной куче начиная с Java 7 и далее. http://www.oracle.com/technetwork/java/javase/jdk7-relnotes-418459.html#jdk7changes

Ashwinee K Jha
источник
1
«Но имейте в виду, что JVM поддерживает кэш в постоянном пуле памяти, размер которого обычно ограничен…». Вы можете это объяснить? Я не понял
saplingPro
2
«интернированные» строки хранятся в специальной области памяти в JVM. Эта область памяти обычно имеет фиксированный размер и не является частью обычной кучи Java, где хранятся другие данные. Из-за фиксированного размера может случиться так, что эта область постоянной памяти заполняется всеми вашими строками, что приводит к уродливым проблемам (классы не могут быть загружены и другие вещи).
виолончель
@cello так, это похоже на кеширование?
saplingPro
8
@grassPro: Да, это своего рода кеширование, изначально предоставляемое JVM. Как примечание, из-за слияния Sun / Oracle JVM и JRockit инженеры JVM пытаются избавиться от области постоянной памяти в JDK 8 ( openjdk.java.net/jeps/122 ), поэтому не будет любое ограничение размера в будущем.
Виолончель
9
Программисты также должны знать, что интернирование строк может иметь последствия для безопасности. Если у вас есть чувствительный текст, такой как пароли, в виде строк в памяти, он может оставаться в памяти очень долго, даже если фактические строковые объекты уже давно GC'd. Это может быть неприятно, если плохие парни каким-то образом получат доступ к дампу памяти. Эта проблема существует даже без интернирования (поскольку GC является недетерминированной для начала и т. Д.), Но делает ее несколько хуже. Это всегда хорошая идея, чтобы использовать char[]вместо Stringчувствительного текста и обнулить его, как только он больше не нужен.
Крис
71

Есть несколько «броских интервью» вопросов, например, почему вы получаете равных! если вы выполните приведенный ниже фрагмент кода.

String s1 = "testString";
String s2 = "testString";
if(s1 == s2) System.out.println("equals!");

Если вы хотите сравнить строки, вы должны использовать equals(). Выше будет печатать равно, потому что компилятор для вас testStringуже интернирован . Вы можете интернировать строки самостоятельно, используя метод intern, как показано в предыдущих ответах ....

Maslan
источник
5
Ваш пример хитрый, потому что он приведет к тому же отпечатку, даже если вы используете equalsметод. Возможно, вы захотите добавить new String()сравнение, чтобы показать разницу более четко.
Джаннис Кристофакис
@giannischristofakis, но если мы будем использовать новую String (), не будет ли == неудачной? Java также автоматически усваивает новые строки?
Дипак Сельвакумар
@giannischristofakis, конечно, если вы используете новую String (), она не будет работать ==. но новая String (...). intern () не потерпит неудачу на ==, потому что intern вернет ту же строку. Просто предположим, что компилятор выполняет новую
функцию
42

JLS

JLS 7 3.10.5 определяет его и дает практический пример:

Более того, строковый литерал всегда ссылается на один и тот же экземпляр класса String. Это связано с тем, что строковые литералы - или, в более общем смысле, строки, являющиеся значениями константных выражений (§15.28) - «интернированы», чтобы обмениваться уникальными экземплярами, используя метод String.intern.

Пример 3.10.5-1. Строковые литералы

Программа, состоящая из модуля компиляции (§7.3):

package testPackage;
class Test {
    public static void main(String[] args) {
        String hello = "Hello", lo = "lo";
        System.out.print((hello == "Hello") + " ");
        System.out.print((Other.hello == hello) + " ");
        System.out.print((other.Other.hello == hello) + " ");
        System.out.print((hello == ("Hel"+"lo")) + " ");
        System.out.print((hello == ("Hel"+lo)) + " ");
        System.out.println(hello == ("Hel"+lo).intern());
    }
}
class Other { static String hello = "Hello"; }

и блок компиляции:

package other;
public class Other { public static String hello = "Hello"; }

производит вывод:

true true true true false true

JVMs

JVMS 7 5.1 говорит, что интернирование реализовано магически и эффективно с выделенной CONSTANT_String_infoструктурой (в отличие от большинства других объектов, которые имеют более общие представления):

Строковый литерал является ссылкой на экземпляр класса String и является производным от структуры CONSTANT_String_info (§4.4.3) в двоичном представлении класса или интерфейса. Структура CONSTANT_String_info дает последовательность кодовых точек Unicode, составляющих строковый литерал.

Язык программирования Java требует, чтобы идентичные строковые литералы (то есть литералы, которые содержат одинаковую последовательность кодовых точек) должны ссылаться на один и тот же экземпляр класса String (JLS §3.10.5). Кроме того, если метод String.intern вызывается для какой-либо строки, результатом является ссылка на тот же экземпляр класса, который будет возвращен, если эта строка появится в виде литерала. Таким образом, следующее выражение должно иметь значение true:

("a" + "b" + "c").intern() == "abc"

Для получения строкового литерала виртуальная машина Java проверяет последовательность кодовых точек, заданных структурой CONSTANT_String_info.

  • Если метод String.intern ранее вызывался для экземпляра класса String, содержащего последовательность кодовых точек Unicode, идентичную той, которая задана структурой CONSTANT_String_info, то результатом строкового литерала является ссылка на тот же экземпляр класса String.

  • В противном случае создается новый экземпляр класса String, содержащий последовательность кодовых точек Unicode, заданную структурой CONSTANT_String_info; ссылка на этот экземпляр класса является результатом строкового литерала. Наконец, метод intern нового экземпляра String вызывается.

Bytecode

Давайте декомпилируем некоторый байт-код OpenJDK 7, чтобы увидеть интернирование в действии.

Если мы декомпилируем:

public class StringPool {
    public static void main(String[] args) {
        String a = "abc";
        String b = "abc";
        String c = new String("abc");
        System.out.println(a);
        System.out.println(b);
        System.out.println(a == c);
    }
}

у нас на постоянном пуле:

#2 = String             #32   // abc
[...]
#32 = Utf8               abc

и main:

 0: ldc           #2          // String abc
 2: astore_1
 3: ldc           #2          // String abc
 5: astore_2
 6: new           #3          // class java/lang/String
 9: dup
10: ldc           #2          // String abc
12: invokespecial #4          // Method java/lang/String."<init>":(Ljava/lang/String;)V
15: astore_3
16: getstatic     #5          // Field java/lang/System.out:Ljava/io/PrintStream;
19: aload_1
20: invokevirtual #6          // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: getstatic     #5          // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_2
27: invokevirtual #6          // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: getstatic     #5          // Field java/lang/System.out:Ljava/io/PrintStream;
33: aload_1
34: aload_3
35: if_acmpne     42
38: iconst_1
39: goto          43
42: iconst_0
43: invokevirtual #7          // Method java/io/PrintStream.println:(Z)V

Обратите внимание, как:

  • 0и 3: ldc #2загружается одна и та же константа (литералы)
  • 12: создается новый экземпляр строки (с #2аргументом as)
  • 35: aи cсравниваются как обычные объекты сif_acmpne

Представление константных строк довольно волшебно в байт-коде:

  • у него есть специальная структура CONSTANT_String_info , в отличие от обычных объектов (например new String)
  • структура указывает на структуру CONSTANT_Utf8_info, которая содержит данные. Это единственные необходимые данные для представления строки.

и приведенная выше цитата JVMS, кажется, говорит, что всякий раз, когда Utf8, на который указывают, является тем же самым, тогда идентичные экземпляры загружаются ldc.

Я сделал аналогичные тесты для полей, и:

  • static final String s = "abc"указывает на таблицу констант через атрибут ConstantValue
  • не финальные поля не имеют этого атрибута, но все еще могут быть инициализированы с ldc

Вывод : есть прямая поддержка байт-кода для пула строк, и представление памяти эффективно.

Бонус: сравните это с целочисленным пулом , который не имеет прямой поддержки байт-кода (т.е. не имеет CONSTANT_String_infoаналогов).

Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
19

Обновление для Java 8 или плюс . В Java 8 пространство PermGen (Permanent Generation) удаляется и заменяется метапространством. Память пула строк перемещается в кучу JVM.

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

Еще одна вещь, вы уже знаете, что при сравнении 2 (ссылок) объектов в Java, « ==» используется для сравнения ссылок объекта, « equals» используется для сравнения содержимого объекта.

Давайте проверим этот код:

String value1 = "70";
String value2 = "70";
String value3 = new Integer(70).toString();

Результат:

value1 == value2 ---> правда

value1 == value3 ---> ложь

value1.equals(value3) ---> правда

value1 == value3.intern() ---> правда

Вот почему вы должны использовать ' equals', чтобы сравнить 2 объекта String. И вот как intern()это полезно.

nguyentt
источник
2

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

Я из C # фона, поэтому я могу объяснить, приведя пример из этого:

object obj = "Int32";
string str1 = "Int32";
string str2 = typeof(int).Name;

вывод следующих сравнений:

Console.WriteLine(obj == str1); // true
Console.WriteLine(str1 == str2); // true    
Console.WriteLine(obj == str2); // false !?

Примечание 1 : объекты сравниваются по ссылке.

Примечание 2 : typeof (int). Имя оценивается методом отражения, поэтому оно не оценивается во время компиляции. Вот эти сравнения сделаны во время компиляции.

Анализ результатов: 1) истина, потому что они оба содержат одинаковые литералы, и поэтому сгенерированный код будет иметь только один объект, ссылающийся на «Int32». См Примечание 1 .

2) истина, потому что проверяется содержимое обоих значений, что является одинаковым.

3) ЛОЖЬ, потому что str2 и obj не имеют одинаковые литералы. Смотрите примечание 2 .

Робин Гупта
источник
3
Это сильнее, чем это. Любой строковый литерал, загруженный одним и тем же загрузчиком классов, будет ссылаться на одну и ту же строку. Смотрите спецификации JLS и JVM.
маркиз Лорн
1
@ user207421 на самом деле, даже не имеет значения, к какому загрузчику классов принадлежит строковый литерал.
Хольгер
1
Java interning() method basically makes sure that if String object is present in SCP, If yes then it returns that object and if not then creates that objects in SCP and return its references

for eg: String s1=new String("abc");
        String s2="abc";
        String s3="abc";

s1==s2// false, because 1 object of s1 is stored in heap and other in scp(but this objects doesn't have explicit reference) and s2 in scp
s2==s3// true

now if we do intern on s1
s1=s1.intern() 

//JVM checks if there is any string in the pool with value “abc” is present? Since there is a string object in the pool with value “abc”, its reference is returned.
Notice that we are calling s1 = s1.intern(), so the s1 is now referring to the string pool object having value abc”.
At this point, all the three string objects are referring to the same object in the string pool. Hence s1==s2 is returning true now.
Рохан Кширсагар
источник
0

Из книги Deshmukh программиста OCP Java SE 11 я нашел простейшее объяснение Interning, которое заключалось в следующем: поскольку строки являются объектами и поскольку все объекты в Java всегда хранятся только в пространстве кучи, все строки хранятся в пространстве кучи. Однако Java хранит строки, созданные без использования ключевого слова new, в специальной области пространства кучи, которая называется «пул строк». Java сохраняет строки, созданные с использованием нового ключевого слова, в обычном пространстве кучи.

Назначение пула строк - поддерживать набор уникальных строк. Каждый раз, когда вы создаете новую строку без использования ключевого слова new, Java проверяет, существует ли такая же строка в пуле строк. Если это так, Java возвращает ссылку на тот же объект String, а если нет, Java создает новый объект String в пуле строк и возвращает его ссылку. Так, например, если вы дважды используете строку «hello» в своем коде, как показано ниже, вы получите ссылку на одну и ту же строку. На самом деле мы можем проверить эту теорию, сравнив две разные ссылочные переменные с помощью оператора ==, как показано в следующем коде:

String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2); //prints true

String str3 = new String("hello");
String str4 = new String("hello");

System.out.println(str1 == str3); //prints false
System.out.println(str3 == str4); //prints false 

Оператор == просто проверяет, указывают ли две ссылки на один и тот же объект, и возвращает true, если они это делают. В приведенном выше коде str2 получает ссылку на тот же объект String, который был создан ранее. Однако str3 и str4 получают ссылки на два совершенно разных объекта String. Вот почему str1 == str2 возвращает true, а str1 == str3 и str3 == str4 возвращают false. На самом деле, когда вы делаете новую строку ("привет"); два объекта String создаются вместо одного, если это первый раз, когда строка «hello» используется в любом месте программы - один в пуле строк из-за использования строки в кавычках и один в обычном пространстве кучи, потому что использования нового ключевого слова.

Объединение строк - это способ сохранения программной памяти в Java, избегая создания нескольких объектов String, содержащих одно и то же значение. Можно получить строку из пула строк для строки, созданной с помощью ключевого слова new, с помощью метода intern строки. Это называется «интернирование» строковых объектов. Например,

String str1 = "hello";
String str2 = new String("hello");
String str3 = str2.intern(); //get an interned string obj

System.out.println(str1 == str2); //prints false
System.out.println(str1 == str3); //prints true
Хамза
источник