摘要:看來還是功力不夠,索性拆成了六篇文章,分別從自動內存管理機制類文件結構類加載機制字節碼執行引擎程序編譯與代碼優化高效并發六個方面來做更加細致的介紹。本文先說說虛擬機的自動內存管理機制。在類加載檢查通過后,虛擬機將為新生對象分配內存。
歡迎關注微信公眾號:BaronTalk,獲取更多精彩好文!
書籍真的是常讀常新,古人說「書讀百遍其義自見」還是蠻有道理的。周志明老師的這本《深入理解 Java 虛擬機》我細讀了不下三遍,每一次閱讀都有新的收獲,每一次閱讀對 Java 虛擬機的理解就更進一步。因而萌生了將讀書筆記整理成文的想法,一是想檢驗下自己的學習成果,對學習內容進行一次系統性的復盤;二是給還沒接觸過這部好作品的同學推薦下,在閱讀這部佳作之前能通過我的文章一窺書中的精華。
原想著一篇文章就夠了,但寫著寫著就發現篇幅大大超出了預期。看來還是功力不夠,索性拆成了六篇文章,分別從自動內存管理機制、類文件結構、類加載機制、字節碼執行引擎、程序編譯與代碼優化、高效并發六個方面來做更加細致的介紹。本文先說說 Java 虛擬機的自動內存管理機制。
一. 運行時數據區Java 虛擬機在執行 Java 程序的過程中會把它所管理的內存區域劃分為若干個不同的數據區域。這些區域都有各自的用途,以及創建和銷毀的時間,有些區域隨著虛擬機進程的啟動而存在,有些區域則是依賴線程的啟動和結束而建立和銷毀。Java 虛擬機所管理的內存被劃分為如下幾個區域:
程序計數器程序計數器是一塊較小的內存區域,可以看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型里,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。「屬于線程私有的內存區域」
Java 虛擬機棧就是我們平時所說的棧,每個方法被執行時,都會創建一個棧幀(Stack Frame)用于存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每個方法從被調用到執行完成的過程,就對應著一個棧幀在虛擬機棧中從出棧到入棧的過程。「屬于線程私有的內存區域」
局部變量表:局部變量表是 Java 虛擬機棧的一部分,存放了編譯器可知的基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,不等同于對象本身,根據不同的虛擬機實現,它可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或者其他與此對象相關的位置)和 returnAddress 類型(指向了一條字節碼指令的地址)。本地方法棧
和虛擬機棧類似,只不過虛擬機棧為虛擬機執行的 Java 方法服務,本地方法棧為虛擬機使用的 Native 方法服務。「屬于線程私有的內存區域」
Java 堆對大多數應用而言,Java 堆是虛擬機所管理的內存中最大的一塊,是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一作用就是存放對象實例,幾乎所有的對象實例都是在這里分配的(不絕對,在虛擬機的優化策略下,也會存在棧上分配、標量替換的情況,后面的章節會詳細介紹)。Java 堆是 GC 回收的主要區域,因此很多時候也被稱為 GC 堆。從內存回收的角度看,Java 堆還可以被細分為新生代和老年代;再細一點新生代還可以被劃分為 Eden Space、From Survivor Space、To Survivor Space。從內存回收的角度看,線程共享的 Java 堆可能劃分出多個線程私有的分配緩沖區(Thread Local Allocation Buffer,TLAB)。「屬于線程共享的內存區域」
方法區用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。「屬于線程共享的內存區域」
運行時常量池: 運行時常量池是方法區的一部分,Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池(Constant Pool Table),用于存放在編譯期生成的各種字面量和符號引用。二. 對象的創建、內存布局及訪問定位直接內存:直接內存(Direct Memory)并不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規范中定義的內存區域。Java 中的 NIO 可以使用 Native 函數直接分配堆外內存,然后通過一個存儲在 Java 堆中的 DiectByteBuffer 對象作為這塊內存的引用進行操作。這樣能在一些場景顯著提高性能,因為避免了在 Java 堆和 Native 堆中來回復制數據。直接內存不受 Java 堆大小的限制。
前面介紹了 Java 虛擬機的運行時數據區,了解了虛擬機內存的情況。接下來我們看看對象是如何創建的、對象的內存布局是怎樣的以及對象在內存中是如何定位的。
2.1 對象的創建要創建一個對象首先得在 Java 堆中(不絕對,后面介紹虛擬機優化策略的時候會做詳細介紹)為這個要創建的對象分配內存,分配內存的過程要保證并發安全,最后再對內存進行相應的初始化,這一系列的過程完成后,一個真正的對象就被創建了。
內存分配先說說內存分配,當虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能夠在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化。如果沒有,那必須先執行相應的類加載過程。在類加載檢查通過后,虛擬機將為新生對象分配內存。對象所需的內存大小在類加載完成后便可完全確定,為對象分配內存空間的任務等同于把一塊確定大小的內存從 Java 堆中劃分出來。
在 Java 堆中劃分內存涉及到兩個概念:指針碰撞(Bump the Pointer)、空閑列表(Free List)。
如果 Java 堆中的內存絕對規整,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放著一個指針作為分界點的指示器,那所分配的內存就緊緊是把指針往空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式稱為「指針碰撞」。
如果 Java 堆中的內存并不是規整的,已使用的內存和空閑的內存相互交錯,那就沒辦法簡單的進行指針碰撞了。虛擬機必須維護一個列表來記錄哪些內存是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這種分配方式稱為「空閑列表」。
選擇哪種分配方式是由 Java 堆是否規整來決定的,而 Java 堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
保證并發安全對象的創建在虛擬機中是一個非常頻繁的行為,哪怕只是修改一個指針所指向的位置,在并發情況下也是不安全的,可能出現正在給對象 A 分配內存,指針還沒來得及修改,對象 B 又同時使用了原來的指針來分配內存的情況。解決這個問題有兩種方案:
對分配內存空間的動作進行同步處理(采用 CAS + 失敗重試來保障更新操作的原子性);
把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在 Java 堆中預先分配一小塊內存,稱為本地線程分配緩沖(Thread Local Allocation Buffer, TLAB)。哪個線程要分配內存,就在哪個線程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 時,才需要同步鎖。
初始化內存分配完后,虛擬機要將分配到的內存空間初始化為零值(不包括對象頭),如果使用了 TLAB,這一步會提前到 TLAB 分配時進行。這一步保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用。
接下來設置對象頭(Object Header)信息,包括對象是哪個類的實例、如何找到類的元數據、對象的 Hash、對象的 GC 分代年齡等。
這一系列動作完成之后,緊接著會執行
JVM 中對象的創建過程大致如下圖:
2.2 對象的內存布局在 HotSpot 虛擬機中,對象在內存中的布局可以分為 3 塊:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
對象頭對象頭包含兩部分信息,第一部分用于存儲對象自身的運行時數據,比如哈希碼(HashCode)、GC 分代年齡、鎖狀態標志、線程持有的鎖、偏向線程 ID、偏向時間戳等。這部分數據稱之為 Mark Word。對象頭的另一部分是類型指針,即對象指向它的類元數據指針,虛擬機通過它來確定對象是哪個類的實例;如果是數組,對象頭中還必須有一塊用于記錄數組長度的數據。(并不是所有所有虛擬機的實現都必須在對象數據上保留類型指針,在下一小節介紹「對象的訪問定位」的時候再做詳細說明)。
實例數據對象真正存儲的有效數據,也是在程序代碼中所定義的各種字段內容。
對齊填充無特殊含義,不是必須存在的,僅作為占位符。
2.3 對象的訪問定位Java 程序需要通過棧上的 reference 信息來操作堆上的具體對象。根據不同的虛擬機實現,主流的訪問對象的方式主要有句柄訪問和直接指針兩種。
句柄訪問Java 堆中劃分出一塊內存來作為句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。
使用句柄訪問的好處就是 reference 中存儲的是穩定的句柄地址,在對象被移動時只需要改變句柄中實例數據的指針,而 reference 本身不需要修改。
直接指針在對象頭中存儲類型數據相關信息,reference 中存儲的對象地址。
使用直接指針訪問的好處是速度更快,它節省了一次指針定位的開銷。由于對象訪問在 Java 中非常頻繁,因此這類開銷積少成多也是一項非常可觀的執行成本。HotSpot 中采用的就是這種方式。
三. 垃圾回收器與內存分配策略在前面我們介紹 JVM 運行時數據區的時候說過,程序計數器、虛擬機棧、本地方法棧 3 個區域隨線程而生,隨線程而死;棧中的棧幀隨著方法的進入和退出而有條不紊的執行著入棧和出棧的操作。每一個棧幀中分配多少內存基本上在數據結構確定下來的時候就已經知道了,因此這幾個區域內存的分配和回收是具有確定性的,所以不用過度考慮內存回收的問題,因為在方法結束或者線程結束時,內存就跟著回收了。
而 Java 堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序運行期才能知道會創建哪些對象,這部分內存的分配和回收是動態的,垃圾收集器要關注的就是這部分內存。
3.1 對象回收的判定規則垃圾收集器在做垃圾回收的時候,首先需要判定的就是哪些內存是需要被回收的,哪些對象是「存活」的,是不可以被回收的;哪些對象已經「死掉」了,需要被回收。
引用計數法判斷對象存活與否的一種方式是「引用計數」,即對象被引用一次,計數器就加 1,如果計數器為 0 則判斷這個對象可以被回收。但是引用計數法有一個很致命的缺陷就是它無法解決循環依賴的問題,因此現在主流的虛擬機基本不會采用這種方式。
可達性分析算法可達性分析算法又叫根搜索算法,該算法的基本思想就是通過一系列稱為「GC Roots」的對象作為起始點,從這些起始點開始往下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到 GC Roots 對象之間沒有任何引用鏈的時候(不可達),證明該對象是不可用的,于是就會被判定為可回收對象。
在 Java 中可作為 GC Roots 的對象包含以下幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象;
方法區中類靜態屬性引用的對象;
方法區中常量引用的對象;
本地方法棧中 JNI(Native 方法)引用的對象。
Java 中是四種引用類型無論是通過引用計數器還是通過可達性分析來判斷對象是否可以被回收都設計到「引用」的概念。在 Java 中,根據引用關系的強弱不一樣,將引用類型劃為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)。
強引用:Object obj = new Object()這種方式就是強引用,只要這種強引用存在,垃圾收集器就永遠不會回收被引用的對象。
軟引用:用來描述一些有用但非必須的對象。在 OOM 之前垃圾收集器會把這些被軟引用的對象列入回收范圍進行二次回收。如果本次回收之后還是內存不足才會觸發 OOM。在 Java 中使用 SoftReference 類來實現軟引用。
弱引用:同軟引用一樣也是用來描述非必須對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在 Java 中使用 WeakReference 類來實現。
虛引用:是最弱的一種引用關系,一個對象是否有虛引用的存在完全不影響對象的生存時間,也無法通過虛引用來獲取一個對象的實例。一個對象使用虛引用的唯一目的是為了在被垃圾收集器回收時收到一個系統通知。在 Java 中使用 PhantomReference 類來實現。
生存還是死亡,這是一個問題在可達性分析中判定為不可達的對象,也不一定就是「非死不可的」。這時它們處于「緩刑」階段,真正要宣告一個對象死亡,至少需要經歷兩次標記過程:
第一次標記:如果對象在進行可達性分析后被判定為不可達對象,那么它將被第一次標記并且進行一次篩選。篩選的條件是此對象是否有必要執行 finalize() 方法。對象沒有覆蓋 finalize() 方法或者該對象的 finalize() 方法曾經被虛擬機調用過,則判定為沒必要執行。
第二次標記:如果被判定為有必要執行 finalize() 方法,那么這個對象會被放置到一個 F-Queue 隊列中,并在稍后由虛擬機自動創建的、低優先級的 Finalizer 線程去執行該對象的 finalize() 方法。但是虛擬機并不承諾會等待該方法結束,這樣做是因為,如果一個對象的 finalize() 方法比較耗時或者發生了死循環,就可能導致 F-Queue 隊列中的其他對象永遠處于等待狀態,甚至導致整個內存回收系統崩潰。finalize() 方法是對象逃脫死亡命運的最后一次機會,如果對象要在 finalize() 中挽救自己,只要重新與 GC Roots 引用鏈關聯上就可以了。這樣在第二次標記時它將被移除「即將回收」的集合,如果對象在這個時候還沒有逃脫,那么它基本上就真的被回收了。
方法區回收前面介紹過,方法區在 HotSpot 虛擬機中被劃分為永久代。在 Java 虛擬機規范中沒有要求方法區實現垃圾收集,而且方法區垃圾收集的性價比也很低。
方法區(永久代)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。
廢棄常量的回收和 Java 堆中對象的回收非常類似,這里就不做過多的解釋了。
類的回收條件就比較苛刻了。要判定一個類是否可以被回收,要滿足以下三個條件:
該類的所有實例已經被回收;
加載該類的 ClassLoader 已經被回收;
該類的 Class 對象沒有被引用,無法再任何地方通過反射訪問該類的方法。
3.2 垃圾回收算法 標記-清除算法正如標記-清除的算法名一樣,該算法分為「標記」和「清除」兩個階段:
首先標記出所有需要回收的對象,在標記完成后回收所有被標記的對象。標記-清除算法是一種最基礎的算法,后續其它算法都是在它的基礎上基于不足之處改進而來的。它的不足體現在兩方面:一是效率問題,標記和清除的效率都不高;二是空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后程序的運行過程中又要分配較大對象是,無法找打足夠的連續內存而不得不提前出發下一次 GC。
復制算法為了解決效率問題,于是就有了復制算法,它將內存一分為二劃分為大小相等的兩塊內存區域。每次只使用其中的一塊。當這一塊用完時,就將還存活的對象復制到另一塊上面,然后再把已使用過的內存空間一次清理掉。這樣做的好處是不用考慮內存碎片問題了,簡單高效。只不過這種算法代價也很高,內存因此縮小了一半。
現在的商業虛擬機都采用這種算法來回收新生代,在 IBM 的研究中新生代中的對象 98% 都是「朝生夕死」,所以并不需要按照 1:1 的比例來劃分空間,而是將內存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活的對象一次性復制到另一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過的 Survivor 空間。 HotSpot 默認 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用的內存為整個新生代容量的 90%(80%+10%),只有 10% 會被浪費。當然,98% 的對象可回收只是一般場景下的數據,我們沒辦法保證每次回收后都只有不多于 10% 的對象存活,當 Survivor 空間不夠用時,需要依賴其它內存(這里指老年代)進行分配擔保。如果另外一塊 Survivor 空間沒有足夠空間存放上一次新生代收集下來存活的對象時,這些對象將直接通過分配擔保機制進入老年代。
標記-整理算法通過前面對復制-收集算法的介紹我們知道,其對老年代這種對象存活時間長的內存區域就不適用了,而標記整理的算法就比較適用這一場景。
標記-整理算法的標記過程與「標記-清除」算法一樣,但是后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
分代回收算法當前商業虛擬機的垃圾搜集都采用「分代回收」算法,這種算法并沒有什么新的思想,只是根據對象存活周期的不同將內存劃分為幾塊。一般是將 Java 堆分為新生代和老年代,這樣可以根據各個年代的特點采用最合適的搜集算法。
在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。
而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清除」或者「標記-整理」算法來進行回收。
3.3 內存分配與回收策略所謂自動內存管理,最終要解決的也就是內存分配和內存回收兩個問題。前面我們介紹了內存回收,這里我們再來聊聊內存分配。
對象的內存分配通常是在 Java 堆上分配(隨著虛擬機優化技術的誕生,某些場景下也會在棧上分配,后面會詳細介紹),對象主要分配在新生代的 Eden 區,如果啟動了本地線程緩沖,將按照線程優先在 TLAB 上分配。少數情況下也會直接在老年代上分配。總的來說分配規則不是百分百固定的,其細節取決于哪一種垃圾收集器組合以及虛擬機相關參數有關,但是虛擬機對于內存的分配還是會遵循以下幾種「普世」規則:
對象優先在 Eden 區分配多數情況,對象都在新生代 Eden 區分配。當 Eden 區分配沒有足夠的空間進行分配時,虛擬機將會發起一次 Minor GC。如果本次 GC 后還是沒有足夠的空間,則將啟用分配擔保機制在老年代中分配內存。
這里我們提到 Minor GC,如果你仔細觀察過 GC 日常,通常我們還能從日志中發現 Major GC/Full GC。
Minor GC 是指發生在新生代的 GC,因為 Java 對象大多都是朝生夕死,所有 Minor GC 非常頻繁,一般回收速度也非常快;
Major GC/Full GC 是指發生在老年代的 GC,出現了 Major GC 通常會伴隨至少一次 Minor GC。Major GC 的速度通常會比 Minor GC 慢 10 倍以上。
大對象直接進入老年代所謂大對象是指需要大量連續內存空間的對象,頻繁出現大對象是致命的,會導致在內存還有不少空間的情況下提前觸發 GC 以獲取足夠的連續空間來安置新對象。
前面我們介紹過新生代使用的是標記-清除算法來處理垃圾回收的,如果大對象直接在新生代分配就會導致 Eden 區和兩個 Survivor 區之間發生大量的內存復制。因此對于大對象都會直接在老年代進行分配。
長期存活對象將進入老年代虛擬機采用分代收集的思想來管理內存,那么內存回收時就必須判斷哪些對象應該放在新生代,哪些對象應該放在老年代。因此虛擬機給每個對象定義了一個對象年齡的計數器,如果對象在 Eden 區出生,并且能夠被 Survivor 容納,將被移動到 Survivor 空間中,這時設置對象年齡為 1。對象在 Survivor 區中每「熬過」一次 Minor GC 年齡就加 1,當年齡達到一定程度(默認 15) 就會被晉升到老年代。
動態對象年齡判斷為了更好的適應不同程序的內存情況,虛擬機并不是永遠要求對象的年齡必需達到某個固定的值(比如前面說的 15)才會被晉升到老年代,而是會去動態的判斷對象年齡。如果在 Survivor 區中相同年齡所有對象大小的總和大于 Survivor 空間的一半,年齡大于等于該年齡的對象就可以直接進入老年代。
空間分配擔保在新生代觸發 Minor GC 后,如果 Survivor 中任然有大量的對象存活就需要老年隊來進行分配擔保,讓 Survivor 區中無法容納的對象直接進入到老年代。
寫在最后對于我們 Java 程序員來說,虛擬機的自動內存管理機制為我們在編碼過程中帶來了極大的便利,不用像 C/C++ 等語言的開發者一樣小心翼翼的去管理每一個對象的生命周期。但同時我們也喪失了內存控制的管理權限,一旦發生內存泄漏如果不了解虛擬機的內存管理原理,就很排查問題。希望這篇文章能對大家理解 Java 虛擬機的內存管理機制有所幫助。如果想對 Java 虛擬機有更進一步的了解,推薦大家去讀周志明老師的《深入理解 Java 虛擬機:JVM 高級特性與最佳實踐》這本書。
好了,關于 Java 虛擬機的自動內存管理機制就介紹到這里,下一篇我們來聊聊「類文件結構」。
參考資料:
《深入理解 Java 虛擬機:JVM 高級特性與最佳實踐(第 2 版)》
如果你喜歡我的文章,就關注下我的公眾號 BaronTalk 、 知乎專欄 或者在 GitHub 上添個 Star 吧!
微信公眾號:BaronTalk
知乎專欄:https://zhuanlan.zhihu.com/baron
GitHub:https://github.com/BaronZ88
個人博客:http://baronzhang.com
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74963.html
摘要:運行時數據區域虛擬機在執行程序的過程中會把它管理的內存劃分成若干個不同的數據區域。堆虛擬機所管理的內存中最大的一塊,堆是所有線程共享的一塊內存區域,在虛擬機啟動時創建。 《深入理解Java虛擬機:JVM高級特性與最佳實踐(第二版》讀書筆記 1 概述 對于Java程序員來說,在虛擬機自動內存管理機制下,不再需要像C/C++程序開發程序員這樣為內一個new 操作去寫對應的delete/...
摘要:深入理解虛擬機高級特性與最佳實踐第二版讀書筆記與常見面試題總結本節常見面試題介紹下內存區域運行時數據區。運行時數據區域虛擬機在執行程序的過程中會把它管理的內存劃分成若干個不同的數據區域。 《深入理解Java虛擬機:JVM高級特性與最佳實踐(第二版》讀書筆記與常見面試題總結 本節常見面試題: 介紹下Java內存區域(運行時數據區)。 對象的訪問定位的兩種方式。 1 概述 對于Java...
摘要:所以我們提到的內存回收大都是指堆內存的回收。根據堆內存對對象的代的劃分我們對堆內存有這樣劃分各版本和種類的垃圾回收器各有其用武之地,配合使用它們得到最好的效果十分重要。 這篇文章的素材來自周志明的《深入理解Java虛擬機》。作為Java開發人員,一定程度了解JVM虛擬機的的運作方式非常重要,本文就一些簡單的虛擬機的相關概念和運作機制展開我自己的學習過程。 虛擬機內存分區 java虛擬機...
摘要:運行時數據區域的學習,是學習以及機制的基礎,也是深入理解對象創建及運行過程的前提。了解內存區域劃分,是學習概念的前提。 Java 運行時數據區域的學習,是學習 jvm 以及 GC 機制的基礎,也是深入理解 java 對象創建及運行過程的前提。廢話不多說,直接進入正題: 一張圖總結 showImg(https://segmentfault.com/img/bVOMAn?w=685&h=5...
閱讀 2249·2021-11-23 09:51
閱讀 1077·2021-11-22 15:35
閱讀 4854·2021-11-22 09:34
閱讀 1604·2021-10-08 10:13
閱讀 3023·2021-07-22 17:35
閱讀 2538·2019-08-30 15:56
閱讀 3086·2019-08-29 18:44
閱讀 3095·2019-08-29 15:32