大量内存溢出导致堆大小在大约 8 秒内从大约 64mb 变为 1.5gb。垃圾收集器的问题?

Posted

技术标签:

【中文标题】大量内存溢出导致堆大小在大约 8 秒内从大约 64mb 变为 1.5gb。垃圾收集器的问题?【英文标题】:Massive memory hemorrhage that causes heap size to go from about 64mb, to 1.5gb in roughly 8 seconds. Issue with garbage collector? 【发布时间】:2013-02-14 23:12:44 【问题描述】:

问题来了:

如您所见,内存使用膨胀失控!我不得不向 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% 的时间。我查看了探查器的所有内容,最值得注意的事情(我认为)是垃圾收集器似乎在第四个也是最后一个循环开始时停止了 right

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

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[] 被列为“无法访问,但尚未收集”。这些肯定是peer.getRGBPixels() 创建的int[] 数组,但由于某种原因,它们没有被销毁。不幸的是,这些附加信息只会增加我的困惑,因为我不确定 为什么 GC 不会收集它们


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

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

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

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


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

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

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


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

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

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

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


内存池分析:

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

所有池:

伊甸园池:

老一代:

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

注意:PS Survivor Space 的使用率(显然)为 0


我还有几个问题:

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

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

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

这里发生了什么?

【问题讨论】:

您正在使用您的工具包,为什么不分析内存并查看您正在创建的对象以及它们有多大。看起来您正在内存中创建一些图像,您可能不会从内存中删除这些图像(您从哪里调用 createRasterScreenCapture())? @Augusto 我已经用其他个人资料信息编辑了我的帖子。 1500 int[]s 未被 GC 收集,并列为“无法访问”。我不确定我是否遵循您的最后一个问题.. 所有createArraycap.. 调用都是从同一个FastRobot 对象完成的。是这个意思吗? 首先,我们需要明确指出,将“存储使用率”上升到堆大小限制点不是问题。直到可用的堆空间几乎耗尽时,GC 才会启动,一旦存储被堆“殖民”,它将在 JVM 运行期间保持分配状态。 尝试较小的最大内存。 其次,Java 中的“泄漏”(在使用 JNI 的代码之外)几乎总是(99.999% 的时间)由于程序员“挂在”旧数据上的编码错误(尽管经常是无意的)。当不再使用这些大对象时,始终清除引用其他大对象的对象中的指针(咳咳,“对象引用”)是一种很好的做法。例如,如果您有一个 10K 元素数组用于某事,并且您已经完成了该数组,请清除可能在其他对象中的所有指向它的指针。 【参考方案1】:

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

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

希望有所帮助。

【讨论】:

我只运行了几次,但不幸的是我仍然遇到同样的现象。在第三次或第四次迭代中,内存使用量激增。如果您想查看分析器输出,我已经编辑了问题。【参考方案2】:

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

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

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

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

【讨论】:

以上是关于大量内存溢出导致堆大小在大约 8 秒内从大约 64mb 变为 1.5gb。垃圾收集器的问题?的主要内容,如果未能解决你的问题,请参考以下文章

axios在20秒内给出响应,但具有相同请求有效载荷的相同api在6秒内从邮递员那里得到响应

Java虚拟机八 分析Java堆

当 Java 中存在堆溢出时,我如何知道啥在使用内存?

Node.js——nodejs(内存控制)(转)

32位JVM和64位JVM的最大堆内存分别是多数?32位和64位的JVM,int类型变量的长度是多数?

并行调整图像大小导致内存不足异常