Статьи

Внутренняя память строки

Эта статья основана на моем ответе на StackOverflow . Я пытаюсь объяснить, как класс String хранит тексты, как работает интернирование и постоянный пул.

Главное, что нужно понять, это различие между Java-объектом String и его содержимым — char[] в поле private value . String — это обертка вокруг массива char[] , инкапсулирующая его и делающая невозможным изменение, так что String может оставаться неизменным. Также класс String запоминает, какие части этого массива фактически используются (см. Ниже). Все это означает, что у вас может быть два разных объекта String (достаточно легких), указывающих на один и тот же char[] .

Я покажу вам несколько примеров, вместе с hashCode() каждого String и hashCode() внутреннего поля char[] value (я назову его text, чтобы отличить его от string). Наконец, я покажу javap -c -verbose вместе с константным пулом для моего тестового класса. Пожалуйста, не путайте пул констант класса с пулом строковых литералов. Они не совсем одинаковые. Смотрите также Понимание вывода javap для константного пула .

Предпосылки

Для тестирования я создал такой служебный метод, который нарушает инкапсуляцию String :

1
2
3
4
5
private int showInternalCharArrayHashCode(String s) {
    final Field value = String.class.getDeclaredField("value");
    value.setAccessible(true);
    return value.get(s).hashCode();
}

Он напечатает hashCode() со значением char[] value , эффективно помогая нам понять, указывает ли эта конкретная String на один и тот же текст char[] или нет.

Два строковых литерала в классе

Давайте начнем с самого простого примера.

Java-код

1
2
String one = "abc";
String two = "abc";

Кстати, если вы просто напишите "ab" + "c" , компилятор Java выполнит конкатенацию во время компиляции, и сгенерированный код будет точно таким же. Это работает, только если все строки известны во время компиляции.

Класс постоянный пул
Каждый класс имеет свой собственный пул констант — список постоянных значений, которые можно использовать повторно, если они встречаются в исходном коде несколько раз. Включает общие строки, числа, имена методов и т. Д.
Вот содержимое пула констант в нашем примере выше:

1
2
3
const #2 = String   #38;    //  abc
//...
const #38 = Asciz   abc;

Важно отметить различие между константным объектом String ( #2 ) и текстом в кодировке Unicode "abc" ( #38 ), на который указывает строка.

Байт код
Здесь генерируется байт-код. Обратите внимание, что и one и two ссылки назначаются с одинаковой константой #2 указывающей на строку "abc" :

1
2
3
4
ldc #2; //String abc
astore_1    //one
ldc #2; //String abc
astore_2    //two

Выход
Для каждого примера я печатаю следующие значения:

1
2
3
4
System.out.println("one.value: " + showInternalCharArrayHashCode(one));
System.out.println("two.value: " + showInternalCharArrayHashCode(two));
System.out.println("one" + System.identityHashCode(one));
System.out.println("two" + System.identityHashCode(two));

Не удивительно, что обе пары равны:

1
2
3
4
one.value: 23583040
two.value: 23583040
one: 8918249
two: 8918249

Это означает, что не только оба объекта указывают на один и тот же char[] (один и тот же текст внизу), поэтому тест equals() пройдет. Но даже больше, one и two — это one и те же ссылки! Так что one == two тоже верно. Очевидно, что если one и two указывают на one и тот же объект, то one.value и two.value должны быть равны.

Буквально и new String()  

Java-код
Теперь пример, который мы все ждали — один строковый литерал и одна новая String использующие один и тот же литерал. Как это будет работать?

1
2
String one = "abc";
String two = new String("abc");

Тот факт, что константа "abc" используется в исходном коде два раза, должен дать вам некоторую подсказку…

Класс постоянный пул То же, что и выше.

Байт код

1
2
3
4
5
6
7
8
ldc #2; //String abc
astore_1    //one
 
new #3; //class java/lang/String
dup
ldc #2; //String abc
invokespecial   #4; //Method java/lang/String."<init>":(Ljava/lang/String;)V
astore_2    //two

Смотри внимательно! Первый объект создан так же, как и выше, что неудивительно. Он просто принимает постоянную ссылку на уже созданную String ( #2 ) из пула констант. Однако второй объект создается с помощью обычного вызова конструктора. Но! Первая String передается в качестве аргумента. Это можно декомпилировать в:

1
String two = new String(one);

Выход
Вывод немного удивителен. Вторая пара, представляющая ссылки на объект String понятна — мы создали два объекта String — один был создан для нас в пуле констант, а второй — вручную для two . Но почему первая пара предполагает, что оба объекта String указывают на один и тот же массив char[] value ?!

1
2
3
4
one.value: 41771
two.value: 41771
one: 8388097
two: 16585653

Это становится понятным, когда вы посмотрите на то, как работает конструктор String(String) (здесь это сильно упрощено):

1
2
3
4
5
public String(String original) {
    this.offset = original.offset;
    this.count = original.count;
    this.value = original.value;
}

Увидеть? Когда вы создаете новый объект String на основе существующего, он повторно использует значение char[] value . String s являются неизменяемыми, нет необходимости копировать структуру данных, о которой известно, что она никогда не изменялась. Более того, поскольку new String(someString) создает точную копию существующей строки, и строки являются неизменяемыми, очевидно, что нет причин одновременно существовать двум.
Я думаю, что это подсказка некоторых недоразумений: даже если у вас есть два объекта String , они все равно могут указывать на одно и то же содержимое. И, как вы можете видеть, сам объект String довольно мал.

Модификация времени выполнения и intern()  

Java-код
Допустим, вы изначально использовали две разные строки, но после некоторых модификаций они все одинаковые:

1
2
String one = "abc";
String two = "?abc".substring(1);  //also two = "abc"

Компилятор Java (по крайней мере, мой) не достаточно умен, чтобы выполнить такую ​​операцию во время компиляции, посмотрите:

Класс постоянный пул
Внезапно мы получили две постоянные строки, указывающие на два разных постоянных текста:

1
2
3
4
const #2 = String   #44;    //  abc
const #3 = String   #45;    //  ?abc
const #44 = Asciz   abc;
const #45 = Asciz   ?abc;

Байт код

1
2
3
4
5
6
7
ldc #2; //String abc
astore_1    //one
 
ldc #3; //String ?abc
iconst_1
invokevirtual   #4; //Method String.substring:(I)Ljava/lang/String;
astore_2    //two

Первая строка построена как обычно. Второе создается сначала загрузкой константы "?abc" а затем вызывая ее на substring(1) .

Выход

Здесь нет ничего удивительного — у нас есть две разные строки, указывающие на два разных текста char[] в памяти:

1
2
3
4
one.value: 27379847
two.value: 7615385
one: 8388097
two: 16585653

Ну, тексты на самом деле не отличаются , метод equals() все равно даст true . У нас есть две ненужные копии одного и того же текста.
Теперь нам нужно выполнить два упражнения. Сначала попробуйте запустить:

1
two = two.intern();

перед печатью хеш-кодов. Не только one и two указывают на one и тот же текст, но они являются одной и той же ссылкой!

1
2
3
4
one.value: 11108810
two.value: 11108810
one: 15184449
two: 15184449

Это означает, что оба one.equals(two) и one == two теста пройдут. Также мы сохранили немного памяти, потому что текст "abc" появляется в памяти только один раз (вторая копия будет собираться мусором).
Второе упражнение немного отличается, проверьте это:

1
2
String one = "abc";
String two = "abc".substring(1);

Очевидно, что one и two — это два разных объекта, указывающих на два разных текста. Но почему результат показывает, что они оба указывают на один и тот же массив char[] ?!?

1
2
3
4
one.value: 11108810
two.value: 8918249
one: 23583040
two: 23583040

Я оставлю вам ответ. Он научит вас, как работает substring() , каковы преимущества такого подхода и когда это может привести к большим неприятностям .

Уроки выучены

  • Сам String объект довольно дешев. Это текст, на который он указывает, который занимает большую часть памяти
  • String — это просто обертка вокруг char[] для сохранения неизменности
  • new String("abc") самом деле не так дорого, так как внутреннее текстовое представление используется повторно. Но все же избегайте такой конструкции.
  • Когда String объединяется из постоянных значений, известных во время компиляции, конкатенация выполняется компилятором, а не JVM
  • substring() сложно, но самое главное, это очень дешево, как с точки зрения используемой памяти, так и времени выполнения (постоянная в обоих случаях)

Справка: внутренняя память в виде строк от нашего партнера по JCG Томаша Нуркевича в блоге Java и соседях .