<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          超干貨!徹底搞懂Golang內(nèi)存管理和垃圾回收

          共 17334字,需瀏覽 35分鐘

           ·

          2022-07-16 23:00


          導(dǎo)語 | 現(xiàn)代高級編程語言管理內(nèi)存的方式分自動和手動兩種。手動管理內(nèi)存的典型代表是C和C++,編寫代碼過程中需要主動申請或者釋放內(nèi)存;而Java和Go等語言使用自動的內(nèi)存管理系統(tǒng),由內(nèi)存分配器和垃圾收集器來代為分配和回收內(nèi)存,開發(fā)者只需關(guān)注業(yè)務(wù)代碼而無需關(guān)注底層內(nèi)存分配和回收,雖然語言幫我們處理了這部分,但是還是有必要去了解一下底層的架構(gòu)設(shè)計和執(zhí)行邏輯,這樣可以更好的掌握一門語言,本文主要以go內(nèi)存管理為切入點再到go垃圾回收,系統(tǒng)地講解了go自動內(nèi)存管理系統(tǒng)的設(shè)計和原理。


          一、TCMalloc

          go內(nèi)存管理是借鑒了TCMalloc的設(shè)計思想,TCMalloc全稱Thead-Caching Malloc,是google開發(fā)的內(nèi)存分配器,為了方便理解下面的go內(nèi)存管理,有必要要先熟悉一下TCMalloc。



          (一)Page


          操作系統(tǒng)對內(nèi)存管理以頁為單位,TCMalloc也是這樣,只不過TCMalloc里的Page大小與操作系統(tǒng)里的大小并不一定相等,而是倍數(shù)關(guān)系。



          (二) Span


          一組連續(xù)的Page被稱為Span,比如可以有4個頁大小的Span,也可以有8個頁大小的Span,Span比Page高一個層級,是為了方便管理一定大小的內(nèi)存區(qū)域,Span是TCMalloc中內(nèi)存管理的基本單位。



          (三) ThreadCache


          每個線程各自的Cache,一個Cache包含多個空閑內(nèi)存塊鏈表,每個鏈表連接的都是內(nèi)存塊,同一個鏈表上內(nèi)存塊的大小是相同的,也可以說按內(nèi)存塊大小,給內(nèi)存塊分了個類,這樣可以根據(jù)申請的內(nèi)存大小,快速從合適的鏈表選擇空閑內(nèi)存塊。由于每個線程有自己的ThreadCache,所以ThreadCache訪問是無鎖的。



          (四)CentralCache


          是所有線程共享的緩存,也是保存的空閑內(nèi)存塊鏈表,鏈表的數(shù)量與ThreadCache中鏈表數(shù)量相同,當ThreadCache內(nèi)存塊不足時,可以從CentralCache取,當ThreadCache內(nèi)存塊多時,可以放回CentralCache。由于CentralCache是共享的,所以它的訪問是要加鎖的。



          (五)PageHeap


          PageHeap是堆內(nèi)存的抽象,PageHeap存的也是若干鏈表,鏈表保存的是Span,當CentralCache沒有內(nèi)存的時,會從PageHeap取,把1個Span拆成若干內(nèi)存塊,添加到對應(yīng)大小的鏈表中,當CentralCache內(nèi)存多的時候,會放回PageHeap。



          (六)TCMalloc對象分配


          小對象直接從ThreadCache分配,若ThreadCache不夠則從CentralCache中獲取內(nèi)存,CentralCache內(nèi)存不夠時會再從PageHeap獲取內(nèi)存,大對象在PageHeap中選擇合適的頁組成span用于存儲數(shù)據(jù)。



          二、GO內(nèi)存管理


          經(jīng)過上一節(jié)對TCMalloc內(nèi)存管理的描述,對接下來理解go的內(nèi)存管理會有大致架構(gòu)的熟悉,go內(nèi)存管理架構(gòu)取之TCMalloc不過在細節(jié)上有些出入,先來看一張go內(nèi)存管理的架構(gòu)圖:



          (一)Page


          和TCMalloc中page相同,上圖中最下方淺藍色長方形代表一個page。



          (二)Span


          與TCMalloc中的Span相同,Span是go內(nèi)存管理的基本單位,代碼中為mspan,一組連續(xù)的Page組成1個Span,所以上圖一組連續(xù)的淺藍色長方形代表的是一組Page組成的1個Span,另外,1個淡紫色長方形為1個Span。



          (三)mcache


          mcache與TCMalloc中的ThreadCache類似,mcache保存的是各種大小的Span,并按Span class分類,小對象直接從mcache分配內(nèi)存,它起到了緩存的作用,并且可以無鎖訪問。但mcache與ThreadCache也有不同點,TCMalloc中是每個線程1個ThreadCache,Go中是每個P擁有1個mcach,因為在Go程序中,當前最多有GOMAXPROCS個線程在運行,所以最多需要GOMAXPROCS個mcache就可以保證各線程對mcache的無鎖訪問,下圖是G,P,M三者之間的關(guān)系:



          (四)mcentral


          mcentral與TCMalloc中的CentralCache類似,是所有線程共享的緩存,需要加鎖訪問,它按Span class對Span分類,串聯(lián)成鏈表,當mcache的某個級別Span的內(nèi)存被分配光時,它會向mcentral申請1個當前級別的Span。但mcentral與CentralCache也有不同點,CentralCache是每個級別的Span有1個鏈表,mcache是每個級別的Span有2個鏈表。



          (五)mheap


          mheap與TCMalloc中的PageHeap類似,它是堆內(nèi)存的抽象,把從OS(系統(tǒng))申請出的內(nèi)存頁組織成Span,并保存起來。當mcentral的Span不夠用時會向mheap申請,mheap的Span不夠用時會向OS申請,向OS的內(nèi)存申請是按頁來的,然后把申請來的內(nèi)存頁生成Span組織起來,同樣也是需要加鎖訪問的。但mheap與PageHeap也有不同點:mheap把Span組織成了樹結(jié)構(gòu),而不是鏈表,并且還是2棵樹,然后把Span分配到heapArena進行管理,它包含地址映射和span是否包含指針等位圖,這樣做的主要原因是為了更高效的利用內(nèi)存:分配、回收和再利用。



          (六)內(nèi)存分配


          Go中的內(nèi)存分類并不像TCMalloc那樣分成小、中、大對象,但是它的小對象里又細分了一個Tiny對象,Tiny對象指大小在1Byte到16Byte之間并且不包含指針的對象。小對象和大對象只用大小劃定,無其他區(qū)分,其中小對象大小在16Byte到32KB之間,大對象大小大于32KB。span規(guī)格分類 上面說到go的內(nèi)存管理基本單位是span,且span有不同的規(guī)格,要想?yún)^(qū)分出不同的的span,我們必須要有一個標識,每個span通過spanclass標識屬于哪種規(guī)格的span,golang的span規(guī)格一共有67種,具體如下:


          //from runtime.gosizeclasses.go
          // class bytes/obj bytes/span objects tail waste max waste// 1 8 8192 1024 0 87.50%// 2 16 8192 512 0 43.75%// 3 32 8192 256 0 46.88%// 4 48 8192 170 32 31.52%// 5 64 8192 128 0 23.44%// 6 80 8192 102 32 19.07%// 7 96 8192 85 32 15.95%// 8 112 8192 73 16 13.56%// 9 128 8192 64 0 11.72%// 10 144 8192 56 128 11.82%// 11 160 8192 51 32 9.73%// 12 176 8192 46 96 9.59%// 13 192 8192 42 128 9.25%// 14 208 8192 39 80 8.12%// 15 224 8192 36 128 8.15%// 16 240 8192 34 32 6.62%// 17 256 8192 32 0 5.86%// 18 288 8192 28 128 12.16%// 19 320 8192 25 192 11.80%// 20 352 8192 23 96 9.88%// 21 384 8192 21 128 9.51%// 22 416 8192 19 288 10.71%// 23 448 8192 18 128 8.37%// 24 480 8192 17 32 6.82%// 25 512 8192 16 0 6.05%// 26 576 8192 14 128 12.33%// 27 640 8192 12 512 15.48%// 28 704 8192 11 448 13.93%// 29 768 8192 10 512 13.94%// 30 896 8192 9 128 15.52%// 31 1024 8192 8 0 12.40%// 32 1152 8192 7 128 12.41%// 33 1280 8192 6 512 15.55%// 34 1408 16384 11 896 14.00%// 35 1536 8192 5 512 14.00%// 36 1792 16384 9 256 15.57%// 37 2048 8192 4 0 12.45%// 38 2304 16384 7 256 12.46%// 39 2688 8192 3 128 15.59%// 40 3072 24576 8 0 12.47%// 41 3200 16384 5 384 6.22%// 42 3456 24576 7 384 8.83%// 43 4096 8192 2 0 15.60%// 44 4864 24576 5 256 16.65%// 45 5376 16384 3 256 10.92%// 46 6144 24576 4 0 12.48%// 47 6528 32768 5 128 6.23%// 48 6784 40960 6 256 4.36%// 49 6912 49152 7 768 3.37%// 50 8192 8192 1 0 15.61%// 51 9472 57344 6 512 14.28%// 52 9728 49152 5 512 3.64%// 53 10240 40960 4 0 4.99%// 54 10880 32768 3 128 6.24%// 55 12288 24576 2 0 11.45%// 56 13568 40960 3 256 9.99%// 57 14336 57344 4 0 5.35%// 58 16384 16384 1 0 12.49%// 59 18432 73728 4 0 11.11%// 60 19072 57344 3 128 3.57%// 61 20480 40960 2 0 6.87%// 62 21760 65536 3 256 6.25%// 63 24576 24576 1 0 11.45%// 64 27264 81920 3 128 10.00%// 65 28672 57344 2 0 4.91%// 66 32768 32768 1 0 12.50%


          由上表可見最大的對象是32KB大小,超過32KB大小的由特殊的class表示,該class ID為0,每個class只包含一個對象。所以上面只有列出了1-66。內(nèi)存大小轉(zhuǎn)換,下面還要三個數(shù)組,分別是:class_to_size,size_to_class和class_to_allocnpages3個數(shù)組,對應(yīng)下圖上的3個箭頭:



          以第一列為例,類別1的對象大小是8bytes,所以class_to_size[1]=8;span大小是8KB,為1頁,所以class_to_allocnpages[1]=1,下圖是go源碼中大小轉(zhuǎn)換數(shù)組。



          為對象尋找span,尋找span的流程如下:


          • 計算對象所需內(nèi)存大小size。


          • 根據(jù)size到size class映射,計算出所需的size class。


          • 根據(jù)size class和對象是否包含指針計算出span class。


          • 獲取該span class指向的span。


          以分配一個包含指針大小為20Byte的對象為例,根據(jù)映射表:


          // class  bytes/obj  bytes/span  objects  tail waste  max waste//     1          8        8192     1024           0     87.50%//     2         16        8192      512           0     43.75%//     3         32        8192      256           0     46.88%


          size class 3,它的對象大小范圍是(16,32]Byte,20Byte剛好在此區(qū)間,所以此對象的size class為3,Size class到span class的計算如下:


          // noscan為false代表對象包含指針func makeSpanClass(sizeclass uint8, noscan bool) spanClass {    return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))}


          所以,對應(yīng)的span class為:


          span class = 3 << 1 | 0 = 6


          所以該對象需要的是span class 7指向的span,自此,小對象內(nèi)存分配完成。


          //from runtime.gomalloc.go
          var sizeclass uint8//step1: 確定規(guī)格sizeClassif size <= smallSizeMax-8 { sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]} else { sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]}size = uintptr(class_to_size[sizeclass])// size class到span classspc := makeSpanClass(sizeclass, noscan)//step2: 分配對應(yīng)spanClass 的 spanspan = c.alloc[spc]v := nextFreeFast(span)if v == 0 { v, span, shouldhelpgc = c.nextFree(spc)}x = unsafe.Pointer(v)if needzero &amp;&amp; span.needzero != 0 { memclrNoHeapPointers(unsafe.Pointer(v), size)}


          大對象(>32KB)的分配則簡單多了,直接在mheap上進行分配,首先計算出需要的內(nèi)存頁數(shù)和span class級別,然后優(yōu)先從free中搜索可用的span,如果沒有找到,會從scav中搜索可用的span,如果還沒有找到,則向OS申請內(nèi)存,再重新搜索2棵樹,必然能找到span。如果找到的span比需求的span大,則把span進行分割成2個span,其中1個剛好是需求大小,把剩下的span再加入到free中去。



          三、垃圾回收


          (一)標記-清除


          標記-清除算法是第一種自動內(nèi)存管理,基于追蹤的垃圾收集算法。算法思想在70年代就提出了,是一種非常古老的算法。內(nèi)存單元并不會在變成垃圾立刻回收,而是保持不可達狀態(tài),直到到達某個閾值或者固定時間長度。這個時候系統(tǒng)會掛起用戶程序,也就是STW,轉(zhuǎn)而執(zhí)行垃圾回收程序。垃圾回收程序?qū)λ械拇婊顔卧M行一次全局遍歷確定哪些單元可以回收。算法分兩個部分:標記(mark)和清除(sweep)。標記階段表明所有的存活單元,清掃階段將垃圾單元回收。可視化可以參考下圖。



          標記-清除算法的優(yōu)點也就是基于追蹤的垃圾回收算法具有的優(yōu)點:避免了引用計數(shù)算法的缺點(不能處理循環(huán)引用,需要維護指針)。缺點也很明顯,需要STW。



          (二)三色可達性分析


          三色標記算法是對標記階段的改進,原理如下:


          • 起初所有對象都是白色。


          • 從根出發(fā)掃描所有可達對象,標記為灰色,放入待處理隊列。


          • 從隊列取出灰色對象,將其引用對象標記為灰色放入隊列,自身標記為黑色。


          • 重復(fù)上一步,直到灰色對象隊列為空。此時白色對象即為垃圾,進行回收。



          三色標記的一個明顯好處是能夠讓用戶程序和mark并發(fā)的進行,不過三色標記清除算法本身是不可以并發(fā)或者增量執(zhí)行的,它需要STW,而如果并發(fā)執(zhí)行,用戶程序可能在標記執(zhí)行的過程中修改對象的指針,導(dǎo)致可能將本該死亡的對象標記為存活和本該存活的對象標記為死亡,為了解決這種問題,go v1.8之后使用混合寫屏障技術(shù)支持并發(fā)和增量執(zhí)行,將垃圾收集的時間縮短至0.5ms以內(nèi)。



          (三)gc觸發(fā)


          在堆上分配大于32K byte對象的時候進行檢測此時是否滿足垃圾回收條件,如果滿足則進行垃圾回收


          func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {    ...    shouldhelpgc := false    // 分配的對象小于 32K byte    if size <= maxSmallSize {        ...    } else {        shouldhelpgc = true        ...    }    ...    // gcShouldStart() 函數(shù)進行觸發(fā)條件檢測    if shouldhelpgc && gcShouldStart(false) {        // gcStart() 函數(shù)進行垃圾回收        gcStart(gcBackgroundMode, false)    }}


          上面是自動垃圾回收,還有一種主動垃圾回收,通過調(diào)用runtime.GC(),這是阻塞式的。


          // GC runs a garbage collection and blocks the caller until the// garbage collection is complete. It may also block the entire// program.func GC() {    gcStart(gcForceBlockMode, false)}


          系統(tǒng)gc觸發(fā)條件:觸發(fā)條件主要關(guān)注下面代碼中的中間部分:forceTrigger||memstats.heap_live>=memstats.gc_trigger。forceTrigger是forceGC的標志,后面半句的意思是當前堆上的活躍對象大于我們初始化時候設(shè)置的GC觸發(fā)閾值,在malloc以及free的時候heap_live會一直進行更新。


          // gcShouldStart returns true if the exit condition for the _GCoff// phase has been met. The exit condition should be tested when// allocating.//// If forceTrigger is true, it ignores the current heap size, but// checks all other conditions. In general this should be false.func gcShouldStart(forceTrigger bool) bool {    return gcphase == _GCoff && (forceTrigger || memstats.heap_live >= memstats.gc_trigger) && memstats.enablegc && panicking == 0 && gcpercent >= 0}
          //初始化的時候設(shè)置 GC 的觸發(fā)閾值func gcinit() { _ = setGCPercent(readgogc()) memstats.gc_trigger = heapminimum ...}// 啟動的時候通過 GOGC 傳遞百分比 x// 觸發(fā)閾值等于 x * defaultHeapMinimum (defaultHeapMinimum 默認是 4M)func readgogc() int32 { p := gogetenv("GOGC") if p == "off" { return -1 } if n, ok := atoi32(p); ok { return n } return 100}



          (四)gc過程


          下列源碼是基于go 1.8,由于源碼過長,所以這里盡量只關(guān)注主流程


          • gcStart


          // gcStart 是 GC 的入口函數(shù),根據(jù) gcMode 做處理。// 1. gcMode == gcBackgroundMode(后臺運行,也就是并行), _GCoff -> _GCmark// 2. 否則 GCoff -> _GCmarktermination,這個時候就是主動 GCfunc gcStart(mode gcMode, forceTrigger bool) {    ...    //在后臺啟動 mark worker    if mode == gcBackgroundMode {        gcBgMarkStartWorkers()    }    ...    // Stop The World    systemstack(stopTheWorldWithSema)    ...    if mode == gcBackgroundMode {        // GC 開始前的準備工作
          //處理設(shè)置 GCPhase,setGCPhase 還會 開始寫屏障 setGCPhase(_GCmark)
          gcBgMarkPrepare() // Must happen before assist enable. gcMarkRootPrepare()
          // Mark all active tinyalloc blocks. Since we're // allocating from these, they need to be black like // other allocations. The alternative is to blacken // the tiny block on every allocation from it, which // would slow down the tiny allocator. gcMarkTinyAllocs()
          // Start The World systemstack(startTheWorldWithSema) } else { ... }}


          • Mark


          func gcStart(mode gcMode, forceTrigger bool) {    ...    //在后臺啟動 mark worker    if mode == gcBackgroundMode {        gcBgMarkStartWorkers()    }}
          func gcBgMarkStartWorkers() { // Background marking is performed by per-P G's. Ensure that // each P has a background GC G. for _, p := range &allp { if p == nil || p.status == _Pdead { break } if p.gcBgMarkWorker == 0 { go gcBgMarkWorker(p) notetsleepg(&work.bgMarkReady, -1) noteclear(&work.bgMarkReady) } }}// gcBgMarkWorker 是一直在后臺運行的,大部分時候是休眠狀態(tài),通過 gcController 來調(diào)度func gcBgMarkWorker(_p_ *p) { for { // 將當前 goroutine 休眠,直到滿足某些條件 gopark(...) ... // mark 過程 systemstack(func() { // Mark our goroutine preemptible so its stack // can be scanned. This lets two mark workers // scan each other (otherwise, they would // deadlock). We must not modify anything on // the G stack. However, stack shrinking is // disabled for mark workers, so it is safe to // read from the G stack. casgstatus(gp, _Grunning, _Gwaiting) switch _p_.gcMarkWorkerMode { default: throw("gcBgMarkWorker: unexpected gcMarkWorkerMode") case gcMarkWorkerDedicatedMode: gcDrain(&_p_.gcw, gcDrainNoBlock|gcDrainFlushBgCredit) case gcMarkWorkerFractionalMode: gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit) case gcMarkWorkerIdleMode: gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit) } casgstatus(gp, _Gwaiting, _Grunning) }) ... }}


          Mark階段的標記代碼主要在函數(shù)gcDrain()中實現(xiàn)


          // gcDrain scans roots and objects in work buffers, blackening grey// objects until all roots and work buffers have been drained.func gcDrain(gcw *gcWork, flags gcDrainFlags) {    ...    // Drain root marking jobs.    if work.markrootNext < work.markrootJobs {        for !(preemptible && gp.preempt) {            job := atomic.Xadd(&work.markrootNext, +1) - 1            if job >= work.markrootJobs {                break            }            markroot(gcw, job)            if idle && pollWork() {                goto done            }        }    }
          // 處理 heap 標記 // Drain heap marking jobs. for !(preemptible && gp.preempt) { ... //從灰色列隊中取出對象 var b uintptr if blocking { b = gcw.get() } else { b = gcw.tryGetFast() if b == 0 { b = gcw.tryGet() } } if b == 0 { // work barrier reached or tryGet failed. break } //掃描灰色對象的引用對象,標記為灰色,入灰色隊列 scanobject(b, gcw) }}


          • Sweep


          func gcSweep(mode gcMode) {    ...    //阻塞式    if !_ConcurrentSweep || mode == gcForceBlockMode {        // Special case synchronous sweep.        ...        // Sweep all spans eagerly.        for sweepone() != ^uintptr(0) {            sweep.npausesweep++        }        // Do an additional mProf_GC, because all 'free' events are now real as well.        mProf_GC()        mProf_GC()        return    }
          // 并行式 // Background sweep. lock(&sweep.lock) if sweep.parked { sweep.parked = false ready(sweep.g, 0, true) } unlock(&sweep.lock)}


          對于并行式清掃,在GC初始化的時候就會啟動 bgsweep(),然后在后臺一直循環(huán)


          func bgsweep(c chan int) {    sweep.g = getg()
          lock(&sweep.lock) sweep.parked = true c <- 1 goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1)
          for { for gosweepone() != ^uintptr(0) { sweep.nbgsweep++ Gosched() } lock(&sweep.lock) if !gosweepdone() { // This can happen if a GC runs between // gosweepone returning ^0 above // and the lock being acquired. unlock(&sweep.lock) continue } sweep.parked = true goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1) }}
          func gosweepone() uintptr { var ret uintptr systemstack(func() { ret = sweepone() }) return ret}


          不管是阻塞式還是并行式,最終都會調(diào)用sweepone()。上面說過go內(nèi)存管理都是基于span的,mheap_是一個全局的變量,所有分配的對象都會記錄在mheap_中。在標記的時候,我們只要找到對對象對應(yīng)的span進行標記,清掃的時候掃描span,沒有標記的span就可以回收了。


          // sweeps one span// returns number of pages returned to heap, or ^uintptr(0) if there is nothing to sweepfunc sweepone() uintptr {    ...    for {        s := mheap_.sweepSpans[1-sg/2%2].pop()        ...        if !s.sweep(false) {            // Span is still in-use, so this returned no            // pages to the heap and the span needs to            // move to the swept in-use list.            npages = 0        }    }}
          // Sweep frees or collects finalizers for blocks not marked in the mark phase.// It clears the mark bits in preparation for the next GC round.// Returns true if the span was returned to heap.// If preserve=true, don't return it to heap nor relink in MCentral lists;// caller takes care of it.func (s *mspan) sweep(preserve bool) bool { ...}


          參考資料:

          1.《go語言設(shè)計與實現(xiàn)》


           作者簡介


          冷易

          騰訊后臺開發(fā)工程師

          騰訊后臺開發(fā)工程師,目前負責騰訊醫(yī)藥平臺后端開發(fā)工作,精通java、go底層設(shè)計架構(gòu),有豐富的高并發(fā)性能優(yōu)化,分布式系統(tǒng)開發(fā)經(jīng)驗。



           推薦閱讀


          5G正當時,無人駕駛未來將駛向何方?

          C++異步:structured concurrency實現(xiàn)解析!

          圖文并茂!帶你深度解析Kubernetes

          萬卷共知,一書一頁總關(guān)情,TVP讀書會帶你突圍閱讀迷障!



          溫馨提示:因公眾號平臺更改了推送規(guī)則,公眾號推送的文章文末需要點一下“贊”“在看”,新的文章才會第一時間出現(xiàn)在你的訂閱列表里噢~

          瀏覽 52
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天堂av播放 | 亚洲成v| 成人俺去拉 | 国产人妻性生交大片 | 免费色情电影链接 |