Строковые литералы, использующие вдвое больше ожидаемого объема постоянного пространства для генерации

Это Sun JDK 1.6u21, x64.

У меня есть класс для экспериментов с использованием perm gen, который содержит только одну большую строку (512k символов):

public class Big0 {
     public String bigString =
         "A string with 2^19 characters, should be 1 MB in size";
}

Я проверяю использование perm gen с помощью getUsage().toString() на объекте MemoryPoolMXBean для постоянного поколения (называемого «PS Perm Gen» в u21, хотя он имеет немного разные имена с разными версиями или с разными сборщиками мусора.

Когда я впервые ссылаюсь на класс, скажем, при чтении Big0.class, perm gen перескакивает на ~ 500 КБ - этого я ожидал, поскольку кодировка строки константного пула - UTF-8, а я использую только символы ASCII.

Однако, когда я действительно создаю экземпляр этого класса, perm gen подскакивает на ~ 2 МБ. Поскольку это строка в памяти размером 1 МБ (2 байта на символ UTF16, конечно, без суррогатов), я не понимаю, почему использование памяти удваивается.

Тот же эффект произойдет, если я сделаю строку статичной. Если бы я использовал final, он не компилируется, поскольку я превышаю предел для элементов постоянного пула в 65535 байт (не уверен, почему отключение final также позволяет избежать этого - считайте, что это бонусный вопрос).

Любое понимание ценится!

Изменить: я также должен указать, что это происходит с нестатическими, конечными нестатическими и статическими строками, но не с конечными статическими строками. Поскольку это уже лучшая практика для строковых констант, возможно, это представляет в основном академический интерес.


person BeeOnRope    schedule 21.02.2011    source источник
comment
что, если вы запускали system.gc несколько раз после создания экземпляра этого класса, чтобы очистить весь ненужный мусор из permgen, например, есть ли мимолетный временный след в permgen, который приводит нас к неверному заключению, что есть более высокая отдача.   -  person Ron    schedule 23.02.2011
comment
Я так и сделал, к сожалению, безрезультатно. Я также провел окончательный тест - заполнил germgen - приложение OOM заполнило им эти блоки размером 2,5 МБ, не восстанавливая их, поэтому мы можем в значительной степени предположить, что они не могут быть собраны в текущей реализации.   -  person BeeOnRope    schedule 23.02.2011
comment
Возможно ли, что при выполнении этого задания есть две копии строки? Один для литерала в кавычках (все строки неизменяемы) и один, хранящийся в bigString. Поскольку bigString имеет сильную ссылку на литерал, сборка мусора не уничтожает первую копию (ту, которая находится справа от знака равенства). Причина, по которой final и static работают, заключается в том, что компилятор создает фантомную ссылку. Для меня это низкоуровневый материал, поэтому я не решаюсь публиковать его в качестве ответа.   -  person Jason Sperske    schedule 23.02.2011


Ответы (4)


Думаю, это артефакт твоего тестового класса. Я создал аналогичный класс, а затем декомпилировал его с помощью javap.

Компилятор java [eclipse] разбивает литерал String на фрагменты, каждый не более 64 КБ. Байт-код для инициализации непостоянного поля состоит из объединения исходной строки с последовательностью операций StringBuilder. Хотя заключена именно эта последняя гигантская струна, большие атомы, из которых она состоит, занимают место в постоянном пуле.

person Ron    schedule 23.02.2011
comment
В этом есть чертовски большой смысл. Я также обнаружил, что 1 МБ из 2,5 МБ можно восстановить, если все экземпляры являются мусором (нестатический случай, как указано выше), и в этом случае я предполагаю, что это последняя строка, которая выпущена, чтобы сохранить это, но атомы остаются . - person BeeOnRope; 23.02.2011
comment
Бонусный вопрос: чем отличается статический финал? В этом случае использовалось только 1,5 МБ. Отбрасываются ли в этом случае куски - или метод совсем другой? - person BeeOnRope; 23.02.2011
comment
статический финал (и частный нестатический финал) позволяют компилятору java представлять строку исключительно как константу в пуле констант. Я использовал jmap -histo: live, чтобы измерить размер моего постоянного пула для каждого из тестовых примеров. YMMV IANAL FWIW. - person Ron; 24.02.2011

Символы Java имеют ширину 2 байта на символ (независимо от того, является ли он ASCII или кодовая точка выше 255). Я думаю, что то, что вы видите, - это виртуальная машина Java, переводящая версию строки из внутреннего хранилища файлов классов (измененная UTF8) во внутреннюю расширенную форму, как только класс инициализируется (что выполняется до создания экземпляра)

person Dirk    schedule 21.02.2011
comment
Конечно, я это объяснил. Мои строки состояли из 512 тыс. Символов, поэтому я ожидал, что они будут занимать 1 МБ в памяти (2 байта на символ). - person BeeOnRope; 21.02.2011
comment
Также обратите внимание, что этого не происходит при инициализации класса в моем примере выше. Если я обращаюсь к классу, но не создаю экземпляр, объем памяти никогда не превышает 500 КБ. Только когда я создаю экземпляр своего класса, он перескакивает еще на 2 МБ. - person BeeOnRope; 21.02.2011

Хороший профилировщик памяти (я лично использую и мне очень нравится java-профилировщик yourkit) должен показать вам, где используется память.

person jtahlborn    schedule 21.02.2011
comment
Мне тоже хочется так думать - пробовала МАТ, но информации о пергене нет. Фактически, они документируют, что информация об интернированных строках, к сожалению, недоступна даже из дампов. - person BeeOnRope; 22.02.2011

Хотя формат файла класса указывает модифицированный UTF-8 в качестве формата хранения String литералов, внутренний формат среды выполнения - UTF-16. String хранит свои данные как в кодировке UTF-16 в char[] (однако обычно это зависит от реализации). Большинство символов занимают в этой кодировке 2 байта (символы вне BMP занимают больше).

Я видел ссылки на модифицированный rt.jar, который содержит java.lang.String реализацию со специализированным кодовым путем / хранилищем для строк только для ASCII, что значительно сокращает требования к памяти.

Изменить: похоже, что эта опция нашла свое применение в обычной Oracle JRE с момента обновления 21 Java 6 в соответствии с этой ссылкой < / а>:

-XX: -XX: + UseCompressedStrings

Используйте byte [] для строк, которые могут быть представлены как чистый ASCII. (Представлено в Java 6 Update 21 Performance Release)

(Найдено через этот ответ).

person Joachim Sauer    schedule 21.02.2011
comment
Конечно, но посмотрите мои цифры выше: я хорошо осведомлен о хранении символов во время выполнения. Я ожидал, что строка символов размером 512 КБ займет 1 МБ, но на самом деле используется 2 МБ. - person BeeOnRope; 21.02.2011