2

我有一个问题,即我的应用程序通过连续的图像文件加载将内存大量消耗到“颠簸点”。例如,考虑以下代码,它重复加载和释放 15MB JPEG 文件(用于测试的大文件):

NSURL *inputUrl = [NSURL URLWithString:@"file:///Users/me/Desktop/15MBjpeg.jpg"];  
for(int i=0; i<1000; i++) {  
    NSImage *image = [[NSImage alloc] initWithContentsOfURL:inputUrl];  
    [image release];  
}

由于有大量可用的系统内存,它在前几个中执行得很快,但最终系统在我所说的“临界点”处屈服。在这里,我相信系统释放的内存刚好足以加载下一张图像,因此性能最终会变慢。此外,现在其他应用程序运行缓慢,因为系统必须释放这些占用但现在未使用的内存。

对我来说有意义的是,它是否会分配内存然后让系统释放它,以便我在 Activity Monitor 中的“Real Mem”统计保持较小,而不是通过连续的加载/释放迭代进入千兆字节。在实践中,我可能永远不会在这一点上结束,但是当任何时候所需的实际常驻内存通常很小时,我的活动监视器“Real Mem”统计数据最终超过所有其他应用程序似乎很奇怪。我最初认为这是某种内存泄漏或缓存问题,但它似乎与应用程序中的积极内存分配和系统上的惰性内存释放更相关(如果操作系统有该策略,则不是有什么问题——如果是这样的话事实上它的工作方式)。也许我完全错过了一些东西。

有没有更好的方法来重复加载图像而不会出现这种占用和非主动缓解内存使用行为的情况?也许有一种方法可以强制降低应用程序的内存占用,或者有一种方法可以更智能地了解如何将图像加载到相同的对象或内存位置?我的目标是加载图像,处理它(获取缩略图,更改图像格式等),在内存中删除它,然后再做一次——所有这些都没有观察到内存增长。

--

跟进:

巴伐利亚,谢谢。包装 NSAutoreleasePool 确实解决了加载相同文件时的迭代内存增长:

NSURL *inputUrl = [NSURL URLWithString:@"file:///Users/me/Desktop/15MBjpeg.jpg"];  
for(int i=0; i<1000; i++) {  
    NSAutoreleasePool *apool = [[NSAutoreleasePool alloc] init];
    NSImage *image = [[NSImage alloc] initWithContentsOfURL:inputUrl];  
    [image release];
    [apool drain];
}

但是,它并没有解决在图像被释放(并且 NSAutoreleasePool 耗尽)后内存保持增加的问题。例如,当加载我的 15MB JPEG 图像时,“Real Mem”内存从 8MB 稳定状态跳到大约 25MB,然后停留在那里。(我的应用程序只有一个 Interface Builder 按钮,该按钮具有一个连接 IBAction 的方法,该方法只调用我复制的 for 循环)。我希望在 for 循环完成后(或者如果只有一个图像被加载和释放),“Real Mem”统计数据将减少回名义应用程序级别的内存使用量。

调用 NSImage 功能时也可以加载后台的其他内容似乎是合理的,这可以增加内存。但是,不同大小的图像(15MB、30MB、50MB 等)会按比例增加应用程序的内存,这让我相信它不仅仅是这样的分配。

此外,如果我尝试连续加载单独的图像(例如,15MBjpeg-1.jpg、15MBjpeg-2.jpg 等),内存有时会为每个加载的新图像复合。例如,如果连续加载两个图像,则加载/释放后应用程序的“Real Mem”内存使用量现在大约为 50MB,并且根据我的观察,它永远不会减少。加载后续图像时,此行为会继续存在,因此应用程序在加载多个大图像后可能会占用数百 MB 的“Real Mem”内存。

有趣的是,如果我一遍又一遍地重新加载相同的图像,稳态内存不会增加。这是否表明正在进行某种缓存?同样,我的目标是在不增加内存的情况下对几个不同的图像文件进行批处理。提前致谢。

哦,我正在研究 Heapshot Analysis 文章 ATM,但至少想发布我的进度并查看是否有其他输入。

--

跟进#2

bbum,感谢您的精彩文章。我用我的测试程序运行 Instruments Allocations 并没有发现任何堆增长。在引用的博客文章中,我的方法是,1)单击我的 Interface Builder 界面上的“加载和释放图像”按钮(它调用加载/释放行为),2)每隔几秒钟单击几次“标记堆”在仪器分配中,然后 3) 重复 1) 和 2)。

使用这种方法,随着时间的推移,Heapshots 在 Heap Growth 列中始终报告 0 字节(每 5 秒单击 3 次,持续 15 秒),这意味着没有从基线 Heapshot 分配额外的内存。另外,在Statistics面板中,每次点击“Load and Release Image”按钮都会有一个13.25MB的Malloc,但是Live Bytes是0 Bytes,这意味着它已经被完全释放了。如果我三次单击“加载和释放图像”按钮,图像的总字节数报告为 39.75MB (3 * 13.25MB),这意味着 39.75MB 已分配,但由于实时字节数为 0 已完全释放。分配图快速上升并立即回落,因为这是一个非常快速的操作。内存的稳态使用没有泄漏,也没有增长,这一切似乎都是有道理的。

但是,现在我的“Real Mem”统计数据仍然很高怎么办?我知道活动监视器不是调试内存问题的标准。但是,直到我关闭程序,“Real Mem”仍然保持高位,然后“Real Mem”全部回到“免费”类别,这对我来说很奇怪。

我用两个图像(15MBjpeg-1.jpg、15MBjpeg-2.jpg)测试了同样的方法,只是用同样的方法复制代码,我再次观察到没有堆增长。显然,更多的分配被分配和释放。然而,现在“Real Mem”的增幅大约是仅加载和释放一张图像时的两倍。同样,它不会在测试程序的稳定状态下减少。

还有什么我可以做的吗?以下是单个图像加载/释放的测试代码,任何人都想尝试一下(只需将 IB 按钮连接到 openFiles):

#import "TestMemory.h" // only declares -(IBAction) openFiles:(id)sender;
@implementation TestMemory
-(IBAction) openFiles:(id)sender {
    NSURL *inputUrl = [NSURL URLWithString:@"file:///Users/me/Desktop/15MBjpeg.jpg"];
    NSAutoreleasePool *apool = [[NSAutoreleasePool alloc] init];
    NSImage *image = [[NSImage alloc] initWithContentsOfURL:inputUrl];
    [image release];
    [apool drain];
}
@end

谢谢阅读。:)

--

跟进#3

bbum,不,我没有启用垃圾收集(GC 在项目设置中设置为不支持)。我使用 vmmap 和 Instruments Allocations 中的 VM Tracker 栏调查了内存。当您说 VM Instruments 时,我假设您指的是 VM Tracker 数据,因为它报告的信息与 vmmap 相同。使用我的“加载和释放图像”按钮打开单个图像后,一些重要的内存数量包括以下内容(来自 VM Tracker):

Type         %ofRes  ResSize  VirtSize Res%  %AllDirty  DirtySize
__TEXT       38%     33.84MB  80.45MB  42%   0%         0Bytes
*Dirty*     32%     28.23MB  114.99MB 24%   100%       17.11MB
MALLOC_LARGE 14%     13.25MB  13.25MB  100%  0%         4KB
Carbon       11%     9.86MB   9.86MB   100%  20%        3.46MB
VM_ALLOCATE  9%      8.43MB   48.17MB  18%   49%        8.43MB
...

有趣的是,单个图像的后续加载/释放将Dirty和 VM_ALLOCATE 类型的“驻留大小”增加了 ~.3MB,并且这些类型的“Dirty Size”也随着时间的推移而增加。(VM_ALLOCATE 似乎是Dirty的一个子集)。随后的加载/发布似乎没有其他类型发生变化。

我不确定究竟要从这些数据中拿走什么,或者我如何使用它来让程序释放内存。看起来 VM_ALLOCATE 类型可能是没有被释放的块,但这只是推测。底层 NSImage 初始化实现的某些部分是否有可能保存图像文件的缓存并且不会释放它?同样,如前所述,与第一次加载/释放相比,同一图像文件的每次后续加载/释放几乎不消耗资源(CPU、磁盘研磨等)和挂钟时间,这让我很感兴趣。提前致谢。

4

1 回答 1

2
  • 巴伐利亚说了什么;你试过用NSAutoreleasePool.

  • 这是一个经典的微基准。虽然它肯定表明存在问题,但问题实际上可能是基准与预期的现实世界模式差异太大,以至于错误在于基准。

  • 在高效设计的应用程序中,它不会多次从磁盘读取相同的图像数据。

  • 这是Heapshot 分析的主要候选对象。


(感谢您的跟进;有帮助!)

您描述的症状听起来像是 VM 泄漏(某些东西正在消耗地址而不进行分配;例如,映射内存)或没有被修剪的缓存(包含 VM 分配)。

  • 你启用了 GC 吗?如果是这样,这很可能是因为没有触发 GC 阈值。收集器不知道将来自非 GC 的真正大分配记入 GC 区域。如果您强制收集,它将解决这个特殊的边缘情况。

  • 尝试查看 VM 工具或在命令行中使用 vm_map 来查看在您的应用程序中消耗地址空间的内容。

于 2011-02-19T05:19:44.247 回答