언어 메모리 관리는 언어 디자인의 중요한 측면입니다. 언어능력을 결정하는 중요한 요소입니다. C 언어의 수동 관리든 Java의 가비지 수집이든 언어의 가장 중요한 기능이 되었습니다. 여기서는 동적으로 유형이 지정되는 객체 지향 언어의 메모리 관리 방법을 설명하기 위해 Python 언어를 예로 들어 보겠습니다.
객체의 메모리 사용량
대입문은 언어의 가장 일반적인 기능입니다. 그러나 가장 간단한 할당문도 매우 의미가 있을 수 있습니다. Python의 할당문은 연구할 가치가 있습니다.
a = 1
정수 1은 객체입니다. 그리고 a는 참고사항입니다. 대입문을 사용하여 객체 1에 대한 점을 참조합니다. Python은 동적 유형 지정 언어(동적 유형 지정 참조)이며 개체와 참조가 분리되어 있습니다. Python은 "젓가락"을 사용하여 참조를 통해 실제 음식 개체를 만지고 뒤집습니다.
참조 및 객체
메모리의 객체 저장을 탐색하기 위해 Python의 내장 함수 id()를 사용할 수 있습니다. . 객체의 ID를 반환하는 데 사용됩니다. 실제로 여기서 소위 ID는 개체의 메모리 주소입니다.
a = 1 print(id(a)) print(hex(id(a)))
내 컴퓨터에서는
11246696
'0xab9c68'
을 각각 메모리 주소 10진수 및 16진수 표현으로 반환합니다.
Python에서 Python은 재사용을 위해 이러한 객체를 정수 및 짧은 문자용으로 캐시합니다. 1과 동일한 참조를 여러 개 생성하면 실제로 이러한 모든 참조가 동일한 객체를 가리키도록 만듭니다.
a = 1 b = 1 print(id(a)) print(id(b))
위 프로그램은
11246696
11246696
a와 b가 실제로 점을 가리키는 것을 볼 수 있습니다. 두 참조의 동일한 객체에.
두 참조가 동일한 객체를 가리키는지 확인하려면 is 키워드를 사용할 수 있습니다. is는 두 참조가 가리키는 객체가 동일한지 여부를 확인하는 데 사용됩니다.
# Truea = 1 b = 1 print(a is b) # True a = "good" b = "good" print(a is b) # False a = "very good morning" b = "very good morning" print(a is b) # False a = [] b = [] print(a is b)
위의 주석이 해당 실행 결과입니다. 보시다시피 Python은 정수와 짧은 문자열 을 캐시하므로 각 개체의 복사본은 하나만 있습니다. 예를 들어, 정수 1에 대한 모든 참조는 동일한 객체를 가리킵니다. 대입문을 사용하더라도 객체 자체가 아닌 새 참조만 생성됩니다. 긴 문자열 및 기타 개체에는 동일한 개체가 여러 개 있을 수 있으며 할당 문을 사용하여 새 개체를 만들 수 있습니다.
Python에서 각 객체에는 해당 객체를 가리키는 총 참조 수, 즉 참조 카운트(참조 count)가 있습니다.
sys 패키지의 getrefcount()를 사용하여 객체의 참조 횟수를 볼 수 있습니다. 참조가 getrefcount()에 매개변수로 전달되면 매개변수가 실제로 임시 참조를 생성한다는 점에 유의해야 합니다. 따라서 getrefcount()로 얻은 결과는 예상보다 1이 더 많습니다.
from sys import getrefcount a = [1, 2, 3] print(getrefcount(a)) b = a print(getrefcount(b))
위의 이유로 인해 두 getrefcount는 예상된 1과 2 대신 2와 3을 반환합니다.
객체 참조 객체
테이블, 사전 등과 같은 Python의 컨테이너 객체(컨테이너)는 여러 객체를 포함할 수 있습니다. 실제로 컨테이너 개체에 포함된 것은 요소 개체 자체가 아니라 각 요소 개체에 대한 참조입니다.
객체를 사용자 정의하고 다른 객체를 참조할 수도 있습니다.
class from_obj(object): def init(self, to_obj): self.to_obj = to_obj b = [1,2,3] a = from_obj(b) print(id(a.to_obj)) print(id(b))
보시다시피 a는 객체 b를 참조합니다.
객체 참조 객체는 Python을 구성하는 가장 기본적인 방법입니다. 할당 방법 a = 1조차도 실제로 사전의 키 값 "a"가 있는 요소가 정수 개체 1을 참조하도록 만듭니다. 이 사전 개체는 모든 전역 참조를 기록하는 데 사용됩니다. 사전은 정수 객체 1을 참조합니다. 내장 함수인 globals()를 통해 이 사전을 볼 수 있습니다.
객체 A가 다른 객체 B에서 참조되면 A의 참조 횟수가 1 증가합니다.
from sys import getrefcount a = [1, 2, 3] print(getrefcount(a)) b = [a, a] print(getrefcount(a))
객체 b가 a를 두 번 참조하므로 a의 참조 횟수가 2만큼 증가합니다.
컨테이너 개체에 대한 참조는 매우 복잡한 토폴로지 구조를 형성할 수 있습니다.
x = [1, 2, 3] y = [x, dict(key1=x)] z = [y, (x, y)] import objgraph objgraph.show_refs([z], filename='ref_topo.png')
objgraph는 Python용 타사 패키지입니다. 설치하기 전에 xdot을 설치해야 합니다.
sudo apt-get install xdot sudo pip install objgraph
두 개체가 서로 참조하여 소위 참조 순환을 형성할 수 있습니다.
a = [] b = [a] a.append(b)
자신만을 참조하면 되는 객체도 참조 순환을 형성할 수 있습니다.
a = [] a.append(a) print(getrefcount(a))
引用环会给垃圾回收机制带来很大的麻烦,我将在后面详细叙述这一点。
引用减少
某个对象的引用计数可能减少。比如,可以使用del关键字删除某个引用:
from sys import getrefcount a = [1, 2, 3] b = a print(getrefcount(b)) del a print(getrefcount(b))
del也可以用于删除容器元素中的元素,比如:
a = [1,2,3] del a[0] print(a)
如果某个引用指向对象A,当这个引用被重新定向到某个其他对象B时,对象A的引用计数减少:
from sys import getrefcount a = [1, 2, 3] b = a print(getrefcount(b)) a = 1 print(getrefcount(b))
垃圾回收
吃太多,总会变胖,Python也是这样。当Python中的对象越来越多,它们将占据越来越大的内存。不过你不用太担心Python的体形,它会乖巧的在适当的时候“减肥”,启动垃圾回收(garbage collection),将没用的对象清除。在许多语言中都有垃圾回收机制,比如Java和Ruby。尽管最终目的都是塑造苗条的提醒,但不同语言的减肥方案有很大的差异 (这一点可以对比本文和Java内存管理与垃圾回收)。
从基本原理上,当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如某个新建对象,它被分配给某个引用,对象的引用计数变为1。如果引用被删除,对象的引用计数为0,那么该对象就可以被垃圾回收。比如下面的表:
a = [1, 2, 3] del a
del a后,已经没有任何引用指向之前建立的[1, 2, 3]这个表。用户不可能通过任何方式接触或者动用这个对象。这个对象如果继续待在内存里,就成了不健康的脂肪。当垃圾回收启动时,Python扫描到这个引用计数为0的对象,就将它所占据的内存清空。
然而,减肥是个昂贵而费力的事情。垃圾回收时,Python不能进行其它的任务。频繁的垃圾回收将大大降低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同时采用了分代(generation)回收的策略。这一策略的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。我们的程序往往会产生大量的对象,许多对象很快产生和消失,但也有一些对象长期被使用。出于信任和效率,对于这样一些“长寿”对象,我们相信它们的用处,所以减少在垃圾回收中扫描它们的频率。
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代垃圾回收。
同样可以用set_threshold()来调整,比如对2代对象进行更频繁的扫描。
import gc gc.set_threshold(700, 10, 5)
孤立的引用环
引用环的存在会给上面的垃圾回收机制带来很大的困难。这些引用环可能构成无法使用,但引用计数不为0的一些对象。
a = [] b = [a] a.append(b) del a del b
上面我们先创建了两个表对象,并引用对方,构成一个引用环。删除了a,b引用之后,这两个对象不可能再从程序中调用,就没有什么用处了。但是由于引用环的存在,这两个对象的引用计数都没有降到0,不会被垃圾回收。
孤立的引用环
为了回收这样的引用环,Python复制每个对象的引用计数,可以记为gc_ref。假设,每个对象i,该计数为gc_ref_i。Python会遍历所有的对象i。对于每个对象i引用的对象j,将相应的gc_ref_j减1。
遍历后的结果
在结束遍历后,gc_ref不为0的对象,和这些对象引用的对象,以及继续更下游引用的对象,需要被保留。而其它的对象则被垃圾回收。
总结
Python作为一种动态类型的语言,其对象和引用分离。这与曾经的面向过程语言有很大的区别。为了有效的释放内存,Python内置了垃圾回收的支持。Python采取了一种相对简单的垃圾回收机制,即引用计数,并因此需要解决孤立引用环的问题。
Python与其它语言既有共通性,又有特别的地方。对该内存管理机制的理解,是提高Python性能的重要一步。
위 내용은 Python의 메모리 관리에 대한 자세한 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!