Python中的堆栈内存
以我的理解:
python解释器中也开辟了堆栈,栈是用于存放指令集的,而堆是存放变量的
Python内存分配
以512字节为阙值分为大小对象,大对象直接分配内存,小对象使用专用内存分配器。小对象按固定长度对齐后,再分成不同类别,以便于复用和管理。
首先,向系统内存申请大块Arena内存,按页大小将其分成多个Pool快,这是一级重用单元,每个Pool为一种类别的对象提供内存。Pool被回收后,可重新为另一类别的对象服务
其次,按指定类别大小,将Pool在此分成多个Block块。每个Block块可存一个相同类别的小对象,这是二级重用
按我的理解就是: 内存分配其实就是堆内存的分配, 按照512字节为界限 大对象直接分配 小对象切分后以8字节一段分类存储
至于具体的实现逻辑——原谅我看不懂C的代码
Python的回收机制
引用技术
每个对象头部都有一个引用计数器,引用该对象时,技数增加,反之减少。当技术归零时,立即调用回收函数清理并释放内存
标记清除
为了防止循环引用问题,将对象头部加一个Go链表的数据结构,将所有的数据都串起来,做一个检测装置。如果检测到出现循环引用(即删除对象后引用技术>0)将其回收
分代清除
也有说叫隔代的,忘了=。= 将对象分为3代都有不同的阙值和计数器,每一次经过计算对象技术引用计数,将它们分为可达和不可达两类,所谓可达(存活)对象会移到高一代,不可达对象被释放
Python每个对象占用多少内存
sys.getsizeof(obj)但他不计算object instance的attribute value的大小,因为都是reference它调用的实际是
magic function: sizeof(self)
写个程序,计算一下各种对象的大小:
>>> class A(object): pass
>>> class B: pass
>>> import sys
>>> for x in (None, 1, 1.2, 'c', [], (), {}, set(), B, B(), A, A()):
... print ("{0:20s}\t{1:d}".format(type(x).__name__, sys.getsizeof(x)))
...
NoneType 16
int 28
float 24
str 50
list 64
tuple 48
dict 240
set 224
type 1056
B 56
type 1056
A 56
注意到:tuple=48,而dict=240,是tuple的5倍所以new style class的__slots__
就能看出节省多少空间但使用__slots__
后,只是空间的节省,性能会有损失,因为在lookup attribute时,如果有dict,那就是O(1),而使用__slots__
后,变成了O(n)。因此,__slots__
只能用在属性非常少的环境
__slots__
是规定当前类可对外访问的属性
__all__
是规定当前模块对外可调用的成员
引用计数为-1
当内存泄漏的时候引用计数可能不为0,其实我一直以为Python中没有内存泄漏的,因为高阶语言的话GC都挺完善的,然而,还是太年轻了
如果没有禁用垃圾回收,那么Python中的内存泄露有两种情况:
- 要么是对象被生命周期更长的对象所引用,比如global作用域对象;
- 要么是循环引用中存在del
_cache = []
class OBJ(object):
pass
def func_to_leak():
o = OBJ()
_cache.append(o)
if True:
return
_cache.remove(o)
# 将对象添加到全局的列表中,我们知道全局的对象一般只有当其执行结束才会被清除, 这就属于内存泄漏
class ConnectionHandler(object):
def __init__(self, connection):
self._conn = connection
class Connection(object):
def __init__(self):
self._conn_handler = ConnectionHandler(self)
# 循环引用
单利模式
唯一一个送分题一激动搞成了送命题
class Foo(object):
_instence = None
def __new__(cls):
if _instance == None:
cls._instance = super(Foo).__new__(cls)
return cls._instance
LRU
全称是Least Recently Used
最近最少使用
LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
它的使用场景是:在有限的空间中存储对象时,当空间满时,会按一定的原则删除原有的对象.常用的原则(算法)有LRU,FIFO,LFU等
from collections import OrderedDict
class LRUCache(OrderedDict):
def __init__(self, capacity):
self.capacity = capacity
self.cache = OrderedDict()
def get(self, key, **kwargs):
if self.cache.get(key, None):
value = self.cache.pop(key)
self.cache[key] = value
else:
value = None
return value
def set(self, key, value):
if self.cache.get(key, None):
value = self.cache.pop(key)
self.cache[key] = value
else:
if len(self.cache) == self.capacity:
self.cache.popitem(last=False)
self.cache[key] = value
else:
self.cache[key] = value
当然在python3.6中也有LRU算法的缓存机制
我用有道翻译成了中文
_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
def lru_cache(maxsize=128, typed=False):
"""
最近最少使用缓存装饰。
如果maxsize被设置为None,LRU特性将被禁用,并且缓存可以不受限制地增长。
如果类型是正确的,不同类型的参数将被单独缓存。例如,f(3.0)和f(3)将被视为不同的调用,并具有不同的结果。
对缓存函数的参数必须具有可洗性。使用f.cache info()查看名为tuple(命中、遗漏、maxsize、currsize)的缓存统计信息。
使用f.cache Clear()清除缓存和统计信息。用f包装来访问底层函数。 见:http://en.wikipedia.org/wiki/Cache_algorithms Least_Recently_Used
"""
# 用户只能通过其公共API访问lrucache:
# cache_info, cache_clear, and f.__wrapped__
# lrucache的内部结构被封装在线程安全中
# 允许实现更改(包括可能的C版本)。
# 在没有任何参数的情况下,及早发现对@lrucache的错误调用
# 结果是内部函数被传递给maxsize而不是
# 整数或没有
if maxsize is not None and not isinstance(maxsize, int):
# 如果maxsize 设置的类型不是int 则抛出异常
raise TypeError('Expected maxsize to be an integer or None')
def decorating_function(user_function):
# 这里是主要处理逻辑,在下面(没有进行代码的删除,会比较长)
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
return update_wrapper(wrapper, user_function) # 更新闭包函数
return decorating_function
def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
# 所有lru缓存实例共享的常量
sentinel = object() # 特殊标记用来表示缓存未命中
make_key = _make_key # 根据函数参数生成缓存的key
PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # 链表的各个域
cache = {}
hits = misses = 0
full = False
cache_get = cache.get # 绑定方法去查找key 如果没有返回None
lock = RLock() # 因为链表更新不是线程安全的所以需要加锁
root = [] # root是一个环形双向链表
root[:] = [root, root, None, None] # 初始化节点
if maxsize == 0:
def wrapper(*args, **kwds):
# 没有缓存——只是在成功调用之后的统计更新
nonlocal misses
result = user_function(*args, **kwds) # 被装饰的函数执行后的返回值
misses += 1
return result
elif maxsize is None:
def wrapper(*args, **kwds):
# 简单的缓存,没有顺序或大小限制
nonlocal hits, misses
key = make_key(args, kwds, typed) # 从可选的位置和关键字参数中创建一个缓存键
result = cache_get(key, sentinel)
if result is not sentinel:
hits += 1
return result
result = user_function(*args, **kwds)
cache[key] = result
misses += 1
return result
else:
def wrapper(*args, **kwds):
#用于跟踪访问的大小限制缓存
nonlocal root, hits, misses, full
key = make_key(args, kwds, typed)
with lock:
link = cache_get(key) # 缓存命中
if link is not None:
# 将被访问的节点移动到环形列表前面
link_prev, link_next, _key, result = link
link_prev[NEXT] = link_next
link_next[PREV] = link_prev
last = root[PREV]
last[NEXT] = root[PREV] = link
link[PREV] = last
link[NEXT] = root
hits += 1
return result
result = user_function(*args, **kwds)
with lock:
if key in cache:
# 到达这里意味着这个key被缓存
# 锁被释放。
# 也就是这个节点已经移动了,缓存也更新了,我们只需要返回
# 计算结果并更新遗漏的计数。
pass
elif full: # 新增缓存结果,一处访问频率低的节点
# 下面的操作是使用root当前指向的节点存储key 和 result
oldroot = root
oldroot[KEY] = key
oldroot[RESULT] = result
# 接下来将原 root 指向的下一个节点作为新的 root
# 同时将新 root 节点的key 和 result 清空
# 这样使用频率最低的节点结果就从缓存中移除了
root = oldroot[NEXT]
oldkey = root[KEY]
oldresult = root[RESULT]
root[KEY] = root[RESULT] = None
# Now update the cache dictionary.
del cache[oldkey]
# Save the potentially reentrant cache[key] assignment
# for last, after the root and links have been put in
# a consistent state.
cache[key] = oldroot
else: # 仅更新缓存节点
# 新增节点插入到 root 节点的前面
last = root[PREV]
link = [last, root, key, result]
last[NEXT] = root[PREV] = cache[key] = link
full = (len(cache) >= maxsize)
misses += 1
return result
def cache_info():
"""缓存统计数据"""
with lock:
return _CacheInfo(hits, misses, maxsize, len(cache))
def cache_clear():
"""清除缓存和缓存统计信息"""
nonlocal hits, misses, full
with lock:
cache.clear()
root[:] = [root, root, None, None]
hits = misses = 0
full = False
wrapper.cache_info = cache_info
wrapper.cache_clear = cache_clear
return wrapper
总结下:
如果使用LRU缓存机制,它会创建一个双向环形链表,通过在缓存命中时,将节点移动到队列的前边的方式,从而间接地记录了最近经常访问的节点。当缓存空间满了后,会自动“移除”位于环形队列尾部最近命中频率最低的节点,从而为新增缓存节点腾出了空间。
使用:
from functools import lru_cache
@lru_cache(5)
def demo(i):
print('demo', i)
return i
for i in range(5):
demo(i)
for i in range(5):
demo(i)
输出结果
demo 0
demo 1
demo 2
demo 3
demo 4