大量的内存出血导致堆大小在大约8秒钟内从约64MB增加到1.5GB。与垃圾收集器有关的问题?
大量的内存出血导致堆大小在大约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这里到底发生了什么?
问题出现的原因是将对象创建从方法移动到类的字段上,其中一个移动的依赖是"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)获取更多链接。
希望能对您有所帮助。您可以查看问题的分析结果。
大规模的内存出血导致堆大小在大约8秒内从约64MB增加到1.5GB。是否存在垃圾收集器的问题?
尝试手动指定垃圾收集器。
Concurrent Mark Sweep是一个很好的通用垃圾收集器,它在低暂停和合理吞吐量之间保持了很好的平衡。
如果您使用的是Java 7或更高版本,Java 6的话,G1收集器可能更好,因为它还能够防止内存碎片化。
您可以查看Java SE Hotspot虚拟机垃圾收集调优页面获取更多信息和指引:-D