一、移动端的内存回收机制
GC(Garbage Collection),垃圾回收机制,简单地说就是程序中及时处理废弃不用的内存对象的机制,防止内存中废弃对象堆积过多造成内存泄漏
常见的垃圾回收算法有引用计数法(Reference Counting)、标注并清理(Mark and Sweep GC)、拷贝(Copying GC)和逐代回收(Generational GC)等算法。
1、iOS端
Objective-C语言本身是支持垃圾回收机制的,但有平台局限性,仅限于Mac桌面系统开发中,而在iPhone和iPad等苹果移动终端设备中是不支持垃圾回收机制的。在移动设备开发中的内存管理是采用MRC(Manual Reference Counting)以及iOS5以后的ARC(Automatic Reference Counting),本质都是RC引用计数,通过引用计数的方式来管理内存的分配与释放,从而防止内存泄漏。
iOS采用引用计数算法回收内存,当对象引用计数为0时,对象会执行反初始化方法并被回收。如果两个对象互相引用对方,就会造成循环强引用,导致内存泄漏。
2、Android端
Android系统采用的是标注并删除和拷贝GC,并不是大多数JVM实现里采用的逐代回收算法,根搜索算法回收内存,该算法通过GC Roots作为起点搜索,搜索通过的路径称为引用链,当一个对象没有被GC Roots的引用链连接的时候,这个对象就会被回收。即使A和B两个对象互相引用对方,只要A和B都不在引用链上,这两个对象都会被回收。
下图中的每个圆节点代表对象,箭头代表可达路径,当圆节点与 GC Roots 存在可达路径时,表示无法回收(黄色圆节点),反之则可以回收(蓝色圆节点)。
GC Root
- 虚拟机栈(栈帧中的局部变量)中的引用的对象。
- 方法区域中的类静态属性引用的对象。
- 方法区域中常量引用的对象。
- 本地方法栈中 JNI(Native 方法)的引用的对象。
- 运行中线程引用的对象
3、GC与引用计数RC的区别
另外引用计数RC和垃圾回收GC是有区别的。
- GC垃圾回收是宏观的,对整体进行内存管理,将所有对象看做一个集合,然后在GC循环中定时检测活动对象和非活动对象,及时将用不到的非活动对象释放掉来避免内存泄漏,也就是说用不到的垃圾对象是交给GC来管理释放的,而无需开发者关心,比如Java中的垃圾回收机制;
- 引用计数是局部性的,开发者要管理控制每个对象的引用计数,单个对象引用计数为0后会马上被释放掉。ARC自动引用计数则是一种改进,由编译器帮助开发者自动管理控制引用计数(自动在合适的时机发送release和retain消息)。另外自动释放池autorelease pool则像是一个局部的垃圾回收,将部分垃圾对象集中释放,相对于单个释放会有一定延迟。
二、Flutter的runtime
Flutter使用dart
语言作为其开发语言和运行环境。dart
的runtime
是一直存在的,但是在debug
和release
模式下有一些区别。
- 在
debug
模式下,dart
大部分组件都放在设备上,例如runtime
、JIT(Android)
、interpreter(iOS)
、debug
和profile
services
。 - 在
release
模式下,只剩下runtime
,而这也是Flutter App能够运行起来的最基本组件。
在runtime
中,存在一个在初始化对象时为其分配内存,对象不再被使用的时候回收内存的组件,即GC。 在Flutter
中存在很多对象。以Stateless
Widget
为例,其在State
发生变化或者Widget
不可见的时候不断地发生重建和销毁(注意,此处是指Widget树中的Widget,对于Element树和RenderObject树来说,element和renderObject是可变的,而且其初始化生成需要消耗很多资源。因此在大多数情况下他们是会被回收利用的)。这些Widget
的生命周期都很短,对于一个UI比较复杂的APP来说,可能会有数千个Widget
需要被经常回收创建。
所以有些开发者可能会采取一些措施来避免太过频繁的GC。比如为了保持一个引用的Widget
对象不会被回收,将其放在state
中(这样并不是说真的不会被回收,只是创建回收的频率被降低了,因为state是属于element的,而element的生命周期是比较长的)。
这么做是没有必要的,首先Widget
是一个很轻量级的对象,它的创建和回收并不会占用很多资源,真正占用资源的是Element
和RenderObject
。其次dart
的GC机制能够快速有效的进行对象回收,不用担心Widget
创建过多导致OOM
出现。
三、Dart Garbage Collector
Dart的垃圾回收是分代的:年轻代和老年代
1、调度
为了最小化GC对应用程序和UI性能的影响(因为dart的GC有一种类似于JVM中stop the world的机制,导致APP对事件无响应、UI无法刷新),GC与Flutter engine建立联系,当engine检测到应用程序处于空闲状态且没有用户交互时,它会发出通知。这样就使得GC有了收集的窗口从而不影响性能。
GC还可以在这些空闲的窗口期运行滑动压缩,从而通过减少内存碎片来最小化内存开销。
2、年轻代
这个阶段旨在清除寿命较短的短暂对象,例如stateless widgets。虽然它是阻塞的,但它比老年代mark-sweep快得多,并且当与调度结合使用时,几乎不会影响程序的运行。
实际上,对象被分配给内存中的连续空间,并且在创建对象时,它们被分配下一个可用空间,直到分配的内存被填充完毕。 dart
使用指针碰撞的方式来给这些对象分配空间(之所以没有空闲列表的方法是因为dart
在GC之后都会采用滑动压缩的方式来把内存碎片清除掉),这个过程非常迅速。
分配给新对象的连续空间由两部分组成。任何时候只使用一半:一半处于活动状态(活动空间),另一半处于非活动状态(非活动空间)。新生成对象在活动空间那一半中分配,一旦那一半填充完毕,不可回收对象将被从活动空间复制到非活动空间(忽略可被回收的对象)。这样,非活动空间转变变为活动状态,开始为新对象分配内存,并重复该过程。
要确定哪些对象是否可被回收,收集器将以root对象(例如堆栈变量)开始,并检查它们引用的对象。然后把引用的对象移动到另一半空间。在那里它检查这些移动的对象指向的内容,并移动这些引用的对象。如此反复,直到移动所有活动对象到另一半空间。始终没有被引用的对象将被回收。
3、老年代(并行标记和并发扫描)
当对象经历过一定次数的GC仍然存在,或者其生命周期较长(个人猜测类似于element
和RenderObject
这种需要多次复用,可变且创建比较耗费性能),将其放入老年代区域中。老年代采用标记整理的方法来回收对象。
这种GC技术有两个阶段:首先遍历对象图,并标记仍在使用的对象。在第二阶段期间,扫描整个存储器,并且回收未标记的任何对象。然后清除所有标志。
在标记的时候,该线程中内存区域是处于不可修改的状态,类似于JVM中stop the world
,所以这个时候可能会导致ANR
(只是类似于ANR
的表现,其产生原因还是不一样的),但是由于dart
优秀的schedule
机制和老年代GC频率很低的原因,基本上不会出现这个问题。
需要注意的是,如果APP不支持弱年代假设(即大多数对象的生命期都很短;从年老对象到年轻对象的引用非常少),上面的分代设计就不那么有效了,但是考虑到Flutter中的Widget
、Element
、RenderObject
关系,我们不需要担心这个问题。
4、根据ioslate特性来优化
与JVM内存模型不同的是,dart
中每个isolate
都有自己的独立的堆栈内存空间,其各自的GC不会影响到其他isolate
的。所以我们可以通过把部分占用内存空间较大且生命周期较短的对象方法其他isolate
中,这样即使另外一个isolate
GC了,并不会对我们显示UI的isolate
造成影响。