阿里面試: 說說強引用、軟引用、弱引用、虛引用吧

我們都知道 JVM 垃圾回收中,GC判斷堆中的對象實例或數據是不是垃圾的方法有引用計數法和可達性算法兩種。
無論是通過引用計數算法判斷對象的引用數量,還是通過根搜索算法判斷對象的引用鏈是否可達,判定對象是否存活都與“引用”有關。

引用
先說說引用,Java中的引用,類似 C 語言中的指針。初學 Java時,我們就知道 Java 數據類型分兩大類,基本類型和引用類型。

基本類型:編程語言中內置的最小粒度的數據類型。它包括四大類八種類型:
4種整數類型:byte、short、int、long
2種浮點數類型:float、double
1種字符類型:char
1種布爾類型:boolean
引用類型:引用類型指向一個對象,不是原始值,指向對象的變量是引用變量。在 Java 裏,除了基本類型,其他類型都屬於引用類型,它主要包括:類、接口、數組、枚舉、註解

有了數據類型,JVM對程序數據的管理就規範化了,不同的數據類型,它的存儲形式和位置是不一樣的
怎麼跑偏了,迴歸正題,通過引用,可以對堆中的對象進行操作。引用《Java編程思想》中的一段話,

”每種編程語言都有自己的數據處理方式。有些時候,程序員必須注意將要處理的數據是什麼類型。你是直接操縱元素,還是用某種基於特殊語法的間接表示(例如C/C++裏的指針)來操作對象。所有這些在 Java 裏都得到了簡化,一切都被視爲對象。因此,我們可採用一種統一的語法。儘管將一切都“看作”對象,但操縱的標識符實際是指向一個對象的“引用”(reference)。”

比如:

Person person = new Person("張三");

這裏的 person 就是指向Person 實例“張三”的引用,我們一般都是通過 person 來操作“張三”實例。

在 JDK 1.2 之前,Java 中的引用的定義很傳統:如果 reference 類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱該 refrence 數據是代表某塊內存、某個對象的引用。這種定義很純粹,但是太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態,對於如何描述一些“食之無味,棄之可惜”的對象就顯得無能爲力。

比如我們希望能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;如果內存在進行垃圾收集後還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的應用場景。
在 JDK 1.2 之後,Java 對引用的概念進行了擴充,將引用分爲

強引用(Strong Reference)
軟引用(Soft Reference)
弱引用(Weak Reference)
虛引用(Phantom Reference)

這四種引用強度依次逐漸減弱。

Java 中引入四種引用的目的是讓程序自己決定對象的生命週期,JVM 是通過垃圾回收器對這四種引用做不同的處理,來實現對象生命週期的改變。
image.png

FinalReference 類是包內可見,其他三種引用類型均爲 public,可以在應用程序中直接使用。
強引用
在 Java 中最常見的就是強引用,把一個對象賦給一個引用變量,這個引用變量就是一個強引用。類似 “Object obj = new Object()” 這類的引用。

當一個對象被強引用變量引用時,它處於可達狀態,是不可能被垃圾回收器回收的,即使該對象永遠不會被用到也不會被回收。

當內存不足,JVM 開始垃圾回收,對於強引用的對象,就算是出現了 OOM 也不會對該對象進行回收,打死都不收。因此強引用有時也是造成 Java 內存泄露的原因之一。

對於一個普通的對象,如果沒有其他的引用關係,只要超過了引用的作用域或者顯示地將相應(強)引用賦值爲 null,一般認爲就是可以被垃圾收集器回收。(具體回收時機還要要看垃圾收集策略)。

public class StrongRefenenceDemo {

    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = o1;
        o1 = null;
        System.gc();
        System.out.println(o1);  //null
        System.out.println(o2);  //java.lang.Object@2503dbd3
    }
}

demo 中儘管 o1已經被回收,但是 o2 強引用 o1,一直存在,所以不會被GC回收
軟引用
軟引用是一種相對強引用弱化了一些的引用,需要用java.lang.ref.SoftReference 類來實現,可以讓對象豁免一些垃圾收集。

軟引用用來描述一些還有用,但並非必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中並進行第二次回收。如果這次回收還是沒有足夠的內存,纔會拋出內存溢出異常。

對於只有軟引用的對象來說:當系統內存充足時它不會被回收,當系統內存不足時它纔會被回收。

//VM options: -Xms5m -Xmx5m
public class SoftRefenenceDemo {

    public static void main(String[] args) {
        softRefMemoryEnough();
        System.out.println("------內存不夠用的情況------");
        softRefMemoryNotEnough();
    }

    private static void softRefMemoryEnough() {
        Object o1 = new Object();
        SoftReference<Object> s1 = new SoftReference<Object>(o1);
        System.out.println(o1);
        System.out.println(s1.get());

        o1 = null;
        System.gc();

        System.out.println(o1);
        System.out.println(s1.get());
    }

     /**
     * JVM配置`-Xms5m -Xmx5m` ,然後故意new一個一個大對象,使內存不足產生 OOM,看軟引用回收情況
     */
    private static void softRefMemoryNotEnough() {
        Object o1 = new Object();
        SoftReference<Object> s1 = new SoftReference<Object>(o1);
        System.out.println(o1);
        System.out.println(s1.get());

        o1 = null;

        byte[] bytes = new byte[10 * 1024 * 1024];

        System.out.println(o1);
        System.out.println(s1.get());
    }
}

Output

java.lang.Object@2503dbd3
java.lang.Object@2503dbd3
null
java.lang.Object@2503dbd3
------內存不夠用的情況------
java.lang.Object@4b67cf4d
java.lang.Object@4b67cf4d
java.lang.OutOfMemoryError: Java heap space
	at reference.SoftRefenenceDemo.softRefMemoryNotEnough(SoftRefenenceDemo.java:42)
	at reference.SoftRefenenceDemo.main(SoftRefenenceDemo.java:15)
null
null

軟引用通常用在對內存敏感的程序中,比如高速緩存就有用到軟引用,內存夠用的時候就保留,不夠用就回收。
我們看下 Mybatis 緩存類 SoftCache 用到的軟引用

public Object getObject(Object key) {
    Object result = null;
    SoftReference<Object> softReference = (SoftReference)this.delegate.getObject(key);
    if (softReference != null) {
        result = softReference.get();
        if (result == null) {
            this.delegate.removeObject(key);
        } else {
            synchronized(this.hardLinksToAvoidGarbageCollection) {
                this.hardLinksToAvoidGarbageCollection.addFirst(result);
                if (this.hardLinksToAvoidGarbageCollection.size() > this.numberOfHardLinks) {
                    this.hardLinksToAvoidGarbageCollection.removeLast();
                }
            }
        }
    }
    return result;
}

弱引用
弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

弱引用需要用java.lang.ref.WeakReference類來實現,它比軟引用的生存期更短。

對於只有弱引用的對象來說,只要垃圾回收機制一運行,不管 JVM 的內存空間是否足夠,都會回收該對象佔用的內存。

public class WeakReferenceDemo {

    public static void main(String[] args) {
        Object o1 = new Object();
        WeakReference<Object> w1 = new WeakReference<Object>(o1);

        System.out.println(o1);
        System.out.println(w1.get());

        o1 = null;
        System.gc();

        System.out.println(o1);
        System.out.println(w1.get());
    }
}

Weak reference objects, which do not prevent their referents from being made finalizable, finalized, and then reclaimed. Weak references are most often used to implement canonicalizing mappings.

官方文檔這麼寫的,弱引用常被用來實現規範化映射,JDK 中的 WeakHashMap 就是一個這樣的例子

面試官:既然你都知道弱引用,那能說說 WeakHashMap 嗎

public class WeakHashMapDemo {

    public static void main(String[] args) throws InterruptedException {
        myHashMap();
        myWeakHashMap();
    }

    public static void myHashMap() {
        HashMap<String, String> map = new HashMap<String, String>();
        String key = new String("k1");
        String value = "v1";
        map.put(key, value);
        System.out.println(map);

        key = null;
        System.gc();

        System.out.println(map);
    }

    public static void myWeakHashMap() throws InterruptedException {
        WeakHashMap<String, String> map = new WeakHashMap<String, String>();
        //String key = "weak";
        // 剛開始寫成了上邊的代碼
        //思考一下,寫成上邊那樣會怎麼樣? 那可不是引用了
        String key = new String("weak");
        String value = "map";
        map.put(key, value);
        System.out.println(map);
        //去掉強引用
        key = null;
        System.gc();
        Thread.sleep(1000);
        System.out.println(map);
    }
}

我們看下 ThreadLocal 中用到的弱引用

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    //......
}

虛引用
虛引用也稱爲“幽靈引用”或者“幻影引用”,它是最弱的一種引用關係。

虛引用,顧名思義,就是形同虛設,與其他幾種引用都不太一樣,一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。

虛引用需要java.lang.ref.PhantomReference 來實現。

如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收,它不能單獨使用也不能通過它訪問對象,虛引用必須和引用隊列(RefenenceQueue)聯合使用。

虛引用的主要作用是跟蹤對象垃圾回收的狀態。僅僅是提供了一種確保對象被 finalize 以後,做某些事情的機制。

PhantomReference 的 get 方法總是返回 null,因此無法訪問對應的引用對象。其意義在於說明一個對象已經進入 finalization 階段,可以被 GC 回收,用來實現比 finalization 機制更靈活的回收操作。

換句話說,設置虛引用的唯一目的,就是在這個對象被回收器回收的時候收到一個系統通知或者後續添加進一步的處理。

Java 允許使用 finalize() 方法在垃圾收集器將對象從內存中清除出去之前做必要的清理工作。

public class PhantomReferenceDemo {

    public static void main(String[] args) throws InterruptedException {
        Object o1 = new Object();
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();
        PhantomReference<Object> phantomReference = new PhantomReference<Object>(o1,referenceQueue);

        System.out.println(o1);
        System.out.println(referenceQueue.poll());
        System.out.println(phantomReference.get());

        o1 = null;
        System.gc();
        Thread.sleep(3000);

        System.out.println(o1);
        System.out.println(referenceQueue.poll()); //引用隊列中
        System.out.println(phantomReference.get());
    }

}
java.lang.Object@4554617c
null
null
null
java.lang.ref.PhantomReference@74a14482
null

引用隊列
ReferenceQueue 是用來配合引用工作的,沒有ReferenceQueue 一樣可以運行。

SoftReference、WeakReference、PhantomReference 都有一個可以傳遞 ReferenceQueue 的構造器。
創建引用的時候,可以指定關聯的隊列,當 GC 釋放對象內存的時候,會將引用加入到引用隊列。如果程序發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的內存被回收之前採取必要的行動,這相當於是一種通知機制。

當關聯的引用隊列中有數據的時候,意味着指向的堆內存中的對象被回收。通過這種方式,JVM 允許我們在對象被銷燬後,做一些我們自己想做的事情。
image.png

最後,稍微瞭解下源碼中的實現
Reference源碼(JDK8)

強軟弱虛四種引用,我們有了個大概的認識,我們也知道除了強引用沒有對應的類型表示,是普遍存在的。剩下的三種引用都是 java.lang.ref.Reference 的直接子類。

那就會有疑問了,我們可以通過繼承 Reference,自定義引用類型嗎?

Abstract base class for reference objects. This class defines the operations common to all reference objects. Because reference objects are implemented in close cooperation with the garbage collector, this class may not be subclassed directly.

JDK 官方文檔是這麼說的,Reference是所有引用對象的基類。這個類定義了所有引用對象的通用操作。因爲引用對象是與垃圾收集器緊密協作而實現的,所以這個類可能不能直接子類化。

Reference 的4種狀態
1.Active:新創建的引用實例處於Active狀態,但當GC檢測到該實例引用的實際對象的可達性發生某些改變(實際對象處於 GC roots 不可達)後,它的狀態將變化爲Pending或者Inactive。如果 Reference 註冊了ReferenceQueue,則會切換爲Pending,並且Reference會加入pending-Reference鏈表中,如果沒有註冊ReferenceQueue,會切換爲Inactive。

2.Pending:當引用實例被放置在pending-Reference 鏈表中時,它處於Pending狀態。此時,該實例在等待一個叫Reference-handler的線程將此實例進行enqueue操作。如果某個引用實例沒有註冊在一個引用隊列中,該實例將永遠不會進入Pending狀態

3.Enqueued:在ReferenceQueue隊列中的Reference的狀態,如果Reference從隊列中移除,會進入Inactive狀態

4.Inactive:一旦某個引用實例處於Inactive狀態,它的狀態將不再會發生改變,同時說明該引用實例所指向的實際對象一定會被GC所回收
image.png

Reference的構造函數和成員變量

public abstract class Reference<T> {
   //引用指向的對象
   private T referent;    
   // reference被回收後,當前Reference實例會被添加到這個隊列中
   volatile ReferenceQueue<? super T> queue;
   //下一個Reference實例的引用,Reference實例通過此構造單向的鏈表
   volatile Reference next;
   //由transient修飾,基於狀態表示不同鏈表中的下一個待處理的對象,主要是pending-reference列表的下一個元素,通過JVM直接調用賦值
   private transient Reference<T> discovered;
   // 等待加入隊列的引用列表,這裏明明是個Reference類型的對象,官方文檔確說是個list?
   //因爲GC檢測到某個引用實例指向的實際對象不可達後,會將該pending指向該引用實例,
   //discovered字段則是用來表示下一個需要被處理的實例,因此我們只要不斷地在處理完當前pending之後,將discovered指向的實例賦予給pending即可。所以這個pending就相當於是一個鏈表。
   private static Reference<Object> pending = 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;
    }
}

Reference 提供了兩個構造器,一個帶引用隊列 ReferenceQueue,一個不帶。

帶 ReferenceQueue 的意義在於我們可以從外部通過對 ReferenceQueue 的操作來瞭解到引用實例所指向的實際對象是否被回收了,同時我們也可以通過 ReferenceQueue 對引用實例進行一些額外的操作;但如果我們的引用實例在創建時沒有指定一個引用隊列,那我們要想知道實際對象是否被回收,就只能夠不停地輪詢引用實例的get() 方法是否爲空了。

值得注意的是虛引用 PhantomReference,由於它的 get() 方法永遠返回 null,因此它的構造函數必須指定一個引用隊列。

這兩種查詢實際對象是否被回收的方法都有應用,如 WeakHashMap 中就選擇去查詢 queue 的數據,來判定是否有對象將被回收;而 ThreadLocalMap,則採用判斷 get() 是否爲 null 來作處理。

實例方法(和ReferenceHandler線程不相關的方法)

private static Lock lock = new Lock();
// 獲取持有的referent實例
public T get() {
    return this.referent;
}
// 把持有的referent實例置爲null
public void clear() {
    this.referent = null;
}
// 判斷是否處於enqeued狀態
public boolean isEnqueued() {
    return (this.queue == ReferenceQueue.ENQUEUED);
}
// 入隊參數,同時會把referent置爲null
public boolean enqueue() {
    return this.queue.enqueue(this);
}

ReferenceHandler線程

通過上文的討論,我們知道一個Reference實例化後狀態爲Active,其引用的對象被回收後,垃圾回收器將其加入到pending-Reference鏈表,等待加入ReferenceQueue。

ReferenceHandler線程是由Reference靜態代碼塊中建立並且運行的線程,它的運行方法中依賴了比較多的本地(native)方法,ReferenceHandler線程的主要功能就pending list中的引用實例添加到引用隊列中,並將pending指向下一個引用實例。

// 控制垃圾回收器操作與Pending狀態的Reference入隊操作不衝突執行的全局鎖
// 垃圾回收器開始一輪垃圾回收前要獲取此鎖
// 所以所有佔用這個鎖的代碼必須儘快完成,不能生成新對象,也不能調用用戶代碼
static private class Lock { }
private static Lock lock = new Lock();

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 {
        ensureClassInitialized(InterruptedException.class);
        ensureClassInitialized(Cleaner.class);
    }

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

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

static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
        synchronized (lock) {
            // 判斷pending-Reference鏈表是否有數據
            if (pending != null) {
                // 如果有Pending Reference,從列表中取出
                r = pending;
                c = r instanceof Cleaner ? (Cleaner) r : null;
                // unlink 'r' from 'pending' chain
                pending = r.discovered;
                r.discovered = null;
            } else {
    // 如果沒有Pending Reference,調用wait等待
                if (waitForNotify) {
                    lock.wait();
                }
                // retry if waited
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        Thread.yield();
        return true;
    } catch (InterruptedException x) {
        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;
}

//ReferenceHandler線程是在Reference的static塊中啓動的
static {
    // ThreadGroup繼承當前執行線程(一般是主線程)的線程組
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    // 創建線程實例,命名爲Reference Handler,配置最高優先級和後臺運行(守護線程),然後啓動
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    // ReferenceHandler線程有最高優先級
    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);
        }
    });
}

由於ReferenceHandler線程是Reference的靜態代碼創建的,所以只要Reference這個父類被初始化,該線程就會創建和運行,由於它是守護線程,除非 JVM 進程終結,否則它會一直在後臺運行(注意它的run()方法裏面使用了死循環)。

ReferenceQueue源碼

public class ReferenceQueue<T> {

    public ReferenceQueue() { }
 // 內部類Null類繼承自ReferenceQueue,覆蓋了enqueue方法返回false
    private static class Null<S> extends ReferenceQueue<S> {
        boolean enqueue(Reference<? extends S> r) {
            return false;
        }
    }
  // 用於標識沒有註冊Queue
    static ReferenceQueue<Object> NULL = new Null<>();
    // 用於標識已經處於對應的Queue中
    static ReferenceQueue<Object> ENQUEUED = new Null<>();

    // 靜態內部類,作爲鎖對象
    static private class Lock { };
    /* 互斥鎖,用於同步ReferenceHandler的enqueue和用戶線程操作的remove和poll出隊操作 */
    private Lock lock = new Lock();
    // 引用鏈表的頭節點
    private volatile Reference<? extends T> head = null;
    // 引用隊列長度,入隊則增加1,出隊則減少1
    private long queueLength = 0;

    // 入隊操作,只會被Reference實例調用
    boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
        synchronized (lock) {
   // 如果引用實例持有的隊列爲ReferenceQueue.NULL或者ReferenceQueue.ENQUEUED則入隊失敗返回false
            ReferenceQueue<?> queue = r.queue;
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            assert queue == this;
            // 當前引用實例已經入隊,那麼它本身持有的引用隊列實例置爲ReferenceQueue.ENQUEUED
            r.queue = ENQUEUED;
            // 如果鏈表沒有元素,則此引用實例直接作爲頭節點,否則把前一個引用實例作爲下一個節點
            r.next = (head == null) ? r : head;
            // 當前實例更新爲頭節點,也就是每一個新入隊的引用實例都是作爲頭節點,已有的引用實例會作爲後繼節點
            head = r;
            // 隊列長度增加1
            queueLength++;
            // 特殊處理FinalReference,VM進行計數
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(1);
            }
            // 喚醒所有等待的線程
            lock.notifyAll();
            return true;
        }
    }

    // 引用隊列的poll操作,此方法必須在加鎖情況下調用
    private Reference<? extends T> reallyPoll() {       /* Must hold lock */
        Reference<? extends T> r = head;
        if (r != null) {
            @SuppressWarnings("unchecked")
            Reference<? extends T> rn = r.next;
            // 更新next節點爲頭節點,如果next節點爲自身,說明已經走過一次出隊,則返回null
            head = (rn == r) ? null : rn;
            r.queue = NULL;
            // 當前頭節點變更爲環狀隊列,考慮到FinalReference尚爲inactive和避免重複出隊的問題
            r.next = r;
            // 隊列長度減少1
            queueLength--;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(-1);
            }
            return r;
        }
        return null;
    }

    // 隊列的公有poll操作,主要是加鎖後調用reallyPoll
    public Reference<? extends T> poll() {
        if (head == null)
            return null;
        synchronized (lock) {
            return reallyPoll();
        }
    }
// 移除引用隊列中的下一個引用元素,實際上也是依賴於reallyPoll的Object提供的阻塞機制
    public Reference<? extends T> remove(long timeout)
        throws IllegalArgumentException, InterruptedException
    {
        if (timeout < 0) {
            throw new IllegalArgumentException("Negative timeout value");
        }
        synchronized (lock) {
            Reference<? extends T> r = reallyPoll();
            if (r != null) return r;
            long start = (timeout == 0) ? 0 : System.nanoTime();
            for (;;) {
                lock.wait(timeout);
                r = reallyPoll();
                if (r != null) return r;
                if (timeout != 0) {
                    long end = System.nanoTime();
                    timeout -= (end - start) / 1000_000;
                    if (timeout <= 0) return null;
                    start = end;
                }
            }
        }
    }

    public Reference<? extends T> remove() throws InterruptedException {
        return remove(0);
    }

    void forEach(Consumer<? super Reference<? extends T>> action) {
        for (Reference<? extends T> r = head; r != null;) {
            action.accept(r);
            @SuppressWarnings("unchecked")
            Reference<? extends T> rn = r.next;
            if (rn == r) {
                if (r.queue == ENQUEUED) {
                    // still enqueued -> we reached end of chain
                    r = null;
                } else {
                    // already dequeued: r.queue == NULL; ->
                    // restart from head when overtaken by queue poller(s)
                    r = head;
                }
            } else {
                // next in chain
                r = rn;
            }
        }
    }
}

ReferenceQueue只存儲了Reference鏈表的頭節點,真正的Reference鏈表的所有節點是存儲在Reference實例本身,通過屬性 next 拼接的,ReferenceQueue提供了對Reference鏈表的入隊、poll、remove等操作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章