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

67

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

在运行时使用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 和问题无关。

有什么想法?


9
只是确认一下:只有.NET框架发生了变化。您是从32位机器转换到64位吗?您是从DEP切换到没有DEP吗?您是从PAE切换到没有PAE吗?您是从ngen切换到JIT吗?这些提示可以触发更多信息。 - sehe
1
这里甚至没有足够的信息来猜测。你是否使用了BlockingCollectionConcurrentQueue类?ConcurrentQueue存在内存泄漏问题,可能是一个问题所在。 - Jim Mischel
2
ConcurrentQueue 在 .NET 3.5 中不存在,因此这不可能是问题的原因。 - phoog
1
@phoog:实际上,ConcurrentQueue 在 .NET 的并行扩展中是存在的。从 3.5 转换到 4.0 可能涉及将其替换为 System.Collections.Concurrent。我知道我们在转换到 4.0 时做了这件事。 - Jim Mischel
1
@Jim Mischel:啊哈,感谢您纠正我。但是如果这些类正在使用中,OP不是必须更改引用和/或命名空间“using”语句吗? - phoog
显示剩余3条评论
6个回答

86

这是一个有趣的问题。

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

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

这解决了问题。


1
谢谢您的报告。很遗憾我们不能对这个问题进行多次投票,因为这是一个非常好的发现,值得在SO上得到良好的索引。 - sehe
2
我不确定你是如何找到这个修复方法的,但是太棒了! - Brian Dishaw
6
根据文档,ExecuteReportInCurrentAppDomain已被弃用。即使使用它,每次运行报表时,这些计算DLL都会被重新创建,然后无用但未卸载。我们发现唯一的解决方法是每次运行报表时生成一个新进程。当进程死亡时,所有内容都会被自动释放,包括不再需要的DLL。这只是在处理MS报告时要考虑的一个思路。 - Andy
4
如果将RDLC处理移动到不同的应用程序域以防止内存泄漏,为什么要将其移回当前应用程序域?这似乎是通过牺牲一个内存泄漏来换取另一个内存泄漏的权衡。 - Robert
4
这太疯狂了,人们怎么不为这个问题大声疾呼?它在.NET 4.5中仍然存在。 - Aaron Hudon
显示剩余9条评论

16

我遇到过这个确切的问题。而且应用程序域是被创建而不是清理的事实是正确的。然而,我不建议回退到传统方式。它们可以通过ReleaseSandboxAppDomain()进行清理。

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

我还做了其他一些清理工作:

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

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


2
谢谢,经过测试,您上面所说的方法有效,我发现报告不再泄漏了。 :) 这就是我具体做的。希望能对其他人有所帮助: localReport.SubreportProcessing -= reportDataProcessor.SubreportProcessingHandler; localReport.DataSources.Clear(); localReport.ReleaseSandboxAppDomain(); localReport.Dispose(); - Brian
1
这似乎对我没有任何改变。即使进行了所有建议的清理,仍然存在严重的内存泄漏问题。 - Jim
1
所以,这对我有效,但只有在通过app.config启用CAS时才有效。sues999,我想你已经打开了这个标志或者正在运行.NET 3.5或更早版本。ReleaseSandboxAppDomain()除非创建了沙箱应用程序域,否则不会执行任何操作,这需要CAS。默认情况下,启用CAS时,LocalReport将使用沙箱AppDomain,因此选择的答案发布者似乎是相反的。我预计在.NET 4.0中,默认情况下报告在当前应用程序域中运行,这就是为什么它泄漏的原因。只有应用程序域拆除似乎才能释放报告生成留下的东西。 - JonathanN

5

我来晚了,但是我有一个真正的解决方案,并可以解释为什么!

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

希望这可以帮助到你!

编辑

显然,在生成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。 - RMD
我给你点了个赞,因为你的回答很不错,只是不完整。 - RMD

2

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

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

ExecuteReportInCurrentAppDomain 的功能与之相同。

感谢这个社交MSDN参考文献


2
这种方法的问题在于,您无法在传统的 CAS 模型中使用动态类型 - 使用 ViewBag 将导致异常。 - pkmiec
是的,我们正在研究这个问题,并在Web版本上运行时遇到了以下异常:安全异常 描述:应用程序尝试执行安全策略不允许的操作。要授予此应用程序所需的权限,请联系系统管理员或更改配置文件中的应用程序信任级别。 异常详细信息:System.Security.SecurityException:请求失败。 - MikeG
2
我在基于ASP.NET的应用程序中遇到了相同的问题,使用动态类型进行序列化和反序列化。因此,我无法使用<trust legacyCasModel="true" level="Full"/>。Adrian Nichols在以下网址提供了帮助代码:https://github.com/AdrianNichols/ssrs-non-native-functions/blob/17ee83c9988acc638eb11f961caf0b2a6b77b555/SSRS_Demo/Business/reportHelper.cs关键字:RenderReportToMemoryAsPDFInAnotherAppDomain方法和ReportHelperInAppDomain类。 - Kiquenet

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文件复制到相同的输出文件夹中。然后,您可以不指定路径直接运行它,只使用可执行文件名(例如:"SomethingReports.exe")。您还可以从主程序中删除报表dll文件。
其中一个问题是,如果您从未发布过报表程序,那么您将会收到一个清单错误。只需虚拟发布一次以生成清单,然后它就可以正常工作了。
一旦您完成了这个步骤,当打印报表时,非常好的一点是您的常规程序的内存保持不变。报表程序出现,占用比您的主程序更多的内存,然后消失,完全清理掉您的主程序,使其不再占用更多内存。
另一个问题可能是,每个报表实例现在将占用比以前更多的内存,因为它们现在是完全独立的程序。如果用户打印了很多报表并且从未关闭它们,它将非常快地使用大量内存。但我认为这仍然比较好,因为只需关闭报表即可轻松地回收该内存。
这也使得您的报表独立于主程序。它们甚至可以在关闭主程序后继续保持打开状态,并且您也可以手动从命令行或其他来源生成它们。

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