Що таке інтернування Java String?


234

Що таке String Interning на Java, коли я повинен його використовувати і чому ?



2
якщо String a = new String("abc"); String b = new String("abc"); тодіa.intern() == b.intern()
Асанка Сірівардена

Приклад замовлення рядків Приклад інтернування: algs4.cs.princeton.edu/12oop/MutableString.java.html
Ronak Poriya

Чи String.intern()залежить від того ClassLoader, чи створюють різні завантажувачі класів "різні" String, викликаючи різні interns?
АлікЕльзін-кілака

1
@ AlikElzin-kilaka ні, завантажувачі класів абсолютно не мають значення для строкового інтернування. Наступного разу, коли у вас виникне запитання, будь ласка, відкрийте нове питання, а не публікувати його як коментар до іншого питання.
Холгер

Відповіді:


233

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

В основному, String.intern () для ряду рядків гарантуватиме, що всі рядки, що мають однаковий вміст, мають однакову пам'ять. Отже, якщо у вас є список імен, де "john" з'являється 1000 разів, стажуючись, ви гарантуєте, що лише одному "john" є фактично виділена пам'ять.

Це може бути корисно для зменшення потреби в пам'яті вашої програми. Але майте на увазі, що кеш підтримується JVM у постійному пулі пам’яті, який зазвичай обмежений за розміром порівняно з купою, тому вам не слід використовувати інтерна, якщо у вас не надто багато повторюваних значень.


Детальніше про обмеження пам'яті використання intern ()

З одного боку, це правда, що ви можете видалити String дублікати, інтерналізуючи їх. Проблема полягає в тому, що інтерналізовані рядки переходять до Постійної генерації, яка є областю JVM, яка зарезервована для некористувацьких об'єктів, таких як Класи, Методи та інші внутрішні об'єкти JVM. Розмір цієї площі обмежений і зазвичай набагато менший, ніж купа. Виклик інтерна () на 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 Особливості та удосконалення

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


1
"Але майте на увазі, що кеш підтримується JVM в постійному пулі пам'яті, який зазвичай обмежений за розміром ......" Чи можете ви пояснити це? Я не зрозумів
саджанецьPro

2
"інтерновані" рядки зберігаються в спеціальній області пам'яті в JVM. Ця область пам'яті зазвичай має фіксований розмір і не є частиною звичайної Java Heap, де зберігаються інші дані. Через фіксований розмір може статися, що ця постійна область пам'яті заповнюється всіма вашими рядками, що призводить до некрасивих проблем (класи не можна завантажувати та інші речі).
віолончель

@cello так, це схоже на кешування?
саджанецьПро

8
@grassPro: Так, це своєрідне кешування, таке, яке надається JVM. Як зауважимо, через злиття JVM Sun / Oracle та JRockit інженери JVM намагаються позбутися області постійної пам'яті в JDK 8 ( openjdk.java.net/jeps/122 ), тому не буде будь-яке обмеження розміру в майбутньому.
віолончель

9
Програмісти також повинні знати, що строкове інтернування може мати наслідки для безпеки. Якщо у вас є чутливий текст, такий як паролі, як рядки в пам'яті, він може залишатися в пам'яті дуже довго, навіть якщо фактичні об'єкти рядка вже давно були GC'd. Це може бути клопітно, якщо погані хлопці якимось чином отримують доступ до дампа пам’яті. Ця проблема існує навіть без інтернування (оскільки GC не детерміновано починати з тощо), але це робить дещо гірше. Завжди корисно використовувати char[]замість Stringчутливого тексту та нульове, як тільки це більше не потрібно.
chris

71

Є декілька запитань щодо "привабливого інтерв'ю", наприклад, чому ви отримуєте рівні! якщо ви виконаєте наведений нижче код.

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

Якщо ви хочете порівняти рядки, які ви повинні використовувати equals(). Вищенаведене буде друкувати дорівнює, тому що компілятор testStringвже інтернований для вас. Ви можете інтернувати рядки самостійно, використовуючи метод стажування, як показано в попередніх відповідях ....


5
Ваш приклад хитрий, тому що це призведе до того ж друку, навіть якщо ви використовуєте equalsметод. Ви можете додати new String()порівняння, щоб більш чітко показати відмінність.
giannis christofakis

@giannischristofakis, але якби ми використовували нову String (), чи не буде помилка ==? Ява автоматично також інтерналізує нові струни?
Діпак Сельвакумар

@giannischristofakis, звичайно, якщо ви будете використовувати новий String (), він не вдасться на ==. але новий String (...). intern () не вийде з ладу ==, тому що інтерн поверне той самий рядок. Простий компілятор припустімо, що він робить новий String (). Стажер у літературі
maslan

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; посилання на цей екземпляр класу є результатом рядкового буквеного виведення. Нарешті, використовується інтерн-метод нового екземпляра String.

Байт-код

Давайте декомпілюємо деякий байт-код 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аргументом)
  • 35: aі cпорівнюються із звичайними об'єктамиif_acmpne

Представлення постійних рядків є досить магічним у байт-коді:

  • вона має виділену CONSTANT_String_info структуру, на відміну від звичайних об'єктів (наприклад new String)
  • структура вказує на структуру CONSTANT_Utf8_info, яка містить дані. Це єдині необхідні дані для представлення рядка.

а цитата JVMS вище, схоже, говорить про те, що всякий раз, коли вказано на Utf8, є однаковим, тоді завантажуються однакові екземпляри ldc.

Я зробив подібні тести для полів, і:

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

Висновок : існує пряма підтримка байт-коду для пулу рядків, і представлення пам'яті є ефективним.

Бонус: порівняйте це з пулом Integer , який не підтримує прямий байт-код (тобто немає CONSTANT_String_infoаналога).


19

Оновлення для Java 8 або плюс . У Java 8 простір PermGen (Постійне покоління) видаляється та замінюється мета-пробілом. Пам'ять пулу String переміщується до купи JVM.

Порівняно з Java 7, розмір пулу String збільшується в купі. Отже, у вас є більше місця для інтерналізованих рядків, але у вас менше пам'яті для всієї програми.

Ще одне, що ви вже знали, що при порівнянні 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' для порівняння двох об'єктів String. І ось як intern()корисно.


2

Струнне інтернування - це техніка оптимізації компілятором. Якщо у вас є два однакові рядкові літерали в одному блоці компіляції, то створений код гарантує, що існує лише один об'єкт рядка, створений для всього екземпляра цього літералу (символи, укладені в подвійні лапки) в складі.

Я з C # background, тому я можу пояснити, наводячи приклад із цього:

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

У книзі програміста Дешмуха OCP Java SE 11 я знайшов найпростіше пояснення для Interning, яке було наступним чином: Оскільки рядки є об'єктами і оскільки всі об'єкти на Java завжди зберігаються лише в купі простору, всі рядки зберігаються в купі простору. Однак Java зберігає рядки, створені без використання нового ключового слова, в спеціальній області простору купи, яка називається "рядок рядків". Java зберігає рядки, створені за допомогою нового ключового слова, у звичайному просторі купи.

Метою пулу рядків є підтримка набору унікальних рядків. Щоразу, коли ви створюєте новий рядок, не використовуючи нове ключове слово, Java перевіряє, чи є одна і та ж рядок у пулі рядків. Якщо це так, Java повертає посилання на той самий об’єкт String, а якщо ні, Java створює новий об'єкт String в пулі рядків і повертає його посилання. Так, наприклад, якщо ви використовуєте рядок "привіт" двічі у своєму коді, як показано нижче, ви отримаєте посилання на ту саму рядок. Насправді ми можемо перевірити цю теорію, порівнявши дві різні опорні змінні за допомогою оператора ==, як показано в наступному коді:

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 повертає істину, але str1 == str3 і str3 == str4 повертають помилково. Насправді, коли ви робите новий String ("привіт"); два об'єкти String створюються замість лише одного, якщо це вперше рядок "привіт" використовується в будь-якій точці програми - один у ряді рядків через використання рядка, що цитується, і один у звичайному просторі купи, оскільки використання нового ключового слова.

Об'єднання рядків - це спосіб збереження пам'яті програми Java, уникаючи створення декількох об'єктів String, що містять однакове значення. Можна отримати рядок із пулу рядків для рядка, створеного за допомогою нового ключового слова, за допомогою методу інтернації String. Він називається "інтернування" струнних об'єктів. Наприклад,

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.