Reference 、ReferenceQueue 詳解

ReferenceQueue

引用隊列,在檢測到適當(dāng)?shù)目傻竭_(dá)性更改后,垃圾回收器將已注冊的引用對象添加到該隊列中

實現(xiàn)了一個隊列的入隊(enqueue)和出隊(poll還有remove)操作,內(nèi)部元素就是泛型的Reference,并且Queue的實現(xiàn),是由Reference自身的鏈表結(jié)構(gòu)( 單向循環(huán)鏈表 )所實現(xiàn)的。

ReferenceQueue名義上是一個隊列,但實際內(nèi)部并非有實際的存儲結(jié)構(gòu),它的存儲是依賴于內(nèi)部節(jié)點之間的關(guān)系來表達(dá)。可以理解為queue是一個類似于鏈表的結(jié)構(gòu),這里的節(jié)點其實就是reference本身。可以理解為queue為一個鏈表的容器,其自己僅存儲當(dāng)前的head節(jié)點,而后面的節(jié)點由每個reference節(jié)點自己通過next來保持即可。

  • 屬性
    head:始終保存當(dāng)前隊列中最新要被處理的節(jié)點,可以認(rèn)為queue為一個后進(jìn)先出的隊列。當(dāng)新的節(jié)點進(jìn)入時,采取以下的邏輯:
            r.next = (head == null) ? r : head;
            head = r;

然后,在獲取的時候,采取相應(yīng)的邏輯:

Reference<? extends T> r = head;
        if (r != null) {
            head = (r.next == r) ?
                null :
                r.next; // Unchecked due to the next field having a raw type in Reference
            r.queue = NULL;
            r.next = r;
  • 方法
    enqueue():待處理引用入隊
    boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
        synchronized (lock) {
            // Check that since getting the lock this reference hasn't already been
            // enqueued (and even then removed)
            ReferenceQueue<?> queue = r.queue;
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            assert queue == this;
            r.queue = ENQUEUED;
            r.next = (head == null) ? r : head;
            head = r;
            queueLength++;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(1);
            }
            lock.notifyAll(); // ①
            return true;
        }
    }

① lock.notifyAll(); ??通知外部程序之前阻塞在當(dāng)前隊列之上的情況。( 即之前一直沒有拿到待處理的對象,如ReferenceQueue的remove()方法 )

Reference

java.lang.ref.Reference 為 軟(soft)引用、弱(weak)引用、虛(phantom)引用的父類。

因為Reference對象和垃圾回收密切配合實現(xiàn),該類可能不能被直接子類化。
可以理解為Reference的直接子類都是由jvm定制化處理的,因此在代碼中直接繼承于Reference類型沒有任何作用。但可以繼承jvm定制的Reference的子類。
例如:Cleaner 繼承了 PhantomReference
public class Cleaner extends PhantomReference<Object>

構(gòu)造函數(shù)

其內(nèi)部提供2個構(gòu)造函數(shù),一個帶queue,一個不帶queue。其中queue的意義在于,我們可以在外部對這個queue進(jìn)行監(jiān)控。即如果有對象即將被回收,那么相應(yīng)的reference對象就會被放到這個queue里。我們拿到reference,就可以再作一些事務(wù)。

而如果不帶的話,就只有不斷地輪詢reference對象,通過判斷里面的get是否返回null( phantomReference對象不能這樣作,其get始終返回null,因此它只有帶queue的構(gòu)造函數(shù) )。這兩種方法均有相應(yīng)的使用場景,取決于實際的應(yīng)用。如weakHashMap中就選擇去查詢queue的數(shù)據(jù),來判定是否有對象將被回收。而ThreadLocalMap,則采用判斷get()是否為null來作處理。

    /* -- Constructors -- */

    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

如果我們在創(chuàng)建一個引用對象時,指定了ReferenceQueue,那么當(dāng)引用對象指向的對象達(dá)到合適的狀態(tài)(根據(jù)引用類型不同而不同)時,GC 會把引用對象本身添加到這個隊列中,方便我們處理它,因為“引用對象指向的對象 GC 會自動清理,但是引用對象本身也是對象(是對象就占用一定資源),所以需要我們自己清理。”

Reference鏈表結(jié)構(gòu)內(nèi)部主要的成員有

① pending 和 discovered

    /* List of References waiting to be enqueued.  The collector adds
     * References to this list, while the Reference-handler thread removes
     * them.  This list is protected by the above lock object. The
     * list uses the discovered field to link its elements.
     */
    private static Reference<Object> pending = null;

    /* When active:   next element in a discovered reference list maintained by GC (or this if last)
     *     pending:   next element in the pending list (or null if last)
     *   otherwise:   NULL
     */
    transient private Reference<T> discovered;  /* used by VM */

可以理解為jvm在gc時會將要處理的對象放到這個靜態(tài)字段上面。同時,另一個字段discovered:表示要處理的對象的下一個對象。即可以理解要處理的對象也是一個鏈表,通過discovered進(jìn)行排隊,這邊只需要不停地拿到pending,然后再通過discovered不斷地拿到下一個對象賦值給pending即可,直到取到了最有一個。因為這個pending對象,兩個線程都可能訪問,因此需要加鎖處理。

if (pending != null) {
     r = pending;
    // 'instanceof' might throw OutOfMemoryError sometimes
    // so do this before un-linking 'r' from the 'pending' chain...
    c = r instanceof Cleaner ? (Cleaner) r : null;
    // unlink 'r' from 'pending' chain
    pending = r.discovered;
    r.discovered = null;

② referent
private T referent; /* Treated specially by GC */
??referent字段由GC特別處理
referent:表示其引用的對象,即我們在構(gòu)造的時候需要被包裝在其中的對象。對象即將被回收的定義:此對象除了被reference引用之外沒有其它引用了( 并非確實沒有被引用,而是gcRoot可達(dá)性不可達(dá),以避免循環(huán)引用的問題 )。如果一旦被回收,則會直接置為null,而外部程序可通過引用對象本身( 而不是referent,這里是reference#get() )了解到回收行為的產(chǎn)生( PhntomReference除外 )。

③ next

    /* When active:   NULL
     *     pending:   this
     *    Enqueued:   next reference in queue (or this if last)
     *    Inactive:   this
     */
    @SuppressWarnings("rawtypes")
    Reference next;

next:即描述當(dāng)前引用節(jié)點所存儲的下一個即將被處理的節(jié)點。但next僅在放到queue中才會有意義( 因為,只有在enqueue的時候,會將next設(shè)置為下一個要處理的Reference對象 )。為了描述相應(yīng)的狀態(tài)值,在放到隊列當(dāng)中后,其queue就不會再引用這個隊列了。而是引用一個特殊的ENQUEUED。因為已經(jīng)放到隊列當(dāng)中,并且不會再次放到隊列當(dāng)中。

④ discovered

    /* When active:   next element in a discovered reference list maintained by GC (or this if last)
     *     pending:   next element in the pending list (or null if last)
     *   otherwise:   NULL
     */
    transient private Reference<T> discovered;  /* used by VM */

??被VM使用
discovered:當(dāng)處于active狀態(tài)時:discoverd reference的下一個元素是由GC操縱的( 如果是最后一個了則為this );當(dāng)處于pending狀態(tài):discovered為pending集合中的下一個元素( 如果是最后一個了則為null );其他狀態(tài):discovered為null

⑤ lock

    static private class Lock { }
    private static Lock lock = new Lock();

lock:在垃圾收集中用于同步的對象。收集器必須獲取該鎖在每次收集周期開始時。因此這是至關(guān)重要的:任何持有該鎖的代碼應(yīng)該盡快完成,不分配新對象,并且避免調(diào)用用戶代碼。

⑥ pending

    /* List of References waiting to be enqueued.  The collector adds
     * References to this list, while the Reference-handler thread removes
     * them.  This list is protected by the above lock object. The
     * list uses the discovered field to link its elements.
     */
    private static Reference<Object> pending = null;

pending:等待被入隊的引用列表。收集器會添加引用到這個列表,直到Reference-handler線程移除了它們。這個列表被上面的lock對象保護(hù)。這個列表使用discovered字段來連接它自己的元素( 即pending的下一個元素就是discovered對象 )。

⑦ queue
volatile ReferenceQueue<? super T> queue;
queue:是對象即將被回收時所要通知的隊列。當(dāng)對象即被回收時,整個reference對象( 而不是被回收的對象 )會被放到queue里面,然后外部程序即可通過監(jiān)控這個queue拿到相應(yīng)的數(shù)據(jù)了。
這里的queue( 即,ReferenceQueue對象 )名義上是一個隊列,但實際內(nèi)部并非有實際的存儲結(jié)構(gòu),它的存儲是依賴于內(nèi)部節(jié)點之間的關(guān)系來表達(dá)。可以理解為queue是一個類似于鏈表的結(jié)構(gòu),這里的節(jié)點其實就是reference本身。可以理解為queue為一個鏈表的容器,其自己僅存儲當(dāng)前的head節(jié)點,而后面的節(jié)點由每個reference節(jié)點自己通過next來保持即可。

  • Reference 實例( 即Reference中的真是引用對象referent )的4中可能的內(nèi)部狀態(tài)值
    Queue的另一個作用是可以區(qū)分不同狀態(tài)的Reference。Reference有4種狀態(tài),不同狀態(tài)的reference其queue也不同:
    • Active:新創(chuàng)建的引用對象都是這個狀態(tài),在 GC 檢測到引用對象已經(jīng)到達(dá)合適的reachability時,GC 會根據(jù)引用對象是否在創(chuàng)建時制定ReferenceQueue參數(shù)進(jìn)行狀態(tài)轉(zhuǎn)移,如果指定了,那么轉(zhuǎn)移到Pending,如果沒指定,轉(zhuǎn)移到Inactive。
    • Pending:pending-Reference列表中的引用都是這個狀態(tài),它們等著被內(nèi)部線程ReferenceHandler處理入隊(會調(diào)用ReferenceQueue.enqueue方法)。沒有注冊的實例不會進(jìn)入這個狀態(tài)。
    • Enqueued:相應(yīng)的對象已經(jīng)為待回收,并且相應(yīng)的引用對象已經(jīng)放到queue當(dāng)中了。準(zhǔn)備由外部線程來詢問queue獲取相應(yīng)的數(shù)據(jù)。調(diào)用ReferenceQueue.enqueued方法后的Reference處于這個狀態(tài)中。當(dāng)Reference實例從它的ReferenceQueue移除后,它將成為Inactive。沒有注冊的實例不會進(jìn)入這個狀態(tài)。
    • Inactive:即此對象已經(jīng)由外部從queue中獲取到,并且已經(jīng)處理掉了。即意味著此引用對象可以被回收,并且對內(nèi)部封裝的對象也可以被回收掉了( 實際的回收運行取決于clear動作是否被調(diào)用 )。可以理解為進(jìn)入到此狀態(tài)的肯定是應(yīng)該被回收掉的。一旦一個Reference實例變?yōu)榱薎nactive,它的狀態(tài)將不會再改變。



jvm并不需要定義狀態(tài)值來判斷相應(yīng)引用的狀態(tài)處于哪個狀態(tài),只需要通過計算next和queue即可進(jìn)行判斷。

  • Active:queue為創(chuàng)建一個Reference對象時傳入的ReferenceQueue對象;如果ReferenceQueue對象為空或者沒有傳入ReferenceQueue對象,則為ReferenceQueue.NULL;next==null;
  • Pending:queue為初始化時傳入ReferenceQueue對象;next==this(由jvm設(shè)置);
  • Enqueue:當(dāng)queue!=null && queue != ENQUEUED 時;設(shè)置queue為ENQUEUED;next為下一個要處理的reference對象,或者若為最后一個了next==this;
  • Inactive:queue = ReferenceQueue.NULL; next = this.

通過這個組合,收集器只需要檢測next屬性為了決定是否一個Reference實例需要特殊的處理:如果next==null,則實例是active;如果next!=null,為了確保并發(fā)收集器能夠發(fā)現(xiàn)active的Reference對象,而不會影響可能將enqueue()方法應(yīng)用于這些對象的應(yīng)用程序線程,收集器應(yīng)通過discovered字段鏈接發(fā)現(xiàn)的對象。discovered字段也用于鏈接pending列表中的引用對象。

??外部從queue中獲取Reference

  • WeakReference對象進(jìn)入到queue之后,相應(yīng)的referent為null。
  • SoftReference對象,如果對象在內(nèi)存足夠時,不會進(jìn)入到queue,自然相應(yīng)的referent不會為null。如果需要被處理( 內(nèi)存不夠或其它策略 ),則置相應(yīng)的referent為null,然后進(jìn)入到queue。通過debug發(fā)現(xiàn),SoftReference是pending狀態(tài)時,referent就已經(jīng)是null了,說明此事referent已經(jīng)被GC回收了。
  • FinalReference對象,因為需要調(diào)用其finalize對象,因此其reference即使入queue,其referent也不會為null,即不會clear掉。
  • PhantomReference對象,因為本身get實現(xiàn)為返回null。因此clear的作用不是很大。因為不管enqueue還是沒有,都不會清除掉。

Q:??如果PhantomReference對象不管enqueue還是沒有,都不會清除掉reference對象,那么怎么辦?這個reference對象不就一直存在這了??而且JVM是會直接通過字段操作清除相應(yīng)引用的,那么是不是JVM已經(jīng)釋放了系統(tǒng)底層資源,但java代碼中該引用還未置null??
A:不會的,雖然PhantomReference有時候不會調(diào)用clear,如Cleaner對象 。但Cleaner的clean()方法只調(diào)用了remove(this),這樣當(dāng)clean()執(zhí)行完后,Cleaner就是一個無引用指向的對象了,也就是可被GC回收的對象。

active ——> pending :Reference#tryHandlePending
pending ——> enqueue :ReferenceQueue#enqueue
enqueue ——> inactive :Reference#clear



重要方法

① clear()

    /**
     * Clears this reference object.  Invoking this method will not cause this
     * object to be enqueued.
     *
     * <p> This method is invoked only by Java code; when the garbage collector
     * clears references it does so directly, without invoking this method.
     */
    public void clear() {
        this.referent = null;
    }

調(diào)用此方法不會導(dǎo)致此對象入隊。此方法僅由Java代碼調(diào)用;當(dāng)垃圾收集器清除引用時,它直接執(zhí)行,而不調(diào)用此方法。
clear的語義就是將referent置null。
清除引用對象所引用的原對象,這樣通過get()方法就不能再訪問到原對象了( PhantomReference除外 )。從相應(yīng)的設(shè)計思路來說,既然都進(jìn)入到queue對象里面,就表示相應(yīng)的對象需要被回收了,因為沒有再訪問原對象的必要。此方法不會由JVM調(diào)用,而JVM是直接通過字段操作清除相應(yīng)的引用,其具體實現(xiàn)與當(dāng)前方法相一致。

② ReferenceHandler線程

    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();

        // provide access in SharedSecrets
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }

其優(yōu)先級最高,可以理解為需要不斷地處理引用對象。

    private static class ReferenceHandler extends Thread {

        private static void ensureClassInitialized(Class<?> clazz) {
            try {
                Class.forName(clazz.getName(), true, clazz.getClassLoader());
            } catch (ClassNotFoundException e) {
                throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
            }
        }

        static {
            // pre-load and initialize InterruptedException and Cleaner classes
            // so that we don't get into trouble later in the run loop if there's
            // memory shortage while loading/initializing them lazily.
            ensureClassInitialized(InterruptedException.class);
            ensureClassInitialized(Cleaner.class);
        }

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            while (true) {
                tryHandlePending(true);
            }
        }
    }

③ tryHandlePending()

    /**
     * Try handle pending {@link Reference} if there is one.<p>
     * Return {@code true} as a hint that there might be another
     * {@link Reference} pending or {@code false} when there are no more pending
     * {@link Reference}s at the moment and the program can do some other
     * useful work instead of looping.
     *
     * @param waitForNotify if {@code true} and there was no pending
     *                      {@link Reference}, wait until notified from VM
     *                      or interrupted; if {@code false}, return immediately
     *                      when there is no pending {@link Reference}.
     * @return {@code true} if there was a {@link Reference} pending and it
     *         was processed, or we waited for notification and either got it
     *         or thread was interrupted before being notified;
     *         {@code false} otherwise.
     */
    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    // 'instanceof' might throw OutOfMemoryError sometimes
                    // so do this before un-linking 'r' from the 'pending' chain...
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // unlink 'r' from 'pending' chain
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // The waiting on the lock may cause an OutOfMemoryError
                    // because it may try to allocate exception objects.
                    if (waitForNotify) {
                        lock.wait();
                    }
                    // retry if waited
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            // Give other threads CPU time so they hopefully drop some live references
            // and GC reclaims some space.
            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
            // persistently throws OOME for some time...
            Thread.yield();
            // retry
            return true;
        } catch (InterruptedException x) {
            // retry
            return true;
        }

        // Fast path for cleaners
        if (c != null) {
            c.clean();
            return true;
        }

        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

這個線程在Reference類的static構(gòu)造塊中啟動,并且被設(shè)置為高優(yōu)先級和daemon狀態(tài)。此線程要做的事情,是不斷的檢查pending 是否為null,如果pending不為null,則將pending進(jìn)行enqueue,否則線程進(jìn)入wait狀態(tài)。

由此可見,pending是由jvm來賦值的,當(dāng)Reference內(nèi)部的referent對象的可達(dá)狀態(tài)改變時,jvm會將Reference對象放入pending鏈表。并且這里enqueue的隊列是我們在初始化( 構(gòu)造函數(shù) )Reference對象時傳進(jìn)來的queue,如果傳入了null( 實際使用的是ReferenceQueue.NULL ),則ReferenceHandler則不進(jìn)行enqueue操作,所以只有非RefernceQueue.NULL的queue才會將Reference進(jìn)行enqueue。

ReferenceQueue是作為 JVM GC與上層Reference對象管理之間的一個消息傳遞方式,它使得我們可以對所監(jiān)聽的對象引用可達(dá)發(fā)生變化時做一些處理

參考

http://www.importnew.com/21633.html
http://hongjiang.info/java-referencequeue/
http://www.cnblogs.com/jabnih/p/6580665.html
http://www.importnew.com/20468.html
http://liujiacai.net/blog/2015/09/27/java-weakhashmap/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內(nèi)容