您正在使用这种方法攻击垃圾收集器。在循环中加载 15-40mb 的对象总是会引发 OutOfMemoryException。这是因为对象直接进入大对象堆,所有大于 85K 的对象都这样做。大对象立即成为第 2 代对象,并且从 .Net 4.5.1(您请求它)开始,内存不会自动压缩,并且在早期版本中根本不会被压缩。
因此,即使您最初加载对象并且应用程序继续运行,这些对象也很有可能会挂起,即使完全取消引用,也会将大对象堆碎片化。一旦发生碎片,例如用户关闭控件一两分钟做其他事情并再次打开控件,很可能所有新对象都无法插入 LOH - 分配发生时内存必须是连续的。出于性能原因,GC 在 Gen 2 和 LOH 上运行收集的频率要低得多 - memcpy 由 GC 在后台使用,这在较大的内存块上很昂贵。
此外,如果您从正在使用的控件引用所有这些图像,则不会释放所消耗的内存,想象一下选项卡。这样做的整个想法是错误的。根据用户需要使用缩略图或加载全尺寸图像,并注意消耗的内存。
更新
与其告诉你应该做什么和不应该做什么,我决定尝试帮助你做到这一点:)
我编写了一个小程序,它在一个包含 440 个 jpeg 文件的目录上运行,总大小为 335 兆字节。当我第一次运行您的代码时,我得到了 OutOfMemoryException 并且表单仍然没有响应。
第 1 步
首先要注意的是,如果您要编译为 x86 或 AnyCpu,则需要将其更改为 x64。右键单击项目,转到构建选项卡并将目标平台设置为 x64。
这是因为在 32 位 x86 平台上可以寻址的内存量是有限的。所有 .Net 进程都在虚拟地址空间内运行,CLR 堆大小将是操作系统允许的任何进程,并且实际上不在开发人员的控制范围内。但是,它将分配尽可能多的内存 - 我在 64 位 Windows 8.1 上运行,因此更改目标平台为我提供了几乎无限量的内存空间可供使用 - 直到物理内存的限制,您的进程将被允许.
执行此操作后,您的代码不会导致 OutOfMemoryException
第 2 步
我将目标框架从 VS 2013 中的默认 4.5 更改为 4.5.1。我这样做是为了可以使用 GCSettings.LargeObjectHeapCompactionMode,因为它仅在 4.5.1 中可用。我注意到关闭表单需要很长时间,因为 GC 正在做大量的工作来释放内存。基本上我会在 loadPics 代码的末尾设置它,因为它允许大对象堆在下一次阻塞垃圾收集时不会碎片化。我相信这对您的应用程序至关重要,因此如果可能,请尝试使用此版本的框架。您也应该在早期版本上对其进行测试,以查看与您的应用交互时的差异。
第 3 步
由于应用仍然没有响应,我让代码异步运行
第 4 步
由于代码现在在与 UI 线程不同的线程上运行,因此在访问表单时会导致 GUI 跨线程异常,因此我不得不使用 Invoke 将消息从代码的线程发回 UI 线程。这是因为 UI 控件只能从 UI 线程访问。
代码
private async void button1_Click(object sender, EventArgs e)
{
await LoadAllPics();
}
private async Task LoadAllPics()
{
IEnumerable<string> files = Directory.EnumerateFiles(@"C:\Dropbox\Photos", "*.JPG", SearchOption.AllDirectories);
await Task.Run(() =>
{
foreach(string file in files)
{
Invoke((MethodInvoker)(() =>
{
PictureBox pic = new PictureBox() { Image = Image.FromFile(file) };
this.Controls.Add(pic);
}));
}
}
);
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
}