Swing 拖放导致内存泄漏

Posted

技术标签:

【中文标题】Swing 拖放导致内存泄漏【英文标题】:Memory Leak with Swing Drag and Drop 【发布时间】:2011-02-18 00:16:16 【问题描述】:

我有一个接受***文件的 JFrame。然而,在发生丢弃之后,对框架的引用会无限期地保存在一些 Swing 内部类中。我认为处置框架应该释放其所有资源,那么我做错了什么?

例子

import java.awt.datatransfer.DataFlavor;
import java.io.File;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.TransferHandler;

public class DnDLeakTester extends JFrame 
    public static void main(String[] args) 
        new DnDLeakTester();

        //Prevent main from returning or the jvm will exit
        while (true) 
            try 
                Thread.sleep(10000);
             catch (InterruptedException e) 

            
        
    
    public DnDLeakTester() 
        super("I'm leaky");

        add(new JLabel("Drop stuff here"));

        setTransferHandler(new TransferHandler() 
            @Override
            public boolean canImport(final TransferSupport support) 
                return (support.isDrop() && support
                        .isDataFlavorSupported(DataFlavor.javaFileListFlavor));
            

            @Override
            public boolean importData(final TransferSupport support) 
                if (!canImport(support)) 
                    return false;
                

                try 
                    final List<File> files = (List<File>) 
                            support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);

                    for (final File f : files) 
                        System.out.println(f.getName());
                    
                 catch (Exception e) 
                    e.printStackTrace();
                

                return true;
            
        );

        setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        pack();
        setVisible(true);
    

要重现,运行代码并将一些文件放在框架上。关闭框架,以便将其处理掉。

为了验证泄漏,我使用 JConsole 进行堆转储并使用 Eclipse Memory Analysis tool 对其进行分析。它表明 sun.awt.AppContext 通过其哈希图持有对框架的引用。看起来 TransferSupport 有问题。

image of path to GC root http://img402.imageshack.us/img402/4444/dndleak.png

我做错了什么?我应该要求 DnD 支持代码以某种方式自行清理吗?

我正在运行 JDK 1.6 更新 19。

【问题讨论】:

我开始认为这是一个 JVM 错误。相关的 DnD 类没有清除违规引用的代码,因此除非以某种方式从 AppContext 的映射中删除 DropHandler(我不太了解该类),否则泄漏将继续存在。 这个论坛帖子描述了一个类似的问题 [forums.java.net/jive/thread.jspa?messageID=276311].它没有得到任何回应。 【参考方案1】:

虽然 DropHandler 没有从静态 AppContext 映射中删除,但这并不是真正的根本原因,而只是链条中的原因之一。 (drop 处理程序旨在成为一个单例,并且在 AppContext 类被卸载之前不会被清除,这在实践中永远不会。)使用单例 DropHandler 是设计使然。

泄漏的真正原因是 DropHandler 设置了一个 TransferSupport 的实例,该实例被每个 DnD 操作重用,并在 DnD 操作期间为其提供对 DnD 中涉及的组件的引用。问题是当 DnD 完成时它不会清除引用。如果DnD 退出时DropHandler 调用TransferSupport.setDNDVariables(null,null),那么问题就会消失。这也是最合乎逻辑的解决方案,因为仅在 DnD 进行时才需要对组件的引用。其他方法(例如清除 AppContext 映射)是在规避设计,而不是修复一个小的疏忽。

但是即使我们解决了这个问题,框架仍然不会被收集。不幸的是,似乎还有另一个问题:当我注释掉所有与 DnD 相关的代码时,减少到只是一个简单的 JFrame,这也没有被收集。重制参考位于javax.swing.BufferStrategyPaintManager。对此有一个bug report,尚未修复。

因此,如果我们修复 DnD,我们会遇到另一个与重新绘制有关的保留问题。幸运的是,所有这些错误都只保留一帧(希望是同一帧!),所以它并没有想象的那么糟糕。框架被释放,因此本地资源被释放,所有内容都可以被移除,从而被释放,从而降低内存泄漏的严重性。

所以,最后回答你的问题,你并没有做错什么,你只是给 JDK 中的一些错误一点时间!

更新:重绘管理器错误有一个快速修复 - 添加

-Dswing.bufferPerWindow=false

到jvm启动选项避免了这个bug。消除此错误后,发布 DnD 错误的修复程序是有意义的:

要解决 DnD 问题,您可以在 importData() 的末尾添加对该方法的调用。

            private void cancelDnD(TransferSupport support)
            
                /*TransferSupport.setDNDVariables(Component component, DropTargetEvent event) 
                Call setDNDVariables(null, null) to free the component.
*/
                try
                
                    Method m = support.getClass().getDeclaredMethod("setDNDVariables", new Class[]  Component.class, DropTargetEvent.class );
                    m.setAccessible(true);
                    m.invoke(support, null, null);
                    System.out.println("cancelledDnd");
                
                catch (Exception e)
                
                
            

【讨论】:

感谢您的信息。我发现 BufferStrategyPaintManager 保留了一些类,但被 DnD 的东西分心了。我接受反射技巧会起作用 - 但我不打算使用它 当然,是否使用修复程序是您的选择,但现在至少您知道问题的范围并可以决定如何处理它。喜欢你。我可能不会费心添加修复程序,并且知道可以部分缓解问题,可能什么都不做,或者最多从处置的框架中删除内容。但事后看来,现在问题的严重程度是已知的。我很想知道为什么会发生泄漏以及后果是什么,并且很高兴知道如果我们突然发现需要修复,那是可用的。今晚我可以睡个好觉了! :-)【参考方案2】:

如果你把它添加到你的班级,它会改变你的结果吗?

@Override
public void dispose()

    setTransferHandler(null);
    setDropTarget(null);    // New
    super.dispose();

更新: 我在 dispose 中添加了另一个调用。我认为将放置目标设置为 null 应该会释放更多引用。我在组件上看不到任何其他可用于获取 DnD 代码以释放您的组件的内容。

【讨论】:

感谢您的建议德文。不,恐怕这不会改变任何事情。 抱歉,还是没有运气。我也尝试过 getDropTarget().setComponent(null),但它也不起作用。问题是在 TransferHandler 或其内部类中没有任何代码可以从 AppContext 中删除 DropHandler。只是没有 remove() 来补充 put() 似乎没有任何方法可以释放 AppContext 持有的引用。幸运的是,put 中使用的 key 是 DropTarget 的 Class,所以只能有 1 个未完成的实例。记录它并通过 dispose() 释放其他所有内容来最小化损坏。比如getContentPane().removeAll()。【参考方案3】:

在搜索了相关类的来源后,我确信这是 TransferHandler 中不可避免的泄漏。

AppContext 映射中的对象 DropHandler 永远不会被删除。由于键是 DropHandler.class 对象,并且 DropHandler 是一个私有内部类,因此可以从 TransferHandler 外部删除它的唯一方法是清空整个地图,或者使用反射技巧。

DropHandler 持有对 TransferSupport 对象的引用,该对象永远不会被清除。 TransferSupport 对象包含对组件的引用(在我的例子中是一个 JFrame),它也没有被清除。

【讨论】:

以上是关于Swing 拖放导致内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

常见的内存泄漏原因及解决方法

安卓内存分析——常见内存泄漏场景二

LeakCanary检测内存泄漏

Android内存泄漏

内存泄漏与溢出

JVM内存泄漏导致内存溢出(OOM)的场景