最近有同事分享了一次java基础,里面提到了java基础类型的包装类型(比如Integer)和String都有自己的常量池,我突然想到在python中也有类似的东西,所以本来想写一篇关于分析python和java常量池的博客,但是后来我发现这里面的内容还挺多的,所以我决定先从整型常量池入手,下次再分析字符串的。
需要提到的是,我只是因为找到的有关python小整型池和大整型池的资料,才特意介绍python的。我并没有找到关于java大整型对象存储技术的文献,但是我猜都能猜到,如果java真有这方面的优化需求,是一定会有类似的东西,如果之后我了解到了,我会在自己的博客中再去介绍的。
java常量池
首先是java的常量池,我搜索了java的常量池,大部分提到了,“java中基本类型的包装类的大部分都实现了常量池技术”这句话,它们都提到了对于Interger这样的对象(Byte,Short,Long,Character类似)在值小于127的时候,会默认使用内存池中已经创建好的数据。
这也就是为什么:
1
2
3
|
Integer
i1
=
100
;
Integer
i2
=
100
;
|
的时候, i1 == i2
会返回true,而在
1
2
3
|
Integer
i2
=
200
;
Integer
i2
=
200
;
|
的时候, i1 == i2
会返回False。
这个很好理解,首先java中 ==
对对象而言,代表是否是同一引用(equal用来比较是否值相等)。还有就是我们没有 new Integer,而是直接使用看上去像是赋值的操作,因为使用了装箱操作。
这里需要提到的是如果i1和i2都是int型的基础类型,那么他们无论有多大,比较起来都是相等的,因为这部分内存是放在栈的,它们仅仅是基础类型,而非引用堆中的对象。
python整型常量池
先说明的是,python中没有基础类型这种东西,万物皆对象,而且==
在比较两个整型的时候就是在比较值大小,比较是否是同一对象的引用使用is关键字。
python中的整型常量池分为小整型对象池和大整型对象池。(这个说法出自python源码分析)
对于一些常用的整型,python也是提前初始化好的:
在[-5, 257)这个区间内的整数,被称为小整数对象,类似于java常量池,是一开始就初始化好的。
1
2
3
4
5
6
7
|
a
=
1
b
=
1
a
==
b
# True
a
is
b
# True
print
id
(
a
)
print
id
(
b
)
|
而对于超过此区间的对象,就需要在每次需要的时候创建了。
1
2
3
4
5
6
7
|
a
=
1000
b
=
1000
a
==
b
# True
a
is
b
# False
print
id
(
a
)
print
id
(
b
)
|
上面的例子是针对解释器运行,如果是运行脚本,还有不同的地方,后面会再解释。
但是这些要创建的大整型对象,并不是直接在一块堆内存上创建的(如果是那效率就太挫了),而是维护了一个专门的数据结构,我们称其为大整型对象池。
这个池的数据结构类似于一个单向链表,每一个节点是一个可以存放python Int对象的数组。然后在每次创建python大整型INT对象的时候,如果单向链表中没有空间可用,那就会创建一块新的python Int数组空间,链接到单向链表中。否则就直接使用数组中的内存就可以了。之所以要数组和链表结合使用,就是因为数组的查找要快,而链表的释放和创建自由灵活。这种模型在内存管理中很常见。
而且你应该知道python的对象回收机制,就是在引用计数减为0的时候,这个内存就会被回收。所以对我们刚才提到的大整型对象如果没有引用指向它,它就会被python虚拟机回收。但是坑的地方是,它并不会释放给操作系统,而仅仅是回收给这个大整型对象池的free区,用于再次使用。这也就是说你的这个大整型对象池,只增不减。这听上去很像是内存泄漏。
你可以试一试,如果创建了一大堆的python int对象,你的内存将飙高到几个G,然后即使你del 了这些用于存放int对象的容器,你的python进程的内存也没有变小,也就是说除非你的python进程结束,否则这些内存永远不会还给操作系统。
这是个问题?
这其实不是问题,首先它并不是内存泄漏,严格意义来讲,内存泄漏是指无法找到内存空间了,比如c++和c中的指针的作用域没了,访问那些内存的方式你找不到了。但是实际上对于大整型内存池而言,我们可以找到这些内存,它们也可以被我们重复使用。而且现代操作系统,内存是用缺页分配的。所以不会占用那么多“真实的内存”。
这是设计缺陷?
工程上的事,往往就是在做去权衡。还有就是那些认为这是设计缺陷的人,简直就是在说“我比python作者聪明”。我仔细想了想,这些可以重复使用的内存块,它们分散在链表上数组的各处,根本没什么好的办法释放它们,所以在运行效率上权衡,就只能设计成这样了。
证明
口说无凭,看看cpython的实现源码
1
2
3
4
5
|
typedef
struct
{
PyObject_HEAD
;
long
ob_ival
;
}
PyIntObject
;
|
上面就是提到的PyIntObject的C底层数据结构。
1
2
3
4
5
6
7
8
9
10
|
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
static
PyIntObject
*
small_ints
[
NSMALLNEGINTS
+
NSMALLPOSINTS
]
;
#endif
|
上面就是小整型常量池的大小定义
1
2
3
4
5
|
struct
_intblock
{
struct
_intblock
*
next
;
PyIntObject
objects
[
N_INTOBJECTS
]
;
}
;
|
上面就是我们说的用于给大整型对象的内存块节点,可以看到,每个节点是一个PyIntObject数组。
1
2
3
4
5
|
typedef
struct
_inblock
PyIntBlock
;
static
PyIntBlock
*
block_list
=
NULL
;
static
PyIntObject
*
free_list
=
NULL
;
|
其中block_list指针,就是大整型数组对象链表的头节点,而free_list就是可用内存(空闲内存)的头节点。
而且我们提到,被删除的PyIntObject对象,它的空间可以被重新使用,这种重新使用的方式就是指它们会以单链表的形式串连在一起,而表头就是free_list
遗留问题,解释器运行和python程序运行
前面提到的那段python代码,在python的解释器运行和作为python代码运行,结果不一样的,我猜测可能是因为解释器是逐条执行,而python代码则存在整体处理的过程。
最后我在网上找到这样一种解答:
“Cpython代码的编译单元是函数,也就是说每个函数会单独编译,对于同一个编译单元中出现相同值的常量,只会出现一份。对于不同单元的编译单元,值相同的常量不一定会应用到运行时的同一对象。”
写两个例子,就全都明白了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
def
m
(
)
:
a
=
1
print
id
(
a
)
def
n
(
)
:
b
=
1
print
id
(
b
)
if
__name__
==
"__main__"
:
m
(
)
n
(
)
def
m
(
)
:
a
=
1000
print
id
(
a
)
def
n
(
)
:
b
=
1000
print
id
(
b
)
if
__name__
==
"__main__"
:
m
(
)
n
(
)
|
所以解释器逐条执行的,应该是不同的编译单元。