python3 常量池

最近有同事分享了一次java基础,里面提到了java基础类型的包装类型(比如Integer)和String都有自己的常量池,我突然想到在python中也有类似的东西,所以本来想写一篇关于分析python和java常量池的博客,但是后来我发现这里面的内容还挺多的,所以我决定先从整型常量池入手,下次再分析字符串的。

需要提到的是,我只是因为找到的有关python小整型池和大整型池的资料,才特意介绍python的。我并没有找到关于java大整型对象存储技术的文献,但是我猜都能猜到,如果java真有这方面的优化需求,是一定会有类似的东西,如果之后我了解到了,我会在自己的博客中再去介绍的。

java常量池

首先是java的常量池,我搜索了java的常量池,大部分提到了,“java中基本类型的包装类的大部分都实现了常量池技术”这句话,它们都提到了对于Interger这样的对象(Byte,Short,Long,Character类似)在值小于127的时候,会默认使用内存池中已经创建好的数据。

这也就是为什么:

Python
Integer i1 = 100; Integer i2 = 100;
1
2
3
Integer i1 = 100 ;
Integer i2 = 100 ;
 

的时候, i1 == i2会返回true,而在

Python
Integer i2 = 200; Integer i2 = 200;
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常量池,是一开始就初始化好的。

Python
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 = 1
b = 1
a == b # True
a is b # True
print id ( a )
print id ( b )
 

而对于超过此区间的对象,就需要在每次需要的时候创建了。

Python
a = 1000 b = 1000 a == b # True a is b # False 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的实现源码

Python
typedef struct{ PyObject_HEAD; long ob_ival; } PyIntObject;
1
2
3
4
5
typedef struct {
     PyObject_HEAD ;
     long ob_ival ;
} PyIntObject ;
 

上面就是提到的PyIntObject的C底层数据结构。

Python
#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
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
 

上面就是小整型常量池的大小定义

Python
struct _intblock{ struct _intblock *next; PyIntObject objects[N_INTOBJECTS]; };
1
2
3
4
5
struct _intblock {
     struct _intblock * next ;
     PyIntObject objects [ N_INTOBJECTS ] ;   
} ;
 

上面就是我们说的用于给大整型对象的内存块节点,可以看到,每个节点是一个PyIntObject数组。

Python
typedef struct _inblock PyIntBlock; static PyIntBlock *block_list = NULL; static PyIntObject *free_list = NULL;
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代码的编译单元是函数,也就是说每个函数会单独编译,对于同一个编译单元中出现相同值的常量,只会出现一份。对于不同单元的编译单元,值相同的常量不一定会应用到运行时的同一对象。”

写两个例子,就全都明白了

Python
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()
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 ( )
 

所以解释器逐条执行的,应该是不同的编译单元。




  • zeropython 微信公众号 5868037 QQ号 5868037@qq.com QQ邮箱
<think>好的,我现在需要回答用户关于Java字符串常量池中字符串拼接行为和规则的问题。根据用户提供的三个引用,我需要整合这些信息,并确保回答准确且符合用户的要求。 首先,用户提到在JDK9之后,字符串拼接遵循一定的规则,无论拼接的是常量还是变量,都会检查字符串常量池是否存在对应的字符串,不存在则创建新对象。引用1和引用2都提到了常量池的存在和用途,特别是池化技术的作用。引用3则介绍了intern()方法的作用,即在常量池中查找或添加字符串。 接下来,我需要将这些信息结构化。用户的问题涉及拼接的行为和规则,可能需要分情况讨论:字面量拼接、变量与字面量拼接、变量间拼接,以及使用intern()方法的情况。同时,要指出JDK版本的影响,比如JDK9之后的字符串处理优化。 需要注意的是,用户提供的引用中提到,无论常量还是变量拼接,都遵循同样的规则,即检查常量池是否存在,不存在则创建新的。这可能与之前的版本不同,比如JDK8及以前使用StringBuilder进行拼接,结果不会入池,除非显式调用intern()。因此,在回答中需要区分不同JDK版本的处理方式。 另外,用户示例中的Python代码可能与问题无关,但系统级指令要求生成相关问题,所以最后需要列出相关问题。同时,回答需使用中文,正确使用LaTeX格式,行内公式用$...$,独立公式用$$...$$,但这个问题可能涉及较少的数学公式,更多是代码和规则描述。 现在,组织回答结构: 1. 概述字符串常量池的作用和设计目的,引用引用2的内容。 2. 分点说明不同拼接情况下的行为,结合引用1的规则。 3. 解释intern()方法的作用,引用引用3。 4. 对比不同JDK版本的变化,特别是JDK9之后的优化。 5. 示例代码说明各种情况。 6. 总结关键点。 7. 生成相关问题。 需要确保每个引用在回答中正确标注,例如在解释规则时添加[^1],池技术时[^2],intern()方法时[^3]。 可能遇到的疑问点:用户的问题是否涉及编译时优化?比如字面量拼接在编译时处理,而变量拼接在运行时处理。例如,String s = "a" + "b"可能在编译时合并为"ab",并直接查看常量池是否存在。而变量拼接如s1 + s2会在运行时生成新对象,除非调用intern()。但根据引用1,JDK9之后无论常量还是变量,拼接后的处理是否相同?需要确认这一点。 根据引用1的描述,在JDK9之后,不论是字符串常量和变量,还是变量与变量拼接,都遵循同样的规则:拼接后的字符串如果在常量池中存在,则直接引用;否则创建新的String对象。这可能与之前的版本不同,以前可能对于变量拼接使用StringBuilder,结果不会自动入池,而字面量拼接在编译时优化并入池。 因此,在回答中需要明确说明JDK9前后的差异,并指出当前规则是基于JDK9及以后的版本。 此外,可能需要提到字符串常量池的位置,比如在堆内存中(JDK7之后),但用户的问题可能不涉及这个细节,除非必要。 现在,整理这些点,形成结构清晰的回答,并确保引用正确。</think>### Java字符串常量池的拼接行为与规则 #### 1. 字符串常量池的设计目的 字符串常量池是Java为优化`String`对象内存分配设计的特殊存储区域。它通过“池化技术”预存字符串字面量,避免重复创建对象,从而节省内存。 #### 2. 拼接行为规则(JDK9+) - **字面量拼接** 编译期直接合并并检查常量池,若存在则直接引用,否则创建新对象: ```java String s1 = "Hello" + "World"; // 编译后等价于"HelloWorld",检查常量池后引用 ``` - **变量与字面量拼接** 运行时生成新对象,但结果**不会自动入池**: ```java String s2 = "Hello"; String s3 = s2 + "World"; // 新对象,不在常量池中 ``` - **变量间拼接** 类似变量与字面量拼接,结果对象不入池: ```java String s4 = s2 + s3; // 新对象 ``` #### 3. `intern()`方法的作用 强制将字符串对象添加到常量池(若池中不存在),并返回池中引用: ```java String s5 = new String("Hello").intern(); // 返回常量池中的"Hello" ``` #### 4. JDK9+的优化 JDK9引入`StringConcatFactory`,优化拼接逻辑,但**行为规则不变**:拼接结果仅在常量池存在时引用,否则新建对象。 #### 5. 示例对比 ```java // 示例1:字面量拼接(编译期优化) String a = "ab"; String b = "a" + "b"; // 常量池已存在"ab",b直接引用 // 示例2:变量拼接(运行期生成) String c = "a"; String d = c + "b"; // 新对象,不在常量池 String e = d.intern(); // 将"ab"加入常量池,e引用池中对象[^3] ``` #### 6. 关键总结 - **常量池规则**:拼接结果存在则引用,否则新建[^1]。 - **变量拼接**:结果不入池(除非调用`intern()`)。 - **JDK版本差异**:JDK9前使用`StringBuilder`拼接,JDK9后优化实现但行为一致。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值