32

这是问题所在:

内存使用膨胀失控

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

基本应用总结(上下文)

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

我很快发现库存的 Robot 类在速度方面真的很糟糕,所以我打开了源代码,去掉了所有重复的工作和浪费的开销,并将它重新构建为我自己的名为 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 类的约 30 fps。

第一次测试:

当我在主程序中开始出现内存不足错误时,我设置了这个非常简单的测试来监视 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,这可疑地对应于垃圾收集器停止的确切时间,以及内存峰值。

在此处输入图像描述

现在,这是我第一次进行性能分析,更不用说我第一次想到垃圾收集——它总是在后台神奇地工作——所以,我不确定是什么,如果有的话,我已经发现了。

其他个人资料信息

在此处输入图像描述

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


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

在声名狼藉和 Hot Licks 的建议下,我将最大堆大小设置为明显更小。虽然这确实阻止了它在内存使用量上的 1gb 跳跃,但它仍然没有解释为什么程序在进入第 4 次迭代时会膨胀到其最大堆大小。

在此处输入图像描述

如您所见,确切的问题仍然存在,只是变小了。;) 这个解决方案的问题是,由于某种原因,程序仍在消耗它所能消耗的所有内存——从第一次迭代开始,fps 性能也有显着变化,消耗的内存非常少,并且最后一次迭代,它会消耗尽可能多的内存。

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


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

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

在此处输入图像描述

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


将本地 Peer 添加到函数范围后的结果:

在 David Waters 的建议下,我修改了该createArrayCapture()函数,使其拥有一个本地Peer对象。

不幸的是,内存使用模式没有变化。

在此处输入图像描述

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


内存池分析:

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

所有游泳池:

所有游泳池

伊甸池:

伊甸园池

老一代:

老一代
几乎所有的内存使用似乎都在这个池中。

注意:PS 幸存者空间(显然)使用率为 0


我有几个问题:

(a) Garbage Profiler 图的意思是我认为的意思吗?还是我混淆了因果关系的相关性?正如我所说,我在这些问题的未知领域。

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

(c) 我该如何解决这个问题?

这里发生了什么?

4

2 回答 2

4

尝试手动指定垃圾收集器。

Concurrent Mark Sweep 是一种很好的通用型,它在低暂停和合理的吞吐量之间提供了良好的平衡。

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

您可以查看Java SE Hotspot Virtual Machine Garbage Collection Tuning页面以获取更多信息和指针:-D

于 2013-03-02T11:13:31.267 回答
3

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

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

很可能对等点会保留在对象生命周期内拍摄的所有屏幕截图,当对等点超出范围、Robot 中的方法结束、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 内存解释 (SUN JVM)以获得更多链接。

希望有帮助。

于 2013-02-28T17:46:46.133 回答