大量内存出血,导致堆大小在大约8秒内从约64mb变为1.5gb。垃圾回收器有问题?测试 2内存池分析:

2022-09-01 10:01:52

问题出在:

the memory usage balloons out of control

如您所见,内存使用量激增失控!我不得不向JVM添加参数以增加堆大小,以避免在我弄清楚发生了什么时出现内存不足错误。不好!

基本应用程序摘要(用于上下文)

此应用程序(最终)将用于基本的屏幕CV和模板匹配类型的东西,以实现自动化目的。我想实现尽可能高的帧速率来观看屏幕,并通过一系列单独的消费者线程处理所有处理。

我很快发现,库存机器人类在速度方面确实很糟糕,所以我打开了源代码,取出了所有重复的精力和浪费的开销,并将其重建为我自己的类,称为FastRobot。

类代码:

public class FastRobot {
    private Rectangle screenRect;
    private GraphicsDevice screen;
    private final Toolkit toolkit;
    private final Robot elRoboto;
    private final RobotPeer peer;
    private final Point gdloc;
    private final DirectColorModel screenCapCM;
    private final int[] bandmasks;

    public FastRobot() throws HeadlessException, AWTException {
        this.screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
        this.screen = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
        toolkit = Toolkit.getDefaultToolkit();
        elRoboto = new Robot();
        peer = ((ComponentFactory)toolkit).createRobot(elRoboto, screen);

        gdloc = screen.getDefaultConfiguration().getBounds().getLocation();
        this.screenRect.translate(gdloc.x, gdloc.y);

        screenCapCM = new DirectColorModel(24,
                /* red mask */    0x00FF0000,
                /* green mask */  0x0000FF00,
                /* blue mask */   0x000000FF);
        bandmasks = new int[3];
        bandmasks[0] = screenCapCM.getRedMask();
        bandmasks[1] = screenCapCM.getGreenMask();
        bandmasks[2] = screenCapCM.getBlueMask();

        Toolkit.getDefaultToolkit().sync();
    }

    public void autoResetGraphicsEnv() {
        this.screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
        this.screen = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
    }

    public void manuallySetGraphicsEnv(Rectangle screenRect, GraphicsDevice screen) {
        this.screenRect = screenRect;
        this.screen = screen;
    }


    public BufferedImage createBufferedScreenCapture(int pixels[]) throws HeadlessException, AWTException {
//      BufferedImage image;
        DataBufferInt buffer;
        WritableRaster raster;

        pixels = peer.getRGBPixels(screenRect);
        buffer = new DataBufferInt(pixels, pixels.length);

        raster = Raster.createPackedRaster(buffer, screenRect.width, screenRect.height, screenRect.width, bandmasks, null);
        return new BufferedImage(screenCapCM, raster, false, null);
    }

    public int[] createArrayScreenCapture() throws HeadlessException, AWTException {
            return peer.getRGBPixels(screenRect);
    }

    public WritableRaster createRasterScreenCapture(int pixels[]) throws HeadlessException, AWTException {
    //  BufferedImage image;
        DataBufferInt buffer;
        WritableRaster raster;

        pixels = peer.getRGBPixels(screenRect);
        buffer = new DataBufferInt(pixels, pixels.length);

        raster = Raster.createPackedRaster(buffer, screenRect.width, screenRect.height, screenRect.width, bandmasks, null);
    //  SunWritableRaster.makeTrackable(buffer);
        return raster;
    }
}

从本质上讲,我对原始版本所做的所有更改都是从函数体中移动许多分配,并将它们设置为类的属性,以便不会每次都调用它们。这样做实际上对帧速率有重大影响。即使在我严重不足的笔记本电脑上,它也从库存机器人类的约4 fps到我的FastRobot类的约30fps。

第一次测试:

当我在主程序中开始出现失记错误时,我设置了这个非常简单的测试来关注FastRobot。注意:这是生成上述堆配置文件的代码。

public class TestFBot {
    public static void main(String[] args) {
        try {
            FastRobot fbot = new FastRobot();

            double startTime = System.currentTimeMillis();
            for (int i=0; i < 1000; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

        } catch (AWTException e) {
            e.printStackTrace();
        }
    }
}

检查:

它不会每次都这样做,这真的很奇怪(而且令人沮丧!实际上,它很少使用上述代码。但是,如果我有多个 for 循环背靠背,内存问题变得很容易重现。

测试 2

public class TestFBot {
    public static void main(String[] args) {
        try {
            FastRobot fbot = new FastRobot();

            double startTime = System.currentTimeMillis();
            for (int i=0; i < 1000; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

            startTime = System.currentTimeMillis();
            for (int i=0; i < 500; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

            startTime = System.currentTimeMillis();
            for (int i=0; i < 200; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

            startTime = System.currentTimeMillis();
            for (int i=0; i < 1500; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

        } catch (AWTException e) {
            e.printStackTrace();
        }
    }
}

检查

失控的堆现在是可重复的,我会说大约80%的时间。我已经通过分析器查看了所有内容,最值得注意的是(我认为)的是,垃圾回收器似乎在第四个也是最后一个循环开始时停止了。

上述代码给出的输出形式给出了以下时间:

Time taken: 24.282    //Loop1
Time taken: 11.294    //Loop2
Time taken: 7.1       //Loop3
Time taken: 70.739    //Loop4

现在,如果将前三个循环相加,则其总和为 42.676,这可疑地对应于垃圾回收器停止和内存峰值的确切时间。

enter image description here

现在,这是我第一次参加剖析牛仔竞技表演, 更不用说我第一次想到垃圾收集了-- 它总是在后台神奇地工作-- 所以,我不确定我发现了什么,如果有的话。

其他个人资料信息

enter image description here

奥古斯托建议查看内存配置文件。有1500多个被列为“无法访问,但尚未收集”。这些肯定是他们创建的数组,但由于某种原因,它们没有被破坏。不幸的是,这些额外的信息只会增加我的困惑,因为我不确定为什么GC不会收集它们int[]int[]peer.getRGBPixels()


使用小堆参数 -Xmx256m 的配置文件:

在无可辩驳和 Hot Licks 的建议下,我将最大堆大小设置为明显较小的值。虽然这确实可以防止它在内存使用量上跳跃1gb,但它仍然没有解释为什么程序在进入第4次迭代时膨胀到其最大堆大小。

enter image description here

如您所见,确切的问题仍然存在,它只是变得更小了。;)这个解决方案的问题在于,由于某种原因,程序仍然消耗了它所能消耗的所有内存 - 与第一次迭代相比,fps性能也发生了显着变化,前者消耗很少的内存,而最终的迭代则消耗尽可能多的内存。

问题仍然是为什么它会膨胀?


点击“强制垃圾回收”按钮后的结果:

在jtahlborn的建议下,我按下了“强制垃圾收集”按钮。它工作得很好。它从1gb的内存使用量下降到60mb左右的基线。

enter image description here

所以,这似乎是治愈的方法。现在的问题是,如何以编程方式强制GC执行此操作?


将本地 Peer 添加到函数的作用域后的结果:

在David Waters的建议下,我修改了函数,使其包含本地对象。createArrayCapture()Peer

遗憾的是,内存使用模式没有变化。

enter image description here

在第3次或第4次迭代中仍然变得巨大。


内存池分析:

来自不同内存池的屏幕截图

所有池:

All pools

伊甸园池:

Eden Pool

老将军:

Old Gen
几乎所有的内存使用量似乎都属于这个池。

注意:PS Survivor Space(显然)有0次使用


我剩下几个问题:

(a) 垃圾探查器图是否意味着我认为它意味着什么?还是我把相关性与因果关系混为一谈?正如我所说,我有这些问题处于一个未知的领域。

(b) 如果垃圾回收器...我该怎么办..?为什么它完全停止,然后在程序的其余部分以较低的速率运行?

(c) 如何解决此问题?

这是怎么回事?


答案 1

请尝试手动指定垃圾回收器。

并发标记扫描是一个很好的通用扫描,可在低暂停和合理吞吐量之间提供良好的平衡。

如果您使用的是Java 7或更高版本的Java 6,G1收集器可能更好,因为它还能够防止内存碎片。

您可以查看 Java SE 热点虚拟机垃圾回收调整页面以获取更多信息和指针:-D


答案 2

您说您将对象创建从方法移动到类上的字段。您移动的依赖项之一是“对等”吗?例如:

peer = ((ComponentFactory)toolkit).createRobot(elRoboto, screen);

很可能是对等体保留了对象生命周期的所有屏幕截图,当对等体移出范围时,这将被清除,机器人中方法的结束,FastRobot的类的寿命。

尝试将 peer 的创建和范围移回您的方法,看看有什么区别。

public int[] createArrayScreenCapture() throws HeadlessException, AWTException {
        RobotPeer localPeer = ((ComponentFactory)toolkit).createRobot(elRoboto, screen);
        return localPeer.getRGBPixels(screenRect);
}

尝试 2

所以,这似乎是治愈的方法。现在的问题是,我如何在语法上强制GC这样做?

您可以调用 System.gc() 来请求垃圾回收。请注意,这是一个请求,而不是一个需求。JVM 只有在认为值得的时候才会运行垃圾回收。

如您所见,确切的问题仍然存在,它只是变得更小了。;)这个解决方案的问题在于,由于某种原因,程序仍然消耗了它所能消耗的所有内存 - 与第一次迭代相比,fps性能也发生了显着变化,前者消耗很少的内存,而最终的迭代则消耗尽可能多的内存。

问题仍然是为什么它会膨胀?

JVM 将尝试仅在绝对必要时才运行主要垃圾回收(使用大部分堆)。(阅读年轻一代与老一代以及年轻一代,伊甸园空间和幸存者空间)。因此,预计长寿命或内存密集型Java应用程序将接近最大堆大小。值得注意的是,要使内存进入旧一代,它必须在3次次要GC运行中幸存下来(Eden => Survivor 1 => Survivor 2 => Old Generation [取决于您正在运行的JVM以及您在命令行参数上选择的GC方案。

至于为什么这种行为会改变,它可能是任何数量的事情。最后一个循环是最长的,System.getCurrentTimeMillis() 是否阻塞了足够长的时间,以便 GC 在不同的线程上运行?所以问题只出现在更长的循环中?拍摄屏幕截图的过程对我来说听起来很低级,我假设通过调用操作系统内核来实现,这是否阻止了内核空间中的进程阻止其他线程运行?(这将停止gc在后台线程中运行)。

查看 http://www.javacodegeeks.com/2012/01/practical-garbage-collection-part-1.html,了解垃圾回收领域的介绍。或者Java Memory解释(SUN JVM)为堆更多的链接。

希望有所帮助。


推荐