内存管理
本文档旨在介绍QuecPython内存管理原理、注意事项和常见问题,指导客户使用QuecPython内存相关功能。
概述
内存指的是程序运行过程中使用的存储空间,用来存储当前运行程序的指令和数据,存储的内容会实时变化。内存采用的存储介质为RAM(random access memory)随机存储器,可以读取数据也可以写入数据,存储的数据在掉电之后会丢失。内存管理是嵌入式处理系统中的重要部分,主要负责对内存资源进行有效的分配和使用。因为内存的存储容量较小,且存储的内容是动态变化的,可以采用时分复用的策略来满足空间使用的需求。
各个平台物理内存大小
平台 | 内存大小 |
---|---|
ECx00U & EGx00U系列 | 16MB |
EC800GCNGA | 16MB |
ECx00GCNLD & ECx00GCNLB系列 | 8MB |
ECx00N & EGxxxN系列 | 16MB |
ECx00MCNLA & ECx00MCNLE & EG810M系列 | 8MB |
ECx00MCNLC & ECx00MCNLF & ECx00MCNCC系列 | 4MB |
xC200A系列 | 32MB |
ECx00E系列 | 1.25MB |
BG95 & BG600L系列 | 32MB |
QuecPython内存管理机制
根据运行程序中的数据使用方式,可以将内存空间分为静态存储空间和动态存储空间。而内存管理针对的是动态存储空间。整个内存的分布如下图:
其中代码段和常量数据属于只读空间。全局变量属于静态存储空间。堆属于动态存储空间。而从全局变量里划分出来的GC内存又属于动态存储空间。从堆里分配出来的栈空间也属于动态存储空间。
静态内存
概述
静态存储空间,指的是存储的数据在内存中的位置即地址是固定不变的。包含的数据类型有:全局变量和静态变量,这里指的是C语言开发层的变量类型。
全局变量
即访问属性为全局的变量,程序中的所有函数均可访问。分为未初始化的和已初始化的变量,两者的区别是已初始化的变量类型需要在非易失性存储器如ROM或flash中存储一份相同的数据。因为内存中的数据在掉电后就会丢失,而已初始化的数据需要每次断电上电重启后仍然保持,就需要在非易失性存储器中保存一份。
静态变量
即使用static字段修饰定义的变量。GC管理的整块空间就属于该类型的空间。
动态内存
概述
动态存储空间,指的是存储的数据在内存中的位置即地址是变化的。包含的数据有:堆和栈空间中分配的数据,这里指的是C语言开发层中的堆和栈。
栈
栈空间用来存放函数的局部变量和形参。栈空间在给定之后,具体的使用由系统自动分配和释放,开发人员不可控制。且每个线程会指定不同的栈空间,线程删除后会释放该资源,线程栈空间也是从堆空间申请的。从数据结构来讲,栈空间只是栈后进先出数据结构的一种应用,函数调用和退出比较符合这种数据结构特点,分配释放使用效率较高,适合系统使用。
堆
堆是一块大的内存空间,由开发人员分配和释放。开发人员可以控制。从数据结构来讲一般采用链表的形式管理,可以随机存取,比较灵活,适合开发人员使用。
RTOS内存管理算法
概述
这里内存管理只针对堆内存空间。模组在开机运行后,会预留一大块内存空间作为堆内存。该堆内存空间大小会随着应用程序的启动和停止动态变化。不同的RTOS类型拥有不同的内存管理机制。当前模组使用的RTOS类型主要有threadx和freertos两种。
常用算法
First Fit (首次适应算法)
First Fit要求空闲分区链表以地址从小到大的顺序连接。分配内存时,从链表的第一个空闲分区开始查找,将最先能够满足要求的空闲分区分配给进程。
Next Fit (循环首次适应算法)
Next Fit由First Fit算法演变而来。分配内存时,从上一次刚分配过的空闲分区的下一个开始查找,直至找到能满足要求的空闲分区。
Best Fit (最佳适应算法)
从所有空闲分区中找出能满足要求的、且大小最小的空闲分区。为了加快查找速度,Best Fit算法会把所有空闲分区按其容量从小到大的顺序链接起来,这样第一次找到的满足大小要求的内存必然是最小的空闲分区。
Worst Fit (最坏适应算法)
从所有空闲分区中找出能满足要求的、且大小最大的空闲分区。Worst Fit算法按其容量从大到小的顺序链接所有空闲分区。
Two Level Segregated Fit (TLSF)
使用两层链表来管理空闲内存,将空闲分区大小进行分类,每一类用一个空闲链表表示,其中的空闲内存大小都在某个特定值或者某个范围内。这样存在多个空闲链表,所以又用一个索引链表来管理这些空闲链表,该表的每一项都对应一种空闲链表,并记录该类空闲链表的表头指针。
Buddy systems(伙伴算法)
Segregated Fit算法的变种,具有更好的内存拆分和回收合并效率。伙伴算法有很多种类,比如Binary Buddies,Fibonacci Buddies等。Binary Buddies是最简单也是最流行的一种,将所有空闲分区根据分区的大小进行分类,每一类都是具有相同大小的空闲分区的集合,使用一个空闲双向链表表示。Binary Buddies中所有的内存分区都是2的幂次方。
内存算法 | 优点 | 缺点 |
---|---|---|
First Fit | 高地址空间大空闲块被保留 | 低地址空间被不断拆分,造成碎片;每次都从第一个空闲分区开始查找,增加了查找时的系统开销 |
Next Fit | 空闲分区分布比较均匀,算法开销小 | 缺乏大内存空闲块 |
Best Fit | 用最小内存满足要求,保留大内存空闲块 | 每次分配后所拆分出来的剩余空闲内存总是最小的,造成许多小碎片,算法开销大 |
Worst Fit | 每次分配后所拆分出来的剩余空闲内存仍较大,减小碎片产生 | 缺乏大内存空闲块,算法开销大 |
TLSF | 查找效率高,时间复杂度小,碎片问题表现良好 | 内存回收时算法复杂,系统开销大 |
Buddy systems | 内部碎片比较严重 | 外部碎片较少 |
threadx
分配
内存分配使用首次适应(first-fit)算法,每次从第一个空闲内存块开始查找,直到找到大小合适的内存则返回成功。
内存块的管理方式采用的是链表方式,地址按从低到高排列。具体链接方式为每一块内存的前面8个字节是控制字段,其中前4字节存储下一块内存的起始地址,随后4个字节如果是空闲内存则存储TX_BYTE_BLOCK_FREE特定值,如果是已分配的内存则存储内存池管理结构体的起始地址。
初始化时将整个内存空间分成了2块,第一块和最后一块。第一块中的前4个字节存储最后一块的起始地址,后四个字节设置为TX_BYTE_BLOCK_FREE,因为还没有使用,标记为空闲块。最后一块的前4个字节存放内存池管理结构体的起始地址,后4字节标记为TX_BYTE_BLOCK_ALOC,表示最后一块。
下图为第一次分配内存后,最初的第一块内存被分为了两块:第一块和第二块。第一块返回给应用程序,第一块前面8个字节由于是控制字段,所以返回应用程序的内存起始地址memory_ptr跨过了前面8个字节,指向分配的地址。第一块的前4字节存储第二块内存起始地址,随后4个字节指向了内存池管理结构,标志着第一块内存已经被占用,不是空闲内存了。第二块内存前4个字节指向第三块内存(最后一块内存),继续构成单向链表。随后4个字节存储TX_BYTE_BLOCK_FREE,表示还是空闲内存块。
释放
内存释放时,根据释放内存首地址减8,计算出内存块控制字段的首地址,通过前四字节找到内存池管理结构体起始地址,可以进行内存释放操作。释放后,4到8字节存储TX_BYTE_BLOCK_FREE表示为空闲内存块。
这时如果内存是连续的,并不会进行合并或内存整理。等到申请内存时,发现请求大小大于当前块的大小,开始查找下一块时,这时发现当前块和下一块内存连续,就把当前和下一块合并为—块。
释放内存后,会检查挂起链表中是否有线程,如果有尝试分配内存并恢复线程执行。线程恢复是按照FIFO顺序恢复,并没有按照线程优先级高低顺序。但是可以在线程释放前调用t_byte_pool_prioritize,把最高优先级线程移动到挂起链表最前面,从而先恢复最高优先级线程。
碎片处理
内存在多次分配和释放后,可能会出现大量小的内存块,这种现象称为内存碎片化。
当需要分配一个较大内存时,每次可能需要先遍历大量小内存,这样会使查找开销增加,算法性能下降。由于每个内存块都占用8个字节的控制字段,大量小内存会导致内存的浪费。查找过程中,发现两个邻居内存块是地址连续的,那么把这两个内存块合并成为一个内存块,称为内存整理。
下图为多次内存分配和释放后的结构,第一块被占用,第二块,第三块,第四块为空闲内存块。第二块大小为64字节,第三块大小为64字节,第四块内存大小为256字节。假如现在申请分配内存大小为128字节,在分配查找过程中,发现第二块太小不满足,但第二块和第三块地址连续,于是把第二块和第三块合并为一块,并且发现合并后正好满足请求,返回给应用程序,如下面第二个图。
freertos
FreeRTOS提供了5种内存分配方法,FreeRTOS使用者可以选择其中的某一个方法,或者使用自己的内存分配方法。这5种方法对应FreeRTOS源码中的5个文件,分别为:heap_1.c、 heap_2.c、heap_3.c、 heap_4.c和heap_5.c。每种方法适用的场景如下:
方法 | 适用场景 |
---|---|
heap_1 | 支持内存分配,但是不支持回收。适用于一些比较小的嵌入式设备,系统启动后申请内存,在程序生命期内一般没有释放的需求。 |
heap_2 | 支持内存回收,但是不会把碎片合并。使用最佳适应算法(best fit)。适用于每次申请内存大小都比较固定的场景。 |
heap_3 | 直接在标准库的malloc和free接口上加上线程安全。 |
heap_4 | 支持内存分配,支持内存回收,支持内存碎片合并。适用于频繁分配释放不确定大小内存的场景。 |
heap_5 | 支持多个不连续的区域组成堆,适用于一些内存分布不连续的嵌入式设备。 |
模组里使用的是heap_4方法,这里主要介绍heap_4的算法实现。
分配
内存分配使用首次适应(fisrt-fit)算法,每次从头开始查找空闲内存块,找到大小合适内存,返回成功。
通过一个链表维护未分配的内存。链表节点定义:
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock;
/*<< The next free block in the list. */
size_t xBlockSize;
/*<< The size of the free block. */
} BlockLink_t;
两个变量分别是指向下一块内存的地址指针pxNextFreeBlock 以及自己的内存大小xBlockSize。申请内存的时候,系统会在申请内存大小xWantedSize的基础上加上heapSTRUCT_SIZE(链表节点的大小)作为最终申请的大小。然后从链表头开始遍历未分配内存链表,查找符合大小的内存块。同时会判断当前这块内存是否有剩余(大于所申请大小),如果有,就把剩余的内存再新建一个未分配内存块节点,插入到未分配链表中,供下次分配使用。整体算法和上述threadx类似。
释放
内存释放时,根据释放的内存地址,向前索引到对应链表节点,取出该内存块的信息,调用链表插入函数,将这个节点归还。
碎片处理
通过上述分配过程看到,内存在多次分配和释放后,同样会产生内存碎片。所以需要把内存碎片合并成大块内存,合并算法和上述threadx类似。为了实现这个合并算法,空闲内存链表是按内存地址大小进行存储的。例如, 准备插入的内存块P, 系统查找到内存地址对应于其前面的内存块A,判断 A 和 P 之间是否还有其他被分配的块,如果没有,直接合并。 然后再判断内存地址对应于其后面的内存C 的位置关系,P和C之间没有其他被分配的内存块的话,就直接合并。
应用场景
底层动态内存申请和释放
底层程序运行过程中需要内存空间存储数据时就会调用malloc接口从堆内存中申请一定大小的内存。当业务执行结束不再需要该块空间时就会调用free接口释放内存并还原到堆内存中。
创建线程
程序运行过程中需要创建新的线程时,需要给该线程分配栈空间,该栈空间也是从堆内存中申请。当线程执行结束被删除时,会释放栈空间并还原到堆内存中。底层和应用层的程序都有可能创建线程。
常见问题:
如何避免内存碎片
内存管理算法可以一定程度上避免内存碎片,但是没法完全避免。少用动态内存分配的函数(尽量使用栈空间)。分配内存和释放内存尽量在同一个函数。尽量一次性申请较大的内存,而避免反复申请小内存(减少内存分割)。自行设计内存池管理内存。
堆安全剩余量
虽然堆空间是动态分配释放的,但是需要保证堆空间有足够的剩余以保证复杂业务时的申请需求。堆空间的总量是由底层决定的,固件生成后该值就确定了。调用接口_thread.get_heap_size
可以查看当前堆空间剩余量,具体接口用法参考wiki描述。
内存泄露
常见内存泄漏的场景有:
- 分配和释放内存的操作不成对,即业务开始运行时申请的内存在业务结束时没有进行释放,然后下次业务再次运行时又重新申请内存,导致内存不断减少,最终可能导致整个内存被耗尽,进而导致整个程序申请不到内存而崩溃。
- 重复创建线程,即底层或者应用层创建线程和删除线程处理不成对,导致重复创建相同业务的线程,而每创建一个线程都要从堆中分配一个栈空间,最终导致整个堆空间被耗尽。
内存越界
常见内存越界的场景有:
数组越界,C语言实现层在使用一个数组类型的变量时,操作的索引号超过了实际分配的数组大小,就会导致内存越界到其他内存空间。可能会将其他内存空间的数据破坏导致系统程序异常。
栈溢出,即应用层创建线程时传入的栈空间大小小于业务实际运行需要的空间。这样可能导致业务运行过程的数据操作越界到其他内存空间,将其他内存空间的数据破坏导致系统程序异常。
内存申请失败异常信息
底层内存申请失败通常会触发系统crash异常。
应用层GC内存
概述
GC(Garbage Collection)垃圾回收机制,用于python语言编写程序时的内存管理。该机制实现了自动内存管理,不需要程序员写代码来管理内存。GC做的事情就是解放程序员的双手,找出内存中不用的资源并释放这块内存。
应用场景
GC空间中分配的数据包含C语言开发层的变量和python语言开发层的变量。
管理算法
常用算法
常用的垃圾回收机制包括:引用计数(reference counting)、标记清除(Mark and Sweep)、分代回收(Generational garbage collector)。
引用计数
引用计数算法的原理是:每个对象维护一个ob_ref字段,用来记录该对象当前被引用的次数,每当新的引用指向该对象时,它的引用计数ob_ref加1,每当该对象的引用失效时计数ob_ref减1,一旦对象的引用计数为0,该对象立即被回收,对象占用的内存空间将被释放。导致引用计数+1的情况:对象被创建、对象被引用、对象被作为参数传入到一个函数中、对象作为一个元素存储在容器中。导致引用计数-1的情况:对象的别名被显式销毁、对象的别名被赋予新的对象、一个对象离开它的作用域,例如函数执行完毕时,函数中的局部变量、对象所在的容器被销毁,或从容器中删除对象。
它的缺点一个是需要额外的空间维护引用计数,另一个是不能解决对象的“循环引用”带来的内存无法释放的问题。如下为循环引用示例,循环引用导致引用计数永不为 0,进而无法将其回收。
a = [1, 2] # 引用计数为 1
b = [2, 3] # 引用计数为 1
a.append(b) # 引用计数为 2
b.append(a) # 引用计数为 2
del a # 引用计数为 1
del b # 引用计数为 1
标记清除
标记清除算法分成两个阶段:标记阶段,遍历所有的对象,如果还有对象引用它,那么就标记该对象为可达(reachable)。清除阶段,再次遍历所有的对象,如果发现某个对象没有标记为可达,则将其回收。
对象之间会通过引用(指针)连在一起,构成一个有向图,对象构成有向图的节点,而引用关系构成有向图的边。从root object出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达(unreachable)的对象就是要被清除的非活动对象。root object,是一些全局变量、调用栈、寄存器,这些对象是不可被删除的。
图中小黑圈为root object,从小黑圈出发,对象 1 可达,那么它将被标记,对象 2、3可间接可达也会被标记,而 4 和 5 不可达。因此 1、2、3 就是活动对象,4 和 5 是非活动对象会被 GC 回收。
分代回收
分代回收算法的原理基于这样的一个统计事实:对于程序,存在一定比例的内存块的生存周期比较短,而剩下的内存块,生存周期会比较长,甚至会从程序开始一直持续到程序结束。生存周期较短对象的比例通常在80%~90% 之间。因此认为,对象存在时间越长,越可能不是垃圾,应该越少去收集。这样在执行标记清除算法时可以有效减小遍历的对象数,从而提高垃圾回收的速度。
具体方法为,将所有的对象分为 0,1,2 三代。所有的新建对象都是 0 代对象,当某一代对象经历过垃圾回收后依然存活,就被归入下一代对象。每代回收可以设置阈值,如每新增101 个需要 GC 的对象,触发一次0代GC。每执行11次0代GC ,触发一次1代 GC。每执行11次1代GC ,触发一次2代GC 。执行某代 GC 前,年轻代对象链表也移入该代,一起GC。一个对象创建后,随着时间推移将被逐步移入老生代,回收频率逐渐降低。
QuecPython管理机制
QuecPython使用的是标记清除机制来管理内存。
初始化
在QuecPython中内存的最小单元是block,每个block的大小是宏MICROPY_BYTES_PER_GC_BLOCK定义的,在我们的代码中是(4 * BYTES_PER_WORD)16字节。即每次申请,至少会分配一个block。每个内存block使用状态分为4种:
FREE(free block),表示该block为未使用状态,用值0x0表示。
HEAD(head of a chain of blocks),表示该block为已分配状态,且为已分配的连续内存的第1块,用值0x1表示。
TAIL(in the tail of a chain of blocks),表示该block为已分配状态,且为已分配的连续内存的第2~n块(n为连续内存的末尾块),用值0x2表示。
MARK(marked head block),表示在内存回收时该block被标记为已标记状态,即有被引用,不进行释放。用值0x3表示。
这些内存block的状态标志有单独的一块内存存放,称为ATB(Allocation Table Byte),位置在GC内存开头的一段空间。因为2个bit可以表示4种状态,这样一个字节就可以记录4个内存block的使用状态,ATB的大小和总的GC内存大小成正比。
除了上述内存使用状态的标志之外,内存block还有一个finaliser标志。表示一个block是或不是finaliser内存,用0和1两种值来表示。finaliser标志也有单独的一块内存存放,称为FTB(Finalizer Table Byte),位置在ATB空间之后的一段空间。因为1个bit即可以表示该状态,故一个字节可以标记8个内存block的finaliser状态。FTB的大小也和总的GC内存大小成正比。finaliser标志和内存分配回收没有直接关系,只是用来区分内部机制的选择。(finaliser标志的作用:内部使用,对象实例化时如果使用m_new_obj_with_finaliser进行内存申请,内存申请会标记为finaliser。回收时判断内存标记为finaliser,且对象结构体第一个成员是mp_obj_base_t base的情况下,释放前会自动找到此对象的delete方法并执行。适用场景:虚拟机重启会将所有资源回收掉,因此C层记录的python层传下来的回调函数信息都会失效,如果C层再次使用这块资源就会触发异常。这时可以通过在GC回收内存时调用对象的delete方法逆初始化底层资源和清除回调函数信息解决。)
GC内存初始化由gc_init
函数实现。首先分配一块大的静态内存空间作为gc总的可用内存空间。然后将整个GC内存划分为三个部分:
ATB(Allocation Table Byte):从GC内存起始地址开始的一块连续内存,用来标记动态申请和释放的内存block的使用状态。
FTB(Finalizer Table Byte):紧接着ATB内存之后的一块连续内存,用来标记内存block是否为finaliser类型。
P(Pool):用来动态申请和释放的内存池,地址区间在ATB、FTB内存之后到GC内存结尾,最小分配单元是block,16字节。
GC内存的分布如下图:
分配
我们在使用gc内存时使用m_new_xxx
接口进行申请,最终都是调用gc_alloc
函数进行gc内存申请。
分配过程:首先将传入的待申请的大小(单位字节)转换为block的个数n_blocks。然后在GC内存中查找连续的大小为n_blocks的一段空闲的内存。若找到大小满足的一段内存,那么就会在该段内存第1个block对应的ATB位置处标记为HEAD(0x1),该段内存第2个block到最后一个block之间所有block对应的ATB位置处标记为TAIL(0x2)。若使用的是m_new_obj_with_finaliser申请的内存,说明该段内存类型是finaliser,将该段内存第1个block对应的FTB位置处标记为finaliser(0x1)。最后返回该段内存第1个block对应的物理地址给到用户。
其中查找到合适的内存块的过程:一般情况下每次是从ATB的起始地址开始遍历,找到一个标记为FREE(0x1)的block计数n_free加1,中途遇到非FREE的block则计数清零重新开始计数。直到n_free大于等于需要申请的内存个数n_blocks则认为找到了合适的内存。有一种情况是如果本次申请的内存大小n_blocks=1,则会将本次找到的空闲block对应的ATB位置作为下一次分配内存时遍历的起始地址。因为该位置之前的block肯定都不是空闲态了,下次遍历不需要再从头开始遍历了。而每次内存回收后遍历起始地址又会恢复到ATB的起始地址。
从上述过程来看,该种分配方法类似于首次适应(fisrt-fit)算法类型。随着分配的次数增加,也会存在不断拆分已有的大块内存的情况,即产生内存碎片。
一次分配之前和分配之后ATB内容分布如下图。
回收
如果gc内存申请时发现内存不足,默认会触发gc_collect
函数进行内存回收。内存回收主要分为两步:
第1步,扫描所有python线程及mp_state_ctx
结构体中记录的一些全局信息。这个扫描动作找到mp_state_ctx
记录的全局信息及线程栈中出现的所有处于GC内存范围内的地址。如果这个地址所在block对应的ATB标志为HEAD(0x1),说明这个内存仍在使用中,那么将其ATB标志由HEAD(0x1)改为MARK(0x3)。
第2步,扫描整个GC内存的ATB标志,这一步将所有处于HEAD(0x1)的block及其之后的标志为TAIL(0x2)的block标记为FREE(0x0)。因为在上一步扫码中未将其标记为MARK(0x3)的block,则认为这个block已不再使用了。这样这一段内存即被回收了。另外将所有处于MARK(0x3)的block标记改为HEAD(0x1),即恢复成正常使用状态。经过上面两步,gc内存回收完成。
一次回收之前和回收之后ATB内容分布如下图。
碎片处理
从上述内存回收过程来看,回收之后连续的空闲块会自动进行合并,下次分配内存时可以一起分配使用。
常见问题
如何配置GC回收阈值
调用接口gc.threshold()
设置回收阈值,单位是字节,当累计已分配的内存大小超过该阈值时则会触发GC内存回收。
如何查询GC内存剩余情况
micropython.mem_info()
,暂未开放该接口,待最新版本固件开放。执行示例如下:
>>> import micropython
>>> micropython.mem_info()
stack: 884
GC: total: 320128, used: 13136, free: 306992
No. of 1-blocks: 228, 2-blocks: 61, max blk sz: 264, max free sz: 19177
其中total代表gc总的内存空间大小,单位字节。used代表当前已分配使用的gc内存空间大小,单位字节。free代表当前剩余可用的gc内存空间大小,单位字节。No. of 1-blocks代表已分配的大小为1个block大小(16字节)的gc内存块的个数。2-blocks代表已分配的大小为2个block大小(2x16字节)的gc内存块的个数。max blk sz代表当前已分配的gc内存块中空间最大的块的大小,单位是block,即16个字节。max free sz代表当前剩余可用的gc内存块中空间最大的块的大小,单位是block,即16个字节。
GC回收什么时候会被触发
已申请内存大小超过设置阈值时。
申请内存失败时。
主动调用回收接口
gc.collect()
。
如何主动触发GC回收
调用接口gc.collect()
,具体接口用法参考wiki描述。
Python 编程注意项
栈溢出,线程栈太小
如果栈空间大小小于业务实际运行需要的空间,可能导致业务运行过程的数据操作越界到其他内存空间,将其他内存空间的数据破坏导致系统程序异常。所以需要根据业务复杂度适当分配线程栈空间大小,可以在创建线程之前调用接口_thread.stack_size()
设置栈大小。具体接口用法参考wiki描述。
变量被GC回收后继续使用
python虚拟机层分配的GC内存变量
底层会保证在使用的变量是可以被标记的,即引用图是可达的。
python层用户定义的变量
Python的作用域:
- L (Local) 局部作用域
- E (Enclosing) 闭包函数外的函数中
- G (Global) 全局作用域
- B (Built-in) 内建作用域
需要全局作用的对象必须定义成全局作用域的类型。同时保证局部作用域的对象,不被全局作用的业务依赖。因为在GC内存回收的时候会将执行结束的局部作用域对象释放掉,如果用户的业务还依赖于该对象则会导致部分应用失效。
规范用法注意事项和示例:
- 如创建了一个局部作用域的对象,并注册了一个回调函数到底层。然后该局部对象在GC内存回收时被回收掉了,此时底层触发了该回调就不会再上报到python层,导致部分功能失效。如下是powerkey的应用示例,注册了一个回调函数。
不正确示例:
from misc import PowerKey
import utime
def callback(info):
global start_time, end_time
if info == 1:
start_time = utime.time()
else:
end_time = utime.time()
if end_time - start_time >= 3:
print('long press')
else:
print('short press')
def run():
PowerKey().powerKeyEventRegister(callback) # GC内存回收后PowerKey对象会被回收,导致注册的回调函数功能失效
run()
正确示例:
from misc import PowerKey
import utime
def callback(info):
global start_time, end_time
if info == 1:
start_time = utime.time()
else:
end_time = utime.time()
if end_time - start_time >= 3:
print('long press')
else:
print('short press')
global pwk
pwk = PowerKey() # PowerKey对象存放在全局对象pwk中,GC内存回收后PowerKey对象仍在,注册的回调函数功能继续生效
pwk.powerKeyEventRegister(callback)
gc内存申请失败
可能原因
总的空间不够。
内存碎片。
如何避免
总的空间不够
需要考虑python代码是否有不必要的全局对象,转换成局部对象实现。
内存碎片
设置GC回收阈值,如设置阈值为GC总内存空间的75%,这样每当累计分配的GC内存大小超过总内存的75%时就触发回收,这样可以保证有一部分较大的连续空闲内存空间。同时注意list.append()操作实际上会申请一个连续的大块内存,随着list的不断扩充可能会导致申请不到连续的大内存空间而返回失败。