C#中的FlowDocument内存问题

8
我目前正在处理释放FlowDocument资源的问题。我正在使用TextRange.Load将rtf文件加载到FlowDocument中。我注意到在这样做后,它会保留这些资源,并且GC不会收集它们。我运行了一个内存分析器并看到这是真的。我已经缩小了范围,发现问题出现在将rtf放入FlowDocument中。如果我不这样做,那么一切都正常。所以我知道这是问题所在。
我希望得到一些指导,了解如何解决这个问题。这里是加载rtf和其他所有内容的代码。我已经注释掉了所有其他代码,甚至将其放在自己的作用域中,并尝试了GC.Collect()。非常感谢您的任何帮助。
编辑: 这是我目前的完整代码。我除了最基本的东西之外什么都没有了。问题仍然存在。您可以看到,FlowDocument和TextRange在其他地方都没有被引用。
    public LoadRTFWindow(string file)
    {
        InitializeComponent();

        using (FileStream reader = new FileStream(file, FileMode.Open))
        {
            FlowDocument doc = new FlowDocument();
            TextRange range = new TextRange(doc.ContentStart, doc.ContentEnd);
            range.Load(reader, System.Windows.DataFormats.Rtf);
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }

我找到了这篇帖子,希望它能帮助我解决问题,但是没有成功。非常感谢提供任何形式的帮助。谢谢。
编辑:我想我应该提一下我检查这个问题的主要方法。我打开了Windows任务管理器,并观察我的应用程序进程正在使用的内存使用情况。当我运行上面的代码时,应用程序从40,000K增加到70,000K,同时进行TextRange.Load() (这是一个大的400页RTF),一旦完成后它降至61,000K并保持不变。我的期望是它会降回到40,000K或者非常接近它。
正如我之前提到的,我使用了一个内存分析器,发现有很多段落、运行等对象仍然存在。
8个回答

6
如果我确认存在内存泄漏问题,以下是我会用来调试问题的步骤:
  1. http://www.microsoft.com/whdc/devtools/debugging/installx86.mspx#a安装Windows调试工具。
  2. 从安装目录启动Windbg。
  3. 启动你的应用程序并进行泄漏内存的操作。
  4. 将Windbg附加到你的应用程序(按F6)。
  5. 输入.loadby sos mscorwks
  6. 输入!dumpheap -type FlowDocument
  7. 检查上述命令的结果。如果你看到多个FlowDocuments,请对于第一列的每个值(它包含地址),执行以下操作:

输入!gcroot <第一列的值>

这将显示谁持有该引用。


1
如果您正在使用 .net 4.0+,请将步骤5替换为.loadby sos clr - maxp

3

我们遇到了类似的问题,我们在不同的线程中创建流文档,在内存分析器中发现对象仍然存在。

据我所知,如link所述:

“当创建FlowDocument时,相对昂贵的格式化上下文对象也会在其StructuralCache中为其创建。当您在紧密循环中创建多个FlowDoc时,将为每个FlowDoc创建一个StructuralCache。如果您在循环结束时调用Gc.Collect,希望恢复一些内存,则StructuralCache具有释放此格式化上下文的终结器,但不会立即释放。终结器有效地安排了一个操作以在DispatcherPriority.Background释放上下文。”

因此,在Dispatcher操作完成之前,Flow文档将一直存在于内存中。因此,想法是完成Dispatcher操作。

如果您在当前正在运行Dispatcher的线程中,请尝试以下代码,它将强制执行队列中的所有操作,因为SystemIdle是最低优先级:

Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.SystemIdle, 
    new DispatcherOperationCallback(delegate { return null; }), null); 

如果您在一个没有运行Dispatcher的线程中,就像我这种情况下只有单个流文档被创建在该线程中,那么您可以尝试以下操作:
var dispatcher = Dispatcher.CurrentDispatcher;
dispatcher.BeginInvokeShutdown(DispatcherPriority.SystemIdle);
Dispatcher.Run();

这将在最后排队关闭,然后运行调度程序以清理FlowDocument,最后关闭调度程序。

1

FlowDocument使用System.Windows.Threading.Dispatcher来释放所有资源。它不使用终结器,因为终结器会阻塞当前线程,直到所有资源都被释放。因此,用户可能会看到一些UI冻结等情况。调度程序在后台线程中运行,对UI的影响较小。
因此,调用GC.WaitForPendingFinalizers();对此没有影响。您只需要添加一些代码来等待并允许调度程序完成其工作。只需尝试添加类似Thread.CurrentThread.Sleep(2000 /* 2秒 */);的内容即可。

编辑: 我认为您是在调试某个应用程序时发现了这个问题。我编写了以下非常简单的测试用例(控制台程序):

    static void Main(string[] args)
    {
        Console.WriteLine("press enter to start");
        Console.ReadLine();

        var path = "c:\\1.rtf";

        for (var i = 0; i < 20; i++)
        {
            using (var stream = new FileStream(path, FileMode.Open))
            {
                var document = new FlowDocument();
                var range = new TextRange(document.ContentStart, document.ContentEnd);

                range.Load(stream, DataFormats.Rtf);
            }
        }

        Console.WriteLine("press enter to run GC.");
        Console.ReadLine();

        GC.Collect();
        GC.WaitForPendingFinalizers();

        Console.WriteLine("GC has finished .");
        Console.ReadLine();
    }

我尝试复现这个问题。我运行了几次,它完美地工作 - 没有泄漏(一直保持在3.2Kb和36个句柄左右)。在VS中以调试模式运行此程序之前,我无法复现它(只需按f5而不是ctrl+f5)。在开始时我收到了20.5Kb,在加载和GC之前为31.7Kb,在GC之后为31Kb。这看起来与您的结果相似。
所以,请尝试在VS下以发布模式运行并复现此问题,好吗?


是的,我注意到FlowDocument继承自System.Windows.Threading.DispatcherObject。我之前尝试过类似的东西,结果和以前一样——什么也没有发生。不过还是值得一试的:)。顺便提一下,Thread.Sleep()是一个静态方法。 - Jasson
哦,抱歉,那时已经很晚了,我有点困。今天我会尝试检查这个问题并进行一些调试。它变得非常有趣。 - zihotki
谢谢并祝好运。我真的被这个问题难住了,但仍在继续寻找解决方案。 - Jasson
我在没有调试模式下运行了我的应用程序,但仍然遇到了这个问题。此外,我已经意识到这不仅发生在这个实例中,而是在整个应用程序中都会发生。这只是其中的一个简化版本。由于我必须在RichTextBox中显示所有内容并将数据存储在byte[]中,因此我经常使用TextRange.Load()。这成为一个重大问题,因为在使用应用程序一段时间后,它将使用大量内存。 - Jasson
我不确定为什么这个问题仍然存在。感谢您迄今为止的帮助。 - Jasson
您的应用程序是商业应用吗?如果您可以分享一些可以重现此问题的资源,我很乐意帮助您找出问题所在。有时问题出现的位置与我们预期的不同。请随时通过zihotki@gmail.com联系我。 - zihotki

1

我之前已经完成了#7。那是我第一次使用Windbg,所以我不知道如何处理地址以查找引用。这是我得到的结果。

 Address       MT     Size
0131c9c0 55cd21d8       84     
013479e0 55cd21d8       84     
044dabe0 55cd21d8       84     
total 3 objects
Statistics:
      MT    Count    TotalSize Class Name
55cd21d8        3          252 System.Windows.Documents.FlowDocument
Total 3 objects
0:011> !gcroot 0131c9c0
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 47c
Scan Thread 2 OSTHread be8
Scan Thread 4 OSTHread 498
DOMAIN(001657B0):HANDLE(WeakSh):911788:Root:0131ff98(System.EventHandler)->
0131fcd4(System.Windows.Documents.AdornerLayer)->
012fad68(MemoryTesting.Window2)->
0131c9c0(System.Windows.Documents.FlowDocument)
DOMAIN(001657B0):HANDLE(WeakSh):911cb0:Root:0131ca90(MS.Internal.PtsHost.PtsContext)->
0131cb14(MS.Internal.PtsHost.PtsContext+HandleIndex[])->
0133d668(MS.Internal.PtsHost.TextParagraph)->
0131c9c0(System.Windows.Documents.FlowDocument)
DOMAIN(001657B0):HANDLE(WeakSh):9124a8:Root:01320a2c(MS.Internal.PtsHost.FlowDocumentPage)->
0133d5d0(System.Windows.Documents.TextPointer)->
0131ca14(System.Windows.Documents.TextContainer)->
0131c9c0(System.Windows.Documents.FlowDocument)

(我将其放在代码块中以便更容易阅读) 这是在我关闭窗口后发生的。所以看起来它被一些东西引用了。既然我知道了这一点,我该如何释放这些引用,以便它们可以释放FlowDocument。
感谢您的帮助。我觉得我终于有了一些进展。

0

GC.Collect() 单独使用并不能收集所有内容,您需要运行:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

此外,我发现运行时并不总是立即释放已收集的内存,您应该检查实际堆大小而不是依赖任务管理器。

我尝试了那个方法,似乎有一点帮助,同时查看实际堆栈也比任务管理器显示的好一些,但总体问题仍然存在。我相当确定这是某种类型的内存泄漏,但是我无法确定具体是什么。我提供的代码据我所知完全独立,因此我感到困惑,因为我以前从未遇到过这样的情况。还有其他建议吗?感谢迄今为止的帮助。 - Jasson

0

请确保 FlowDocument 的父级不会挂起,参见此处。 "实例化 FlowDocument 会自动生成一个承载内容的父级 FlowDocumentPageViewer。" 如果该控件挂起,可能是问题的根源。


我没有在任何地方引用父级,所以我不认为FlowDocumentPageViewer是我的问题。我在原帖中填写了更多的代码。 - Jasson

0

考虑释放该文件句柄。同时,考虑使用"using"语句而不是调用IDisposable.Dispose方法(无意冒犯)。


我实际上已经在代码中使用了它,但不确定为什么没有放进去。我会立即更新代码。当你说我应该考虑释放文件句柄时,你是指FileStream吗?我想不是。我不确定你在说什么。感谢你的帮助。 - Jasson
你新编辑的代码看起来更加清晰。需要考虑到C#代码是JIT编译的事实,一旦执行某些操作,你的工作集就会变得更大。 如果你反复执行相同的代码(它已经被JIT编译),那么你就会有一个内存泄漏问题,你分配的内存会增长。我不认为你以上的代码存在内存泄漏问题。 - GregC
我相当确定这是一个内存泄漏问题。我对代码进行了一些调试,发现如果在窗口中显示文本,则大量的内存会分配给 RichTextBox 控件。似乎当我关闭窗口时,大部分内存都没有被回收。去掉 RichTextBox 显示后,我发现仍然有很多内存进入 FlowDocument,但没有被回收。这与 RichTextBox 问题几乎相同,因为它包含一个 FlowDocument。但是,这个 FlowDocument 的范围非常有限,这就是我感到困惑的原因。 - Jasson

0

我尝试重现你的问题,但在我的机器上没有发生。

任务管理器显示工作集大小,这并不是程序内存使用的准确表示。请尝试使用perfmon。

  1. 开始 -> 运行 -> perfmon.msc
  2. 为您的应用程序添加.NET CLR Memory / #Bytes in All Heaps

现在重复实验并检查该计数器是否继续增加。如果没有,这意味着您没有泄漏任何托管内存。


使用 perfmon 我发现在我的示例程序中,第一次运行它时内存似乎没有被回收;然而,后续运行似乎能够回收内存。在我的实际应用程序中,内存根本没有被回收。我在第二个窗口中运行此代码。可能这就是原因?但为什么仅第一次会回收内存呢?我也在 WPF MSDN 论坛上发了这个问题。http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/15e2b42e-1975-4d68-8ddb-59bbcd1f0633/ - Jasson
你是否有第二个窗口的引用仍然存在?你是否已经从第一个窗口订阅了第二个窗口中任何组件公开的事件? - Senthil Kumar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接