Странное производство мусора при создании экземпляров объектов в Java

Я профилирую поведение мусора java.lang.String, и похоже, что каждый раз, когда вы создаете экземпляр строки в первый раз внутри любого класса, всегда генерируется мусор. Кто-нибудь знает, почему?

public abstract class AbstractTest {

    protected static String SERGIO = "sergio";

    private String someValue;

    public void init() {
        this.someValue = new String(SERGIO);
    }
}

public class Test extends AbstractTest {

    private static String JULIA = "julia";

    private Runtime runtime = Runtime.getRuntime();
    private String anotherValue;
    private String yetAnother;

    private void gc() throws InterruptedException {
        System.gc();
        Thread.sleep(100);
    }

    private long usedMemory() {
        return runtime.maxMemory() - runtime.freeMemory();
    }

    public void test() throws Exception {
        gc();
        this.anotherValue = new String(SERGIO); // a bunch of garbage is created!
        long usedMemory = usedMemory();
        gc();
        long usedMemoryAfterGC = usedMemory();
        System.out.println("Collected: " + (usedMemory - usedMemoryAfterGC));
        gc();
        this.yetAnother = new String(JULIA); // no more garbage
        usedMemory = usedMemory();
        gc();
        usedMemoryAfterGC = usedMemory();
        System.out.println("Collected: " + (usedMemory - usedMemoryAfterGC));
    }

    public static void main(String[] args) throws Exception {
        Test t = new Test();
        t.test();
    }

Вывод:

Собрано: 704336
Собрано: 0

Хорошо. В первый раз он создает мусор, а последующие экземпляры не производят мусора.

Что странно, так это то, что когда вы принудительно создаете строку в суперклассе, он все равно создает мусор в подклассе при первом создании там экземпляра строки:

public void test() throws Exception {
    gc();
    init(); // creates a String in the superclass
    gc();
    this.yetAnother = new String(JULIA);
    long usedMemory = usedMemory();
    gc();
    long usedMemoryAfterGC = usedMemory();
    System.out.println("Collected: " + (usedMemory - usedMemoryAfterGC));
}

Вывод:

Собрано: 348648

Есть идеи, почему?

(Кстати, я запускаю это на MAC и JDK 1.6.0_37)

EDIT1: я немного изменил код, чтобы было ясно, что интернализация строк здесь не виновата, по крайней мере, так не выглядит.

EDIT2: Если вы замените String на Object во всем коде, вы получите тот же мусор, поэтому я предполагаю, что это связано с тем, как в Java происходит выделение объектов через new. При первом размещении объекта в классе вы получаете мусор. Второй раз нет. Странно, это для каждого класса.

EDIT3: я написал статья в блоге, где я рассказываю о том, как заставить сборщик мусора профилировать ваши приложения для создания мусора, как я делаю в приведенном выше коде.


person TraderJoeChicago    schedule 21.11.2012    source источник
comment
Возможно, стоит отметить, что вызов System.gc(); не гарантирует, что сборщик мусора запустится немедленно. Это похоже на эй, когда у тебя есть шанс, было бы неплохо, если бы ты сбежал   -  person user489041    schedule 22.11.2012
comment
Не понимаю, что вы имеете в виду под мусором - чего вы ожидали?   -  person Andy    schedule 22.11.2012
comment
Мусор = удаленные объекты в куче, которые в конечном итоге будут собраны сборщиком мусора. Если вы заметили, я не тот, кто производит мусор, но JVM делает это под капотом. Классы JDK часто делают это, но на этот раз это выглядит немного странно.   -  person TraderJoeChicago    schedule 22.11.2012
comment
@user489041 user489041 Он спит после System.gc(), поэтому есть большая вероятность, что GC запустится. И похоже, что это потому, что вещи собираются... Может быть, freeMemory() просто как-то сломалась?   -  person chrisapotek    schedule 22.11.2012
comment
Кстати, new String("julia") можно и нужно заменить на "julia".   -  person Tomasz Nurkiewicz    schedule 22.11.2012
comment
@TomaszNurkiewicz: Это приведет к тому, что он будет объединен в пул. Так что это имеет значение в отношении производительности. Но я не думаю, что это проблема в этом вопросе.   -  person Cratylus    schedule 22.11.2012


Ответы (2)


Литеральная строка внутри класса будет "интернирована" при первой ссылке (если не раньше). Интернирование обычно включает отказ от исходной версии String и использование интернированной версии, и в процессе может быть создано и удалено еще несколько объектов.

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

person Hot Licks    schedule 21.11.2012
comment
@Cratylus - Там определенно есть стажер. Согласно спецификации JVM, каждая статически определенная (литеральная) строка в классе должна быть интернирована, прежде чем ее можно будет использовать. А в случае строки static final, унаследованной от родительского класса, значение обрабатывается как литерал в дочернем классе — значение в родительском классе не упоминается после компиляции. Если вы сделаете javap в дочернем классе, вы увидите эти строки в буквальном пуле. - person Hot Licks; 22.11.2012
comment
@HotLicks Я сам не делаю никакой интернализации в линиях, которые измеряю, как вы можете видеть в моем коде. Однако я не знаю, выполняет ли new String(...) какую-либо внутреннюю интернализацию. Создаваемый мусор кажется чрезмерным для интернализированных строк. Вы также говорите, что пул строк не является глобальным? Какое отношение интернализированный строковый литерал имеет к классу, в котором он находится? Строковый литерал foo в классе A — это тот же объект строкового литерала foo в классе B, происходящий из того же пула строк. Нет? - person TraderJoeChicago; 22.11.2012
comment
Это делает не new String, а ссылка на строковый литерал, такой как julia. При первой ссылке на такой литерал он должен быть интернирован. (Интернирование — это, в некотором роде, процесс создания глобального пула строковых констант из отдельных строковых литералов в каждом классе. Вот почему литерал foo будет иметь один и тот же адрес, где бы вы на него ни ссылались.) - person Hot Licks; 22.11.2012
comment
Что ж, как вы можете видеть в моем (обновленном) коде, в линиях, которые я измеряю, ничего не усваивается. Поэтому не похоже, что интернализация или пул строк имеют какое-либо отношение к проблеме. :( - person TraderJoeChicago; 22.11.2012
comment
new String(SERGIO) ссылается на статическую окончательную строку sergio в AbstractTest. Поскольку строка статическая final, она копируется в Test (и вы можете увидеть ее там, в пуле констант, если вы javap Test). Перед вызовом new String необходимо сослаться на запись пула констант (используемую в качестве параметра), что приводит к интернированию sergio. - person Hot Licks; 22.11.2012
comment
(Подсказка: сделайте SERGIO не равным final, . Тогда Test будет ссылаться на AbstractTest, а не использовать собственный строковый литерал.) - person Hot Licks; 22.11.2012
comment
Спасибо за попытку помочь, но я внес предложенные вами изменения, но они не повлияли на результат. Если можете, скопируйте код и запустите его на своем компьютере, чтобы увидеть мусор. Я редактирую OP, чтобы удалить последнее ключевое слово. Мы до сих пор не смогли доказать, что проблема связана с пулом строк или интернализацией. :( - person TraderJoeChicago; 22.11.2012
comment
Ну, никак не может быть, чтобы что-то из этого могло составлять 348648 байт. Ваши измерения никоим образом не отражают объем кучи, пройденный во время интернирующих операций. Как мы сразу сказали, вы не можете измерить использование кучи так, как пытаетесь. - person Hot Licks; 22.11.2012
comment
Похоже, проблема связана с созданием объектов в целом, а не со строками в частности. См. EDIT2 в ОП. Спасибо! - person TraderJoeChicago; 22.11.2012
comment
Нет проблем. Сбор мусора не происходит до тех пор, пока не создадутся триггерные условия. System.gc() — это всего лишь предложение выполнить GC, и это не произойдет, если не будут соблюдены другие параметры. В вашем примере вам удалось настроить так, чтобы GC выполнялся в этот конкретный момент времени (или, скорее, чтобы GC завершался в этот конкретный момент времени — 100 мс, вероятно, недостаточно долго для завершения одного GC). - person Hot Licks; 22.11.2012
comment
Вопрос о том, должен был произойти GC или нет, не может быть и речи. Дело в том, что происходит GC и собирается мусор. Если бы сборка мусора не выполнялась, я бы увидел НОЛЬ как собранный мусор. Так что это не имеет ничего общего с исходным вопросом. :) Конечно это не проблема и не баг. Просто какая-то странная деталь, которую я хотел бы знать, как она работает под капотом. - person TraderJoeChicago; 22.11.2012
comment
ГК происходит. Возможно, давно этого не было. Так что собирает нетривиальное количество мусора. 350 КБ — это немного с точки зрения того, что необходимо для запуска JVM. - person Hot Licks; 22.11.2012

После прочтения ответа Питера здесь ясно, что виноват TLAB. Если вы используете опцию -XX:-UseTLAB для отключения TLAB, проблема исчезнет. Из того, что я понял из здесь, похоже, что с TLAB поток выделяет большой кусок памяти изначально, чтобы избежать условий гонки позже. Я смог доказать, что виновником был TLAB, установив для него больший размер с помощью -XX:TLABSize=64m и увидев, что эта сумма выделяется.

person TraderJoeChicago    schedule 23.11.2012