精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

使用GC、Objgraph干掉Python內存泄露與循環引用!

開發 后端
本文介紹兩個更致命的問題:內存泄露與循環引用。內存泄露是讓所有程序員都聞風喪膽的問題,輕則導致程序運行速度減慢,重則導致程序崩潰;而循環引用是使用了引用計數的數據結構、編程語言都需要解決的問題。本文揭曉這兩個問題在python語言中是如何存在的,然后試圖利用gc模塊和objgraph來解決這兩個問題。

Python使用引用計數和垃圾回收來做內存管理,前面也寫過一遍文章《Python內存優化》,介紹了在python中,如何profile內存使用情況,并做出相應的優化。本文介紹兩個更致命的問題:內存泄露與循環引用。內存泄露是讓所有程序員都聞風喪膽的問題,輕則導致程序運行速度減慢,重則導致程序崩潰;而循環引用是使用了引用計數的數據結構、編程語言都需要解決的問題。本文揭曉這兩個問題在python語言中是如何存在的,然后試圖利用gc模塊和objgraph來解決這兩個問題。

注意:本文的目標是Cpython,測試代碼都是運行在Python2.7。另外,本文不考慮C擴展造成的內存泄露,這是另一個復雜且頭疼的問題。

一分鐘版本

  1. python使用引用計數和垃圾回收來釋放(free)Python對象
  2. 引用計數的優點是原理簡單、將消耗均攤到運行時;缺點是無法處理循環引用
  3. Python垃圾回收用于處理循環引用,但是無法處理循環引用中的對象定義了__del__的情況,而且每次回收會造成一定的卡頓
  4. gc module是python垃圾回收機制的接口模塊,可以通過該module啟停垃圾回收、調整回收觸發的閾值、設置調試選項
  5. 如果沒有禁用垃圾回收,那么Python中的內存泄露有兩種情況:要么是對象被生命周期更長的對象所引用,比如global作用域對象;要么是循環引用中存在__del__
  6. 使用gc module、objgraph可以定位內存泄露,定位之后,解決很簡單
  7. 垃圾回收比較耗時,因此在對性能和內存比較敏感的場景也是無法接受的,如果能解除循環引用,就可以禁用垃圾回收。
  8. 使用gc module的DEBUG選項可以很方便的定位循環引用,解除循環引用的辦法要么是手動解除,要么是使用weakref

python內存管理

Python中,一切都是對象,又分為mutable和immutable對象。二者區分的標準在于是否可以原地修改,“原地“”可以理解為相同的地址。可以通過id()查看一個對象的“地址”,如果通過變量修改對象的值,但id沒發生變化,那么就是mutable,否則就是immutable。比如:

  1. >>> a = 5;id(a) 
  2.   
  3. 35170056 
  4. >>> a = 6;id(a) 
  5. 35170044 
  6. >>> lst = [1,2,3]; id(lst) 
  7. 39117168 
  8. >>> lst.append(4); id(lst) 
  9. 39117168  

a指向的對象(int類型)就是immutable, 賦值語句只是讓變量a指向了一個新的對象,因為id發生了變化。而lst指向的對象(list類型)為可變對象,通過方法(append)可以修改對象的值,同時保證id一致。

判斷兩個變量是否相等(值相同)使用==, 而判斷兩個變量是否指向同一個對象使用 is。比如下面a1 a2這兩個變量指向的都是空的列表,值相同,但是不是同一個對象。

  1. >>> a1, a2 = [], [] 
  2. >>> a1 == a2 
  3. True 
  4. >>> a1 is a2 
  5. False  

為了避免頻繁的申請、釋放內存,避免大量使用的小對象的構造析構,python有一套自己的內存管理機制。在巨著《Python源碼剖析》中有詳細介紹,在python源碼obmalloc.h中也有詳細的描述。如下所示: 

 

可以看到,python會有自己的內存緩沖池(layer2)以及對象緩沖池(layer3)。在Linux上運行過Python服務器的程序都知道,python不會立即將釋放的內存歸還給操作系統,這就是內存緩沖池的原因。而對于可能被經常使用、而且是immutable的對象,比如較小的整數、長度較短的字符串,python會緩存在layer3,避免頻繁創建和銷毀。例如:

  1. >>> a, b = 1, 1 
  2. >>> a is b 
  3. True 
  4. >>> a, b = (), () 
  5. >>> a is b 
  6. True 
  7. >>> a, b = {}, {} 
  8. >>> a is b 
  9. False  

本文并不關心python是如何管理內存塊、如何管理小對象,感興趣的讀者可以參考伯樂在線和csdn上的這兩篇文章。

本文關心的是,一個普通的對象的生命周期,更明確的說,對象是什么時候被釋放的。當一個對象理論上(或者邏輯上)不再被使用了,但事實上沒有被釋放,那么就存在內存泄露;當一個對象事實上已經不可達(unreachable),即不能通過任何變量找到這個對象,但這個對象沒有立即被釋放,那么則可能存在循環引用。

引用計數

引用計數(References count),指的是每個Python對象都有一個計數器,記錄著當前有多少個變量指向這個對象。

將一個對象直接或者間接賦值給一個變量時,對象的計數器會加1;當變量被del刪除,或者離開變量所在作用域時,對象的引用計數器會減1。當計數器歸零的時候,代表這個對象再也沒有地方可能使用了,因此可以將對象安全的銷毀。Python源碼中,通過Py_INCREF和Py_DECREF兩個宏來管理對象的引用計數,代碼在object.h

  1. #define Py_INCREF(op) (                         \ 
  2.     _Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA       \ 
  3.     ((PyObject*)(op))->ob_refcnt++) 
  4.   
  5. #define Py_DECREF(op)                                   \ 
  6.     do {                                                \ 
  7.         if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA       \ 
  8.         --((PyObject*)(op))->ob_refcnt != 0)            \ 
  9.             _Py_CHECK_REFCNT(op)                        \ 
  10.         else                                            \ 
  11.         _Py_Dealloc((PyObject *)(op));                  \ 
  12.     } while (0)  

通過sys.getrefcount(obj)對象可以獲得一個對象的引用數目,返回值是真實引用數目加1(加1的原因是obj被當做參數傳入了getrefcount函數),例如:

  1. >>> import sys 
  2. >>> s = 'asdf' 
  3. >>> sys.getrefcount(s) 
  4. >>> a = 1 
  5. >>> sys.getrefcount(a) 
  6. 605  

從對象1的引用計數信息也可以看到,python的對象緩沖池會緩存十分常用的immutable對象,比如這里的整數1。

引用計數的優點在于原理通俗易懂;且將對象的回收分布在代碼運行時:一旦對象不再被引用,就會被釋放掉(be freed),不會造成卡頓。但也有缺點:額外的字段(ob_refcnt);頻繁的加減ob_refcnt,而且可能造成連鎖反應。但這些缺點跟循環引用比起來都不算事兒。

什么是循環引用,就是一個對象直接或者間接引用自己本身,引用鏈形成一個環。且看下面的例子:

  1. # -*- coding: utf-8 -*- 
  2. import objgraph, sys 
  3. class OBJ(object): 
  4.     pass 
  5.   
  6. def show_direct_cycle_reference(): 
  7.     a = OBJ() 
  8.     a.attr = a 
  9.     objgraph.show_backrefs(a, max_depth=5, filename = "direct.dot"
  10.   
  11. def show_indirect_cycle_reference(): 
  12.     a, b = OBJ(), OBJ() 
  13.     a.attr_b = b 
  14.     b.attr_a = a 
  15.     objgraph.show_backrefs(a, max_depth=5, filename = "indirect.dot"
  16.   
  17. if __name__ == '__main__'
  18.     if len(sys.argv) > 1: 
  19.         show_direct_cycle_reference() 
  20.     else
  21.         show_indirect_cycle_reference()  

運行上面的代碼,使用graphviz工具集(本文使用的是dotty)打開生成的兩個文件,direct.dot 和 indirect.dot,得到下面兩個圖

 

通過屬性名(attr, attr_a, attr_b)可以很清晰的看出循環引用是怎么產生的

前面已經提到,對于一個對象,當沒有任何變量指向自己時,引用計數降到0,就會被釋放掉。我們以上面左邊那個圖為例,可以看到,紅框里面的OBJ對象想在有兩個引用(兩個入度),分別來自幀對象frame(代碼中,函數局部空間持有對OBJ實例的引用)、attr變量。我們再改一下代碼,在函數運行技術之后看看是否還有OBJ類的實例存在,引用關系是怎么樣的:

  1. # -*- coding: utf-8 -*- 
  2. import objgraph, sys 
  3. class OBJ(object): 
  4.     pass 
  5.   
  6. def direct_cycle_reference(): 
  7.     a = OBJ() 
  8.     a.attr = a 
  9.      
  10. if __name__ == '__main__'
  11.     direct_cycle_reference() 
  12.     objgraph.show_backrefs(objgraph.by_type('OBJ')[0], max_depth=5, filename = "direct.dot" 

 

 

修改后的代碼,OBJ實例(a)存在于函數的local作用域。因此,當函數調用結束之后,來自幀對象frame的引用被解除。從圖中可以看到,當前對象的計數器(入度)為1,按照引用計數的原理,是不應該被釋放的,但這個對象在函數調用結束之后就是事實上的垃圾,這個時候就需要另外的機制來處理這種情況了。

python的世界,很容易就會出現循環引用,比如標準庫Collections中OrderedDict的實現(已去掉無關注釋):

  1. class OrderedDict(dict): 
  2.     def __init__(self, *args, **kwds): 
  3.         if len(args) > 1: 
  4.             raise TypeError('expected at most 1 arguments, got %d' % len(args)) 
  5.         try: 
  6.             self.__root 
  7.         except AttributeError: 
  8.             self.__root = root = []                     # sentinel node 
  9.             root[:] = [root, root, None] 
  10.             self.__map = {} 
  11.         self.__update(*args, **kwds)  

注意第8、9行,root是一個列表,列表里面的元素之自己本身!

垃圾回收

這里強調一下,本文中的的垃圾回收是狹義的垃圾回收,是指當出現循環引用,引用計數無計可施的時候采取的垃圾清理算法。

在python中,使用標記-清除算法(mark-sweep)和分代(generational)算法來垃圾回收。在《Garbage Collection for Python》一文中有對標記回收算法,然后在《Python內存管理機制及優化簡析》一文中,有對前文的翻譯,并且有分代回收的介紹。在這里,引用后面一篇文章:

在Python中, 所有能夠引用其他對象的對象都被稱為容器(container). 因此只有容器之間才可能形成循環引用. Python的垃圾回收機制利用了這個特點來尋找需要被釋放的對象. 為了記錄下所有的容器對象, Python將每一個 容器都鏈到了一個雙向鏈表中, 之所以使用雙向鏈表是為了方便快速的在容器集合中插入和刪除對象. 有了這個 維護了所有容器對象的雙向鏈表以后, Python在垃圾回收時使用如下步驟來尋找需要釋放的對象:

  1. 對于每一個容器對象, 設置一個gc_refs值, 并將其初始化為該對象的引用計數值.
  2. 對于每一個容器對象, 找到所有其引用的對象, 將被引用對象的gc_refs值減1.
  3. 執行完步驟2以后所有gc_refs值還大于0的對象都被非容器對象引用著, 至少存在一個非循環引用. 因此 不能釋放這些對象, 將他們放入另一個集合.
  4. 在步驟3中不能被釋放的對象, 如果他們引用著某個對象, 被引用的對象也是不能被釋放的, 因此將這些 對象也放入另一個集合中.
  5. 此時還剩下的對象都是無法到達的對象. 現在可以釋放這些對象了.

關于分代回收:

除此之外, Python還將所有對象根據’生存時間’分為3代, 從0到2. 所有新創建的對象都分配為第0代. 當這些對象 經過一次垃圾回收仍然存在則會被放入第1代中. 如果第1代中的對象在一次垃圾回收之后仍然存貨則被放入第2代. 對于不同代的對象Python的回收的頻率也不一樣. 可以通過gc.set_threshold(threshold0[, threshold1[, threshold2]]) 來定義. 當Python的垃圾回收器中新增的對象數量減去刪除的對象數量大于threshold0時, Python會對第0代對象 執行一次垃圾回收. 每當第0代被檢查的次數超過了threshold1時, 第1代對象就會被執行一次垃圾回收. 同理每當 第1代被檢查的次數超過了threshold2時, 第2代對象也會被執行一次垃圾回收.

注意,threshold0,threshold1,threshold2的意義并不相同!

為什么要分代呢,這個算法的根源來自于weak generational hypothesis。這個假說由兩個觀點構成:首先是年親的對象通常死得也快,比如大量的對象都存在于local作用域;而老對象則很有可能存活更長的時間,比如全局對象,module, class。

垃圾回收的原理就如上面提示,詳細的可以看Python源碼,只不過事實上垃圾回收器還要考慮__del__,弱引用等情況,會略微復雜一些。

什么時候會觸發垃圾回收呢,有三種情況:

  1. 達到了垃圾回收的閾值,Python虛擬機自動執行
  2. 手動調用gc.collect()
  3. Python虛擬機退出的時候

對于垃圾回收,有兩個非常重要的術語,那就是reachable與collectable(當然還有與之對應的unreachable與uncollectable),后文也會大量提及。

reachable是針對python對象而言,如果從根集(root)能到找到對象,那么這個對象就是reachable,與之相反就是unreachable,事實上就是只存在于循環引用中的對象,Python的垃圾回收就是針對unreachable對象。

而collectable是針對unreachable對象而言,如果這種對象能被回收,那么是collectable;如果不能被回收,即循環引用中的對象定義了__del__, 那么就是uncollectable。Python垃圾回收對uncollectable對象無能為力,會造成事實上的內存泄露。

gc module

這里的gc(garbage collector)是Python 標準庫,該module提供了與上一節“垃圾回收”內容相對應的接口。通過這個module,可以開關gc、調整垃圾回收的頻率、輸出調試信息。gc模塊是很多其他模塊(比如objgraph)封裝的基礎,在這里先介紹gc的核心API。

  1. gc.enable(); gc.disable(); gc.isenabled() 

開啟gc(默認情況下是開啟的);關閉gc;判斷gc是否開啟

  1. gc.collection() 

執行一次垃圾回收,不管gc是否處于開啟狀態都能使用

  1. gc.set_threshold(t0, t1, t2); gc.get_threshold() 

設置垃圾回收閾值; 獲得當前的垃圾回收閾值

注意:gc.set_threshold(0)也有禁用gc的效果

  1. gc.get_objects() 

返回所有被垃圾回收器(collector)管理的對象。這個函數非常基礎!只要python解釋器運行起來,就有大量的對象被collector管理,因此,該函數的調用比較耗時!

比如,命令行啟動python

  1. >>> import gc 
  2. >>> len(gc.get_objects()) 
  3. 3749  
  1. gc.get_referents(*obj) 

返回obj對象直接指向的對象

  1. gc.get_referrers(*obj) 

返回所有直接指向obj的對象

下面的實例展示了get_referents與get_referrers兩個函數

  1. >>> class OBJ(object): 
  2.   
  3. ... pass 
  4. ... 
  5. >>> a, b = OBJ(), OBJ() 
  6. >>> hex(id(a)), hex(id(b)) 
  7. ('0x250e730''0x250e7f0'
  8.   
  9.   
  10. >>> gc.get_referents(a) 
  11. [<class '__main__.OBJ'>] 
  12. >>> a.attr = b 
  13. >>> gc.get_referents(a) 
  14. [{'attr': <__main__.OBJ object at 0x0250E7F0>}, <class '__main__.OBJ'>] 
  15. >>> gc.get_referrers(b) 
  16. [{'attr': <__main__.OBJ object at 0x0250E7F0>}, {'a': <__main__.OBJ object at 0x0250E730>, 'b': <__main__.OBJ object at 0x0250E7F0>, 'OBJ': <class '__main__.OBJ'>, '__builtins__': <modu 
  17. le '__builtin__' (built-in)>, '__package__': None, 'gc': <module 'gc' (built-in)>, '__name__''__main__''__doc__': None}] 
  18. >>>  

a, b都是類OBJ的實例,執行”a.attr = b”之后,a就通過‘’attr“這個屬性指向了b。

  1. gc.set_debug(flags) 

設置調試選項,非常有用,常用的flag組合包含以下

gc.DEBUG_COLLETABLE: 打印可以被垃圾回收器回收的對象

gc.DEBUG_UNCOLLETABLE: 打印無法被垃圾回收器回收的對象,即定義了__del__的對象

gc.DEBUG_SAVEALL:當設置了這個選項,可以被拉起回收的對象不會被真正銷毀(free),而是放到gc.garbage這個列表里面,利于在線上查找問題

內存泄露

既然Python中通過引用計數和垃圾回收來管理內存,那么什么情況下還會產生內存泄露呢?有兩種情況:

第一是對象被另一個生命周期特別長的對象所引用,比如網絡服務器,可能存在一個全局的單例ConnectionManager,管理所有的連接Connection,如果當Connection理論上不再被使用的時候,沒有從ConnectionManager中刪除,那么就造成了內存泄露。

第二是循環引用中的對象定義了__del__函數,這個在《程序員必知的Python陷阱與缺陷列表》一文中有詳細介紹,簡而言之,如果定義了__del__函數,那么在循環引用中Python解釋器無法判斷析構對象的順序,因此就不錯處理。

在任何環境,不管是服務器,客戶端,內存泄露都是非常嚴重的事情。

如果是線上服務器,那么一定得有監控,如果發現內存使用率超過設置的閾值則立即報警,盡早發現些許還有救。當然,誰也不希望在線上修復內存泄露,這無疑是給行駛的汽車換輪子,因此盡量在開發環境或者壓力測試環境發現并解決潛在的內存泄露。在這里,發現問題最為關鍵,只要發現了問題,解決問題就非常容易了,因為按照前面的說法,出現內存泄露只有兩種情況,在第一種情況下,只要在適當的時機解除引用就可以了;在第二種情況下,要么不再使用__del__函數,換一種實現方式,要么解決循環引用。

那么怎么查找哪里存在內存泄露呢?武器就是兩個庫:gc、objgraph

在上面已經介紹了gc這個模塊,理論上,通過gc模塊能夠拿到所有的被garbage collector管理的對象,也能知道對象之間的引用和被引用關系,就可以畫出對象之間完整的引用關系圖。但事實上還是比較復雜的,因為在這個過程中一不小心又會引入新的引用關系,所以,有好的輪子就直接用吧,那就是objgraph。

objgraph

objgraph的實現調用了gc的這幾個函數:gc.get_objects(), gc.get_referents(), gc.get_referers(),然后構造出對象之間的引用關系。objgraph的代碼和文檔都寫得比較好,建議一讀。

下面先介紹幾個十分實用的API

  1. def count(typename) 

返回該類型對象的數目,其實就是通過gc.get_objects()拿到所用的對象,然后統計指定類型的數目。

  1. def by_type(typename) 

返回該類型的對象列表。線上項目,可以用這個函數很方便找到一個單例對象

  1. def show_most_common_types(limits = 10) 

打印實例最多的前N(limits)個對象,這個函數非常有用。在《Python內存優化》一文中也提到,該函數能發現可以用slots進行內存優化的對象

  1. def show_growth() 

統計自上次調用以來增加得最多的對象,這個函數非常有利于發現潛在的內存泄露。函數內部調用了gc.collect(),因此即使有循環引用也不會對判斷造成影響。

值得一提,該函數的實現非常有意思,簡化后的代碼如下:

  1. def show_growth(limit=10, peak_stats={}, shortnames=True, file=None): 
  2.     gc.collect() 
  3.     stats = typestats(shortnames=shortnames) 
  4.     deltas = {} 
  5.     for namecount in iteritems(stats): 
  6.         old_count = peak_stats.get(name, 0) 
  7.         if count > old_count: 
  8.             deltas[name] = count - old_count 
  9.             peak_stats[name] = count 
  10.     deltas = sorted(deltas.items(), key=operator.itemgetter(1), 
  11.                     reverse=True 

注意形參peak_stats使用了可變參數作為默認形參,這樣很方便記錄上一次的運行結果。在《程序員必知的Python陷阱與缺陷列表》中提到,使用可變對象做默認形參是最為常見的python陷阱,但在這里,卻成為了方便的利器!

  1. def show_backrefs() 

生產一張有關objs的引用圖,看出看出對象為什么不釋放,后面會利用這個API來查內存泄露。

該API有很多有用的參數,比如層數限制(max_depth)、寬度限制(too_many)、輸出格式控制(filename output)、節點過濾(filter, extra_ignore),建議使用之間看一些document。

  1. def find_backref_chain(obj, predicate, max_depth=20, extra_ignore=()): 

找到一條指向obj對象的最短路徑,且路徑的頭部節點需要滿足predicate函數 (返回值為True)

可以快捷、清晰指出 對象的被引用的情況,后面會展示這個函數的威力

  1. def show_chain(): 

將find_backref_chain 找到的路徑畫出來, 該函數事實上調用show_backrefs,只是排除了所有不在路徑中的節點。

查找內存泄露

在這一節,介紹如何利用objgraph來查找內存是怎么泄露的

如果我們懷疑一段代碼、一個模塊可能會導致內存泄露,那么首先調用一次obj.show_growth(),然后調用相應的函數,最后再次調用obj.show_growth(),看看是否有增加的對象。比如下面這個簡單的例子:

  1. # -*- coding: utf-8 -*- 
  2. import objgraph 
  3.   
  4. _cache = [] 
  5.   
  6. class OBJ(object): 
  7.     pass 
  8.   
  9. def func_to_leak(): 
  10.     o  = OBJ() 
  11.     _cache.append(o) 
  12.     # do something with o, then remove it from _cache 
  13.   
  14.     if True: # this seem ugly, but it always exists 
  15.         return 
  16.     _cache.remove(o) 
  17.   
  18. if __name__ == '__main__'
  19.     objgraph.show_growth() 
  20.     try: 
  21.         func_to_leak() 
  22.     except
  23.         pass 
  24.     print 'after call func_to_leak' 
  25.     objgraph.show_growth()  

運行結果(我們只關心后一次show_growth的結果)如下

  1. wrapper_descriptor 1073 +13 
  2. member_descriptor 204 +5 
  3. getset_descriptor 168 +5 
  4. weakref 338 +3 
  5. dict 458 +3 
  6. OBJ 1 +1  

代碼很簡單,函數開始的時候講對象加入了global作用域的_cache列表,然后期望是在函數退出之前從_cache刪除,但是由于提前返回或者異常,并沒有執行到最后的remove語句。從運行結果可以發現,調用函數之后,增加了一個類OBJ的實例,然而理論上函數調用結束之后,所有在函數作用域(local)中聲明的對象都改被銷毀,因此這里就存在內存泄露。

當然,在實際的項目中,我們也不清楚泄露是在哪段代碼、哪個模塊中發生的,而且往往是發生了內存泄露之后再去排查,這個時候使用obj.show_most_common_types就比較合適了,如果一個自定義的類的實例數目特別多,那么就可能存在內存泄露。如果在壓力測試環境,停止壓測,調用gc.collet,然后再用obj.show_most_common_types查看,如果對象的數目沒有相應的減少,那么肯定就是存在泄露。

當我們定位了哪個對象發生了內存泄露,那么接下來就是分析怎么泄露的,引用鏈是怎么樣的,這個時候就該show_backrefs出馬了,還是以之前的代碼為例,稍加修改:

  1. import objgraph 
  2.   
  3. _cache = [] 
  4.   
  5. class OBJ(object): 
  6.     pass 
  7.   
  8. def func_to_leak(): 
  9.     o  = OBJ() 
  10.     _cache.append(o) 
  11.     # do something with o, then remove it from _cache 
  12.   
  13.     if True: # this seem ugly, but it always exists 
  14.         return 
  15.     _cache.remove(o) 
  16.   
  17. if __name__ == '__main__'
  18.     try: 
  19.         func_to_leak() 
  20.     except
  21.         pass 
  22.     objgraph.show_backrefs(objgraph.by_type('OBJ')[0], max_depth = 10, filename = 'obj.dot' 

show_backrefs查看內存泄露

注意,上面的代碼中,max_depth參數非常關鍵,如果這個參數太小,那么看不到完整的引用鏈,如果這個參數太大,運行的時候又非常耗時間。

然后打開dot文件,結果如下

 

可以看到泄露的對象(紅框表示),是被一個叫_cache的list所引用,而_cache又是被__main__這個module所引用。

對于示例代碼,dot文件的結果已經非常清晰,但是對于真實項目,引用鏈中的節點可能成百上千,看起來非常頭大,下面用tornado起一個最最簡單的web服務器(代碼不知道來自哪里,且沒有內存泄露,這里只是為了顯示引用關系),然后繪制socket的引用關關系圖,代碼和引用關系圖如下:

  1. import objgraph 
  2. import errno 
  3. import functools 
  4. import tornado.ioloop 
  5. import socket 
  6.   
  7. def connection_ready(sock, fd, events): 
  8.     while True
  9.         try: 
  10.             connection, address = sock.accept() 
  11.             print 'connection_ready', address 
  12.         except socket.error as e: 
  13.             if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN): 
  14.                 raise 
  15.             return 
  16.         connection.setblocking(0) 
  17.         # do sth with connection 
  18.   
  19.   
  20. if __name__ == '__main__'
  21.     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) 
  22.     sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
  23.     sock.setblocking(0) 
  24.     sock.bind(("", 8888)) 
  25.     sock.listen(128) 
  26.   
  27.     io_loop = tornado.ioloop.IOLoop.current() 
  28.     callback = functools.partial(connection_ready, sock) 
  29.     io_loop.add_handler(sock.fileno(), callback, io_loop.READ
  30.     #objgraph.show_backrefs(sock, max_depth = 10, filename = 'tornado.dot'
  31.     # objgraph.show_chain( 
  32.     #     objgraph.find_backref_chain( 
  33.     #         sock, 
  34.     #         objgraph.is_proper_module 
  35.     #     ), 
  36.     #     filename='obj_chain.dot' 
  37.     # ) 
  38.     io_loop.start() 
  39.   
  40. tornado_server實例 

 

 

可見,代碼越復雜,相互之間的引用關系越多,show_backrefs越難以看懂。這個時候就使用show_chain和find_backref_chain吧,這種方法,在官方文檔也是推薦的,我們稍微改改代碼,結果如下:

  1. import objgraph 
  2.   
  3. _cache = [] 
  4.   
  5. class OBJ(object): 
  6.     pass 
  7.   
  8. def func_to_leak(): 
  9.     o  = OBJ() 
  10.     _cache.append(o) 
  11.     # do something with o, then remove it from _cache 
  12.   
  13.     if True: # this seem ugly, but it always exists 
  14.         return 
  15.     _cache.remove(o) 
  16.   
  17. if __name__ == '__main__'
  18.     try: 
  19.         func_to_leak() 
  20.     except
  21.         pass 
  22.     # objgraph.show_backrefs(objgraph.by_type('OBJ')[0], max_depth = 10, filename = 'obj.dot'
  23.     objgraph.show_chain( 
  24.         objgraph.find_backref_chain( 
  25.             objgraph.by_type('OBJ')[0], 
  26.             objgraph.is_proper_module 
  27.         ), 
  28.         filename='obj_chain.dot' 
  29.     ) 

 

 

上面介紹了內存泄露的第一種情況,對象被“非期望”地引用著。下面看看第二種情況,循環引用中的__del__, 看下面的代碼:

  1. # -*- coding: utf-8 -*- 
  2. import objgraph, gc 
  3. class OBJ(object): 
  4.     def __del__(self): 
  5.         print('Dangerous!'
  6.   
  7. def show_leak_by_del(): 
  8.     a, b = OBJ(), OBJ() 
  9.     a.attr_b = b 
  10.     b.attr_a = a 
  11.   
  12.     del a, b 
  13.     print gc.collect() 
  14.   
  15.     objgraph.show_backrefs(objgraph.by_type('OBJ')[0], max_depth = 10, filename = 'del_obj.dot' 

上面的代碼存在循環引用,而且OBJ類定義了__del__函數。如果沒有定義__del__函數,那么上述的代碼會報錯, 因為gc.collect會將循環引用刪除,objgraph.by_type(‘OBJ’)返回空列表。而因為定義了__del__函數,gc.collect也無能為力,結果如下:

 

從圖中可以看到,對于這種情況,還是比較好辨識的,因為objgraph將__del__函數用特殊顏色標志出來,一眼就看見了。另外,可以看見gc.garbage(類型是list)也引用了這兩個對象,原因在document中有描述,當執行垃圾回收的時候,會將定義了__del__函數的類實例(被稱為uncollectable object)放到gc.garbage列表,因此,也可以直接通過查看gc.garbage來找出定義了__del__的循環引用。在這里,通過增加extra_ignore來排除gc.garbage的影響:

將上述代碼的最后一行改成:

  1. objgraph.show_backrefs(objgraph.by_type('OBJ')[0], extra_ignore=(id(gc.garbage),),  max_depth = 10, filename = 'del_obj.dot'

 

另外,也可以設置DEBUG_UNCOLLECTABLE 選項,直接將uncollectable對象輸出到標準輸出,而不是放到gc.garbage

循環引用

除非定義了__del__方法,那么循環引用也不是什么萬惡不赦的東西,因為垃圾回收器可以處理循環引用,而且不準是python標準庫還是大量使用的第三方庫,都可能存在循環引用。如果存在循環引用,那么Python的gc就必須開啟(gc.isenabled()返回True),否則就會內存泄露。但是在某些情況下,我們還是不希望有gc,比如對內存和性能比較敏感的應用場景,在這篇文章中,提到instagram通過禁用gc,性能提升了10%;另外,在一些應用場景,垃圾回收帶來的卡頓也是不能接受的,比如RPG游戲。從前面對垃圾回收的描述可以看到,執行一次垃圾回收是很耗費時間的,因為需要遍歷所有被collector管理的對象(即使很多對象不屬于垃圾)。因此,要想禁用GC,就得先徹底干掉循環引用。

同內存泄露一樣,解除循環引用的前提是定位哪里出現了循環引用。而且,如果需要在線上應用關閉gc,那么需要自動、持久化的進行檢測。下面介紹如何定位循環引用,以及如何解決循環引用。

定位循環引用

這里還是是用GC模塊和objgraph來定位循環引用。需要注意的事,一定要先禁用gc(調用gc.disable()), 防止誤差。

這里利用之前介紹循環引用時使用過的例子: a, b兩個OBJ對象形成循環引用

  1. # -*- coding: utf-8 -*- 
  2. import objgraph, gc 
  3. class OBJ(object): 
  4.     pass 
  5.   
  6. def show_cycle_reference(): 
  7.     a, b = OBJ(), OBJ() 
  8.     a.attr_b = b 
  9.     b.attr_a = a 
  10.   
  11. if __name__ == '__main__'
  12.     gc.disable() 
  13.     for _ in xrange(50): 
  14.         show_cycle_reference() 
  15.     objgraph.show_most_common_types(20)  

運行結果(部分):

  1. wrapper_descriptor 1060 
  2.  
  3. dict 555 
  4.  
  5. OBJ 100  

上面的代碼中使用的是show_most_common_types,而沒有使用show_growth(因為growth會手動調用gc.collect()),通過結果可以看到,內存中現在有100個OBJ對象,符合預期。當然這些OBJ對象沒有在函數調用后被銷毀,不一定是循環引用的問題,也可能是內存泄露,比如前面OBJ對象被global作用域中的_cache引用的情況。怎么排除是否是被global作用域的變量引用的情況呢,方法還是objgraph.find_backref_chain(obj),在__doc__中指出,如果找不到符合條件的應用鏈(chain),那么返回[obj],稍微修改上面的代碼:

  1. # -*- coding: utf-8 -*- 
  2. import objgraph, gc 
  3. class OBJ(object): 
  4.     pass 
  5.   
  6. def show_cycle_reference(): 
  7.     a, b = OBJ(), OBJ() 
  8.     a.attr_b = b 
  9.     b.attr_a = a 
  10.   
  11. if __name__ == '__main__'
  12.     gc.disable() 
  13.     for _ in xrange(50): 
  14.         show_cycle_reference() 
  15.     ret = objgraph.find_backref_chain(objgraph.by_type('OBJ')[0], objgraph.is_proper_module) 
  16.     print ret  

上面的代碼輸出:

  1. [<__main__.OBJ object at 0x0244F810>] 

驗證了我們的想法,OBJ對象不是被global作用域的變量所引用。

在實際項目中,不大可能到處用objgraph.show_most_common_types或者objgraph.by_type來排查循環引用,效率太低。有沒有更好的辦法呢,有的,那就是使用gc模塊的debug 選項。在前面介紹gc模塊的時候,就介紹了gc.DEBUG_COLLECTABLE 選項,我們來試試:

  1. # -*- coding: utf-8 -*- 
  2. import gc, time 
  3. class OBJ(object): 
  4.     pass 
  5.   
  6. def show_cycle_reference(): 
  7.     a, b = OBJ(), OBJ() 
  8.     a.attr_b = b 
  9.     b.attr_a = a 
  10.   
  11. if __name__ == '__main__'
  12.     gc.disable() # 這里是否disable事實上無所謂 
  13.     gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_OBJECTS) 
  14.     for _ in xrange(1): 
  15.         show_cycle_reference() 
  16.     gc.collect() 
  17.     time.sleep(5)  

上面代碼第13行設置了debug flag,可以打印出collectable對象。另外,只用調用一次show_cycle_reference函數就足夠了(這也比objgraph.show_most_common_types方便一點)。在第16行手動調用gc.collect(),輸出如下:

  1. gc: collectable <OBJ 023B46F0> 
  2.  
  3. gc: collectable <OBJ 023B4710> 
  4.  
  5. gc: collectable <dict 023B7AE0> 
  6.  
  7. gc: collectable <dict 023B7930>  

注意:只有當對象是unreachable且collectable的時候,在collect的時候才會被輸出,也就是說,如果是reachable,比如被global作用域的變量引用,那么也是不會輸出的。

通過上面的輸出,我們已經知道OBJ類的實例存在循環引用,但是這個時候,obj實例已經被回收了。那么如果我想通過show_backrefs找出這個引用關系,需要重新調用show_cycle_reference函數,然后不調用gc.collect,通過show_backrefs 和 by_type繪制。有沒有更好的辦法呢,可以讓我在一次運行中發現循環引用,并找出引用鏈?答案就是使用DEBUG_SAVEALL,下面為了展示方便,直接在命令行中操作(當然,使用ipython更好)

  1. >>> import gc, objgraph 
  2. >>> class OBJ(object): 
  3. ... pass 
  4. ... 
  5. >>> def show_cycle_reference(): 
  6. ... a, b = OBJ(), OBJ() 
  7. ... a.attr_b = b 
  8. ... b.attr_a = a 
  9. ... 
  10. >>> gc.set_debug(gc.DEBUG_SAVEALL| gc.DEBUG_OBJECTS) 
  11. >>> show_cycle_reference() 
  12. >>> print 'before collect', gc.garbage 
  13. before collect [] 
  14. >>> print gc.collect() 
  15. >>> 
  16. >>> for o in gc.garbage: 
  17. ... print o 
  18. ... 
  19. <__main__.OBJ object at 0x024BB7D0> 
  20. <__main__.OBJ object at 0x02586850> 
  21. {'attr_b': <__main__.OBJ object at 0x02586850>} 
  22. {'attr_a': <__main__.OBJ object at 0x024BB7D0>} 
  23. >>> 
  24. >>> objgraph.show_backrefs(objgraph.at(0x024BB7D0), 5, filename = 'obj.dot'
  25. Graph written to obj.dot (13 nodes) 
  26. >>>  

上面在調用gc.collect之前,gc.garbage里面是空的,由于設置了DEBUG_SAVEALL,那么調用gc.collect時,會將collectable對象放到gc.garbage。此時,對象沒有被釋放,我們就可以直接繪制出引用關系,這里使用了objgraph.at,當然也可以使用objgraph.by_type, 或者直接從gc.garbage取對象,結果如下:

 

出了循環引用,可以看見還有兩個引用,gc.garbage與局部變量o,相信大家也能理解。

消滅循環引用

找到循環引用關系之后,解除循環引用就不是太難的事情,總的來說,有兩種辦法:手動解除與使用weakref。

手動解除很好理解,就是在合適的時機,解除引用關系。比如,前面提到的collections.OrderedDict:

  1. >>> root = [] 
  2. >>> root[:] = [root, root, None] 
  3. >>> 
  4. >>> root 
  5. [[...], [...], None] 
  6. >>> 
  7. >>> del root[:] 
  8. >>> root 
  9. []  

更常見的情況,是我們自定義的對象之間存在循環引用:要么是單個對象內的循環引用,要么是多個對象間的循環引用,我們看一個單個對象內循環引用的例子:

  1. class Connection(object): 
  2.     MSG_TYPE_CHAT = 0X01 
  3.     MSG_TYPE_CONTROL = 0X02 
  4.     def __init__(self): 
  5.         self.msg_handlers = { 
  6.             self.MSG_TYPE_CHAT : self.handle_chat_msg, 
  7.             self.MSG_TYPE_CONTROL : self.handle_control_msg 
  8.         } 
  9.   
  10.     def on_msg(self, msg_type, *args): 
  11.         self.msg_handlers[msg_type](*args) 
  12.   
  13.     def handle_chat_msg(self, msg): 
  14.         pass 
  15.   
  16.     def handle_control_msg(self, msg): 
  17.         pass  

上面的代碼非常常見,代碼也很簡單,初始化函數中為每種消息類型定義響應的處理函數,當消息到達(on_msg)時根據消息類型取出處理函數。但這樣的代碼是存在循環引用的,感興趣的讀者可以用objgraph看看引用圖。如何手動解決呢,為Connection增加一個destroy(或者叫clear)函數,該函數將 self.msg_handlers 清空(self.msg_handlers.clear())。當Connection理論上不在被使用的時候調用destroy函數即可。

對于多個對象間的循環引用,處理方法也是一樣的,就是在“適當的時機”調用destroy函數,難點在于什么是適當的時機。

另外一種更方便的方法,就是使用弱引用weakref, weakref是Python提供的標準庫,旨在解決循環引用。

weakref模塊提供了以下一些有用的API:

(1)weakref.ref(object, callback = None)

創建一個對object的弱引用,返回值為weakref對象,callback: 當object被刪除的時候,會調用callback函數,在標準庫logging (__init__.py)中有使用范例。使用的時候要用()解引用,如果referant已經被刪除,那么返回None。比如下面的例子

  1. # -*- coding: utf-8 -*- 
  2. import weakref 
  3. class OBJ(object): 
  4.     def f(self): 
  5.         print 'HELLO' 
  6.   
  7. if __name__ == '__main__'
  8.     o = OBJ() 
  9.     w = weakref.ref(o) 
  10.     w().f() 
  11.     del o 
  12.     w().f()  

運行上面的代碼,第12行會拋出異常:AttributeError: ‘NoneType’ object has no attribute ‘f’。因為這個時候被引用的對象已經被刪除了

(2)weakref.proxy(object, callback = None)

創建一個代理,返回值是一個weakproxy對象,callback的作用同上。使用的時候直接用 和object一樣,如果object已經被刪除 那么跑出異常 ReferenceError: weakly-referenced object no longer exists。

  1. # -*- coding: utf-8 -*- 
  2. import weakref 
  3. class OBJ(object): 
  4.     def f(self): 
  5.         print 'HELLO' 
  6.   
  7. if __name__ == '__main__'
  8.     o = OBJ() 
  9.     w = weakref.proxy(o) 
  10.     w.f() 
  11.     del o 
  12.     w.f()  

注意第10行 12行與weakref.ref示例代碼的區別

(3)weakref.WeakSet

這個是一個弱引用集合,當WeakSet中的元素被回收的時候,會自動從WeakSet中刪除。WeakSet的實現使用了weakref.ref,當對象加入WeakSet的時候,使用weakref.ref封裝,指定的callback函數就是從WeakSet中刪除。感興趣的話可以直接看源碼(_weakrefset.py),下面給出一個參考例子:

  1. # -*- coding: utf-8 -*- 
  2. import weakref 
  3. class OBJ(object): 
  4.     def f(self): 
  5.         print 'HELLO' 
  6.   
  7. if __name__ == '__main__'
  8.     o = OBJ() 
  9.     ws = weakref.WeakSet() 
  10.     ws.add(o) 
  11.     print len(ws) #  1 
  12.     del o 
  13.     print len(ws) # 0  

(4)weakref.WeakValueDictionary, weakref.WeakKeyDictionary

實現原理和使用方法基本同WeakSet

總結

本文的篇幅略長,首選是簡單介紹了python的內存管理,重點介紹了引用計數與垃圾回收,然后闡述Python中內存泄露與循環引用產生的原因與危害,最后是利用gc、objgraph、weakref等工具來分析并解決內存泄露、循環引用問題。

references

  • Garbage Collector Interface
  • objgraph
  • Garbage Collection for Python
  • 禁用Python的GC機制后,Instagram性能提升10%
  • Python內存管理機制及優化簡析
  • library weakref 
責任編輯:龐桂玉 來源: Python開發者
相關推薦

2025-05-22 09:35:24

2024-11-27 13:38:30

2021-05-27 21:47:12

Python垃圾回收

2017-05-04 16:07:11

Tomcat內存泄露

2009-06-11 10:25:36

Java GC幽靈引用

2021-04-14 10:14:34

JVM生產問題定位內存泄露

2009-06-16 11:11:07

Java內存管理Java內存泄漏

2015-05-14 15:38:40

javajava內存泄露

2011-11-17 13:59:41

Java內存管理內存泄露

2016-09-08 16:16:26

iOS移動應用內存泄漏

2010-10-25 10:10:27

ibmdwJava

2010-09-25 11:23:15

Java內存泄露

2020-02-09 17:23:17

Python數據字典

2017-02-21 16:40:16

Android垃圾回收內存泄露

2013-12-17 16:01:02

iOSXcodeInstruments

2017-12-11 11:00:27

內存泄露判斷

2020-06-23 09:48:09

Python開發內存

2015-09-16 15:32:37

Android Tra內存管理

2013-08-07 10:07:07

Handler內存泄露

2022-10-10 11:37:14

Gomap內存
點贊
收藏

51CTO技術棧公眾號

国产成人天天5g影院在线观看| 国产精品男女视频| 国语精品视频| 午夜精品福利久久久| 精品一区2区三区| 成人毛片一区二区三区| 亚洲xxx拳头交| 日韩成人在线视频网站| 欧美男女交配视频| 美女日批视频在线观看| 久久久综合精品| 亚洲一区二区三区sesese| 国产区在线观看视频| 欧美第一精品| 日韩电影中文 亚洲精品乱码| 少妇一级淫免费放| 9999热视频在线观看| 国产精品色呦呦| 久久av一区二区| 国产免费黄色网址| 久久青草久久| 久久久久久久一区二区三区| 黄色免费一级视频| 青草久久视频| 欧美成人精精品一区二区频| 免费看涩涩视频| 小早川怜子影音先锋在线观看| 综合色中文字幕| 奇米888一区二区三区| 亚洲h视频在线观看| 美女视频免费一区| 日本精品久久久久影院| 久久精品久久国产| 欧美精品一级| 久久国产精品首页| 强制高潮抽搐sm调教高h| 开心激情综合| 欧美变态tickle挠乳网站| 伊人影院综合在线| 欧美精品高清| 欧美性xxxx极品高清hd直播| 国产91在线亚洲| 国产淫片在线观看| 国产精品久久久久久久岛一牛影视| 久久一区免费| 四虎成人免费在线| 99久精品国产| 精品日本一区二区| 天堂网在线资源| 不卡在线视频中文字幕| 国产精品一区二区三区四区五区 | 粉嫩高清一区二区三区精品视频| 伊人免费在线观看| 蜜乳av一区二区三区| 国产精品久久久久久久久久| 亚洲一区二区三区激情| 亚洲中文字幕无码一区二区三区| 69xxx在线| 亚洲三级电影网站| 青青草原网站在线观看| 精品美女在线观看视频在线观看 | 国产无遮挡免费视频| 午夜激情一区| 欧美国产激情18| 国产一级一级片| 亚洲精品1区| 97av视频在线| 国产女主播喷水视频在线观看 | 黄色成人在线免费| 国产素人在线观看| 自拍偷拍亚洲视频| 91成人在线免费观看| 天天色综合社区| 国产福利亚洲| 欧美一区二区三区视频在线观看| 婷婷激情小说网| 红杏aⅴ成人免费视频| 亚洲国产免费av| 国产全是老熟女太爽了| 成人一二三区| 精品自拍视频在线观看| 国产黄色片视频| 久久亚洲国产精品一区二区| 国产精品一区电影| 99视频在线观看免费| 99久久免费视频.com| 日韩欧美视频第二区| 麻豆网站在线看| 亚洲不卡在线观看| 一本久道中文无码字幕av| 国产乱子精品一区二区在线观看| 日韩午夜激情免费电影| 少妇精品一区二区| 久久免费大视频| 欧美激情在线一区| 超碰在线免费97| 国产成人亚洲精品狼色在线| 久久国产手机看片| 免费在线观看av片| 狠狠爱在线视频一区| 激情六月丁香婷婷| 精品国产亚洲一区二区三区在线| 日韩精品在线电影| 成人高潮免费视频| 久久九九精品| 国产精品乱子乱xxxx| 成人亚洲性情网站www在线观看| 亚洲精品一卡二卡| 成人在线免费播放视频| 日韩精品视频中文字幕| 国产亚洲精品激情久久| 豆国产97在线 | 亚洲| 免费精品视频在线| 精品婷婷色一区二区三区蜜桃| 日本电影全部在线观看网站视频| 黄网站色欧美视频| 91丝袜超薄交口足| 日韩av二区| 日本国产欧美一区二区三区| www.久久综合| 中文字幕一区二区三区色视频| 妞干网在线观看视频| www.久久热| 中文字幕少妇一区二区三区| 国产成人精品片| 国产高清精品在线| 一卡二卡3卡四卡高清精品视频| 久草在线资源福利站| 日韩欧美一区电影| 色老板免费视频| 免费在线一区观看| 日本精品免费| 超碰aⅴ人人做人人爽欧美| 精品国产乱码久久| 国产一区二区视频在线观看免费| 免费亚洲电影在线| 欧美一区二区综合| 中文不卡1区2区3区| 精品成人一区二区三区四区| 亚洲最大的黄色网址| 麻豆国产精品777777在线| 欧美日韩国产精品一卡| 国产欧洲在线| 欧美精品一区二区精品网| 欧美日韩大片在线观看| 国产乱人伦精品一区二区在线观看| 亚洲人成影视在线观看| 国产91亚洲精品久久久| 最近中文字幕日韩精品 | 日韩精品一区二区三区视频在线观看| 亚洲 欧美 国产 另类| 秋霞午夜av一区二区三区| 日产中文字幕在线精品一区| 惠美惠精品网| 亚洲片在线资源| 欧美一区免费看| 久久久久久久久一| 免费激情视频在线观看| 国产成人影院| 国产精品午夜一区二区欲梦| 91青青在线视频| 欧美日韩成人在线一区| 97精品在线播放| 国产一区二区不卡老阿姨| 欧美性视频在线播放| 精品视频在线播放一区二区三区| 久色乳综合思思在线视频| 国产三级精品在线观看| 一二三区精品视频| 男女性杂交内射妇女bbwxz| 亚洲清纯自拍| 欧美一区二区三区在线免费观看| 性欧美videohd高精| 色先锋资源久久综合5566| 国产又粗又黄又爽视频| 樱花草国产18久久久久| 星空大象在线观看免费播放| 丝瓜av网站精品一区二区| 亚洲欧美久久久久一区二区三区| 精品国产三区在线| 26uuu日韩精品一区二区| 黄色片免费在线| 欧美丰满美乳xxx高潮www| 欧美激情精品久久| 91丝袜国产在线播放| 15—17女人毛片| 欧美激情日韩| 久久国产精品99久久久久久丝袜| 久久久精品一区二区毛片免费看| 久热精品在线视频| 日本v片在线免费观看| 欧美日韩国产在线播放网站| 午夜伦理精品一区 | 91在线观看高清| 免费黄色一级网站| 欧美一区网站| 欧美污视频久久久| 久久wwww| 国产成人在线播放| 在线电影福利片| 亚洲欧洲午夜一线一品| 国内老熟妇对白hdxxxx| 日本精品一级二级| 免费看一级一片| 国产日产亚洲精品系列| 亚洲欧洲国产视频| 免费观看30秒视频久久| 欧美视频在线免费播放| 久久亚洲精品中文字幕蜜潮电影| 国产亚洲福利社区| 亚洲网站三级| 国产91免费看片| 免费电影视频在线看| 最近2019中文字幕第三页视频| 老牛影视av牛牛影视av| 欧美日韩亚洲综合| 91video| 亚洲一二三四久久| 美国一级片在线观看| 91麻豆免费视频| 麻豆传媒在线看| 久久国产三级精品| 波多野结衣作品集| 国产精品毛片一区二区三区| 日本高清视频免费在线观看| 欧美色图一区| 欧美精品亚洲| 女仆av观看一区| 99精彩视频在线观看免费| 欧美亚洲黄色| 国产精品mp4| 免费成人在线电影| 欧美激情视频网| 制服丝袜在线播放| 操人视频在线观看欧美| 美女黄视频在线观看| 国产亚洲视频在线观看| 男男电影完整版在线观看| 亚洲精品91美女久久久久久久| av在线免费在线观看| 欧美精品久久天天躁| 亚洲一区中文字幕永久在线| 色又黄又爽网站www久久| 欧美日韩综合在线观看| 精品久久久久久久久久久| 精品视频久久久久| 夜夜嗨av一区二区三区中文字幕 | 成人在线播放免费观看| 少妇久久久久久| 免费av在线| 久久综合电影一区| 亚洲91av| 韩国v欧美v日本v亚洲| 51av在线| 日韩美女视频免费在线观看| 中文字幕在线看片| 日韩免费在线免费观看| 国产一区一一区高清不卡| 国产精品综合网站| 国产精品免费精品自在线观看| 成人两性免费视频| 99久久人爽人人添人人澡| 国产美女在线精品免费观看| 日韩影视高清在线观看| 日本高清不卡一区二区三| 成人毛片在线| a级网站在线观看| 精品福利电影| 国模杨依粉嫩蝴蝶150p| 黄色精品一二区| 国内自拍偷拍视频| 91蜜桃视频在线| 少妇太紧太爽又黄又硬又爽小说| 亚洲色图另类专区| 一级aaa毛片| 在线观看中文字幕不卡| 国产精品怡红院| 亚洲电影第1页| 成a人片在线观看www视频| 久久夜色精品国产| 成人免费观看在线观看| 国产精品激情av在线播放| 国产不卡精品| 久久青青草原一区二区| 色999日韩| 蜜臀av色欲a片无码精品一区| 日韩午夜免费视频| 日韩无套无码精品| 国产传媒一区在线| 风间由美一二三区av片| 国产精品久久久久久福利一牛影视| 永久免费看mv网站入口| 久久综合av免费| 中文字幕五月天| 亚洲国产日产av| 依依成人在线视频| 91精品国产福利| 亚洲色图欧美视频| 久久精品亚洲热| sm捆绑调教国产免费网站在线观看| 国产成人福利网站| а√中文在线天堂精品| 日韩精品极品视频在线观看免费| 中文字幕免费精品| 国产免费毛卡片| 韩国av一区二区三区| 伊人网在线视频观看| 亚洲男人电影天堂| 免费看日批视频| 亚洲国产精久久久久久久| 在线国产91| 日本精品免费观看| 亚洲专区**| 亚洲成人自拍视频| 久久精品国产清高在天天线| 污免费在线观看| 国产午夜精品美女毛片视频| 国产在线视频在线观看| 欧美亚洲国产一区二区三区va| 亚洲第一天堂影院| 伊人久久精品视频| 成人影院在线播放| 成人免费淫片视频软件| 国内黄色精品| 久久久久久久久久久99| 国产91丝袜在线18| 成年人视频软件| 日韩欧美在线观看| 天天色综合久久| 欧美成人手机在线| 国产精一区二区| 日日夜夜精品网站| 亚洲人人精品| 先锋资源av在线| 亚洲黄色小说网站| 丰满少妇xoxoxo视频| 亚洲人a成www在线影院| 丰满大乳少妇在线观看网站| 成人在线一区二区| 大色综合视频网站在线播放| 少妇高潮喷水在线观看| 国内精品久久久久影院一蜜桃| www亚洲色图| 在线视频你懂得一区| 无码精品人妻一区二区三区影院| 在线观看成人黄色| 在线看欧美视频| 欧美少妇一区| 免费人成精品欧美精品| 山东少妇露脸刺激对白在线| 欧美性极品少妇| 国产精品天堂| 欧美性视频在线| 国产成人三级| 99草草国产熟女视频在线| 国产亚洲综合在线| 日韩精品成人免费观看视频| 亚洲精品99999| 欧美特黄aaaaaaaa大片| 麻豆成人在线播放| 日本欧美大码aⅴ在线播放| 少妇精品无码一区二区免费视频| 欧美视频免费在线| se在线电影| 国产精品亚洲综合天堂夜夜| 在线成人超碰| 韩国一区二区三区四区| 亚洲欧美日韩久久| 欧美一级淫片aaaaaa| 久久久亚洲影院| 国产a久久精品一区二区三区| 日韩一级片免费视频| 国产成人精品亚洲日本在线桃色| 久久久久久国产精品视频 | 亚洲欧美高清视频| 久久乐国产精品| 秋霞影院一区二区三区| 成人一级片网站| 国产喂奶挤奶一区二区三区| 中文字幕乱码人妻二区三区| 中文字幕亚洲图片| 亚洲国产伊人| 黄色一级片黄色| 久久久综合网站| 中文字幕一级片| 欧美精品videossex88| 久久99国产精品久久99大师| 久久精品影视大全| 亚洲精品成a人| 日本在线视频1区| 91中文字幕在线| 亚洲精品裸体| 2017亚洲天堂| 亚洲激情第一页| 成人网av.com/| 九九爱精品视频| 亚洲色图19p| 亚洲欧美一区二区三| 国产成人免费91av在线| 欧美日韩精品免费观看视频完整|