.NET 4.0中的极高内存使用率

本文关键字:内存 使用率 高内存 NET | 更新日期: 2023-09-27 17:58:13

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

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

在4.0上运行的完全相同的服务使用3GB到5GB+的内存,每秒的内存不足4倍。事实上,随着内存使用率的不断攀升,该服务通常会停止运行,直到我的系统达到99%的使用率,页面文件交换变得疯狂。

我不确定这是否与垃圾收集有关,但我很难弄清楚。我的窗口服务通过下面的配置文件开关使用"服务器"GC:

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

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

想法?

.NET 4.0中的极高内存使用率

这是一个有趣的问题。

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

基本上,微软改变了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);

这解决了问题。

我遇到了这个确切的问题。的确,应用程序域是创建的,而不是清理的。然而,我不建议恢复传统。它们可以通过ReleaseSandboxAppDomain()进行清理。

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

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

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

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

我已经很晚了,但我有一个真正的解决方案,可以解释为什么!

事实证明,LocalReport正在使用。NET Remoting动态创建一个子应用程序域并运行报告,以避免内部某个地方发生泄漏。然后我们注意到,最终,该报告将在10到20分钟后释放所有内存。对于那些生成了大量PDF的人来说,这是行不通的。然而,这里的关键是他们正在使用。NET远程处理。Remoting的一个关键部分是所谓的"租赁"。租赁意味着它将保留Marshal对象一段时间,因为Remoting通常设置成本很高,而且可能会多次使用。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()如果你想加倍确信你正在尽一切努力释放内存。

希望这能有所帮助!

编辑

显然,你应该打电话给GC。生成PDF报告后收集(0),否则由于某种原因,内存使用率可能仍然很高。

您可能想要

  • 评测堆
  • 使用WinDbg+SOS.dll来确定泄漏的资源以及引用所在的位置

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

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

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

CCD_ 2的工作原理相同。

感谢此Social MSDN参考资料。

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

我试着做同样的事情:使用一个单独的应用程序域并将报告整理到它上面。我认为这是一个糟糕的解决方案,很快就会造成混乱。

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

棘手的部分是将信息传递给单独的程序。使用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。

这样做的一个问题是,如果您从未真正发布过报告程序,则会得到一个明显的错误。只需伪发布一次,生成一个清单,然后它就可以工作了。

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

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

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