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对象(例如堆栈变量)开始,并检查它们引用的对象。然后把引用的对象移动到另一半空间。在那里它检查这些移动的对象指向的内容,并移动这些引用的对象。如此反复,直到移动所有活动对象到另一半空间。始终没有被引用的对象将被回收。
DartVM的内存分配策略非常简单,创建对象时只需要在现有堆上移动指针,内存增长始终是线形的,省去了查找可用内存段的过程:
Dart中类似线程的概念叫做Isolate,每个Isolate之间是无法共享内存的,所以这种分配策略可以让Dart实现无锁的快速分配。
Dart的垃圾回收也采用了多生代算法,新生代在回收内存时采用了“半空间”算法,触发垃圾回收时Dart会将当前半空间中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存:
整个过程中Dart只需要操作少量的“活跃”对象,大量的没有引用的“死亡”对象则被忽略,这种算法也非常适合Flutter框架中大量Widget重建的场景。
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造成影响。