趣谈python的内存分配机制

原文地址:https://www.cnblogs.com/Du704/p/11543339.html

虽然我们现在得益于时代和技术的发展,不用再担心内存的问题;但是遥想当年,都是恨不得一个钢镚掰成俩份用,所以我就想深入了解一下,在python中内存分配的一些小秘密。

首先我会频繁地用到sys模块里的getsizeof()方法,简单介绍下:

  • 该方法返回对象的字节大小(bytes)。
  • 它只计算直接占用的内存,而不是计算对象内所引用对象的内存。

举个例子:

import sys

a = [1, 2]
b = [a, a] # 即 [[1, 2], [1, 2]] # a、b 都只有两个元素,所以直接占用的大小相等(只和元素个数有关,和元素是什么无关)
sys.getsizeof(a) # 结果:80
sys.getsizeof(b) # 结果:80

上例说明了一件事:一个静态创建的列表,如果只包含两个元素,那它自身占用的内存就是 80 字节,不管其元素所指向的对象是什么。

我们已经知道如何测量了,那么我们就一起来探索吧!

1、空对象是“空”的吗?

不知道大家有没有好奇过,空的对象如空字符串、空列表、空字典,他们到底占不占用内存呢?如果占用内存,又占用多少呢?

直接揭晓答案吧:

import sys

print(sys.getsizeof(''))      # 49
print(sys.getsizeof([])) # 64
print(sys.getsizeof(tuple())) # 48
print(sys.getsizeof(set())) # 224
print(sys.getsizeof(dict())) # 240 # 参照:
print(sys.getsizeof(None)) # 16
print(sys.getsizeof(False)) # 24
print(sys.getsizeof(0)) # 24
print(sys.getsizeof(1)) # 28
print(sys.getsizeof(True)) # 28

可见,虽然都是空对象,但是这些对象在内存分配上并不为“空”,而且分配得还挺大(记住这几个数字哦,后面会考)。

排序下就是:基础数字<空元组 < 空字符串 < 空列表 < 空集合 < 空字典。

如果一定要解释的话,我的理解就是:不同类在创建的时候自带的属性不一样,如引用计数、使用量信息等等,还有一部分内存是预分配的。


2、内存扩充是均匀的吗?

空对象并不为空,一部分原因是 Python 解释器为它们预分配了一些初始空间。在不超出初始内存的情况下,每次新增元素,就使用已有内存,因而避免了再去申请新的内存。

那么,如果初始内存被分配完之后,新的内存是怎么分配的呢?

import sys
s = 'abcdefghijklmnopqrstuvwxyz' a = []
for i in s:
a.append(i)
print(len(a) , f"a 的内存占用{sys.getsizeof(a)}") b = set()
for i in s :
b.add(i)
print(len(b),f"b 的内存占用{sys.getsizeof(b)}") c = dict()
for i in s :
c[i] = i
print(len(c) , f"c 的内存占用{sys.getsizeof(c)}")

我们给三类循环添加了26个元素,结果如何呢?

由此能看出可变对象在扩充时的秘密:

  • 超额分配机制:申请新内存时并不是按需分配的,而是多分配一些,因此当再添加少量元素时,不需要马上去申请新内存
  • 非均匀分配机制:三类对象申请新内存的频率是不同的,而同一类对象每次超额分配的内存并不是均匀的,而是逐渐扩大的

3、列表等于列表?

以上的可变对象在扩充时,有相似的分配机制,在动态扩容时可明显看出效果。

那么,静态创建的对象是否也有这样的分配机制呢?它跟动态扩容比,是否有所区别呢?

先看看集合与字典:

# 静态创建对象
set_1 = {1, 2, 3, 4}
set_2 = {1, 2, 3, 4, 5}
dict_1 = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5}
dict_2 = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6} sys.getsizeof(set_1) # 224
sys.getsizeof(set_2) # 736
sys.getsizeof(dict_1) # 240
sys.getsizeof(dict_2) # 368

看到这个结果,再对比上一节的截图,可以看出:在元素个数相等时,静态创建的集合/字典所占的内存跟动态扩容时完全一样。

这个结论是否适用于列表对象呢?一起看看:

list_1 = ['a', 'b']
list_2 = ['a', 'b', 'c']
list_3 = ['a', 'b', 'c', 'd']
list_4 = ['a', 'b', 'c', 'd', 'e'] sys.getsizeof(list_1) # 80
sys.getsizeof(list_2) # 88
sys.getsizeof(list_3) # 96
sys.getsizeof(list_4) # 104

上一节的截图显示,列表在前 4 个元素时都占 96 字节,在 5 个元素时占 128 字节,与这里明显矛盾。

所以,这个秘密昭然若揭:在元素个数相等时,静态创建的列表所占的内存有可能小于动态扩容时的内存!

也就是说,这两种列表看似相同,实际却不同!列表不等于列表!

EMMM.....Interesting......


4、消减元素就会释放内存?

前面提到了,扩充可变对象时,可能会申请新的内存。

那么,如果反过来缩减可变对象,减掉一些元素后,新申请的内存是否会自动回收掉呢?

import sys
a = [1, 2, 3, 4]
sys.getsizeof(a) # 初始值:96
a.append(5) # 扩充后:[1, 2, 3, 4, 5]
sys.getsizeof(a) # 扩充后:128
a.pop() # 缩减后:[1, 2, 3, 4]
sys.getsizeof(a) # 缩减后:128

如代码所示,列表在一扩一缩后,虽然回到了原样,但是所占用的内存空间可没有自动释放啊。其它的可变对象同理。

这就是 Python 的小秘密了,“胖子无法减重原理”:瘦子变胖容易,缩减身型也容易,但是体重减不掉,哈哈~~~


4、空字典等于空字典?

使用 pop() 方法,只会缩减可变对象中的元素,但并不会释放已申请的内存空间。

还有个 clear() 方法,它会清空可变对象的所有元素,让我们试试看吧:

import sys
a = [1, 2, 3]
b = {1, 2, 3}
c = {'a':1, 'b':2, 'c':3} sys.getsizeof(a) # 88
sys.getsizeof(b) # 224
sys.getsizeof(c) # 240 a.clear() # 清空后:[]
b.clear() # 清空后:set()
c.clear() # 清空后:{},也即 dict()

调用 clear() 方法,我们就获得了几个空对象。

在第一小节里,它们的内存大小已经被查验过了。(前面说过会考的,请默写下)

但是,如果这时再查验的话,你会惊讶地发现,这些空对象的大小跟前面查的并不完全一样!

# 承接前面的清空操作:
sys.getsizeof(a) # 64
sys.getsizeof(b) # 224
sys.getsizeof(c) # 72

空列表与空元组的大小不变,然而空字典(72)竟然比前面的空字典(240)要小很多!

也就是说,列表与元组在清空元素后,回到起点不变初心,然而,字典这家伙却是“赔了夫人又折兵”,不仅把“吃”进去的全吐出来了,还把自己的老本给亏掉了!

EMMM.....Interesting......

以上就是 Python 在分配内存时的几个小秘密啦,看完之后,又可以出去装逼了呢!

如有心得或者疑问,欢迎留言一起交流哦~

在这个大世界里还有很多秘密等待着我们去探索,一起加油吧~!

装逼手册之 python中的内存分配的小秘密的更多相关文章

  1. Python 中的内存管理

    Python 中一切皆对象,这些对象的内存都是在运行时动态地在堆中进行分配的,就连 Python 虚拟机使用的栈也是在堆上模拟的.既然一切皆对象,那么在 Python 程序运行过程中对象的创建和释放就 ...

  2. C语言中的内存分配与释放

    C语言中的内存分配与释放 对C语言一直都是抱着学习的态度,很多都不懂,今天突然被问道C语言的内存分配问题,说了一些自己知道的,但感觉回答的并不完善,所以才有这篇笔记,总结一下C语言中内存分配的主要内容 ...

  3. rt-thread中动态内存分配之小内存管理模块方法的一点理解

    @2019-01-18 [小记] rt-thread中动态内存分配之小内存管理模块方法的一点理解 > 内存初始化后的布局示意 lfree指向内存空闲区首地址 /** * @ingroup Sys ...

  4. Java基础-Java中的内存分配与回收机制

    Java基础-Java中的内存分配与回收机制 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一. 二.

  5. java中子类实例化过程中的内存分配

    知识点: 子类继承父类之后,实例化子类时,内存中子类是如何分配内存的呢? 下面,自己会结合一个例子,解释一下,一个子类实例化过程中,内存是如何分配的 参考博客:http://www.cnblogs.c ...

  6. Java实例化对象过程中的内存分配

    Java实例化对象过程中的内存分配: https://blog.csdn.net/qq_36934826/article/details/82685791 问题引入这里先定义一个很不标准的“书”类,这 ...

  7. Netty 中的内存分配浅析

    Netty 出发点作为一款高性能的 RPC 框架必然涉及到频繁的内存分配销毁操作,如果是在堆上分配内存空间将会触发频繁的GC,JDK 在1.4之后提供的 NIO 也已经提供了直接直接分配堆外内存空间的 ...

  8. Netty 中的内存分配浅析-数据容器

    本篇接续前一篇继续讲 Netty 中的内存分配.上一篇 先简单做一下回顾: Netty 为了更高效的管理内存,自己实现了一套内存管理的逻辑,借鉴 jemalloc 的思想实现了一套池化内存管理的思路: ...

  9. C语言中动态内存分配的本质是什么?

    摘要:C语言中比较重要的就是指针,它可以用来链表操作,谈到链表,很多时候为此分配内存采用动态分配而不是静态分配. 本文分享自华为云社区<[云驻共创]C语言中动态内存分配的本质>,作者: G ...

随机推荐

  1. ID转名称到手方案01

    > 好久没有写技术文章了,那就重新捡起来,从今天开始,分享这段时间的收获吧 ------------ > ## 其实很多时候,我们只需要鱼,而不是渔,呐,给你鱼. ### 这次的分享主题是 ...

  2. fiddler抓HTTPS及APP请求的配置教程

    一.fiddler安装 Fiddler官网下载Fiddler进行安装:https://www.telerik.com/fiddler 二.fiddler设置抓取HTTPS的请求 fiddler默认设置 ...

  3. shiro登录名的获取

    登录名的获取:通过的SecurityUtils的shiro import org.apache.shiro.SecurityUtils; //登录用户名 String loginAccount = S ...

  4. 10.源码分析---SOFARPC内置链路追踪SOFATRACER是怎么做的?

    SOFARPC源码解析系列: 1. 源码分析---SOFARPC可扩展的机制SPI 2. 源码分析---SOFARPC客户端服务引用 3. 源码分析---SOFARPC客户端服务调用 4. 源码分析- ...

  5. offsetX、clientX、screenX、pageX、layerX

    pageX/Y 原点相对于文档窗口左上角. offsetX/Y 原点相对于触发事件元素的左上角,需要注意的是,offset是有负值的,如果你的元素有边框,那么offset会是负值. (黑色为鼠标点击点 ...

  6. deepin 15.11 安装 pyenv

    GitHub:官方环境:https://github.com/pyenv/pyenv/wiki/Common-build-problems GitHub:官方文档:https://github.com ...

  7. python 38 线程队列与协程

    目录 1. 线程队列 1.1 先进先出(FIFO) 1.2 后进先出(LIFO)堆栈 1.3 优先级队列 2. 事件event 3. 协程 4. Greenlet 模块 5. Gevent模块 1. ...

  8. egret之消除游戏开发

    1.地图 (1)地图形状不同,尺寸不变 (2)背景图变化 2.步数 (1)不同关卡步数不同 (2)步数为01,游戏失败 3.道具 4.消除 (1)>=3可消除 (2)不可消除时,自动打乱 5.数 ...

  9. MSIL实用指南-数学运算

    C#支持的数学运算是加.减.乘.除.取模,它们对应的指令是Add.Sub.Mul.Div.Rem. 这五个运算都需要两个参数,它们的通用步骤1.生成加载左边变量2.生成加载右边变量3.生成运算指令 实 ...

  10. 数据结构C线性表现实

    linearList.h #ifndef _INC_STDIO_8787 #define _INC_STDIO_8787 #include <stdio.h> #include <m ...