内部类与静态嵌套类的 GC 性能影响

2022-09-01 19:16:17

我刚刚遇到了一个奇怪的效果,在跟踪它时,我注意到收集内部与静态嵌套类似乎存在很大的性能差异。请考虑以下代码片段:

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 MB)运行时,此代码大约需要 20 分钟才能在我的计算机上执行。现在,只需将内部类替换为静态嵌套类,我就可以将运行时减少到 6 分钟以内。以下是更改:

    private static class Pointer {

                Pointer p = new Pointer();

现在,我从这个小实验中得出的结论是,使用内部类会使GC更难确定是否可以收集对象,在这种情况下,静态嵌套类的速度提高了3倍以上。

我的问题是,这个结论是否正确;如果是,原因是什么,如果不是,为什么内部类在这里要慢得多?


答案 1

我想这是由于2个因素。你已经谈到了第一个问题。第二种是使用非静态内部类会导致更多的内存使用。你为什么问?因为非静态内部类还可以访问其包含类的数据成员和方法,这意味着您正在分配一个基本上扩展超类的 Pointer 实例。对于非静态内部类,您不会扩展包含类。以下是我正在谈论的一个例子

测试.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。在这种特殊情况下,您会看到该曲线的陡峭部分介于内部类是静态的还是非静态的之间。非静态内部类并不是真正的原因,除了使用更多的内存和更多的链接。我见过许多其他代码更改“导致”GC崩溃,它们恰好是将其推过边缘的倒霉汁液,堆限制应该简单地设置得更高。这种非线性行为通常不应被视为代码的问题 - 它是JVM固有的。

当然,另一方面,膨胀就是膨胀。在当前情况下,一个好习惯是“默认”使内部类成为静态的,除非对外部实例的访问是有用的。