- 内存
- 计算机的本质作用
- 对数据进行存储和运算。首先我们需要知道我们目前使用的计算机都是二进制的计算机,就以为着计算机只可以存储和运算二进制的数据。例如下载好的一部电影,该电影可以存储到计算机中,计算机中存储的是基于二进制的电影数据,然后我们可以通过相关的视频播放软件结合相关的硬件对电影的二进制数据进行相关的运算操作,所产生的结果就是我们可以看到电影的画面和听到音频的声音。
- 问题:阐述计算机如何计算1+2的结果?
- 阐述:简单理解为,首先可以将1和2输入到计算机中,然后计算机会将1和2转换成二进制的数据进行数据存储,然后通过加法器进行两个二进制数值的计算并返回结果。
- 分析:上述的阐述中提到,计算机首先需要存储1和2这两个数值,那么计算机如何进行数据的存储呢?那么毫无疑问,计算机可以将数据直接存储到内存中。
- 什么是编程中的变量?
- 我们在编程世界中,可以将某个数值直接赋值给一个变量,但是最终数值会被存储到计算机的内存中,因此我们可以理解为,变量表示的就是计算机中进行数据存储的某一块内存。
- 如何形象化的理解计算机的内存?
- 举例:将计算机的内存空间映射到我们现实生活中的话,内存就好比是我们在现实生活中三维立体的空间。生活在北京的北漂们,几乎都居住的是一个独立的公寓或者合租在一个几居室的某一个房间中,那么北漂甲就好比是数据,而他所居住的房间则就是存储数据的一块内存空间。
- 分析:从上述案例中,我们可以得知北漂甲居住的房间会有两个基本的属性,
- 其一就是房间空间的大小
- 其二就是房间的一个位置标识(门牌号)
- 那么计算机中存储数据的内存空间也会有这两个最基本的属性:内存空间大小和内存空间的地址。
- 内存空间的大小可以表示该空间可以存储数据值的大小范围,内存空间的地址(用十六进制数值表示)可以用来通过寻址定位、查找到该内存空间中所存储的数据值。
- 引用:当一个变量中存储的是某一块内存空间的地址,则该变量即可成为那块内存空间的引用。a=10,a就是10所在内存空间的一个引用。
- 指向:当一个变量中存储了一块内存空间的地址,则称该变量(引用)指向了那块内存。
- 不同类型数据占用内存空间的大小:整形(4字节),浮点型(8字节),字符型(1字节)
- 计算机的本质作用
- 顺序表
- 顺序表就是集合中存储的元素是有顺序的。
- 顺序表的结构可以分为两种形式:
- 单数据类型
- 多数据类型
- 对象的内存使用
- 赋值语句是语言最常见的功能了。但即使是最简单的赋值语句,也可以很有内涵。
a = 1整数1为一个对象。而a是一个引用。利用赋值语句,引用a指向对象1。Python是动态类型的语言对象与引用 分离。Python像使用“筷子”那样,通过引用来接触和翻动真正的食物——对象。 为了探索对象在内存的存储,我们可以求助于Python的内置函数id()。它用于返回对象的身份(identity)。其实,这里所谓的身份,就是该对象的内存地址。
a = 1 print(id(a)) print(hex(id(a)))
- 缓存机制
- 在Python中,整数和短小的字符,Python都会缓存这些对象,以便重复使用。当我们创建多个等于1的引用时,实际上是让所有这些引用指向同一个对象。
a = 1
b = 1
#a和b实际上是指向同一个对象的两个引用
print(id(a))
print(id(b))
- 为了检验两个引用指向同一个对象,我们可以用is关键字。is用于判断两个引用所指的对象是否相同。
# True a = 1 b = 1 print(a is b) # True a = "good" b = "good" print(a is b) # False a = [] b = [] print(a is b)可以看到,由于Python缓存了整数和短字符串,因此每个对象只存有一份。比如,所有整数1的引用都指向同一对象。即使使用赋值语句,也只是创造了新的引用,而不是对象本身。长的字符串和其它对象可以有多个相同的对象,可以使用赋值语句创建出新的对象。
- 引用计数
- 在Python中,每个对象都有存有指向该对象的引用总数,即引用计数(reference count)。
- 我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数。
- 需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。
-
from sys import getrefcount class Stu(): pass s1 = Stu() s2 = s1 #s2和s1有着共同的指向 print(getrefcount(s2))
Python的一个容器对象(container),比如列表、字典等,可以包含多个对象。实际上,容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。
引用的减少某个对象的引用计数可能减少。比如,可以使用del关键字删除某个引用:
from sys import getrefcount
class Stu():
pass
s1 = Stu()
s2 = s1 #s2和s1有着共同的指向
s2 = 'bobo' #将stu对象的引用计数-1
print(getrefcount(s1))
del也可以用于删除容器元素中的元素,比如:
pass
如果某个引用指向对象A,当这个引用被重新定向到某个其他对象B时,对象A的引用计数减少:
pass垃圾回收
- 吃太多,总会变胖,Python也是这样。当Python中的对象越来越多,它们将占据越来越大的内存。
- 不过你不用太担心Python的体形,它会乖巧的在适当的时候“减肥”,启动垃圾回收(garbage collection),将没用的对象清除。在许多语言中都有垃圾回收机制,比如Java和Ruby。尽管最终目的都是塑造苗条的体型,但不同语言的减肥方案有很大的差异 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NaYpaLkR-1654421939596)(imgs/11.png)]
- 从基本原理上,当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。
- 比如某个新建对象,它被分配给某个引用,对象的引用计数变为1。如果引用被删除,对象的引用计数为0,那么该对象就可以被垃圾回收。比如下面的代码:
from sys import getrefcount
class Stu():
pass
s1 = Stu()
s2 = s1 #s2和s1有着共同的指向
s2 = 'bobo' #将stu对象的引用计数-1
s1 = 'jay'
#思考:Stu()对象的引用计数为0.则表示该内存丢失了所有的引用,则内存中的数据无法被访问
-
del a后,已经没有任何引用指向之前建立的[1, 2, 3]这个列表对象。用户不可能通过任何方式接触或者动用这个对象。这个对象如果继续待在内存里,就成了不健康的脂肪。当垃圾回收启动时,Python扫描到这个引用计数为0的对象,就将它所占据的内存清空。
-
但是,减肥是个昂贵而费力的事情。垃圾回收时,Python不能进行其它的任务。频繁的垃圾回收将大大降低Python的工作效率。如果内存中的对象不多,就没有必要总启动垃圾回收。因此只会在特定条件下,自动启动垃圾回收。
- 当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。
-
我们可以通过gc模块的get_threshold()方法,查看该阈值:
import gc print(gc.get_threshold())
-
返回(700, 10, 10),后面的两个10是与分代回收相关的阈值,后面可以看到。700即是垃圾回收启动的阈值。可以通过gc中的set_threshold()方法重新设置。
-
我们也可以手动启动垃圾回收,即使用gc.collect()。
-
Python同时采用了分代回收的策略。这一策略的基本假设是:存活时间越久的对象,越不可能在后面的程序中变成垃圾。
-
我们的程序往往会产生大量的对象,许多对象很快产生和消失,但也有一些对象长期被使用。出于信任和效率,对于这样一些“长寿”对象,Python相信它们是有用处的,所以减少在垃圾回收中扫描它们的频率。
-
Python将所有的对象分为0,1,2三代。
-
所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。
-
垃圾回收启动时,一定会扫描所有的0代对象,进行垃圾对象回收。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。
-
这个次数即上面get_threshold()返回的(700, 10, 10)返回的两个10。也就是说,每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收。
-
这里的700指的是:当分配对象的个数减去释放对象的个数的差值大于700时,就会产生一次0代回收。
-
-
同样可以用set_threshold()来调整,比如对2代对象进行更频繁的扫描。
pass标记清除
- 引用计数,并不能解决所有的问题,一旦出现了循环引用,那么,这些对象的引用次数永远都是大于0的,但是这些对象都是不可用的垃圾数据。下面的代码展示了一种循环引用的情况:
#为了便于观察循环引用导致的内存泄漏问题,我定义了两个类,DictA,和DictB两个类,他们均继承了字典类。
import gc
class DictA(dict):
def __del__(self):
print('DictA对象被销毁')
class DictB(dict):
def __del__(self):
print('DictB对象被销毁')
下面的代码,先为你演示没有循环引用的情况:
a = DictA()
b = DictB()
a = 1
b = 1
print('ok')
下面的代码,演示有循环引用的情况:
a = DictA() #DictA和DictB对象引用计数为各自为1
b = DictB()
a['b'] = b # 循环引用:然后让字典的键a和b再次指向DictA和DictB对象,因此其引用计数为+1为2
b['a'] = a
a = 1 #引用a和b不在指向DictA和DictB对象,则其引用计数-1为1,则该对象引用计数无法被减到0
b = 1
print('ok')
那么标记清除如何可以解决循环引用问题呢?标记清除的原理是什么?
标记清除可以处理这种循环引用的情况,它分为两个阶段:
-
第一阶段:标记阶段
- GC会把所有活动对象打上标记,这些活动的对象就如同一个个的点,而他们之间的引用关系则构成边,最终点和边构成了一个有向图
-
第二阶段:搜索清除阶段
从根对象(root)出发,沿着有向边遍历整个图,不可达的对象就是需要清理的垃圾对象。这个根对象就是全局对象,调用栈,寄存器。
在上图中,从root出发后,可以到达 1 2 3 4,而5, 6, 7均不能到达,其中6和7互相引用,这3个对象都会被回收。



