【问题标题】:Very High Memory Usage in .NET 4.0.NET 4.0 中的内存使用率非常高
【发布时间】:2011-06-02 22:03:39
【问题描述】:

我有一个最近从 .NET 3.5 迁移到 .NET 4.0 的 C# Windows 服务。没有进行其他代码更改。

在 3.5 上运行时,给定工作负载的内存利用率大约为 1.5 GB 内存,吞吐量为每秒 20 X。 (在这个问题的上下文中,X 无关紧要。)

在 4.0 上运行的完全相同的服务使用 3GB 到 5GB+ 的内存,每秒速度不到 4 X。事实上,随着内存使用量继续攀升,服务通常最终会停止,直到我的系统处于 99% 的利用率并且页面文件交换变得疯狂。

我不确定这是否与垃圾收集有关,或者与什么有关,但我无法弄清楚。我的窗口服务通过如下所示的配置文件开关使用“服务器”GC:

  <runtime>
    <gcServer enabled="true"/>
  </runtime>

将此选项更改为 false 似乎并没有什么不同。此外,根据我对 4.0 中新 GC 所做的阅读,大的变化只影响工作站 GC 模式,而不影响服务器 GC 模式。所以也许GC与这个问题无关。

想法?

【问题讨论】:

  • 只是仔细检查:只有 .NET 框架发生了变化。还是您从 32 位机器转到 64 位?您是否从 DEP 转到无 DEP?您是否从 PAE 转到无 PAE?你是从 ngen 转到 JIT 的吗?只是提示触发更多信息
  • 这里没有足够的信息来冒险猜测。您是否偶然使用了BlockingCollectionConcurrentQueue 类? ConcurrentQueue 有内存泄漏,这可能是个问题。
  • ConcurrentQueue 在 .NET 3.5 中不存在,所以这不是问题。
  • @phoog:实际上,ConcurrentQueue 确实存在于 .NET 的并行扩展中。从 3.5 转换到 4.0 可能需要将其替换为 System.Collections.Concurrent。我知道当我们转换到 4.0 时我做到了。
  • @Jim Mischel:啊哈,谢谢你让我正确。但是,如果这些类正在使用中,OP 是否不必更改引用和/或命名空间 using 语句?

标签: c# .net .net-4.0 garbage-collection rdlc


【解决方案1】:

嗯,这很有趣。

事实证明,根本原因是 SQL Server Reporting Services 的 LocalReport 类 (v2010) 在 .NET 4.0 上运行时的行为发生了变化。

基本上,Microsoft 改变了 RDLC 处理的行为,以便每次处理报表时都在单独的应用程序域中完成。这实际上是为了解决由于无法从应用程序域卸载程序集而导致的内存泄漏。当 LocalReport 类处理一个 RDLC 文件时,它实际上会动态创建一个程序集并将其加载到应用程序域中。

在我的例子中,由于我正在处理大量的报告,这导致创建了大量的 System.Runtime.Remoting.ServerIdentity 对象。这是我对原因的提示,因为我对为什么处理 RLDC 需要远程处理感到困惑。

当然,要调用另一个应用程序域中的类的方法,远程处理正是您使用的。在 .NET 3.5 中,这不是必需的,因为默认情况下,RDLC 程序集被加载到同一个应用程序域中。但是,在 .NET 4.0 中,默认情况下会创建一个新的应用程序域。

修复相当简单。首先,我需要使用以下配置启用旧版安全策略:

  <runtime>
    <NetFx40_LegacySecurityPolicy enabled="true"/>
  </runtime>

接下来,我需要通过调用以下命令来强制在与我的服务相同的应用程序域中处理 RDLC:

myLocalReport.ExecuteReportInCurrentAppDomain(AppDomain.CurrentDomain.Evidence);

这解决了问题。

【讨论】:

  • 感谢您报告此问题。遗憾的是,我们不能多次支持这个问题,因为这是一个非常好的发现,值得在 SO 上得到很好的索引
  • 我不确定您是如何找到此修复程序的,但是太棒了!
  • ExecuteReportInCurrentAppDomain 根据文档已被弃用。即使有了它,仍然存在内存泄漏,因为每次运行报告时,这些计算 DLL 都会重新创建,然后未使用但未卸载。我们发现解决此问题的唯一方法是在每次我们想要运行报告时生成一个新进程。当进程终止时,一切都会自动释放,包括不再需要的 DLL。只是在使用 MS 报告时要考虑的一个想法。
  • 如果 RDLC 处理被移动到不同的应用程序域以阻止内存泄漏,为什么要将它移动到当前的应用程序域?似乎是一种内存泄漏的权衡。
  • 这太疯狂了,人们怎么不为这个问题尖叫?它仍然存在于 .NET 4.5 中。
【解决方案2】:

我遇到了这个确切的问题。确实,应用程序域是创建而不是清理的。但是我不建议恢复到旧版。它们可以通过 ReleaseSandboxAppDomain() 清除。

LocalReport report = new LocalReport();
...
report.ReleaseSandboxAppDomain();

我还做一些其他的事情来清理:

取消订阅任何 SubreportProcessing 事件, 清除数据源, 处理报告。

我们的 Windows 服务每秒处理多个报告,并且没有泄漏。

【讨论】:

  • 谢谢,在测试了您上面所说的内容后,我看到报告不再泄漏。 :) 这就是我所做的。我希望它可以帮助其他人:localReport.SubreportProcessing -= reportDataProcessor.SubreportProcessingHandler; localReport.DataSources.Clear(); localReport.ReleaseSandboxAppDomain(); localReport.Dispose();
  • 这对我来说似乎没有任何改变。即使进行了所有建议的清理,仍然存在严重的内存泄漏。
  • 所以,这对我有用,但前提是我通过 app.config 启用 CAS。 sues999,我认为您已打开此标志或正在运行 .NET 3.5 或更早版本。除非创建沙盒应用程序域,否则 ReleaseSandboxAppDomain() 不会执行任何操作,这需要 CAS。默认情况下,在启用 CAS 的情况下,LocalReport 将使用沙盒 AppDomain,所以看起来所选的答案海报有点倒退。我希望在 .NET 4.0 中,报告默认在当前应用程序域中运行,这就是它泄漏的原因。只有 AppDomain 拆解似乎可以释放报告生成留下的内容。
【解决方案3】:

我已经很晚了,但我有一个真正的解决方案并且可以解释原因!

事实证明,这里的 LocalReport 正在使用 .NET Remoting 动态创建子 appdomain 并运行报告,以避免内部某处泄漏。然后我们注意到,最终,报告将在 10 到 20 分钟后释放所有内存。对于生成大量 PDF 的人来说,这是行不通的。但是,这里的关键是他们使用的是 .NET Remoting。 Remoting 的关键部分之一是称为“租赁”的东西。租赁意味着它将保留该 Marshal 对象一段时间,因为远程处理通常设置成本很高,并且可能会被多次使用。 LocalReport RDLC 正在滥用此功能。

默认情况下,租用时间是... 10 分钟!此外,如果有东西向它发出各种调用,它会再增加 2 分钟的等待时间!因此,它可以随机在 10 到 20 分钟之间,具体取决于呼叫的排列方式。幸运的是,您可以更改此超时发生的时间。不幸的是,您只能为每个应用程序域设置一次...因此,如果您需要生成 PDF 以外的远程处理,您可能需要让另一个服务运行它,以便您可以更改默认值。为此,您需要做的就是在启动时运行这 4 行代码:

    LifetimeServices.LeaseTime = TimeSpan.FromSeconds(5);
    LifetimeServices.LeaseManagerPollTime = TimeSpan.FromSeconds(5);
    LifetimeServices.RenewOnCallTime = TimeSpan.FromSeconds(1);
    LifetimeServices.SponsorshipTimeout = TimeSpan.FromSeconds(5);

您会看到内存使用量开始上升,然后在几秒钟内您应该会看到内存开始回落。我花了几天时间使用内存分析器来真正追踪并了解发生了什么。

您不能将 ReportViewer 包装在 using 语句中(Dispose 崩溃),但如果您直接使用 LocalReport 应该可以。处理完毕后,如果您想更加确定自己正在尽一切可能释放内存,则可以调用 GC.Collect()。

希望这会有所帮助!

编辑

显然,您应该在生成 PDF 报告后调用 GC.Collect(0),否则由于某种原因,内存使用率可能仍然很高。

【讨论】:

    【解决方案4】:

    你可能想要

    也许某些 API 改变了语义,或者甚至可能在 4.0 版本的框架中存在错误

    【讨论】:

    • 有趣的是,我确实运行了 CLR Profiler。当任务管理器显示 1.5 GB 的内存使用量时,“根”堆显示的使用量不到 400MB。换句话说,除非我理解错了,否则有 1.1 GB 的未收集垃圾。同样,当服务根据任务管理器使用 2.4 GB 时,CLR Profiler 显示根为 1.1 GB。
    • 我赞成你的回答,因为这是一个很好的建议,只是不是完整的答案。
    【解决方案5】:

    为了完整起见,如果有人正在寻找等效的 ASP.Net web.config 设置,它是:

      <system.web>
        <trust legacyCasModel="true" level="Full"/>
      </system.web>
    

    ExecuteReportInCurrentAppDomain 的工作原理相同。

    感谢Social MSDN reference

    【讨论】:

    • 这种方法的问题是您不能将动态类型与旧的 cas 模型一起使用 - 使用 ViewBag 会导致异常。
    • 是的,我们正在调查此问题,并在 Web 版本上运行时遇到此异常:安全异常描述:应用程序试图执行安全策略不允许的操作。要授予此应用程序所需的权限,请联系您的系统管理员或在配置文件中更改应用程序的信任级别。异常详细信息:System.Security.SecurityException:请求失败。
    • I experienced the same problem in .NET 4.5 in ASP.NET based application with heavy use of dynamic types for serialization and deserialization. So I could not use &lt;trust legacyCasModel="true" level="Full"/&gt;, Adrian Nichols 在 github.com/AdrianNichols/ssrs-non-native-functions/blob/… Keys 中有一个帮助代码:RenderReportToMemoryAsPDFInAnotherAppDomain i> 方法和 ReportHelperInAppDomain
    【解决方案6】:

    似乎 Microsoft 尝试将报告放入自己的单独内存空间以解决所有内存泄漏问题,而不是修复它们。在这样做的过程中,他们引入了一些严重的崩溃,并最终导致更多的内存泄漏无论如何。他们似乎缓存了报表定义,但从不使用它,从不清理它,每个新报表都会创建一个新的报表定义,占用越来越多的内存。

    我也尝试过做同样的事情:使用单独的应用程序域并将报告编组给它。我认为这是一个糟糕的解决方案,而且很快就会弄得一团糟。

    我所做的与此类似:将程序的报告部分拆分为自己的单独报告程序。无论如何,这证明是组织代码的好方法。

    棘手的部分是将信息传递给单独的程序。使用Process 类启动报告程序的新实例并在命令行上传递它需要的任何参数。第一个参数应该是一个枚举或类似的值,指示应该打印的报告。我在主程序中的代码如下所示:

    const string sReportsProgram = "SomethingReports.exe";
    
    public static void RunReport1(DateTime pDate, int pSomeID, int pSomeOtherID) {
       RunWithArgs(ReportType.Report1, pDate, pSomeID, pSomeOtherID);
    }
    
    public static void RunReport2(int pSomeID) {
       RunWithArgs(ReportType.Report2, pSomeID);
    }
    
    // TODO: currently no support for quoted args
    static void RunWithArgs(params object[] pArgs) {
       // .Join here is my own extension method which calls string.Join
       RunWithArgs(pArgs.Select(arg => arg.ToString()).Join(" "));
    }
    
    static void RunWithArgs(string pArgs) {
       Console.WriteLine("Running Report Program: {0} {1}", sReportsProgram, pArgs);
       var process = new Process();
       process.StartInfo.FileName = sReportsProgram;
       process.StartInfo.Arguments = pArgs;
       process.Start();
    }
    

    报告程序看起来像:

    [STAThread]
    static void Main(string[] pArgs) {
       Application.EnableVisualStyles();
       Application.SetCompatibleTextRenderingDefault(false);
    
       var reportType = (ReportType)Enum.Parse(typeof(ReportType), pArgs[0]);
       using (var reportForm = GetReportForm(reportType, pArgs))
          Application.Run(reportForm);
    }
    
    static Form GetReportForm(ReportType pReportType, string[] pArgs) {
       switch (pReportType) {
          case ReportType.Report1: return GetReport1Form(pArgs);
          case ReportType.Report2: return GetReport2Form(pArgs);
          default: throw new ArgumentOutOfRangeException("pReportType", pReportType, null);
       }
    }
    

    您的GetReportForm 方法应该拉取报表定义,使用相关参数来获取数据集,将数据和任何其他参数传递给报表,然后将报表放在表单上的报表查看器中并返回一个参考表格。请注意,可以提取此过程的大部分内容,以便您基本上可以说“使用此数据和这些参数从该程序集中给我一份此报告的表格”。

    另外请注意,两个程序都必须能够看到与该项目相关的数据类型,因此希望您已将数据类提取到它们自己的库中,这两个程序可以共享引用。将所有数据类都放在主程序中是行不通的,因为您将在主程序和报表程序之间存在循环依赖关系。

    也不要过度使用参数。在报告程序中进行您需要的任何数据库查询;不要传递一个庞大的对象列表(这可能无论如何都行不通)。您应该只传递简单的东西,如数据库 ID 字段、日期范围等。如果您有特别复杂的参数,您可能需要将 UI 的那部分也推送到报告程序,而不是在命令行上将它们作为参数传递。

    您还可以在主程序中引用报表程序,生成的 .exe 和任何相关的 .dll 将被复制到同一个输出文件夹。然后,您可以在不指定路径的情况下运行它,而只使用可执行文件名本身(即:“SomethingReports.exe”)。您还可以从主程序中删除报告 dll。

    其中一个问题是,如果您从未真正发布过报告程序,您将收到一个明显的错误。只需虚拟发布一次,生成清单,然后它就会工作。

    一旦您完成了这项工作,很高兴看到您的常规程序的内存在打印报告时保持不变。报告程序出现,占用的内存比您的主程序多,然后消失,完全清理它,您的主程序占用的内存不超过它已经拥有的内存。

    另一个问题可能是每个报表实例现在将比以前占用更多内存,因为它们现在是完全独立的程序。如果用户打印了很多报表并且从不关闭它们,它会很快消耗大量内存。但我认为这仍然要好得多,因为只需关闭报告即可轻松回收内存。

    这也使您的报告独立于您的主程序。即使在关闭主程序后它们也可以保持打开状态,您可以从命令行手动生成它们,也可以从其他来源生成它们。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-31
      • 1970-01-01
      • 1970-01-01
      • 2015-11-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多