【问题标题】:Memory Continues to Increase when Loading and Releasing NSImage加载和释放 NSImage 时内存继续增加
【发布时间】:2011-02-18 23:34:58
【问题描述】:

我有一个问题,即我的应用程序在连续加载图像文件时将内存大量消耗到“抖动点”。例如,考虑以下代码,它重复加载和释放 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)每隔几秒钟单击几次“标记堆”在 Instruments Allocations 中,然后 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”又回到了“Free”类别,这对我来说很奇怪。

我用两个图像(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<br> __TEXT       38%     33.84MB  80.45MB  42%   0%         0Bytes<br> *Dirty*     32%     28.23MB  114.99MB 24%   100%       17.11MB
MALLOC_LARGE 14%     13.25MB  13.25MB  100%  0%         4KB<br> Carbon       11%     9.86MB   9.86MB   100%  20%        3.46MB<br>VM_ALLOCATE  9%      8.43MB   48.17MB  18%   49%        8.43MB<br> ...

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

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

【问题讨论】:

  • 您是否尝试过在每次for 迭代开始时创建一个自动释放池,并在迭代结束时将其排空?
  • 您找到解决方案了吗?我想我面临着类似的问题stackoverflow.com/questions/8754124/…
  • (我已经很久没有对此发表评论了)。不,我还没有找到任何解决方案。我最好的猜测是我只是在观察操作系统如何处理内存。由于没有任何泄漏,我认为这不是我可以控制的。

标签: cocoa load memory-management nsimage


【解决方案1】:
  • Bavarious 所说的;你试过用NSAutoreleasePool包围它吗?

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

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

  • 这是Heapshot analysis 的主要候选人。


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

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

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

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

【讨论】:

  • 感谢 Bavarious 和 bbum。添加 NSAutoreleasePool 缓解了迭代内存增长。请参阅我的问题的 FOLLOW-UP 部分,该部分解决了 NSAutoreleasePool 似乎无法解决的另一个问题。进一步的帮助将不胜感激。 (我不确定是否可以编辑我的原始帖子,但我不能在这里用 600 个字符说出所有内容)。
  • +1 Heapshot 绝对是一个受欢迎的工具,非常适合这类问题。
  • 我运行了 Allocations 并进行了 Heapshots,结果显示没有堆增长。但是,“Real Mem”仍然没有减少。有关详细信息,请参阅我的问题中的 FOLLOW-UP #2。再次,我非常感谢任何进一步的帮助。
  • 我查看了 Instruments Allocations 中的 vmmap 和 VM Tracker 数据。请在 FOLLOW-UP #3 中查看我的结果。再次感谢您迄今为止的帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-06-30
  • 1970-01-01
  • 2016-08-17
  • 2011-06-10
  • 2018-12-04
  • 1970-01-01
  • 2021-08-09
相关资源
最近更新 更多