Подтвердить что ты не робот

Производительность GC для внутреннего класса и статического вложенного класса

Я просто наткнулся на странный эффект и, отслеживая его, заметил, что, по-видимому, существует существенная разница в производительности для сбора внутренних и статических вложенных классов. Рассмотрим этот фрагмент кода:

public class Test {
    private class Pointer {
        long data;
        Pointer next;
    }
    private Pointer first;

    public static void main(String[] args) {
        Test t = null;
        for (int i = 0; i < 500; i++) {
            t = new Test();
            for (int j = 0; j < 1000000; j++) {
                Pointer p = t.new Pointer();
                p.data = i*j;
                p.next = t.first;
                t.first = p;
            }
        }
    }
}

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

При работе с ограниченным ограничением памяти (например, 100 МБ) этот код занимает около 20 минут для выполнения на моей машине. Теперь, просто заменив внутренний класс статическим вложенным классом, я могу сократить время выполнения до менее 6 минут. Вот изменения:

    private static class Pointer {

и

                Pointer p = new Pointer();

Теперь мои выводы из этого небольшого эксперимента заключаются в том, что использование внутренних классов значительно усложняет работу GC, если объекты могут быть собраны, делая статические вложенные классы более чем в 3 раза быстрее в этом случае.

Мой вопрос в том, правильно ли это заключение; если да, то в чем причина, и если нет, то почему здесь более низкие внутренние классы?

4b9b3361

Ответ 1

Я бы предположил, что это связано с двумя факторами. Первый, который вы уже коснулись. Второй использует нестатические внутренние классы, что приводит к большему использованию памяти. Почему ты спрашиваешь? Поскольку нестатические внутренние классы также имеют доступ к своим членам классов и методам классов, что означает, что вы выделяете экземпляр указателя, который в основном расширяет суперкласс. В случае нестатических внутренних классов вы не расширяете содержащий класс. Вот пример того, что я говорю о

Test.java(нестатический внутренний класс)

public class Test {
    private Pointer first;

    private class Pointer {
        public Pointer next;
        public Pointer() {
            next = null;
        }
    }

    public static void main(String[] args) {
        Test test = new Test();
        Pointer[] p = new Pointer[1000];
        for ( int i = 0; i < p.length; ++i ) {
            p[i] = test.new Pointer();
        }

        while (true) {
            try {Thread.sleep(100);}
            catch(Throwable t) {}
        }
    }
}

Test2.java(статический внутренний класс)

public class Test2 {
    private Pointer first;

    private static class Pointer {
        public Pointer next;
        public Pointer() {
            next = null;
        }
    }

    public static void main(String[] args) {
        Test test = new Test();
        Pointer[] p = new Pointer[1000];
        for ( int i = 0; i < p.length; ++i ) {
            p[i] = new Pointer();
        }

        while (true) {
            try {Thread.sleep(100);}
            catch(Throwable t) {}
        }
    }
}

Когда оба запускаются, вы можете видеть, что нестатические занимают больше места кучи, чем статические. В частности, использовалась нестатическая версия 2,279,624 B и статическая версия 10,485,760 1,800,000 B.

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

Ответ 2

Стоимость сбора мусора растет очень нелинейно, когда вы приближаетесь к максимальному размеру кучи (-Xmx), с почти бесконечным искусственным пределом, где JVM, наконец, сдаётся и выбрасывает OutOfMemoryError. В этом конкретном случае вы видите, что крутая часть этой кривой находится между внутренним классом, являющимся статическим или нестатическим. Нестатический внутренний класс на самом деле не является причиной, отличной от использования большего объема памяти и наличия большего количества ссылок. Я видел, что многие другие изменения кода "вызывают" хэп-трекинг, где они просто оказались несчастным соком, который толкнул его по краю, и предел кучи должен быть просто установлен выше. Это нелинейное поведение обычно не следует рассматривать как проблему с кодом, присущим JVM.

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