这个问题是我的同学提出来的,原帖在http://bbs.csdn.net/topics/390933411
大概是这样:
- (IBAction)touchToCreateThread:(id)sender {
int i=10000;
NSString * data;
dispatch_queue_t queue = dispatch_queue_create("com.wang.queue", NULL);
while(i>0)
{
data = [[NSString alloc]initWithFormat:@"%i",i];
NSString * str = data;
dispatch_async(queue, ^{{
[self print:str];
}});
i--;
}
}
-(void) print:(NSString*)str
{
NSLog(@"%@",str);
str = nil;
}
执行方法前:
执行方法后:
进程中线程变化:
结果是这样子的,最后内存就卡在了60MB左右(这是只点一次button的情况,如果你在线程调度过程中再多点几次button,内存会升得更高),意思就是系统给这个进程分配了大量的空间,最后都没有回收,但是不应该这样啊!苹果不是有ARC吗?
既然ARC不管用了,让我们试试手动release吧,把ARC机制关闭后,执行如下代码:
- (IBAction)touchToCreateThread:(id)sender {
int i=10000;
__block NSString * data;
dispatch_queue_t queue = dispatch_queue_create("com.wang.queue", NULL);
while(i>0)
{
dispatch_async(queue, ^{{
data = [[NSString alloc]initWithFormat:@"%i",i];
[self print:data];
[data release];
}});
i--;
}
}
还是一个样,内存卡在了天上。
在我们的想象中,应该是系统建立线程分配给该进程执行所需的任务——这些都没错,都实际地运行了,但是为什么在线程调度完毕后,大量的数据没有被销毁呢?
让我们更深入地去了解一下:
按照代码,我们可以这么理解,因为使用了dispatch_async,也就是线程调度中的block不是随着方法顺序执行的,而是在10000次循环中不断地往系统调度队列中添加新的预调度代码块(block),也就是系统的预调度队列中新增了10000个块,并且这些块全都由queue线程来执行,一旦当前的这个方法执行完毕,当系统开始调度queue线程的时候,之前加入到预调度队列中的block就会被执行,因为我们没有设定调度方式,所以系统默认了serial,但是改为concurrent,也就是系统会并发地执行这些推入到预调度队列的block罢了,不影响结果,而且这不是这篇文章的重点,不再赘述!
而且,[data release]似乎没有像我们想象中的那样,把增加的内存给释放了,这是为什么呢?原因就在于,执行该block时候的data与在上述方法中的data并非同一个,data会被先行备份!所以你没法在不给data加上__block关键字的时候在block内(也就是多线程执行时)修改data的值,接下来属于我的妄加猜测,为变量增加__block关键字的值后,一旦在block内部修改了block外的变量,系统会直接对外部的这个变量进行对应操作,而非对被缓存的数据进行操作,也没法对这被缓存的数据进行操作,要是我懂看汇编码的话,也许可以依靠暂停来看一下汇编码,但是不会看只好作出这样的猜测,未必对,但是是挺合理的。
至此,我们可以明白了一点,dispatch_async,会将一个block中的所有用到的变量都进行备份保存(到内存),上述的第一次操作中,新建block缓存了block内的数据,但是由于我们建立了一万个这样的block给系统调度,就会造成大量的缓存垃圾,并且这些block需要先被备份,生成block本身也会造成少量开销,要验证这一点,只需把第一段代码中dispatch_async的block里的内容去掉就可以了,就是不断地把塞一个空的block到系统预调用队列中,同样会增加不可消的存储空间,10000个空block,大概产生1-2M(未必准确,反正差不多)的内存垃圾。而且这些产生过的垃圾(包含block本身的固定开支以及block中的内容),并不会消失。
(提示:上述的预调用队列指的也就是queue线程,queue线程中本身会有一个队列,存储了所有将被调用的栈,然后当下次系统调用选择到queue线程的时候,会根据queue的本身属性(serial或concurrent)来调用queue线程的存储空间上保存着的block,serial当然是FIFO,concurrent就是并发了嘛,然后系统调用又对应了一堆操作系统上所有进程所开出的线程,不断的在这些线程在来回调用,终有一时会调用到queue)
也就是说,系统在执行完这些被丢入到预调用队列中的block后,所产生的缓存就相当于分配给进程了,再也不回收。这样并非没有好处,因为当你点击按钮,等待block被执行完后,你尝试再点击按钮,内存只有少量的增长(1-2M),这说明,之前产生的内容缓存又被使用了,而那少量的增长则是新建block所产生的缓存,至于这个缓存为什么没有消失,我也解释不清,一方面是因为生成block的开销很低,一万个就1-2M(而我们很少会同时往一个线程里同时塞如此多的block),我目前的理解就是系统不会回收这个小缓存——也就是一个bug,不过bug的前提是你往一条线程里塞了一万个待处理block!并且,可以试试把while循环去掉,然后猛点button,然后点个十几次过后(还是二三十次),内存也会有少量的增长,也就是说,一个个block的塞到一个线程里面,这些个生成block产生的开销,也不会消失!
由此,苹果操作系统的线程调用只避免了大垃圾——也就是block内容的缓存可以被复用,而小垃圾——生成block的开销并没有处理!
而假如,我们将循环放进queue线程里执行的话,点击一次button,内存变化并不会怎么增长。
- (IBAction)touchToCreateThread:(id)sender {
__block int i=10000;
__block NSString * data;
dispatch_queue_t queue = dispatch_queue_create("com.wang.queue", NULL);
dispatch_async(queue, ^{{
while(i>0)
{
data = [[NSString alloc]initWithFormat:@"%i",i];
[data release];
i--;
}
}});
}
因为这次我们只往queue线程中塞了一个block!而它需要备份的block只有一个,所以占不了什么内存空间!
其实,这些缓存垃圾并非无法回收,也是有法子的,不过你没法控制,这些缓存垃圾隶属于那个执行它们的那条线程,一旦当系统回收这条线程,那么这些垃圾也会被清掉。来,让我们做一个小实验!
我们把最开始的代码修改成这个样子:
- (IBAction)touchToCreateThread:(id)sender {
int i=10000;
__block NSString * data;
while(i>0)
{
dispatch_queue_t queue = dispatch_queue_create("com.wang.queue", NULL);
data = [[NSString alloc]initWithFormat:@"%d",i];
dispatch_async(queue, ^{{
[self print:data];
}});
i--;
}
}
产生了大量的线程,而且它们都同ID,并且有一个奇怪的现象!你在中间过程暂停程序,发现只有514条线程,而且你继续运行程序,稍过一会(不要太久,太久程序就执行完了,线程就自动挂了),还是514条线程,这难道是一个上限值吗?关于这点,我们稍后再议
现在我们来看内存,
涨了涨了涨了!至今都没有问题,但是我们讨论的是线程消亡时线程所拥有的缓存也会被清理掉,让我们再等等,等程序执行完毕,内存又会有一个小的降幅:
在我看来(未必正确,希望得到大神指点!),这个原因,大致如此,系统的资源有限,不可能你一个进程想开多少条线程就开多少条线程,而苹果上面显示出来的线程数量应该是进程中正在执行任务的线程数量,而514很有可能就是当前情况下的单个进程所能分配到的线程的最大值,一旦要求大于最大值,系统应该是做了拖延的操作,然后后面的操作继续复用这些线程来进行执行,生成线程的开销很小,所以514条线程不会令内存产生多大的变化,而当前情况和最初那个一条线程10000个block的情况所造成的内存变化在数值上是差不多的,这么说来,这一大堆的内存主要是线程缓存block时所产生的。
好的,那么再来解释一下最后这种内存占用减少的情况的原因,原因很简单,在我看来,还是因为系统分配资源不能太多,不能一直处于最大值点,所以当进程开了如此之多的线程后,系统觉得不合适,就向进程回收了一部分的线程,如之前所说,线程缓存block所造成的内存占用是会随线程被系统回收而被清理掉。