援用计数办法非常简单。对象的存活性能够通过援用关系的创立和删除间接断定,从而无需向追踪式回收器那样先通过遍历堆找出所有的存活对象,而后再反向确定出未遍历到的垃圾对象。
援用计数算法基于计算对每个调配对象的指针援用数的想法。这是一种间接的办法,也恰好是天然增量的,因为它在整个程序中分配内存治理开销。
该算法依赖于一个非常简单的不变式:当且仅当指向某个对象的援用数量大于 0 时,该对象才有可能是存活的。
那么该算法是怎么运作的呢?
援用计数怎么运作?
在援用计数办法下,每个调配的对象都蕴含一个援用计数字段。
内存管理器负责保护不变量,即每个对象的援用计数始终等于对该对象的间接指针援用的数量,当创立或者删除某一对象的援用时减少或缩小该对象的援用计数。
上面给出了该算法的根本版本:
new 办法:用来创立一个对象,new() 调配一个新对象。为简洁起见,咱们疏忽了对象类型,假如所有对象的类型和大小都雷同。
delete 办法:实现援用计数的缩小,当客户端程序不再须要对象时调用 delete()
update 办法:update() 是在零碎中执行指针调配的惟一办法。实现对援用对象计数的减少,咱们在删除之前递增,这正确处理了 source == target 的状况。
def new():
obj = allocate_memory()
obj.set_reference_count(1)
return obj
def delete(obj):
obj.decrement_reference_count()
if obj.get_reference_count() == 0:
for child in children(obj):
delete(child)
release_memory(obj)
def update(source, target):
target.increment_reference_count()
delete(source)
source = target
解决循环援用
毫无疑问,援用计数的最大毛病是它无奈回收循环存储。在简略援用计数办法下,双向链表或非简略图等循环数据结构无奈无效回收,并且会透露内存。以下示例显示了该问题:
在 delete(A) 和 delete(C) 之后,咱们最终失去了一个对象子图的不可拜访但连贯的组件,该组件无奈从任何根拜访,但因为非零援用,咱们无奈回收其节点。
侥幸的是,所有其余垃圾收集技术(标记扫描、标记压缩、复制等)都能够轻松解决循环构造。这就是为什么应用援用计数作为次要垃圾收集机制的零碎在堆耗尽后利用跟踪收集算法的状况并不少见。
援用计数算法的优缺点
援用计数的内存治理开销摊派在程序运行过程中,同时一旦某个对象成为垃圾对象就能够失去立即回收。
而且该算法间接操作指针的起源与指标,因而其局部性不会比它所服务的应用程序差,且通常优于须要跟踪所有流动对象的跟踪 GC。该算法的长处如下:
响应性:内存治理开销散布在整个程序中,与跟踪收集器相比,它通常会导致系统更加晦涩和响应迅速。请留神,解决开销与最初一个指针指向的子图的大小相干,并且在某些状况下可能并不重要。
立刻内存重用:与跟踪收集器不同,在收集器执行之前,无法访问的内存放弃未调配状态(通常在堆耗尽时);援用计数办法容许立刻从新应用抛弃的内存。这种立刻重用可为缓存带来更好的工夫局部性,从而缩小页面谬误。它还简化了资源清理,因为能够立刻调用终结器,从而更快地开释系统资源。立刻重用空间还能够进行优化,例如数据结构的就地更新。
易于实现:就实现细节而言,基于援用计数的收集是最简略的垃圾回收机制。如果语言运行时不容许指针操作和 / 或程序员无奈确定 / 操作对象根,则实现特地容易。
管制 vs 正确性:援用计数零碎能够为程序员提供对对象调配和解除调配的齐全管制。它能够容许程序员在其认为平安的中央优化援用计数开销。这的确带来了正确性挑战,并且须要更高的编码纪律。即便没有奇妙的优化,客户端程序的接口和援用计数计划之间也存在严密耦合。它要求客户端正确调用减少 / 缩小援用计数的操作。
空间开销:每个对象承载援用计数字段的空间开销。实践上,对于十分小的对象,这可能相当于 50% 的开销。这种开销须要与内存单元的立刻重用以及援用计数在收集期间不依赖于堆空间的事实相衡量。援用计数零碎能够通过应用单个字节进行援用计数而不是应用全字来缩小空间开销。这样的零碎通过回退跟踪计划(如标记扫描)来减少援用计数,以收集具备最大援用计数(和循环援用)的对象。
毛病:
指针更新开销:与指针更新是收费的跟踪计划不同,援用计数会带来很大的开销,因为每次指针更新都须要更新两个援用计数以放弃程序的正确性。
原子化操作:为了防止多线程竞争可能导致的对象开释过早,援用计数的增减操作记忆加载和存储指针的操作都必须是原子化的,而原子化的操作就须要解决很多线程竞争问题。
循环构造:正如咱们之前所探讨的,援用计数的最大毛病是它无奈回收循环存储。在简略援用计数办法下,双向链表或非简略图等循环数据结构无奈无效回收,并且会透露内存。
最坏状况下,某一个对象的援用计数可能等于堆中对象的总数,就导致援用计数所占的空间必须和某一个指针域大小雷同,这一空间也会十分低廉。
最初,援用计数算法仍有可能进展的呈现。当删除某一个大型构造根节点的最初一个援用时,该算法会递归的删除根节点的每一个子孙节点,线程平安的援用计数回收所导致的最大进展工夫可能会比追踪式回收器的长。
总结
援用计数就实现细节来说,是最简略的垃圾回收机制,因而在泛滥零碎中失去广泛应用,包含如 Lisp、Awk、Perl 和 Python 等编程语言、局部应用程序如 Photoshop、Real Network 的 Rhapsody 音乐服务,打印、扫描及文档管理系统)。
除了内存治理之外,援用计数还被宽泛用作操作系统中的资源管理机制,用于管理文件、套接字等系统资源。
以上就是本次分享的全部内容,当初想要学习编程的小伙伴欢送关注 Python 技术大本营,获取更多技能与教程。