objective-c 实际回收内存资源时间是由系统决定的

来源:互联网 发布:网络歌手好听的歌2017 编辑:程序博客网 时间:2024/06/05 18:44

今天面试的一个有争议话题:面试官说autoreleasepool中的对象在下一次的事件环之前进行内存的回收。这是错误的


退出autoreleasepool之前,系统只是对所有的对象发送release消息。

内存的实际回收既不是在退出autoreleasepool之后,也不是在下一次事件循环之前。


实际的回收时间是由系统决定的。操作系统根据资源(CPU)使用的状况,绝定是否回收实际的内存。

autoreleasepool退出之前只是将所有对象的引用计数减一。

 

如果CPU不是很繁忙,系统根据运行状况会在下一次事件环执行前,实际回收内存。

这只是在instrumen工具调试时系统展现出来的性质。


我们现在联机的情况下做如下的测试:


1 执行下面代码,内存持续走高,CPU使用率到了90%。

for (int i= 0; i < 1100; i++) {

    [self justAlloc];

}

-(void) justAlloc{

    dispatch_async(dispatch_get_main_queue(), ^{

        @autoreleasepool {

            for (int i= 0; i < 10; i++) {

                ssViewController * sdfsf =   [[ssViewController allocinitWithNibName:@"ssViewController"bundle:nil];

                NSLog(@"%@", sdfsf.view);

            }

        }

    });

}

2 执行下面代码,内存基本稳定,CPU使用率在20%以下。

for (int i= 0; i < 1100; i++) {

    [self performSelector:@selector(justAlloc) withObject:nil afterDelay:i/10];

}

3 执行下面代码,内存比2有所增加单基本稳定,上下有所变动,CPU使用率在80%以下。

for (int i= 0; i < 1100; i++) {

      [self performSelector:@selector(justAlloc) withObject:nil afterDelay:i/100];

}



4 执行下面代码,内存持续提升,CPU使用率在90%以上。

for (int i= 0; i < 1100; i++) {

    [self performSelector:@selector(justAlloc) withObject:nil afterDelay:i/100];

}


上面四个实验说明,内存的具体回收是由系统决定的,runloop对回收的时刻是没有绝对的控制的。

只有在系统系统资源不是特别繁忙时,runloop的完当前handler执行完毕时系统会立刻着手于内存资源的回收。

在CPU使用率到90%以上时,可以观察到一个有趣的现象:程序执行完毕,CPU降下来后,系统内存的峰值几乎保存不动了。



下面我们做一个更为极端的测试,来看看为什么这种现象出现,系统到底干了什么?

5 下面代码使内存迅速升到500M产生内存错误!这是block过多导致的。

for (int i= 0; i < 1000*1000; i++) {

    [self performSelector:@selector(justAlloc) withObject:nil afterDelay:0];

}

6下面代码使内存上到550M后,开始出现了磁盘的读写,随后内存下降到500左右。运行一段时间后,也是出现了内存错误。

for (int i= 0; i < 100 *1000; i++) {

        [self performSelector:@selector(justAlloc) withObject:nil afterDelay:0];

    }

7  下面代码运行可真长结束,峰值在85M,结束后峰值回到65M左右保持不变。同样,运行结束后并没有出现内存峰值的完美降低。

 for (int i= 0; i < 10*1000; i++) {

        [self performSelector:@selector(justAlloc) withObject:nil afterDelay:0];

    }

8   下面代码运行可真长结束,峰值在10.4M,结束后峰值回到8.5M左右保持不变。同样,运行结束后并没有出现内存峰值的完美降低。

 for (int i= 0; i < 1*1000; i++) {

        [self performSelector:@selector(justAlloc) withObject:nil afterDelay:0];

    }

上面5中提到block过多直接导致内存错误,以emptyBlock函数替换justAlloc。

5‘,可以发现5还是内存上升到500后产生内存错误。

6’,可视内存升到455M左右,结束运行,并是内存保持在455M,没有任何的降低。粗略计算一下每一个block的代价: (455-3)* 1000 K / (100*1000) = 4.52K。

7‘,可视内存升到50M左右,结束运行,并是内存保持在48.1M,没有任何的降低。粗略计算一下每一个block的代价: (48.1-3) * 1000 K / (10*1000) = 4.51K。

8',以1*1000次循环:结束运行,并是内存保持在3.9M,没有任何的降低。粗略计算一下每一个block的代价:( 7.2-3)* 1000 K / (1*1000) = 4.2K。

-(void) emptyBlock{

    dispatch_async(dispatch_get_main_queue(), ^{

    });

}

对比 7 7’   : 65 - 48.1 = 7M

对比 8 8’   : 8.5 - 7.2 = 0.7M

7 == 0.7 * 10  发生了什么?是巧合吗?正好是循环相差的次数。

在7中峰值从85降到65M说明系统运行结束后确实进行了内存的回收,但是,去除BLOC的影响,还有每次运行0.7* 1000K/ 1000 = 0.7K的内存空间没有释放。


为了验证消除偶发性,我们现在再去以50*1000次做实验 9 9‘:

9 峰值420M  降低后峰值297.2M, 在7中 85 * 5 = 425M  65*5 = 325M   。基本符合  5 被的关系。 

9’ 峰值229.3M , 在7‘ 中 48.1 * 5 = 240.5M   。基本符合  5 被的关系。   block代价: 229.3 M / (50 * 1000) = 4.58 K

分配对象对内存回收后的影响: 297.2M - 229.3M = 68M      7M * 5 = 35 M  出现较大的偏差。


以20*1000次做实验 10  10‘:

10 峰值167M  降低后峰值122.6M, 在7中 85 * 2 = 170M  65*2 = 130M   。基本符合  2 被的关系。

10’ 峰值93.4M , 在7‘ 中 48.1 * 2=96.2M   。基本符合  2 被的关系。   block代价: 93.4M / (20 * 1000) = 4.67 K

分配对象对内存回收后的影响: 122.6M - 93.4M = 29.2M      7M * 2 = 14 M  出现较大的偏差。


以30*1000次做实验 11  11‘:

11 峰值247M  降低后峰值181M, 在7中 85 * 3 = 255M  65*3 = 195M   。基本符合  3 倍的关系。

11’ 峰值138.5M , 在7‘ 中 48.1 * 3=144.3M   。基本符合  3 被的关系。   block代价: 144.3M / (30 * 1000) = 4.81 K

分配对象对内存回收后的影响: 181M - 138.5M = 42.5M      7M * 3 = 21 M  出现较大的偏差。


   循环次数    1000     10 * 1000     20 * 1000     30 * 1000     50* 1000     40* 1000

  对象影响    0.7 M       7M            29.2              42.5              68                 ?


以40*1000次做实验 12  12‘:

12 峰值332M  降低后峰值238.6M, 在7中 85 * 4 = 340M  65*4 = 260M   。基本符合  3 倍的关系。

12’ 峰值183.6M , 在7‘ 中 48.1 * 4=192.4M   。基本符合  3 被的关系。   block代价: 183.6M / (40 * 1000) = 4.59 K    

分配对象对内存回收后的影响: 247.3M - 183.6M = 63.7M      7M * 4 = 28 M  出现较大的偏差。

12''  去掉dispatch 时,对象峰值为 170M ,  结束后稳定在78M  。可以看出峰值正好比12少了block的开销63M。


40*1000与50*1000差距非常小,是否是在当前环境下,这是可运行的最坏情况呢?

当取70*1000 时 

13  85 * 7 = 595M  达到了600 估计会崩溃。但实验表明运行中内存稳定在460M的峰值以下,并同时出现了磁盘的读写,结束后下降到了350M的。

    可知当系统太忙了同时可用内存将好近时,系统放弃了回收内存的实际操作,而选择了虚拟内存机制。

13‘  峰值为319.4           block代价: 319.4 / (70 * 1000) = 4.57 K    


当取60*1000 时 

14  85 * 6 = 510M  在崩溃好近内存边缘。但实验表明运行中到达峰值在490M时下,出现了磁盘的读写,结束后下降到了350M。

14‘  峰值为274.2           block代价: 274.2 / (60 * 1000) = 4.57 K    


以上数据是连接调试的结果。联调会有很多附加的性能要求和资源要求。

在使用instruments时,有可能得到不同的数据但是趋势是基本一致的。

在下一篇文章中我会给出使用instruments时的实验数据及其分析。同样instruments的使用也会很大的耗费资源。


从以上的实验我们可以得出以下的实验性结论:

1 oc中的对象释放与内存的回收是两回事。
2 autoreleasepool实际回收内存资源时间是由系统决定的。NSRunLoop当前事件执行完毕,下一次事件开始之前并不一定去做实际的内存回收操作。
3 block线程中放入过多的block,在内存耗尽是会产生error,而不会启用虚拟内存机制。
4 当其他已释放对象过多而耗尽系统内存时,当CPU过于忙碌,系统会启用虚拟内存机制。

5 很有可能内存回收机制要比虚拟内存复杂的多,费时的多。

6 向队列dispatch block的代价是很高的。4.57 K的内存损耗。



使用instrument下实验

-(void) justBlock{

    dispatch_async(dispatch_get_main_queue(), ^{

    });

}

-(void) justAlloc{

    dispatch_async(dispatch_get_main_queue(), ^{

        @autoreleasepool {

            for (int i=0; i <10; i++) {

                @autoreleasepool {

                ssViewController * sdfsf =   [[ssViewController alloc] initWithNibName:@"ssViewController" bundle:nil];

                NSLog(@"%@", sdfsf.view);

            }}

        }

    });

}

15 内存升到600M崩溃,persist对象的数量达到9,919,313个,而transient对象的数量只有4037个。被释放对象的内存并没有被回收!


for (i= 0; i < 1000 * 1000 * 1000; i++) {

       [self justBlock];

}


16  内存峰值为60M,其中VM很小,多次运行显示,有时在运行期间transient对象的数量有所增加,在运行结束后能够对已释放的对象进行干净的内存回收。


for (i= 0; i < 1000 * 1000; i++) {

       [self justBlock];

}


注意这里有个很重要的不同,连接调试在运行结束后并不触发内存回收机制,而使用instrument运行结束会触发内存回收机制。


17修改justBlock  峰值123M  , persist object 在长时间内存维持在100M左右,transient对象数量持续增加。

最后persist 对象占用内存在17.3稳定运行结束。

同样,在内存达到峰值前,transient对象数据几乎不变。

我们看到,加入内存使用代码对性能的巨大影响。这是科学方法论中有很深刻的讨论。

同时,联机调试反应出来的机制与instrument的是一致的:系统繁忙时,直到到达峰值前,不会进行实际内存的回收。

甚至autorelease 尾部的减一操作,在何时执行的我们也是能确定的,但是这在程序内是无法测量的。我们只知道,在语法上,它在尾部执行了减一操作。




-(void) justAlloc{

    __weaktypeof(self) wself =self;

    dispatch_async(dispatch_get_main_queue(), ^{

        @autoreleasepool {

            for (int i=0; i <10; i++) {

                @autoreleasepool {

            }}

        }

        double freeM = [wselfavailableMemory];

        double usedM = [wselfusedMemory];

        if (usedM > wself.usedM) {

            wself.usedM = usedM;

            wself.freeM = freeM;

        }

        wself.freeLbl.text = [NSStringstringWithFormat:@"%d M", (int)wself.freeM];

        wself.usedLbl.text = [NSStringstringWithFormat:@"%d M", (int)wself.usedM];

    });

}

// 获取当前设备可用内存(单位:MB

- (double)availableMemory

{

    vm_statistics_data_t vmStats;

    mach_msg_type_number_t infoCount =HOST_VM_INFO_COUNT;

    kern_return_t kernReturn =host_statistics(mach_host_self(),

                                               HOST_VM_INFO,

                                               (host_info_t)&vmStats,

                                               &infoCount);

    

    if (kernReturn !=KERN_SUCCESS) {

        returnNSNotFound;

    }

    return ((vm_page_size *vmStats.free_count) /1024.0) / 1024.0;

}


// 获取当前任务所占用的内存(单位:MB

- (double)usedMemory

{

    task_basic_info_data_t taskInfo;

    mach_msg_type_number_t infoCount =TASK_BASIC_INFO_COUNT;

    kern_return_t kernReturn =task_info(mach_task_self(),

                                         TASK_BASIC_INFO,

                                         (task_info_t)&taskInfo,

                                         &infoCount);

    

    if (kernReturn !=KERN_SUCCESS

        ) {

        returnNSNotFound;

    }

    return taskInfo.resident_size /1024.0 /1024.0;

}


 断开instrument,直接在机器上测试:  执行完毕驻留内存达到131M  。 

现在,CPU闲了下来,但系统仍没有做内存的回收。

只有在使用instrument的情况下,系统才能做到在不忙的时候去回收内存。


要认清内存的释放、回收与objective-c中的引用计数清零的区别。

内存的事向来都是操作系统的事。应用层只是告诉操作系统是需要新的资源,还是使用完毕你可以拿回去了。

至于何时做、整么做,对于引用计算范畴内的应用是透明的。


现在又出现一个问题,在非引用计算范畴内是什么情况呢?

下面我们将来一起看一看 free  malloc 。

安照上面的实验与论述,其结果应该是一样的,下图说明了这一点。

无论应用以何种形式进行内存的请求,在那个层次的请求,应用对于内存的释放或是引入计数清理,于操作系统来说只是一个通知,系统对外的接口是统一的。

具体回不回收声明释放的内存,何时回收,是系统决定的。

但是当内存升到500M时,产生内存错误,与block一致,不启动虚拟内存机制。



for (long i=0; i <  VeryBig; i++) {

    [self justAlloc];

}

-(void) justAlloc{

    dispatch_async(dispatch_get_main_queue(), ^{

            for (int i=0; i <10; i++) {

                char * ch =malloc(1024);

                ch[0] = i;

                free(ch);

            }

});

}


说应用层的释放请求,确保了应用内存峰值点,是即其肤浅的说法。

尤其在IOS的引用计数下,说什么引用计数清理了就是释放内存了更是不稳妥的说法。

因为在引用计数的下面的某层一定曾在对free的调用。而具体free的执行时机我们又是不得而知了。

引用计数的清零只是使应用的行为满足了操作系统对其的限制、符合了编程语言在语法和语义是的要求。


SO,可以问引用计数什么时候清零,或是有什么可以降低内存峰值的方式,或是大一点可以问如何管理内存。

但是,不要去问别人什么时候进行内存回收(释放),尤其是在讨论完了引用计算什么 时候清零再去问问内存释放的时间。



结论:

1 对象(内存)的释放与内存的回收是两回事。释放是应用的愿望,回收方式、时间是由系统决定的。
2 autoreleasepool实际回收内存资源时间是由系统决定的。NSRunLoop当前事件执行完毕,下一次事件开始之前系统并不一定去做实际的内存回收操作。
3 dispatch放入过多的block 或是在其中malloc free过多的内存,在内存耗尽时会产生内存error,而不会启用虚拟内存机制。
4 当其他已释放对象过多而耗尽一定数量的系统内存时,当CPU过于忙碌,系统会启用虚拟内存机制。

5 这样看来虚拟内存机制要比内存回收机制占用较少的系统资源。


后记: 里面一定会存在错误或是误解,欢迎大家指出了。同时排版很乱,大家将就看吧!


1 0