.NET应用程序如何减少内存使用?

114

有什么方法可以减少.NET应用程序的内存使用?考虑以下简单的C#程序。

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();
    }
}

x64 模式编译并在 Visual Studio 之外运行,任务管理器显示如下:

Working Set:          9364k
Private Working Set:  2500k
Commit Size:         17480k

如果仅为x86编译,效果会稍微好一些:

Working Set:          5888k
Private Working Set:  1280k
Commit Size:          7012k

我接着尝试了下面这个程序,它和之前的程序相同,但是在运行时初始化后尝试缩小进程大小:

class Program
{
    static void Main(string[] args)
    {
        minimizeMemory();
        Console.ReadLine();
    }

    private static void minimizeMemory()
    {
        GC.Collect(GC.MaxGeneration);
        GC.WaitForPendingFinalizers();
        SetProcessWorkingSetSize(Process.GetCurrentProcess().Handle,
            (UIntPtr) 0xFFFFFFFF, (UIntPtr)0xFFFFFFFF);
    }

    [DllImport("kernel32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetProcessWorkingSetSize(IntPtr process,
        UIntPtr minimumWorkingSetSize, UIntPtr maximumWorkingSetSize);
}

在 Visual Studio 之外的 x86 Release 环境下的结果:

Working Set:          2300k
Private Working Set:   964k
Commit Size:          8408k

这样已经好了一些,但是对于一个这么简单的程序来说,它仍然显得过于臃肿了。有没有什么技巧可以让C#进程变得更加精简?我正在编写一个主要在后台运行的程序。我已经将任何用户界面相关的操作都放在了一个单独的应用程序域中,这意味着用户界面相关的内容可以被安全卸载,但当它只是静态地驻留在后台时占用10MB内存似乎过多。

P.S. 至于为什么我会关心这个问题——(高级)用户倾向于担心这些事情。即使它几乎没有影响性能,半技术人员(我的目标受众)也往往会对后台应用程序的内存使用量感到恼火。甚至当我看到Adobe Updater占用11MB内存时,我也会感到惊慌,而当看到Foobar2000仅占用不到6MB内存甚至在播放时都极少消耗内存时,我会感到安心。我知道在现代操作系统中,这些东西在技术上并不那么重要,但这并不意味着它不会影响感知。


14
为什么你要关注呢?私有工作集非常低。如果内存不必要,现代操作系统会将其换出到磁盘上。现在是2009年。除非你正在嵌入式系统上构建东西,否则你不应该关心10MB。 - Mehrdad Afshari
9
停止使用.NET,程序会更小。加载.NET框架需要将许多大型的DLL文件加载到内存中。 - Adam Sills
2
内存价格呈指数下降趋势(是的,现在您可以订购一台拥有24GB RAM的戴尔家用电脑系统)。除非您的应用程序使用>500MB,否则优化是不必要的。 - Alex
38
@LeakyCode,我非常讨厌现代程序员们这样的想法,你应该关心应用程序的内存使用情况。我可以说,大多数现代应用程序,主要是用Java或C#编写的,当涉及资源管理时很不有效率。正因为如此,在2014年我们可以在Win95上运行的应用程序数量与1998年相同,只需要64MB的RAM...现在一个浏览器实例就占用2GB的RAM,简单的IDE大约需要1GB。内存便宜,但这并不意味着你应该浪费它。 - Petr
8
@Petr,你应该关注资源管理。程序员的时间也是一种资源。请不要过分概括浪费10MB到2GB。 - Mehrdad Afshari
显示剩余2条评论
9个回答

47

由于.NET应用程序需要加载运行时和应用程序本身,因此与本机应用程序相比,.NET应用程序的占用空间会更大。如果您想要非常整洁的应用程序,则.NET可能不是最佳选择。

然而,请记住,如果您的应用程序大部分时间处于休眠状态,那么必要的内存页面将被交换出内存,因此在大多数情况下不会对系统造成太大负担。

如果您想保持占用空间小,那么就需要考虑内存使用情况。以下是一些思路:

  • 减少对象数量,并确保不要将任何实例保留时间过长。
  • 注意 List<T> 和类似类型,它们在需要时会加倍容量,因此可能会导致高达50%的浪费。
  • 您可以考虑使用值类型而不是引用类型来在堆栈上强制使用更多的内存,但请记住,默认堆栈空间只有1 MB。
  • 避免使用超过85000字节的对象,因为它们将进入LOH(大对象堆),而LOH不是紧凑的,因此很容易被分段。

这可能并不是一个详尽无遗的列表,只是一些思路。


同样的技术可以用于减少本地代码大小,也可以用于.NET吗? - Robert Fraser
我猜有一些重叠,但是使用本地代码时,在内存使用方面你有更多的选择。 - Brian Rasmussen

36
  1. 你可能想要查看 Stack Overflow 上的问题 .NET EXE 内存占用
  2. MSDN 博客文章 工作集 != 实际内存占用 旨在揭开工作集、进程内存以及如何对总体内存占用进行准确计算的神秘面纱。

我不会说您应该忽略应用程序的内存占用 -- 显然,更小、更高效的内存占用通常是值得追求的。但是,您应该考虑实际需求。

如果您正在编写一个标准的 Windows Forms 和 WPF 客户端应用程序,该应用程序将在个人电脑上运行,并且很可能是用户操作的主要应用程序,则可以在内存分配方面更加随意。(只要所有分配都被释放。)

但是,针对一些在此处表示不必担心内存使用情况的人:如果您正在编写一个将在终端服务环境中运行、可能由 10、20 或更多用户共享的 Windows Forms 应用程序,则必须考虑内存使用情况。您将需要保持警惕。解决这个问题的最佳方法是通过良好的数据结构设计和遵循关于何时以及如何分配内存的最佳实践。


17

在这种情况下,您需要考虑CLR的内存成本。CLR会为每个 .Net 进程加载并考虑到内存问题。对于如此简单/小的程序,CLR的成本将主导你的内存占用。

构建一个真实的应用程序,并比较其成本与基准程序的成本,这将更有启发性。


8

没有具体的建议,但你可以看一下CLR Profiler(从Microsoft免费下载)。
安装完成后,请查看此how-to page

来自how-to页面:

本指南向您展示如何使用CLR Profiler工具调查应用程序的内存分配情况。您可以使用CLR Profiler识别导致内存问题的代码,例如内存泄漏和过度或低效的垃圾回收。


1
来自微软的更为实时的Profiler信息页面:https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/profiling/ - Gwyneth Llewelyn

7

建议查看一下“真实”应用程序的内存使用情况。

与Java类似,无论程序大小,运行时都存在一定量的固定开销,但在此之后,内存消耗将更加合理。


6

对于这个简单的程序,仍然有减少私有工作集的方法:

  1. 使用NGEN对您的应用程序进行编译。这可以从您的进程中删除JIT编译成本。

  2. 使用MPGO训练您的应用程序以减少内存使用,然后再使用NGEN。


3

有许多方法可以减少你的足迹。

.NET中,你将始终不得不面对的一件事是,IL代码的本地映像大小是巨大的。

而且这个代码不能在应用程序实例之间完全共享。即使NGEN'ed程序集也不是完全静态的,它们仍然有一些需要JITting的小部分。

人们也倾向于编写比必要的内存块更长时间阻塞内存的代码。

一个经常看到的例子:将Datareader加载到DataTable中,只是为了将其写入XML文件。你很容易遇到OutOfMemoryException。另一方面,你可以使用XmlTextWriter并滚动通过Datareader,当你通过数据库游标滚动时发出XmlNodes。 这样,你只有当前的数据库记录和它的XML输出在内存中。这将永远(或不太可能)获得更高的垃圾回收代数,因此可以被重复使用。

相同的道理适用于获取一些实例列表、执行某些操作(生成数千个新实例,可能会在某处保留引用),即使您之后不需要它们,也仍然将所有内容引用到foreach之后。明确地将输入列表和临时副产品设置为null意味着,即使在退出循环之前,这些内存也可以被重用。
C#有一个称为迭代器的优秀功能。它们允许您通过滚动输入来流式传输对象,并仅保留当前实例,直到获得下一个实例。即使使用LINQ,您仍然不需要保留所有内容,只是因为您想要过滤它。

1

回答标题中的一般问题而不是具体问题:

如果您正在使用返回大量数据(例如大型2xN双精度数组)的COM组件,但只需要其中的一小部分,则可以编写一个包装器COM组件,将内存从.NET隐藏,并仅返回所需的数据。

这就是我在我的主要应用程序中所做的,它显着改善了内存消耗。


0

我发现在长时间运行的进程中使用SetProcessWorkingSetSize或EmptyWorkingSet API来强制将内存页面定期写入磁盘,可能会导致机器上所有可用物理内存实际上消失,直到重新启动机器。我们将一个.NET DLL加载到本地进程中,该进程将使用EmptyWorkingSet API(替代使用SetProcessWorkingSetSize)在执行内存密集型任务后减少工作集。我发现,在1天到1周之间的任何时间,机器在任务管理器中显示99%的物理内存使用情况,而没有任何进程显示出使用任何重要的内存使用情况。不久之后,机器将变得无响应,需要进行硬重启。这些机器是超过2打的Windows Server 2008 R2和2012 R2服务器,运行在物理和虚拟硬件上。

也许将.NET代码加载到本地进程中与此有关,但使用EmptyWorkingSet(或SetProcessWorkingSetSize)需自担风险。也许只在应用程序初始启动后使用它一次。我已决定禁用该代码,并让垃圾回收器自行管理内存使用情况。


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