大量的内存出血导致堆大小在大约8秒钟内从约64MB增加到1.5GB。与垃圾收集器有关的问题?

14 浏览
0 Comments

大量的内存出血导致堆大小在大约8秒钟内从约64MB增加到1.5GB。与垃圾收集器有关的问题?

问题:

\n\"内存使用失控\"\n正如您所见,内存使用量失控了!为了避免内存溢出错误,我不得不在JVM中添加参数以增加堆大小,同时找出问题所在。这不好!\n

基本应用程序概述(为了了解背景)

\n这个应用程序(最终)将用于基本的屏幕CV和模板匹配等自动化目的。我希望实现尽可能高的帧率来观看屏幕,并通过一系列单独的消费者线程来处理所有的处理过程。\n我很快发现,原始的Robot类在速度方面真的很糟糕,所以我打开了源代码,删除了所有重复的工作和浪费的开销,并将其重建为我自己的类FastRobot。\n

类的代码:

\n

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;
    }
}

\n本质上,我所做的改变只是将许多分配从函数体中移动出来,并将它们设置为类的属性,这样它们就不会每次被调用。这实际上对帧率产生了重大影响。即使在我配置有严重不足的笔记本电脑上,使用原始的Robot类时,帧率也从约4fps提高到使用我的FastRobot类的约30fps。\n

第一个测试:

\n当我在主程序中遇到内存溢出错误时,我设置了这个非常简单的测试来关注FastRobot。注意:这是产生上面堆剖面的代码。\n

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();
        }
    }
}

\n

检查:

\n它并不总是这样做,这真的很奇怪(也很令人沮丧!)。实际上,使用上述代码,它很少这样做。然而,如果我连续使用多个for循环,内存问题就变得很容易重现。\n

测试2

\n

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();
        }
    }
}

\n

检查

\n现在,这个失控的堆大概可以重现80%的时间。我仔细查看了性能分析器,最值得注意的事情(我认为)是垃圾收集器似乎在第四个和最后一个循环开始时停止了。\n上述代码的输出结果如下:\n

Time taken: 24.282    //循环1
Time taken: 11.294    //循环2
Time taken: 7.1       //循环3
Time taken: 70.739    //循环4

\n现在,如果将前三个循环的时间相加,结果为42.676,这恰好对应于垃圾收集器停止的确切时间和内存激增的时间。\n\"输入图像描述\"\n现在,这是我第一次使用性能分析器,更不用说第一次考虑垃圾收集了 - 它总是在后台以神奇的方式工作 - 所以,我不确定我是否发现了什么。\n

附加的配置文件信息

\n\"输入图像描述\"\nAugusto建议查看内存配置文件。有1500多个被列为“不可达但尚未收集”的int[]。这些肯定是peer.getRGBPixels()创建的int[]数组,但由于某种原因它们没有被销毁。这个额外的信息,不幸的是,只增加了我的困惑,因为我不确定为什么垃圾收集器不会收集它们。\n


\n

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

\n根据irreputable和Hot Licks的建议,我将最大堆大小设置为更小的值。虽然这样可以防止内存使用量跳升到1GB,但仍然无法解释为什么程序在进入第4次迭代时会膨胀到其最大堆大小。\n\"输入图像描述\"\n如您所见,确切的问题仍然存在,只是变得更小了。;) 这种解决方案的问题在于,由于某种原因,程序仍然在耗尽所有可用内存 - 从第一次迭代到最后一次迭代,性能的帧率也有明显的变化。\n问题仍然是,为什么会膨胀呢?\n


\n

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

\n根据jtahlborn的建议,我点击了“强制垃圾回收”按钮。它的效果非常好。内存使用量从1GB降到基线的60MB左右。\n\"输入图像描述\"\n所以,这似乎是解决方法。现在的问题是,如何在程序中以编程方式强制GC执行此操作?\n


\n

在函数作用域中添加本地Peer后的结果:

\n根据David Waters的建议,我修改了createArrayCapture()函数,使其保持一个本地的Peer对象。\n不幸的是,内存使用模式没有变化。\n\"输入图像描述\"\n在第三或第四次迭代时仍然变得非常大。\n


\n

内存池分析:

\n

来自不同内存池的截图

\n

所有内存池:

\n\"所有内存池\"\n

伊甸园内存池:

\n\"伊甸园内存池\"\n

旧代内存池:

\n\"旧代内存池\"
\n几乎所有的内存使用量似乎都在这个内存池中。\n注意:PS Survivor Space的使用量(显然)为0\n


\n

我还留下了一些问题:

\n(a) 垃圾收集器的图形是否意味着我认为的意思?还是我混淆了相关性和因果关系?正如我所说,我对这些问题一窍不通。\n(b) 如果是垃圾收集器...我该怎么办..?为什么它会完全停止,然后在程序的剩余部分以降低的速度运行?\n(c) 我该怎么解决这个问题?\n这里到底发生了什么?

0
0 Comments

问题出现的原因是将对象创建从方法移动到类的字段上,其中一个移动的依赖是"peer"。可能是因为peer在对象的生命周期内一直保留了所有截取的屏幕截图,当peer超出范围时,即在Robot方法结束时,FastRobot类的生命周期结束时,这些截图会被清除。尝试将peer的创建和范围移回方法中,查看差异。

解决方法是调用System.gc()请求垃圾回收。请注意,这只是一个请求,而不是命令。只有当JVM认为值得时,才会运行垃圾回收。

问题仍然存在,只是变小了。这种解决方案的问题是,程序出于某种原因仍然在消耗所有可能的内存,而且从迭代的第一次到最后一次,帧率性能也有明显的变化。问题仍然是为什么内存会膨胀。

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 explained (SUN JVM)获取更多链接。

希望能对您有所帮助。您可以查看问题的分析结果。

0
0 Comments

大规模的内存出血导致堆大小在大约8秒内从约64MB增加到1.5GB。是否存在垃圾收集器的问题?

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

Concurrent Mark Sweep是一个很好的通用垃圾收集器,它在低暂停和合理吞吐量之间保持了很好的平衡。

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

您可以查看Java SE Hotspot虚拟机垃圾收集调优页面获取更多信息和指引:-D

0