目录
1、Python引用机制
2、引用计数
2.1、getrefcount()
2.2、del 删除引用
2.3、[循环引用]
2.4、内存泄露和内存溢出
3、垃圾回收
3.1、gc机制
3.2、效率问题
3.3、三种情况触发垃圾回收
3.3.1、垃圾回收步骤
4、内存池机制
4.1、小整数对象缓冲池
4.2、字符串驻留区
4.2.1、触发驻留机制的几种情况(交互模式)
4.3、python内存管理
5、深拷贝和浅拷贝⭐⭐⭐
5.1、浅拷贝
5.2、深拷贝
1、Python引用机制
•
对象
是储存在内存中的实体。
•
我们在程序中写的
对象名
,只是指向这一对象的引用(reference)
•
引用和对象分离
,是动态类型的核心
•
引用可以随时指向一个新的对象(内存地址会不一样)
2、引用计数
作用:记录对象被引用的次数,当引用计数为0的时候,这个对象就会被销毁回收
当"a=500"时,引用计数就为+1;若是修改之后"a=600",原来的那片空间的引用计数就会-1;若是对象的引用计数为0时,销毁对象,这样就进行了空间的回收。
2.1、getrefcount()
getrefcount(),会创建一个临时的引用,显示出的引用计数会比实际多1
>>> from sys import getrefcount >>> a = 500 >>> getrefcount(a) 2 >>> b = a >>> getrefcount(a) 3 # 容器类型放的都是引用 >>> lst = [a] >>> getrefcount(a) 4
2.2、del 删除引用
>>> del a
>>> gettrefcount(b)
Traceback (most recent call last):
File "", line 1, in
NameError: name 'gettrefcount' is not defined
>>> getrefcount(b)
3
>>> lst
[500]
>>> b
500
>>> a
Traceback (most recent call last):
File "", line 1, in
NameError: name 'a' is not defined
2.3、[循环引用]
引用计数可以解决大部分内存释放问题,但是无法解决循环引用问题
>>> x = [1] >>> y = [2] >>> x.append(y) >>> getrefcount(y) 3 >>> y.append(x) >>> getrefcount(x) 3 >>> x [1, [2, [...]]] >>> y [2, [1, [...]]] >>> del x >>> del y >>> x Traceback (most recent call last): File "", line 1, in NameError: name 'x' is not defined >>> y Traceback (most recent call last): File " ", line 1, in NameError: name 'y' is not defined # 实际上x和y的内存空间并没有被释放
虽然删除x和y,引用计数都减1,但是没有归0,在内存中就不会释放,程序又不能访问这片空间,就会造成内存泄露。没有归0的原因是,空间中两个对象相互引用。
2.4、内存泄露和内存溢出
内存泄露:可以理解为占着茅坑不拉屎。有一部分内存无法被回收释放,进程又无法访问。解决方法为:垃圾回收
内存溢出(out of memory或者说oom):内存不够用,程序需要的内存大于系统内存。
3、垃圾回收
[回收原则]
当python的某个对象的引用计数降为0,可以被垃圾回收
3.1、gc机制
3.2、效率问题
3.3、三种情况触发垃圾回收
(1)调用gc.collect()(2)GC达到阙值时(3)程序退出时
# 只要我们在"2.3、[循环引用]"这节的那段代码后加入这段代码就能够解决循环引用问题了 >>> import gc >>> gc.collect() 2 >>> gc.collect() 0
3.3.1、垃圾回收步骤
[分代(generation)回收]
>>> import gc >>> gc.get_threshold() (700, 10, 10)
700 代表新创建的对象减去从新创建的对象中回收的数量的差值大于700 就进行一次0代回收当0代回收进行10次的时候就进行一代回收(并且一代回收的时候也进行0代回收),同理,当一代回收进行10次的时候就进行一次2代回收(并且二代回收的时候也进行0代回收和一代回收)。不是整个系统里的所有变量都会检查,每次检查的都是0代变量。
[标记清除]
4、内存池机制
[引入]
>>> a = 1 >>> b = 2 >>> from sys import getrefcount >>> getrefcount(a) 799 >>> getrefcount(b) 99 >>> c = 200 >>> getrefcount(c) 3 >>> d = 500 # 这里是生成了2个500对象,因为都不在这个整数池,所以都在内存中新开一片空间 >>> f = 500 >>> getrefcount(d) 2 >>> getrefcount(f) 2 >>> g = 1 >>> getrefcount(g) 800 >>> id(a) 140337534945184 >>> id(g) 140337534945184 >>> id(d) 140337535628144 >>> id(f) 140337535628336
4.1、小整数对象缓冲池
对于[-5,256] 这样的小整数,系统已经初始化好,可以直接拿来用。而对于其他的大整数,系统则提前申请了一块内存空间,等需要的时候在这上面创建大整数对象。正是因为如此,才会出现上面代码执行的情况。
4.2、字符串驻留区
为了检验两个引用指向同一个对象,我们可以用is关键字。is用于判断两个引用所指的对象是否相同。当触发缓存机制时,只是创造了新的引用,而不是对象本身。
>>> str1 = 'abc'
>>> str2 = 'abc'
>>> str1 is str2
True
当我们str1 = 'abc',会在内存中创建一个对象放在专门的空间,这个空间叫做字符串驻留区。然后再赋值str2 = 'abc'的时候,会首先在驻留区中看是否有这个字符串,若是有就直接引用这个字符串的地址;没有就创建。
4.2.1、触发驻留机制的几种情况(交互模式)
(1)字符串长度为0或者1
>>> s2 = '%' >>> s1 = '%' >>> s1 is s2 True >>> s1 = '' >>> s2 = '' >>> s1 is s2 True
(2)符合标识符的字符串(只包含字母数字下划线)
>>> str1 = 'abc' >>> str2 = 'abc' >>> str1 is str2 True >>> str3 = 'abc ' # 不符合标识符命令规则 >>> str4 = 'abc ' >>> str3 is str4 False >>> str5 = 'sjk_11' >>> str6 = 'sjk_11' >>> str5 is str6 True
(3)字符串只在编译时进行驻留,而非运行时
>>> a = 'abc' >>> b = 'ab' + 'c' # 编译时 >>> c = ''.join(['ab','c']) # 运行时 >>> a is b True >>> a is c False
(4)强制驻留(sys.intern)
sys中intern方法可以强制两个字符串指向一个对象
>>> s1 = 'abc ' >>> s2 = 'abc ' >>> s1 is s2 False >>> import sys >>> s1 = sys.intern(s2) >>> s1 is s2 True
(一种奇怪的情况)
>>> str7 = 'a'*20 >>> str8 = 'a'*20 >>> str7 is str8 True >>> str9 = 'a'*21 >>> str10 = 'a'*21 >>> str9 is str10 False # 我也不知道为什么这样,测试了很久,也没有得到结果。若是哪位大佬看到了并且知道是否可以私信小弟或者评论指点一下。感谢了
4.3、python内存管理
引用计数为主,标记清除和分代回收为辅的垃圾回收方式,进行内存回收管理。还引用了小整形缓冲池以及常用字符串驻留区的方式进行内存分配管理
5、深拷贝和浅拷贝⭐⭐⭐
深浅拷贝:主要是针对容器类型里面包含可变容器类型的情况,即容器里边包容器。只有容器里边包含可变数据类型的情况才会有深浅拷贝的区别
>>> a = [1,2,3] >>> b = a >>> b.append(4) # 可变数据类型,若是使用等号复制且b直接改变,那么b改变,a必然也会改变 >>> b [1, 2, 3, 4] >>> a [1, 2, 3, 4] >>> a = 100 # 这个是不可变数据类型 >>> b = a >>> b = 200 >>> b 200 >>> a 100
那么怎么复制可变数据类型,可以改变拷贝版本不让源跟着改变呢?
5.1、浅拷贝
对于容器里包含容器的情况,".copy()'只会拷贝第一层的地址。其他情况拷贝的,比如"="就是与源完全相同。
>>> a = [1,2,3] >>> b = a.copy() # 这种也是浅拷贝,且只是一个单容器。所以使用copy能够直接拷贝对应地址的值 >>> a is b False >>> a [1, 2, 3] >>> b [1, 2, 3] >>> b.append(4) >>> a [1, 2, 3] >>> b [1, 2, 3, 4]
>>> d1
{'a': [1, 2], 'b': 2}
>>> d2 = d1.copy() # 这只是浅拷贝,只拷贝了里边函数的地址。因为这是属于容器里边包容器
>>> d2
{'a': [1, 2], 'b': 2}
>>> d1
{'a': [1, 2], 'b': 2}
>>> d1 is d2
False
>>> d1['a']
[1, 2]
>>> d1['a'].append(3)
>>> d1
{'a': [1, 2, 3], 'b': 2}
>>> d2
{'a': [1, 2, 3], 'b': 2}
>>> id(d1['a'])
140337536794184
>>> id(d2['a'])
140337536794184
>>> lst = [[]]*3
>>> lst[0]
[]
>>> lst[0].append(1)
>>> lst
[[1], [1], [1]]
>>> id(lst[0])
140523544464520
>>> id(lst[1])
140523544464520
>>> id(lst[2])
140523544464520
若是只是浅拷贝,对于容器里边包容器的这种类型,被包含容器拷贝的是原地址,而不是它的值
[要是还不理解。下面深度解析一下]
浅拷贝对于容器包含容器的这种类型的拷贝。可以理解为引用了被包含容器的地址(即最外层[]里边所有内容的地址),然后用一层新的地址包装这些被包含容器的地址。然后这就导致了原a和拷贝b的地址不一样。我们可以把它理解为换汤不换药,汤为最外层的[],药为最外层[]里边所有内容的地址
5.2、深拷贝
深拷贝,会拷贝每一层的值,递归拷贝所有层的数据。
只有上面这一种情况才是深拷贝,其他情况都是浅拷贝
>>> import copy
>>> d3 = copy.deepcopy(d1)
>>> d3
{'a': [1, 2, 3], 'b': 2}
>>> d1
{'a': [1, 2, 3], 'b': 2}
>>> d3 is d1
False
>>> d3['a'].append(4)
>>> d3
{'a': [1, 2, 3, 4], 'b': 2}
>>> d1
{'a': [1, 2, 3], 'b': 2}



