.NET 4.0中的内存使用率非常高

时间:2011-06-02 22:03:39

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

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

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

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

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

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

将此选项更改为false似乎没有任何区别。此外,从我在4.0中的新GC上完成的阅读中,大的变化只影响工作站GC模式,而不影响服务器GC模式。因此GC可能与此问题无关。

想法?

6 个答案:

答案 0 :(得分:83)

这是一个有趣的。

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

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

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

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

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

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

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

myLocalReport.ExecuteReportInCurrentAppDomain(AppDomain.CurrentDomain.Evidence);

这解决了这个问题。

答案 1 :(得分:11)

我遇到了这个问题。确实,app域是创建的而不是清理的。但是,我不建议恢复遗产。它们可以通过ReleaseSandboxAppDomain()进行清理。

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

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

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

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

答案 2 :(得分:4)

你可能想要

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

答案 3 :(得分:2)

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

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

ExecuteReportInCurrentAppDomain的工作方式相同。

感谢Social MSDN reference

答案 4 :(得分:1)

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

我玩弄了同样的事情:使用一个单独的应用程序域并将报告封送到它。我认为这是一个可怕的解决方案,很快就会搞得一团糟。

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

棘手的部分是将信息传递给单独的程序。使用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复制到同一个输出文件夹中。然后,您可以在不指定路径的情况下运行它,并且只使用可执行文件名(即:&#34; SomethingReports.exe&#34;)。您也可以从主程序中删除报告dll。

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

一旦你有了这个工作,看到你的常规节目的记忆在打印报告时保持不变是非常好的。出现报告程序,占用的内存比主程序多,然后消失,完全清理它,主程序占用的内存不会超过现有内存。

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

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

答案 5 :(得分:0)

我对此很迟,但是我有一个真正的解决方案,可以解释原因!

事实证明,这里的LocalReport使用.NET Remoting动态创建子appdomain并运行报表,以避免内部泄漏。然后,我们注意到,最终,该报告将在10到20分钟后释放所有内存。对于生成大量PDF的人来说,这是行不通的。但是,这里的关键是他们正在使用.NET 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);

您将看到内存使用率开始上升,然后在几秒钟之内应该看到内存使用率开始下降。用内存分析器花了我几天时间来真正跟踪下来并了解发生了什么。

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

希望这会有所帮助!

编辑

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