python可以自动回收垃圾吗_python3新特性

python可以自动回收垃圾吗_python3新特性前言现在的高级语言如java,c#等,都采用了垃圾回收机制,而不再像c,c++里,需要用户自己管理内存。自己管理内存及其自由,可以任意申请内存,但这如同一把双刃剑,可能会造成内存泄漏,空指针等bug

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

前言

现在的高级语言如javac#等,都采用了垃圾回收机制,而不再像cc++里,需要用户自己管理内存。自己管理内存及其自由,可以任意申请内存,但这如同一把双刃剑,可能会造成内存泄漏,空指针等bug。
 
python中也同java一样采用了垃圾回收机制,不过不一样的是:python采用的是引用计数机制为主,标记清除分代回收两种机制为辅的策略
 

1.引用计数器

python里一切皆对象,它们的核心就是一个结构体:PyObject

typedef struct_object {
    int ob_refcnt;
    struct_typeobject *ob_type;
} PyObject;

PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数,当有一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除时,它的ob_refcnt就会减少,当引用计数为0时,该对象生命就结束了

#define Py_INCREF(op)   ((op)->ob_refcnt++) //增加计数
	#define Py_DECREF(op) \ //减少计数
    	if (--(op)->ob_refcnt != 0) \
        	; \
   	 else \
        	__Py_Dealloc((PyObject *)(op))

引用计数的优点

  • 1.简单
  • 2.实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时
     

引用计数的缺点

  • 1.维护引用计数消耗资源
  • 2.循环引用的问题无法解决
     

增加引用计数的方式

  • 1.有新的对象引用
  • 2.装进列表
  • 3.作为函数参数
     

减少引用计数的方式

  • 1.新的对象不再使用
  • 2.从列表中移除
  • 3.函数结束
  • 4.del显示销毁
     

获取对象的引用计数方式

sys.getrefcount(对象名)
 

案例

import sys


class A:
    def __new__(cls, *args, **kwargs):
        print("开辟了一些内存")
        return super(A, cls).__new__(cls)

    def __init__(self):
        print(f"创建对象{hex(id(self))}")

    def __del__(self):
        print(f"销毁对象{hex(id(self))}")

list1 = []
a = A()  # 创建一个对象,引用计数默认为2
print(f"初始的引用计数:{sys.getrefcount(a)}")  

b = a  # b对象引用a,引用计数+1
print(f"增加1个引用后a的引用计数:{sys.getrefcount(a)}")

b = 1  # b对象不再引用a,引用计数-1
print(f"减少1个引用后a的引用计数:{sys.getrefcount(a)}")

list1.append(a)  # 将a对象添加到列表中,引用计数+1
print(f"添加列表后a的引用计数:{sys.getrefcount(a)}")

list1.remove(a)  # 将a对象从列表中删除,引用计数-1
print(f"从list1列表中删除后a的引用计数:{sys.getrefcount(a)}")

# 作为函数参数,引用计数+2
def test(a):
    print(f"作为函数参数a的引用计数:{sys.getrefcount(a)}")
test(a)  
print(f"函数结束后a的引用计数:{sys.getrefcount(a)}")
del a

结果

开辟了一些内存
创建对象0x7fcf1ff8a910
初始的引用计数:2
增加1个引用后a的引用计数:3
减少1个引用后a的引用计数:2
添加列表后a的引用计数:3
从list1列表中删除后a的引用计数:2
作为函数参数a的引用计数:4
函数结束后a的引用计数:2
销毁对象0x7fcf1ff8a910

 

2.标记清除

基于引用计数器进行垃圾回收非常方便和简单,但他还是存在循环引用的问题,导致无法正常的回收一些数据,例如:

v1 = [11,22,33]        # refchain中创建一个列表对象,由于v1=对象,所以列表引对象用计数器为2.
v2 = [44,55,66]        # refchain中再创建一个列表对象,因v2=对象,所以列表对象引用计数器为2.
v1.append(v2)        # 把v2追加到v1中,则v2对应的[44,55,66]对象的引用计数器加1,最终为3.
v2.append(v1)        # 把v1追加到v2中,则v1对应的[11,22,33]对象的引用计数器加1,最终为3.
del v1    # 引用计数器-1
del v2    # 引用计数器-1

对于上述代码会发现,执行del操作之后,没有变量再会去使用那两个列表对象,但由于循环引用的问题,他们的引用计数器不为0,所以他们的状态:永远不会被使用、也不会被销毁。项目中如果这种代码太多,就会导致内存一直被消耗,直到内存被耗尽,程序崩溃。
 
为了解决循环引用的问题,引入了标记清除技术,专门针对那些可能存在循环引用的对象进行特殊处理,可能存在循环应用的类型有:列表、元组、字典、集合、自定义类等那些能进行数据嵌套的类型。
 
标记清除:创建特殊链表专门用于保存 列表、元组、字典、集合、自定义类等对象,之后再去检查这个链表中的对象是否存在循环引用,如果存在则让双方的引用计数器均 – 1 。如果减完为0,则垃圾回收
 

3.分代回收

对标记清除中的链表进行优化,将那些可能存在循引用的对象拆分到3个链表,链表称为:0/1/2三代,每代都可以存储对象和阈值,当达到阈值时,就会对相应的链表中的每个对象做一次扫描,除循环引用各自减1并且销毁引用计数器为0的对象。

// 分代的C源码
#define NUM_GENERATIONS 3
struct gc_generation generations[NUM_GENERATIONS] = {
    /* PyGC_Head,                                    threshold,    count */
    {{(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)},   700,        0}, // 0代
    {{(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)},   10,         0}, // 1代
    {{(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)},   10,         0}, // 2代
};

特别注意:0代和1、2代的threshold和count表示的意义不同。

  • 0代,count表示0代链表中对象的数量,threshold表示0代链表对象个数阈值,超过则执行一次0代扫描检查
  • 1代,count表示0代链表扫描的次数,threshold表示0代链表扫描的次数阈值,超过则执行一次1代扫描检查。
  • 2代,count表示1代链表扫描的次数,threshold表示1代链表扫描的次数阈值,超过则执行一2代扫描检查。

分代回收触发机制(GC阈值)

随着你的程序运行,Python解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,这两个值应该保持一致,因为程序新建的每个对象都应该最终被释放掉。当然,事实并非如此。因为循环引用的原因,从而被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,则Python的收集机制就启动了,并且触发上边所说到的零代算法,释放“浮动的垃圾”,并且将剩下的对象移动到一代列表。随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。而Python对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python会将剩下的活跃对象移动到二代列表。通过这种方法,你的代码所长期使用的对象,那些你的代码持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python可以在不同的时间间隔处理这些对象。Python处理零代最为频繁,其次是一代然后才是二代。
 

gc模块的使用

  1. gc.get_count() 获取当前自动执行垃圾回收的计数器,返回一个长度为3的列表
  2. gc.get_threshold() 获取gc模块中自动执行垃圾回收的频率,默认是(700, 10, 10)
  3. gc.set_threshold(threshold0[,threshold1,threshold2]) 设置自动执行垃圾回收的频率
  4. gc.disable() python3默认开启gc机制,可以使用该方法手动关闭gc机制
  5. gc.collect() 手动调用垃圾回收机制回收垃圾
     

案例

import gc
import time

class A():
    def __new__(cls, *args, **kwargs):
        print("new")
        return super(A, cls).__new__(cls)
    def __init__(self):
        print(f"object:born at {hex(id(self))}")
    def __del__(self):
        print(f"{hex(id(self))}被系统回收")
def start():
    while True:
        a = A()
        b = A()
        a.v = b
        b.v = a
        del a
        del b
        print(gc.get_threshold())  # 获取gc模块中自动执行垃圾回收的频率
        print(gc.get_count())  # 获取当前执行垃圾回收的计数器
        time.sleep(0.1)
start()

结果

object:born at 0x7fc5b8a15390
(700, 10, 10)
(698, 4, 1)
new
object:born at 0x7fc5b8a153d0
new
0x7fc5b8896790被系统回收
0x7fc5b8a0a7d0被系统回收
0x7fc5b8a0a810被系统回收
0x7fc5b8a0a850被系统回收
.....
object:born at 0x7fc5b8896790
(700, 10, 10)
(0, 5, 1)

我们可以看到,当0代698的时候,又new了2个对象,达到700时,系统就会自动回收,回收后,原来的1代是4,现在变成了5,而0代又重新从0开始计算了
 

4.小结

在python中维护了一个refchain双向环状链表、这个链表中存储程序创建的所有对象,每种类型的对象中都有一个ob_refcnt引用计数器的值,引用个数+1、-1,最后当引用计数器变为0时会进行垃圾回收(对象销毁、refchain中移除)。
 
但是,python中那些可以有多个元素组成的对象可能会存在出现循环引用的问题,为了解决这个问题,python又引入了标记清除和分代回收,在其内部为4个链表

  • refchain
  • 2代,10次
  • 1代,10次
  • 0代,700次

在源码内部当达到各自的阈值时,会出发扫描链表进行标记清除的动作(有循环就各自-1),但是源码内部还提供了优化机制
 

5.Python缓存

从上文大家可以了解到当对象的引用计数器为0时,就会被销毁并释放内存。而实际上他不是这么的简单粗暴,因为反复的创建和销毁会使程序的执行效率变低。Python中引入了“缓存机制”机制。 例如:引用计数器为0时,不会真正销毁对象,而是将他放到一个名为 free_list 的链表中,之后会再创建对象时不会在重新开辟内存,而是在free_list中将之前的对象来并重置内部的值来使用。

  • float类型,维护的free_list链表最多可缓存100个float对象。
  v1 = 3.14    # 开辟内存来存储float对象,并将对象添加到refchain链表。
  print( id(v1) ) # 内存地址:140599203433232
  del v1    # 引用计数器-1,如果为0则在rechain链表中移除,不销毁对象,而是将对象添加到float的free_list.
  v1 = 3.14    # 优先去free_list中获取对象
  print( id(v1) ) # 内存地址:140599203433232
  # 注意:引用计数器为0时,会先判断free_list中缓存个数是否满了,未满则将对象缓存,已满则直接将对象销毁。
  • int类型,不是基于free_list,而是维护一个small_ints链表保存常见数据(小数据池),小数据池范围:-5 <= value < 257。即:重复使用这个范围的整数时,不会重新开辟内存。
  v1 = 38    # 去小数据池small_ints中获取38整数对象,将对象添加到refchain并让引用计数器+1。
  print( id(v1))  #内存地址:4401668032
  v2 = 38 # 去小数据池small_ints中获取38整数对象,将refchain中的对象的引用计数器+1。
  print( id(v2) ) #内存地址:4401668032
  # 注意:在解释器启动时候-5~256就已经被加入到small_ints链表中且引用计数器初始化为1,代码中使用的值时直接去small_ints中拿来用并将引用计数器+1即可。另外,small_ints中的数据引用计数器永远不会为0(初始化时就设置为1了),所以也不会被销毁。
  • str类型,维护unicode_latin1[256]链表,内部将所有的ascii字符缓存起来,以后使用时就不再反复创建。
  v1 = "A"
  print( id(v1) ) # 输出:140599159374000
  del v1
  v2 = "A"
  print( id(v1) ) # 输出:140599159374000
  # 除此之外,Python内部还对字符串做了驻留机制,针对那么只含有字母、数字、下划线的字符串(见源码Objects/codeobject.c),如果内存中已存在则不会重新在创建而是使用原来的地址里(不会像free_list那样一直在内存存活,只有内存中有才能被重复利用)。
  v1 = "jack"
  v2 = "jack"
  print(id(v1) == id(v2)) # 输出:True
  • list类型,维护的free_list数组最多可缓存80个list对象。
  v1 = [11,22,33]  
  print( id(v1) ) # 输出:4517628816
  del v1
  v2 = ["j","ack"]
  print( id(v2) ) # 输出:4517628816
  • tuple类型,维护一个free_list数组且数组容量20,数组中元素可以是链表且每个链表最多可以容纳2000个元组对象。元组的free_list数组在存储数据时,是按照元组可以容纳的个数为索引找到free_list数组中对应的链表,并添加到链表中。
  v1 = (1,2)
  print( id(v1) )
  del v1  # 因元组的数量为2,所以会把这个对象缓存到free_list[2]的链表中。
  v2 = ("甲壳虫","Alex")  # 不会重新开辟内存,而是去free_list[2]对应的链表中拿到一个对象来使用。
  print( id(v2) )
  • dict类型,维护的free_list数组最多可缓存80个dict对象。
  v1 = {"k1":123}
  print( id(v1) )  # 输出:4515998128
  del v1
  v2 = {"name":"甲壳虫","age":18,"gender":"男"}
  print( id(v2) ) # 输出:4515998128

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/166201.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 计算机职称考试网络模块试题,最新职称计算机考试模块综合试题及答案(1)

    计算机职称考试网络模块试题,最新职称计算机考试模块综合试题及答案(1)敲击字母键C、直接敲击字母键D、按下SHIFT键的同时,敲击字母键8、在MSwindowsXP中,功能键F1的作用是:(B)。A、全选B、提供“帮助”C、粘贴D、撤销9、对于经常光顾的网站,用户可以采用(BCD)方法,以便快速打开网页。A、在IE浏览器中,设置指定该网站的快捷键B、将该网页设置为IE的浏览主页C、在IE浏览器中,创建指定该网站的快捷方式D、将该网页的地址添加到收藏夹中1…

    2022年6月1日
    27
  • linux redis重启,互联网常识:linux下重启redis的方法

    linux redis重启,互联网常识:linux下重启redis的方法跟大家讲解下有关 linux 下重启 redis 的方法 相信小伙伴们对这个话题应该也很关注吧 现在就为小伙伴们说说 linux 下重启 redis 的方法 小编也收集到了有关 linux 下重启 redis 的方法的相关资料 希望大家看到了会喜欢 导语 已经将 redis 加入到 etc 下此时服务器启动 redis 也启动但是却连不上 redis 所有有了以下的过程 学习视频分享 redis 视频教程 查看 redis 状态 syst

    2025年6月1日
    0
  • sql语句的简单用法 db2删除修改字段名,db2一次增加多个字段

    sql语句的简单用法 db2删除修改字段名,db2一次增加多个字段

    2021年7月16日
    127
  • .tex文件中通过空行实现LaTeX换行输出

    .tex文件中通过空行实现LaTeX换行输出【LaTeX换行输出代码示例】\documentclass{article}\begin{document} Happy\TeXing. Hello\LaTeX. Happy\TeXing. Hello\LaTeX.\end{document}【输出结果】

    2022年5月14日
    43
  • 内连接与外连接的区别是什么?_数据库外连接和内连接的区别

    内连接与外连接的区别是什么?_数据库外连接和内连接的区别有两个表A和表B。表A结构如下:Aid:int;标识种子,主键,自增IDAname:varchar数据情况,即用select*fromA出来的记录情况如下图1所示:图1:A表数据表B结构如下:Bid:int;标识种子,主键,自增IDBnameid:int数据情况,即用select*fromB出来的记录情况如下图2所示:图2:B表数据为了把Bid和Aid加以区分,不让大家有误解,所以把B…

    2022年10月9日
    0
  • string转jsonstring_java json转map

    string转jsonstring_java json转mapJava中Json转string方法Java利用Json-lib包进行json对象转换成stringJSONArray转换string方法实例publicstaticvoidmain(String[]args)throwsJSONException{undefined//创建JSONObject对象JSONObjectjson=newJSONObject();//向json中添加数据json.put(“username”,”wanglihong”);json

    2022年9月8日
    1

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号