国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

ThreadLocal源碼解讀:內存泄露問題分析

開發 前端
ThreadLocal 優勢是無鎖化提升并發性能和簡化變量的傳遞邏輯。在實際業務中使用 ThreadLocal 類時應該在恰當位置調用 remove 方法顯式移除值。盡可能的避免觸發 ThreadLocal 清理過時 Entry 的邏輯,從而提高 ThreadLocal 性能。

引言

大家好,我們又見面了。今天依舊是結合源碼為大家分享個人對于 ThreadLocal 的一些理解。今天是第二期,將著重分析 ThreadLocal 內存泄露問題,文章后半篇含重點源碼精講,不容錯過。廢話不多說,坐穩發車咯!

上期回顧

在上一期,我通過閱讀源碼的方式帶大家學習了 ThreadLocal 常用的 API,并在這個過程中深度剖析了 ThreadLocal 的存儲結構。

下面通過我剛剛繪制的一張圖來為大家回顧一下上一節所闡述的存儲結構。

圖片圖片

如果大家對這個存儲結構有所疑惑,可以回看第一期《ThreadLocal 源碼解讀:初識 ThreadLocal》。

引用類型

在 Java 中有四種常用的引用類型,依照引用的強弱排序依次是:強引用、軟引用、弱引用、幻引用(虛引用)。

其中強引用就是我們通常所說的引用,所以這里 Java 并沒有單獨定義一個引用類來表示,并且強引用存在時被引用對象一定不會被垃圾回收器回收。

軟引用在 Java 中使用 SoftReference 類表示,被軟引用單獨引用的對象當系統內存不足的時候會被垃圾回收器所回收,也就是說在發生 OOM 前將會回收軟引用對象,試圖避免 OOM 的發生。

弱引用在 Java 中使用 WeakReference 類表示,被弱引用單獨引用的對象在發生任意垃圾回收時,無論內存是否充足都將會被回收。

幻引用在 Java 中使用 PhantomReference 類表示,是最弱的引用類型,主要用于跟蹤對象是否被垃圾回收,并且幻引用的 get 方法永遠返回 null。

上述三種引用類均繼承 Reference 類,Reference 類通過泛型成員變量 referent 存儲引用對象,并提供了 get 方法用于獲取引用對象,提供 clear 方法用于清理引用對象。

圖片圖片

內存泄露問題剖析

拋出觀點

在探究 ThreadLocal 內存泄漏問題之前,我們首先要明確一下,什么是內存泄露?

這里我們直接引用百度百科提供的答案。


內存泄漏(Memory Leak)是指程序中已動態分配的堆內存由于某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果。

那么 ThreadLocal 在使用過程中存在泄露問題嗎?答案是肯定的,但是要糾正一點,ThreadLocal 的內存泄露問題與 ThreadLocal 對象的弱引用并無關系!這一點在網上可能存在著誤導信息,下面將會為大家論證我的觀點。

推理驗證

首先我們來看一下 Entry 類的定義。

圖片圖片

可以看到 Entry 類繼承了 WeakReference 類,并且將弱引用的 ThreadLocal 對象作為了 ThreadLocalMap 的鍵。

查閱過 ThreadLocal 相關博客的小伙伴可能看過下面種說法。

--start--

ThreadLocal 變量如果未被正確清理,可能會導致內存泄露。因為 ThreadLocalMap 的鍵是 ThreadLocal 對象的弱引用,值是強引用。

當 ThreadLocal 對象不再被外部引用時,ThreadLocalMap 中的鍵會被垃圾回收,但值仍然存在,導致無法被垃圾回收,從而引發內存泄露。

--end--

在這個過程中的確存在內存泄露問題,但這和 ThreadLocalMap 的 key 設計并無關系,這是編寫程序的不嚴謹導致的問題,在使用完 ThreadLocal 后,沒有調用 remove 方法顯式移除值。

任何一個 Java 對象都可能因為使用不當導致內存泄漏,比如聲明了一個類的對象用作成員變量,但是卻從未在代碼里使用過這個成員變量(如下圖),這也是內存泄漏。

圖片圖片

所以并不是因為 ThreadLocalMap 的 key 的弱引用設計,才導致的內存泄露問題。恰恰相反,ThreadLocalMap 的 key 的弱引用設計一定程度上減少了內存泄露的損失。

首先當 ThreadLocalMap 的 key 不再被外部所引用時,ThreadLocal 對象以及通過 ThreadLocal 存儲在 ThreadLocalMap 中的值已經無法在其他地方被獲取,已經發生了內存泄漏。那么這時候垃圾回收器回收掉 ThreadLocalMap 的 key,恰恰為我們釋放了一部分已經泄露的內存。

這時候有人可能會有疑問,那 value 就不管了嗎?當然不是!雖然這是開發者 API 使用不當留下的坑,但是設計者也為我們填了這個坑。

注意看 Entry 類的注釋,這里我直接為大家翻譯出來。

圖片圖片

可以看到官方將 key 為 null 的 Entry 對象稱之為“陳舊條目”,也就是我上一期文章所說的過時 Entry,并且官方指出這些過時 Entry 可以從 ThreadLocalMap 中刪除。

那么不難猜到,ThreadLocal 在設計時一定在某些時機對這些過時 Entry 進行了清理,盡可能的釋放泄露的內存。

這里先給出大家結論,然后我們再去論證:ThreadLocal在調用set(),get(),remove()方法的時候,都可能觸發清理過時Entry的邏輯。。

清理方法源碼剖析

expungeStaleEntry 方法

在討論到 ThreadLocalMap 過時 Entry 清理的問題,就繞不開 ThreadLocalMap 的 expungeStaleEntry 這個方法,見名之意這個方法用于刪除過時 Entry。

下面我將采用在源碼中添加注釋的方式剖析這個方法。

/**
 * Expunge a stale entry by rehashing any possibly colliding entries
 * lying between staleSlot and the next null slot.  This also expunges
 * any other stale entries encountered before the trailing null.  See
 * Knuth, Section 6.4
 *
 * @param staleSlot index of slot known to have null key
 * @return the index of the next null slot after staleSlot
 * (all between staleSlot and this slot will have been checked
 * for expunging).
 */
private int expungeStaleEntry(int staleSlot) {
    // 入參 staleSlot: 待清理位置下標
    
    // 獲取 ThreadLocalMap 中的 Entry 數組。
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    // len 為當前 Entry 數組容量。
    int len = tab.length;

    // expunge entry at staleSlot
    // 清除當前 staleSlot 位置的過時 Entry。
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    // 元素數量減一。
    size--;

    // Rehash until we encounter null
    // 因為 ThreadLocalMap 解決哈希沖突采用的是線性探測法,如將當前下標位置賦值為 null ,但不對后續 Entry
    // 元素進行 rehash 操作,就可能導致存在哈希沖突的后置元素無法被探測到。所以將當前元素清理后需要
    // 對后續元素進行 rehash 操作,直到遇到下一個為 null 的元素。
    ThreadLocal.ThreadLocalMap.Entry e;
    int i;
    // nextIndex 用于向后遞增索引 ((i + 1 < len) ? i + 1 : 0)
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // 此時 Entry 不為 null,key 為 null,Entry 為過時 Entry 需清理掉。
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 此時為有效 Entry,需要進行 rehash 操作重新定位。
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                // 進入到這個分支說明 rehash 后,新的下標與原來下標不等。
                // 將當前下標位置清空。
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                // 從 h 位置開始遍歷,直到遇到為 null 的元素,并將 rehash 后的元素插入到該位置。
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    
    // i 為 staleSlot 后的第一個 null 元素的位置下標。
    return i;
}

了解了 expungeStaleEntry 方法的內部實現細節之后就可以把這個方法當做一個黑盒,作用是清理傳入下標位置的過時 Entry,入參為一個過時 Entry 的下標。

cleanSomeSlots 方法

有了 expungeStaleEntry 方法的基礎我們就可以攻克下一個和清理過時 Entry 相關的方法:cleanSomeSlots。見名之意,這個方法的作用是清除一些過時 Entry。

同樣采用在源碼中添加注釋的方式剖析這個方法。

/**
 * Heuristically scan some cells looking for stale entries.
 * This is invoked when either a new element is added, or
 * another stale one has been expunged. It performs a
 * logarithmic number of scans, as a balance between no
 * scanning (fast but retains garbage) and a number of scans
 * proportional to number of elements, that would find all
 * garbage but would cause some insertions to take O(n) time.
 *
 * @param i a position known NOT to hold a stale entry. The
 * scan starts at the element after i.
 *
 * @param n scan control: {@code log2(n)} cells are scanned,
 * unless a stale entry is found, in which case
 * {@code log2(table.length)-1} additional cells are scanned.
 * When called from insertions, this parameter is the number
 * of elements, but when from replaceStaleEntry, it is the
 * table length. (Note: all this could be changed to be either
 * more or less aggressive by weighting n instead of just
 * using straight log n. But this version is simple, fast, and
 * seems to work well.)
 *
 * @return true if any stale entries have been removed.
 */
private boolean cleanSomeSlots(int i, int n) {
    // 入參 i: 一個已知不為過時 Entry 的下標。掃描從 i 之后的位置開始。
    // 入參 n: 掃描次數控制值
    
    // 是否清理了任意過時 Entry 標志,
    // 為 false 代表本次方法調用未能清理任何過時 Entry,為 true 代表本次方法調用至少清理了一個過時 Entry。
    boolean removed = false;
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    // doWhile 循環,至少執行一次。
    do {
        i = nextIndex(i, len);
        ThreadLocal.ThreadLocalMap.Entry e = tab[i];
        if (e != null && e.get() == null) {
            // 進入到當前分支說明當前 Entry 為過時 Entry。
            // 將 n 置為數組容量,將當前循環遍歷次數進行追增(n 變大了)。
            n = len;
            // 將標志置為 true,證明本次方法調用并不是無功而返。
            removed = true;
            // 調用清理過時 Entry 方法,并將 expungeStaleEntry 方法返回的 null 元素的下標賦值給 i,
            // 在這之間的下標都在 expungeStaleEntry 方法中進行了清理,所以這里直接跳過避免重復操作。
            i = expungeStaleEntry(i);
        }
        // >>>= 無符號右移并賦值,相當于除以 2 操作。
    } while ( (n >>>= 1) != 0);
    // 返回本次是否至少清理了一個過時 Entry。
    return removed;
}

cleanSomeSlots 方法在 set 和 remove 方法調用中會被調用到,這個方法在完全不掃描以及全量掃描中做了一個平衡,采用以對數的方式進行掃描,并且如果發現了過時 Entry 則會再追增對數次掃描,使得在保證 set 方法和 remove 方法的執行效率的情況下一定程度上清理了過時 Entry。

replaceStaleEntry 方法

下面我們來看一下最后一個與清理過時 key 有關的方法:replaceStaleEntry,通過方法名我們可以推測出這個方法的作用是替換過時條目,那么用什么替換呢,是 set 方法傳過來的 Entry。

同樣采用在源碼中添加注釋的方式剖析這個方法,這個方法有些許難度,如果大家不理解,可以多閱讀幾遍。

/**
 * Replace a stale entry encountered during a set operation
 * with an entry for the specified key.  The value passed in
 * the value parameter is stored in the entry, whether or not
 * an entry already exists for the specified key.
 *
 * As a side effect, this method expunges all stale entries in the
 * "run" containing the stale entry.  (A run is a sequence of entries
 * between two null slots.)
 *
 * @param  key the key
 * @param  value the value to be associated with key
 * @param  staleSlot index of the first stale entry encountered while
 *         searching for key.
 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    // 入參 key: set 操作傳過來的 key
    // 入參 value: set 操作傳過來的 value,與參數 key 相關聯
    // 入參 staleSlot: 待替換的過時 Entry 的下標
    
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    ThreadLocal.ThreadLocalMap.Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    // slotToExpunge 變量目標是存儲當前區間段(兩個 null 元素之間),第一個過時 Entry 的下標。
    // 將入參的過時 Entry 下標賦值給 slotToExpunge。
    int slotToExpunge = staleSlot;
    // 這里需要格外注意一下,這里并不是遞增下標,而是對下標進行遞減。
    // prevIndex ((i - 1 >= 0) ? i - 1 : len - 1)。
    // 向前進行遍歷直到遇到為 null 的元素。
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            // 將遍歷過程中過時 Entry 的下標賦值給 slotToExpunge 變量。
            // 經過當前遍歷邏輯,slotToExpunge 將存儲兩個 null 元素之間第一個過時 key 的下標。
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    // 這里是由入參的過時 Entry 下標開始向后遍歷,直到遇到 null 元素。
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
            // 進入當前分支表示在遍歷的過程中找到了被 set 的 Entry 對象的本體。
            // 將和 key 關聯的新 value 值賦值給本體。
            e.value = value;
            // 操作一
            // 將 Entry 對象本體和入參 staleSlot 位置的過時 Entry 進行交換,
            // 結果是set操作的 key 與 value,無論之前本體存儲在哪里,
            // 最終都會存儲在入參的 staleSlot 下標,符合方法名中的 replace 含義。
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                // 如果當前區間段第一個過時 Entry 下標仍是 staleSlot 下標,
                // 那么需要將當前 i 下標賦值給 slotToExpunge ,因為 staleSlot 下標已經存儲了 set 操作的 Entry 對象,
                // 導致當前 i 下標變成了第一個過時 Entry 的下標。
                slotToExpunge = i;
            // 先調用 expungeStaleEntry 方法清除 slotToExpunge 下標的過時 Entry,
            // 再從 expungeStaleEntry 方法返回的 null 元素的下標開始執行 cleanSomeSlots 方法。
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            // 已經完成替換過時條目操作,退出當前方法。
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)
            // 進入當前分支說明當前 Entry 是過時 Entry。
            // 如果當前區間段第一個過時 Entry 下標仍是入參的 staleSlot 下標,
            // 則需要將當前位置下標賦值給 slotToExpunge,因為最終當前位置的過時 Entry 將是
            // 當前區間段的第一個過時 Entry。因為 staleSlot 下標位置的過時 Entry 在之后的邏輯
            // 里要么被交換到當前下標之后(上文操作一),要么被新的 set 傳入的 Entry 覆蓋掉(下文操作二)。
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    // 操作二
    // 代碼執行到當前位置說明 set 的 key 與 value 是一個新的 Entry,在之前并不存在。
    // 以 set 方法傳入的 key 和 value 值 new 一個新的 Entry 對象,并覆蓋在入參的 staleSlot 下標處。
    tab[staleSlot].value = null;
    tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        // slotToExpunge 與 staleSlot 相等則說明當前區間段只有入參 staleSlot 位置有過時 Entry,
        // 并且該過時 Entry 已被覆蓋,所以無需清理,無需進入當前分支。
        
        // 進入當前分支說明當前區間段,除了被覆蓋的過時 Entry,至少還存在一個過時 Entry,
        // slotToExpunge 下標為第一個過時 Entry 的下標。
        // 先調用 expungeStaleEntry 方法清除 slotToExpunge 下標的過時 Entry,
        // 再從 expungeStaleEntry 方法返回的 null 元素的下標開始執行 cleanSomeSlots 方法。
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

在方法中 slotToExpunge 變量之所以始終要存儲當前區間段(兩個 null 元素之間)的第一個過時 Entry,是因為每當刪除一個過時 Entry 后都會對后續 Entry 進行 rehash 操作,如果清理的不是第一個過時 Entry,那么在后續其他邏輯觸發清理第一個過時 Entry 時還會將剛剛 rehash 過的元素再次 rehash 一遍,極大的影響效率。

至此在 ThreadLocalMap 中涉及清理過時Entry的三個方法都已剖析完畢,下面我們來羅列一下什么時候會觸發這三個方法。

清理方法調用梳理

為避免截圖過多影響閱讀體驗,這里將只粘出調用的起點,并給調用鏈路,大家后續可以自己在源碼中點一點。

get方法

調用鏈路:ThreadLocal#get->ThreadLocalMap#getEntry->ThreadLocalMap#getEntryAfterMiss->ThreadLocalMap#expungeStaleEntry

圖片圖片

set方法

調用鏈路 1:ThreadLocal#set->ThreadLocalMap#set->ThreadLocalMap#replaceStaleEntry

調用鏈路 2:ThreadLocal#set->ThreadLocalMap#set->ThreadLocalMap#cleanSomeSlots

調用鏈路 3:ThreadLocal#set->ThreadLocalMap#set->ThreadLocalMap#rehash->ThreadLocalMap#expungeStaleEntries->ThreadLocalMap#expungeStaleEntry

圖片圖片

remove方法

調用鏈路:ThreadLocal#remove->ThreadLocalMap#remove->ThreadLocalMap#expungeStaleEntry

圖片圖片

總結

通過兩期文章的深度剖析,大家應該對 ThreadLocal 的 API 使用以及內存泄露問題有了進一步的理解。

ThreadLocal 優勢是無鎖化提升并發性能和簡化變量的傳遞邏輯。

在實際業務中使用 ThreadLocal 類時應該在恰當位置調用 remove 方法顯式移除值。

盡可能的避免觸發 ThreadLocal 清理過時 Entry 的邏輯,從而提高 ThreadLocal 性能。

例如使用繼承的 ThreadLocal 類,并重寫 finalize 方法,確保 ThreadLocal 對象在被垃圾回收前,remove 方法會被調用。

責任編輯:武曉燕 來源: Java極客技術
相關推薦

2022-08-26 07:33:49

內存JVMEntry

2021-04-23 20:59:02

ThreadLocal內存

2024-10-28 08:15:32

2018-10-25 15:24:10

ThreadLocal內存泄漏Java

2022-10-18 08:38:16

內存泄漏線程

2017-01-11 14:02:32

JVM源碼內存

2024-06-24 08:11:37

2023-11-03 08:10:49

ThreadLoca內存泄露

2023-05-29 07:17:48

內存溢出場景

2013-12-23 09:25:21

2023-09-22 17:34:37

內存remove方法

2021-05-26 08:02:03

ThreadLocal多線程多線程并發安全

2021-05-10 11:55:57

ThreadLocal內存Java

2024-03-22 13:31:00

線程策略線程池

2020-06-23 09:48:09

Python開發內存

2025-10-15 00:26:20

2011-08-16 09:34:34

Nginx

2017-01-12 14:52:03

JVMFinalRefere源碼

2010-10-25 10:10:27

ibmdwJava

2010-05-31 16:53:21

Java
點贊
收藏

51CTO技術棧公眾號

2021中文字幕一区亚洲| 国内精品久久久久久久97牛牛| 国产精品色哟哟| 欧美一区2区三区4区公司二百| 国产精品777777在线播放| 一本到不卡精品视频在线观看 | 亚洲老头老太hd| 全网国产福利在线播放| 国产精品自拍在线| 国产偷国产偷亚洲高清97cao| 欧美色资源站| 久久久精品在线| 中文字幕一区久| 欧美一区二区三区人| 中文字幕av在线| 亚洲欧洲精品天堂一级| 国产美女主播在线播放| 久国产精品韩国三级视频| 国产亚洲福利社区| 国产成人三级| 隔壁老王国产在线精品| 91九色成人| 亚洲一区二区精品| 日本二三区不卡| 肥女人的一级毛片| 色综合久久久久久久久久久| 日本高清好狼色视频| 久久亚区不卡日本| 成人在线视频网| 日韩精品一区二区三区中文在线| 亚洲精品自产拍| 偷拍自拍在线| 欧美国产在线观看| 中文字幕在线亚洲三区| 亚洲手机在线| 久热在线中文字幕色999舞| 日韩三级成人| 国产91在线观看丝袜| 美国av一区二区三区| 91精品啪在线观看国产18| 日本乱人伦a精品| 国产精品网站在线看| 欧美精品videofree1080p| 亚州精品国产| 九九视频直播综合网| 精品视频一区二区三区在线观看 | 国产精品99久久久久| 日韩精品一线二线三线| 午夜一级久久| 麻豆成人小视频| 黄色日韩网站视频| 国产精品专区在线| 2024国产精品| ts人妖交友网站| 亚洲成人www| 番号在线播放| 欧美日韩激情一区二区三区| 黄色在线免费| 日韩精品久久久久久福利| 国产一二在线播放| 色哟哟亚洲精品一区二区| 激情小说一区| 99在线看视频| 精品一区二区在线免费观看| 免费无码毛片一区二三区| 国产精品国产a| 男人久久精品| 日韩国产欧美区| 51精品国产| 亚洲a中文字幕| 久久精品国产免费看久久精品| 黄网站欧美内射| 午夜av区久久| 黑人玩欧美人三根一起进| 久久精品国产亚洲一区二区| 精品成av人一区二区三区| 国产二区不卡| 国产精品123区| 色老板亚洲精品一区| 制服丝袜日韩国产| av在线国产精品| 亚洲在线视频观看| 国产成人在线视频免费播放| 丝袜美女写真福利视频| 日韩一区二区在线免费观看| 日韩精品视频一区二区三区| 成人蜜桃视频| 91网上在线视频| 国产在线观看免费| 日韩中文字幕国产| 亚洲激情久久| 国产极品粉嫩福利姬萌白酱 | 曰本一区二区| 成人做爽爽免费视频| 国产成人亚洲精品狼色在线| 国产精品粉嫩av| 精品国产乱码久久久久久久| 欧美成a人免费观看久久| 久久免费视频1| 久久一区二区三区四区| 999国产在线视频| 欧美国产日本高清在线 | 久草在线资源站资源站| 欧美激情视频一区二区三区不卡| 欧美在线免费| 国产精品亚洲二区在线观看| 欧美日韩国产影片| 亚洲高清999| 欧美日韩亚洲在线| 中文字幕永久在线不卡| 高清视频在线观看三级| 国产精品吴梦梦| 99久久综合99久久综合网站| 久久精品视频免费看| 91国产精品视频在线| 国产精品系列在线播放| 狠狠v欧美ⅴ日韩v亚洲v大胸| 久久国产精品影片| 捆绑紧缚一区二区三区视频| 天堂а√在线8种子蜜桃视频| 久久久极品av| 国产综合久久久久影院| 777电影在线观看| 国产美女精品视频免费观看| 久久久www成人免费无遮挡大片| 欧美女同一区| 国产91aaa| 亚洲亚洲精品在线观看| 伊人精品久久| 996这里只有精品| 日韩欧美一区在线观看| 欧美精品九九| 在线观看你懂| 久久久免费观看视频| eeuss鲁片一区二区三区在线观看| 怡红院在线播放| 成人羞羞视频免费| 黑人巨大精品欧美一区二区一视频 | 高清av一区| 精品综合久久久| 欧美日韩美女在线观看| 亚洲桃色综合影院| 免费在线观看毛片网站| 中文字幕日韩欧美精品在线观看| 噜噜噜在线观看免费视频日韩 | 欧美大片一区二区三区| 综合日韩在线| 亚洲福利二区| 国产精品一区二区三区毛片淫片| 亚洲欧洲色图综合| jizz性欧美23| 第四色婷婷基地| 欧美激情中文网| 欧美国产综合色视频| 日韩成人久久| 老熟妇仑乱视频一区二区| 欧美成人在线免费| 久久久久国色av免费看影院| 福利一区三区| 黄色一级一级片| 97超碰国产精品女人人人爽| 日韩一区日韩二区| 国内精品久久久久久99蜜桃| 中文字幕第5页| 91精品综合久久| 欧美久久一二区| 美洲天堂一区二卡三卡四卡视频| 国产丝袜精品丝袜| 麻豆传媒网站在线观看| 色妞欧美日韩在线| 国产清纯美女被跳蛋高潮一区二区久久w | 久久91在线| 国产私人影院| 国产精品视频最多的网站| 欧美日韩激情美女| 好看不卡的中文字幕| 91高清在线观看视频| 一区二区不卡视频| 日韩精品中文字幕在线一区| 激情另类小说区图片区视频区| 国产精品av一区二区三区 | 欧美日韩一区二区在线| 欧美在线首页| 欧美日韩在线资源| 在线免费观看一区二区三区| 永久免费看mv网站入口亚洲| 国产网红主播福利一区二区| 精品72久久久久中文字幕| 岛国最新视频免费在线观看| 亚洲精品自在在线观看| 日韩视频一区在线| 亚洲免费资源在线播放| 韩日精品在线| 欧美国产大片| 国产三级免费观看| 蜜桃免费一区二区三区| 中文字幕国内精品| 一区二区三区精品| 亚洲妇熟xxxx妇色黄| 国产真实老熟女无套内射|