常見 JVM垃圾回收器 與 內存分配策略

垃圾收集(Garbage Collection,下文簡稱GC),垃圾收集的歷史遠遠比Java久遠。經過半個世紀的發展,今天的內存動態分配與內存回收技術已經相當成熟,一切看起來都進入了“自動化”時代,那爲什麼我們還要去了解垃圾收集和內存分配?答案很簡單:當需要排查各種內存溢出、內存泄漏問題時,當垃圾收集成爲系統達到更高併發量的瓶頸時,我們就必須對這些“自動化”的技術實施必要的監控和調節。
 

一、哪些內存需要回收?

回答這個問題前需要先了解JVM到底有哪些內存區域,可參考:https://blog.csdn.net/weixin_41231928/article/details/107094638

 程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅,棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操作。每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的(儘管在運行期會由即時編譯器進行一些優化,但在基於概念模型的討論裏,大體上可以認爲是編譯期可知的),因此這幾個區域的內存分配和回收都具備確定性,在這幾個區域內就不需要過多考慮如何回收的問題,當方法結束或者線程結束時,內存自然就跟隨着回收了。

那還剩下什麼區域?還有方法區和堆!

對於方法區,上面鏈接對應的文章中也說過:方法區的內存回收目標主要是針對常量池的回收和對類型的卸載,即:廢棄的常量和不再使用的類型,但總的來說方法區的回收觸發條件比較苛刻,發生垃圾回收的頻率比較低。舉個常量池中字面量回收的例子,假如一個字符串“java”曾經進入常量池中,但是當前系統又沒有任何一個字符串對象的值是“java”,換句話說,已經沒有任何字符串對象引用常量池中的“java”常量,且虛擬機中也沒有其他地方引用這個字面量。如果在這時發生內存回收,而且垃圾收集器判斷確有必要的話,這個“java”常量就將會被系統清理出常量池。常量池中其他類(接口)、方法、字段的符號引用也與此類似。

在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區造成過大的內存壓力。判定一個類型是否屬於“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
  1. 該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
  2. 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGiJSP的重加載等,否則通常是很難達成的。
  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方

垃圾回收的主要陣地是,爲什麼這麼說?一個接口的多個實現類需要的內存可能會不一樣,一個方法所執行的不同條件分支所需要的內存也可能不一樣,只有處於運行期間,我們才能知道程序究竟會創建哪些對象,創建多少個對象,這部分內存的分配和回收是動態的。垃圾收集器所關注的正是這部分內存該如何管理。

二、垃圾收集第一步 —— 判斷對象是死是活

垃圾收集器在對堆進行回收前,第一件事情就是要確定這些對象之中哪些還“存活着,哪些已經死去(不會再被使用的對象)。

 

2.1 引用計數法

在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器爲零的對象就認爲是不可能再被使用的。
 
原理很簡單,但引用計數算法(Reference Counting)雖然佔用了一些額外的內存空間來進行計數,最主要的它會產生決對象之間相互循環引用的問題 ,如下代碼。這也是好多主流JVM不使用引用計數法管理內存的原因。
 
    public static  void  testGC(){
        GC objA = new GC();
        GC objB = new GC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;

        System.gc();
    }

 

2.2 可達分析法

這個算法的基本思路就是通過一系列稱爲“GC Roots”的根對象作爲起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程所走過的路徑稱爲“引用鏈Reference Chain),如果某個對象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。
 

對象object 5object 6object 7雖然互有關聯,但是它們到GC Roots是不可達的,因此它們將會被判定爲可回收的對象。
 
其實這種方法的關鍵是先確定GC Root Set,即哪些對象是可以作爲根對象。可以作爲GC Root的對象包含:
  1. 在虛擬機棧(棧幀中的本地變量表)中引用的對象,如各線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。
  2. 在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
  3. 在方法區中常量引用的對象,譬如字符串常量池(String Table)裏的引用
  4. 在本地方法棧中JNI(即通常所說的Native方法)引用的對象
  5. Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如NullPointExcepitonOutOfMemoryError)等,還有系統類加載器。
  6. 所有被同步鎖(synchronized關鍵字)持有的對象。
  7. 反映Java虛擬機內部情況的JMXBeanJVMTI中註冊的回調、本地代碼緩存等。
除了這些固定的GC Roots集合以外,根據用戶所選用的垃圾收集器以及當前回收的內存區域不同,還可以有其他對象“臨時性地加入,共同構成完整GC Roots集合。譬如分代收集和局部回收(Partial GC),如果只針對Java堆中某一塊區域發起垃圾收集時必須考慮到內存區域是虛擬機自己的實現細節,某個區域裏的對象完全有可能被位於堆中其他區域的對象所引用,這時候就需要將這些關聯區域的對象也一併加入GC Roots集合中去,才能保證可達性分析的正確性。

2.3 引用分類

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象是否引用鏈可達,判定對象是否存活都和“引用離不開關係。在JDK 1.2版之後,Java對引用的概念進行了擴充,將引用分爲強引用(Strongly Re-ference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference4種,這4種引用強度依次逐漸減弱。
  • 強引用是最傳統的“引用的定義,即類似“Object obj=new Object()”這種引用關係。無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的對象。
  • 軟引用是用來描述一些還有用,但非必須的對象。只被軟引用關聯着的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。
  • 弱引用也是用來描述那些非必須對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生爲止。當垃圾收集器開始工作,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
  • 虛引用也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的只是爲了能在這個對象被收集器回收時收到一個系統通知。

2.4 其他說明

假如一個對象被可達分析法法判定爲不可達對象,該對象是否就一定會被回收呢?答案是否定的。

即使在可達性分析算法中判定爲不可達的對象,也不是非死不可的,這時候它們暫時還處於緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記,隨後進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,就不會再回收該對象

 

三、垃圾收集算法

前面說了虛擬機怎麼判定一個對象是死是活的基本理論,即引用計數法和可達性分析法。

垃圾收集算法基於這兩種理論可以分爲:引用計數式垃圾收集 和 追蹤式垃圾收集 兩種。但是主流的虛擬機一般都屬於追蹤式垃圾收集,因此此類型的垃圾收集器是重點。

3.1 分代收集理論

3.1.1 什麼是分代?

常用的垃圾收集器的一致的設計原則:收集器應該將Java堆劃分出不同的區域,然後將回收對象依據其年齡(年齡即對象熬過垃圾收集過程的次數)分配到不同的區域之中存儲。顯而易見,如果一個區域中大多數對象都是朝生夕滅,難以熬過垃圾收集過程的話,那麼把它們集中放在一起,每次回收時只關注如何保留少量存活而不是去標記那些大量將要被回收的對象,就能以較低代價回收到大量的空間;如果剩下的都是難以消亡的對象,那把它們集中放在一塊,虛擬機便可以使用較低的頻率來回收這個區域,這就同時兼顧了垃圾收集的時間開銷和內存的空間有效利用。
具體的可以分爲:
  • 新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。目前只有CMS收集器會有單獨收集老年代的行爲。另外請注意“Major GC”這個說法現在有點混淆,在不同資料上常有不同所指,需按上下文區分到底是指老年代的收集還是整堆收集。
  • 混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。目前只有G1集器會有這種行爲。
  • 整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集。

(新生代(Young)、老年代(Old)是HotSpot虛擬機,也是現在業界主流的命名方式)

3.1.2 跨代引用

在新生代中,每次垃圾收集時都發現有大批對象死去,而每次回收後存活的少量對象,將會逐步晉升到老年代中存放。但是實際中分代時會有跨代問題:因爲對象不是孤立的,所以對象間會存在跨代引用。比如新生代中的對象是完全有可能被老年代所引用,一旦新生代中出現了這種情況,虛擬機在回收時就必須在固定的GC Root之外再額外遍歷整個老年代才能最終判定某個對象的“死活”,這樣很顯然有何分代理論相悖了,說好的分代之後每個分代單獨垃圾手機呢?
爲了解決這個矛盾,出現了一個重要的法則:跨代引用相對於同代引用來說僅佔極少數。存在互相引用關係的兩個對象,是應該傾向於同時生存或者同時消亡的。舉個例子,如果某個新生代對象存在跨代引用,由於老年代對象難以消亡,該引用會使得新生代對象在收集時同樣得以存活,進而在年齡增長之後晉升到老年代中,這時跨代引用也隨即被消除了。按照這個法則,就不應再爲了少量的跨代引用去掃描整個老年代,也不必浪費空間專門記錄每一個對象是否存在及存在哪些跨代引用,只需在新生代上建立一個全局的數據結構(該結構被稱爲“記憶集”,Remembered Set),這個結構把老年代劃分成若干小塊,標識出老年代的哪一塊內存會存在跨代引用。此後當發生Minor GC時,只有包含了跨代引用的小塊內存裏的對象纔會被加入到GCRoots進行掃描。雖然這種方法需要在對象改變引用關係(如將自己或者某個屬性賦值)時維護記錄數據的正確性,會增加一些運行時的開銷,但比起收集時掃描整個老年代來說仍然是划算的。
 

3.1.3 分代的意義

這個分代收集理論是垃圾回收的第二個基礎(第一個是判斷對象死活的兩個方法:引用計數和可達性分析),它使得垃圾收集器不需要每次都針對所有的堆內存區域進行收集,而只需要每次只回收其中某一個或者某些部分的區域即可,提升效率和準確性。同時,因爲將堆內存分成了不同的年齡區域,每塊區域是單獨收集的,比如“Minor/Young GC”“Major/Old GC”“Full GC”就是分別針對年輕代、老年代及整個堆的垃圾回收,那就可以對不同的代採取不同的垃圾回收算法,主要有 “標記-複製算法” “標記-清除算法”“標記-整理算法”三種。
 

3.2  標記--清除 算法

算法分爲標記清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後,統一回收掉所有被標記的對象,也可以反過來,標記存活的對象,統一回收所有未被標記的對象。標記過程就是對象“生死”的判定過程。
 
標記-清除算法是最早且最基礎的垃圾收集算法,其他的垃圾回收算法大多以此算法爲基礎進行改進。說到改進那就說明這個算法是有缺陷的,不然還改進幹啥?標記-清除算法主要有兩個缺點:
  1. 第一個是執行效率不穩定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個過程的執行效率都隨對象數量增長而降低;
  2. 內存空間的碎片化問題,標記、清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致當以後在程序運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

 

3.3  標記--複製 算法

也叫複製算法,主要針對標記--清除算法面對大量可回收對象時執行效率的不足進行了改進。

標記--複製 算法將內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當其中一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

如果Java堆中包含大量對象,而且其中大部分是需要被回收的,標記--複製 算法只需要將少數的存活對象複製到另一塊內存即可,內存複製開銷很小,而且每次都是針對整個半區進行內存回收,分配內存時也就不用考慮有空間碎片的複雜情況,只要移動堆頂指針,按順序分配即可。

所以 標記--複製 算法的有點是:1)面對大量要回收的對象時執行效率高;2)沒有內存碎片產生。那它有沒有缺陷呢?可定有啊,這種複製回收算法的代價是將可用內存縮小爲了原來的一半,太浪費空間。其次,在面對堆中有大量的對象,且多數爲不可回收的對象時,那複製的開銷就會變大。所以 標記--複製 比較適用於年輕代的垃圾收集器採用,不適合老年代的垃圾收集器採用。

此外,還存在”內存擔保“問題:這裏是按1:1的比例劃分內存的,但實際上,比如新生代中的對象有98%熬不過第一輪收集,那就沒必要再按這個比例來劃分內存,可以適當縮小保留區域的內存,因爲畢竟存活下來的對象不多,不需要那麼多內存來保存極少活下來的對象。

 

3.4  標記--整理 算法

標記-複製算法在對象存活率較高時就要進行較多的複製操作,效率將會降低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
 
針對老年代對象的存亡特徵,誕生了“標記-整理”(Mark-Compact)算法,其的標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內存空間一端移動,然後直接清理掉邊界以外的內存。標記-清除算法與標記-整理算法的本質差異在於前者是一種非移動式的回收算法,而後者是移動式的。
 

是否移動回收後的存活對象是一項優缺點並存的風險決策:如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區域,移動存活對象並更新所有引用這些對象的地方將會是一種極爲負重的操作,而且這種對象移動操作必須全程暫停用戶應用
程序才能進行。如果跟標記-清除算法那樣完全不考慮移動和整理存活對象的話,彌散於堆中的存活對象導致的
空間碎片化問題就只能依賴更爲複雜的內存分配器和內存訪問器來解決。
 
基於以上兩點,是否移動對象都存在弊端,移動則內存回收時會更復雜,不移動則內存分配時會更復雜。從垃圾收集的停頓時間來看,不移動對象停頓時間會更短,甚至可以不需要停頓,但是從整個程序的吞吐量(使用垃圾收集的用戶程序或用戶線程)來看,移動對象會更划算。
 
另外,還有一種解決方案可以不在內存分配和訪問上增加太大額外負擔,做法是讓虛擬機平時多數時間都採用標記-清除算法,暫時容忍內存碎片的存在,直到內存空間的碎片化程度已經大到影響對象分配時,再採用標記-整理算法收集一次,以獲得規整的內存空間。
 
 

四、經典垃圾回收器

垃圾收集算法是內存回收的方法論,垃圾收集器就是內存回收的實踐者。垃圾收集器的歷史比java還要悠久,因此有很多的經典的垃圾回收器,但好多已經年代久遠,這裏主要介紹兩種:CMS(Concurrent Mark Sweep) 和 G1(Garbage First)。
 

4.1 CMS收集器

Concurrent Mark Sweep翻譯過來就是:併發-標記-清除。這幾個關鍵字一出,想必已經知道CMS背後大致的“故事”。
 
CMSConcurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網網站或者基於瀏覽器的B/S系統的服務端上,這類應用通常都會較爲關注服務的響應速度,希望系統停頓時間儘可能短,以給用戶帶來良好的交互體驗。CMS收集器就非常符合這類應用的需求。
 
CMS基於 標記--清除 算法實現,整個過程分爲四步:
  • 1)初始標記
  • 2)併發標記
  • 3)重新標記
  • 4)併發清除

可以看出CMS中重點是標記,其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”,也就是需要暫停所有線程,那這樣怎麼還說CMS是響應快速的呢?那是因爲初始標記僅僅只是標記一下GC Root能直接關聯到的對象,速度很快;併發標記階段就是從GC Root的直接關聯對象開始遍歷整個對象圖的過程,這個過程雖然很耗時但卻不需要停頓所有用戶線程,用戶線程可以和垃圾收集線程一起併發運行;重新標記階段則是爲了修正併發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比並發標記階段的時間短;最後是併發清除階段,清理刪除掉標記階段判斷的已經死亡的 對象,由於不需要移動存活對象,所以這個階段也是可以與用戶線程同時併發的。

在整個過程中耗時最長的併發標記和併發清除階段中,垃圾收集器線程都可以與用戶線程一起工作,所以從總體上來說,CMS收集器的內存回收過程是與用戶線程一起併發執行的,所以有靈敏的響應。

 

優缺點:

優點:併發收集、低停頓。

缺點:(1)CMS收集器對處理器資源非常敏感。如果應用本來的處理器負載就很高,還要分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然大幅降低。(2)CMS是一款基於標記-清除算法實現的收集器,收集結束時會有大量空間碎片產生。

4.2 Garbage First收集器 -- G1

G1收集器才被Oracle官方稱爲全功能的垃圾收集器”Fully-Featured Garbage Collector)。
 
G1從整體來看是基於標記-整理算法實現的收集器,但從局部(兩個Region之間)上看又是基於“標記-複製算法實現。
 
G1收集器出現之前的所有其他收集器,包括CMS在內,垃圾收集的目標範圍要麼是整個新生代(Minor GC),要麼就是整個老年代(Major GC),再要麼就是整個Java堆(Full GC)。而G1跳出了這個樊籠,它可以面向堆內存任何部分來組成回收集(Collection Set,一般簡稱CSet)進行回收,衡量標準不再是它屬於哪個分代,而是哪塊內存中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式。
 
G1開創的基於Region的堆內存佈局是它能夠實現這個目標的關鍵。雖然G1也仍是遵循分代收集理論設計的,但其堆內存的佈局與其他收集器有非常明顯的差異:G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分爲多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的Region採用不同的策略去處理,這樣無論是新創建的對象還是已經存活了一段時間、熬過多次收集的舊對象都能獲取很好的收集效果。
 
Region中還有一類特殊的Humongous區域,專門用來存儲大對象。G1認爲只要大小超過了一個Region容量一半的對象即可判定爲大對象。每個Region的大小可以通過參數-XX:G1HeapRegionSize設定,取值範圍爲1MB~32MB,且應爲2的N次冪。而對於那些超過了整個Region容量的超級大對象,將會被存放在N個連續的Humongous Region之中,G1的大多數都把Humongous作爲老年代的一部分來進行看待。
 
雖然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了。它將Region作爲單次回收的最小單元,即每次收集到的內存空間都是Region大小的整數倍,這樣可以避免在整個Java堆中進行全區域的垃圾收集。更具體的處理思路是讓G1收集器去跟蹤各個Region裏面的垃圾堆積的“價值大小,然後在後臺維護一個優先級列表,每次根據用戶設定允許的收集停頓時間(使用參數-XXMaxGCPauseMillis指定,默認值是200毫秒),優先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來。 這種使用Region劃分內存空間,以及具有優先級的區域回收方式,保證了G1收集器在有限的時間內獲取儘可能高的收集效率。
 
G1收集器的運作過程大致可劃分爲以下四個步驟:
  • 1)初始標記:僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS指針的值,讓下一階段用戶線程併發運行時,能正確地在可用的Region中分配新對象。這個階段需要停頓線程,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際並沒有額外的停頓。
  • 2)併發標記:從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆裏的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序併發執行。對象圖掃描完成以後,還要重新處理SATB記錄下的在併發時有引用變動的對象。
  • 3)最終標記:對用戶線程做另一個短暫的暫停,用於處理併發階段結束後仍遺留下來的最後那少量的SATB記錄。
  • 4)篩選回收:更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然後把決定回收的那一部分Region的存活對象複製到空的Region中,再清理掉整個舊Region的全部空間。這裏的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程並行完成的。
G1收集器除了併發標記外,其餘階段也是要完全暫停用戶線程的,它並非純粹地追求低延遲。上面的步驟還有需要解釋的地方:
TAMS和SATB。
 
用戶線程改變對象引用關係時,必須保證其不能打破原本的對象圖結構,導致標記結果出現錯誤CMS收集器採用增量更新算法實現,而G1收集器則是通過原始快照(SATB)算法來實現的。此外,垃圾收集對用戶線程的影響還體現在回收過程中新創建對象的內存分配上,程序要繼續運行就肯定會持續有新對象被創建,G1爲每一個Region設計了兩個名爲TAMSTop at Mark Start)的指針,把Region中的一部分空間劃分出來用於併發回收過程中的新對象分配,併發回收時新分配的對象地址都必須要在這兩個指針位置以上。G1收集器默認在這個地址以上的對象是被隱式標記過的,即默認它們是存活的,不納入回收範圍。與CMS中的“Concurrent Mode Failure”失敗會導致Full GC類似,如果內存回收的速度趕不上內存分配的速度,G1收集器也要被迫凍結用戶線程執行,導致Full GC而產生長時間“Stop The World”
 

 

 

 

 

 

 

 

 

 

 

 
HotSpot虛擬機裏面關注吞吐量的Parallel
Scavenge收集器是基於標記-整理算法的
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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