《深入理解 Java 虛擬機》筆記整理

《深入理解 Java 虛擬機》筆記整理

正文
一、Java 內存區域與內存溢出異常
1、運行時數據區域
程序計數器:當前線程所執行的字節碼的行號指示器。線程私有。
Java 虛擬機棧:Java 方法執行的內存模型。線程私有。
本地方法棧:Native 方法執行的內存模型。線程私有。
Java 堆:存放對象實例。分爲新生代(Eden 空間、From Survivor 空間、To Survivor 空間)和老年代。線程共享。
方法區:存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。也稱爲“永久代”。線程共享。
運行時常量池:方法區的一部分,用於存放編譯期生成的各種字面量和符號引用。 線程共享。
直接內存。
2、對象的創建
類加載檢查 -> 分配內存 -> 初始化零值 -> 設置對象頭 -> 執行 init 方法。

類加載檢查:檢查 new 指令的參數能否在常量池中定位到一個類的符號引用,以及這個符號引用代表的類是否已被加載、解析和初始化過。
分配內存:把一塊確定大小的內存從 Java 堆中劃分出來。
初始化零值:將分配到的內存空間初始化爲零值(不包括對象頭)。
設置對象頭:虛擬機需要對對象進行必要的設置,這些信息存放在對象的對象頭中。
執行 init 方法:把對象按照程序員的意願進行初始化。
3、對象的內存佈局
對象頭:
Mark Word:存儲對象自身的運行時數據。
類型指針:存儲對象的類元數據的指針。
實例數據:對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。
對齊填充:僅僅起着佔位符的作用。
4、對象的訪問定位
句柄:引用中存儲的是對象的句柄地址。Java 堆中劃分出一塊內存作爲句柄池,句柄中包含了對象實例數據、類型數據兩者的具體地址信息。
直接指針:引用中存儲的直接就是對象的地址。
5、OutOfMemoryError 異常
Java 堆溢出。
虛擬機棧和本地方法棧溢出。
方法區和運行時常量池溢出。
本機直接內存溢出。
二、垃圾收集器與內存分配策略
1、判斷對象是否可用
引用計數算法:給對象添加一個引用計數器,每當有一個地方引用它時,計數器值加 1;當引用失效時,計數器值減 1;任何時刻計數器爲 0 的對象就是不可能再被使用的。
可達性分析算法:通過一系列被稱爲“GC Roots”的對象作爲起點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連時,則此對象不可用。
2、四種引用
強引用:類似“Object obj = new Object()”的引用。只要強引用還存在,對象就永遠不會回收。
軟引用:用來描述一些還有用但並非必需的對象。內存不足時,對象有可能被回收。
弱引用:用來描述非必需的對象,但強度比軟引用弱。GC時,無論內存是否足夠,對象都會被回收。
虛引用:也稱幽靈引用或幻影引用,虛引用不會對對象的生存時間構成影響。虛引用的唯一作用就是能在對象被回收時收到一個系統通知。
3、垃圾收集算法
標記-清除算法:分爲“標記”和“清除”兩個階段。首先標記出所有需要回收的對象,然後再統一回收所有被標記的對象。會產生大量不連續的內存碎片。
複製算法:將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中一塊。當一塊內存用完時,就將還存活的對象複製到另一塊,然後再把已使用過的內存空間一次清理掉。
標記-整理算法:首先標記出所有需要回收的對象,然後將所有存活對象向一端移動,最後直接清理掉端邊界以外的內存。
分代收集算法:根據對象存活週期的不同,將 Java 堆劃分爲新生代和老年代,然後根據各個年代的特點採用最適當的收集算法。
新生代:採用複製算法。
老年代:採用“標記-清除”或“標記-整理”算法。
4、垃圾收集器
Serial 收集器:單線程。新生代收集器。
ParNew 收集器:Serial 收集器的多線程版本。新生代收集器。
Parallel Scavenge 收集器:多線程。新生代收集器。關注吞吐量。
Serial Old 收集器:Serial 收集器的老年代版本。單線程。使用“標記-整理”算法。
Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本。多線程。使用“標記-整理”算法。
CMS 收集器:併發收集器。使用“標記-清除”算法。關注點是如何縮短垃圾收集時用戶線程的停頓時間。
G1 收集器:面向服務端應用。並行與併發、分代收集、空間整合、可預測停頓時間。
5、內存分配與回收策略
對象優先在 Eden 分配。
大對象直接進入老年代。
長期存活的對象進入老年代。
動態對象年齡判定。
空間分配擔保。
三、虛擬機性能監控與故障處理工具
1、JDK 的命令行工具
jps:顯示正在運行的虛擬機進程。常用命令:jps -l。
jstat:監視虛擬機各種運行狀態信息。常用命令:jstat -gcutil 。
jinfo:顯示虛擬機配置信息。常用命令:jinfo -flags 。
jmap:主要用於生成堆轉儲快照。常用命令:jmap -dump:format=b,file=。
jhat:分析 jmap 生成的堆轉儲快照。常用命令:jhat 。
jstack:顯示虛擬機當前時刻的線程堆棧信息。常用命令:jstack -l 。
2、JDK 的可視化工具
JConsole:Java 監視與管理控制檯。
VisualVM:多合一故障處理工具。
四、類文件結構
1、無關性的基石
各種不同平臺的虛擬機。
所有平臺都統一使用的字節碼存儲格式。
2、Class 類文件的結構
(1)Class 文件的數據類型
無符號數:基本數據類型,以 u1、u2、u4、u8 來分別代表 1 個字節、2 個字節、4 個字節和 8 個字節的無符號數。用於描述數字、索引引用、數量值或按照 UTF-8 編碼構成字符串值。
表:由多個無符號數或其他表作爲數據項構成的複合數據類型,所有表都習慣性地以“_info”結尾。用於描述有層次關係的複合結構數據,整個 Class 文件本質上就是一張表。
(2)Class 文件格式
類型 名稱 數量
u4 magic(魔數) 1
u2 minor_version(次版本號) 1
u2 major_version(主版本號) 1
u2 constant_pool_count(常量池容量計數器) 1
cp_info constant_pool(常量池) constant_pool_count - 1
u2 access_flags(訪問標誌) 1
u2 this_class(類索引) 1
u2 super_class(父類索引) 1
u2 interfaces_count(接口計數器) 1
u2 interfaces(接口索引集合) interfaces_count
u2 fields_count(字段表計數器) 1
field_info fields(字段表集合) fields_count
u2 methods_count(方法表計數器) 1
method_info methods(方法表集合) methods_count
u2 attributes_count(屬性表計數器) 1
attribute_info attributes(屬性表集合) attributes_count
魔數:Class 文件的頭 4 個字節,用於確定該文件是否爲 Class 文件。其值爲:0xCAFEBABE(咖啡寶貝?)。
Class 文件的版本:第 5、6 個字節是次版本號,第 7、8 個字節是主版本號。
常量池:可以理解爲 Class 文件中的資源倉庫。主要存放字面量和符號引用。每一項常量都是一個表。
訪問標誌:用於識別一些類或接口層次的訪問信息,包括:這個 Class 是類還是接口、是否定義爲 public、是否定義爲 abstract、是否聲明爲 final(只有類可設置)等。
類索引、父類索引與接口索引集合:Class 文件由這三項數據確定這個類的繼承關係。
字段表集合:用於描述接口或類中聲明的變量。包括類變量和實例變量,但不包括在方法內部聲明的局部變量。
方法表集合:用於描述接口或類中聲明的方法。
屬性表集合:在 Class 文件、字段表、方法表都可以攜帶自己的屬性表集合,以用於描述某些場景專有的信息。
3、字節碼指令簡介
加載和存儲指令:用於將數據在棧幀中的局部變量表和操作數棧之間來回傳輸。
運算指令:用於對兩個操作數以上的值進行某種特定運算,並把結果重新存入到操作數棧頂。
類型轉換指令:將兩種不同的數值類型進行相互轉換。
對象創建與訪問指令。
操作數棧管理指令:用於直接操作操作數棧。
控制轉移指令:讓 Java 虛擬機有條件或無條件地從指定位置的指令繼續執行程序,而不是從控制轉移指令的下一條指令繼續執行程序。可認爲控制轉移指令就是在有條件或無條件地修改 PC 寄存器的值。
方法調用和返回指令。
異常處理指令。
同步指令:支持方法級的同步和方法內部一段指令序列的同步。
五、虛擬機類加載機制
1、類加載的過程
加載 -> 連接(驗證、準備、解析) -> 初始化。

加載:獲取二進制字節流,並在內存中生成一個代表這個類的 java.lang.Class 對象,作爲方法區這個類的各種數據的訪問入口。
驗證:確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
文件格式驗證:驗證字節流是否符合 Class 文件格式的規範,並且能被當前版本的虛擬機處理。
元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合 Java 語言規範的要求。
字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。
符號引用驗證:對符合引用進行匹配性校驗,確保解析動作能正常執行。
準備:爲類變量分配內存並設置初始值。
解析:將常量池內的符號引用替換爲直接引用。
初始化:根據程序員的主觀計劃去初始化類變量和其他資源。
2、類加載器
啓動類加載器:負責將存放在 lib 目錄的,或者 -Xbootclasspath 參數所指定路徑中的,能被虛擬機識別的類庫加載到虛擬機內存中。
擴展類加載器:負責加載 libext 目錄中的,或者 java.ext.dirs 系統變量所指定路徑中的所有類庫。
應用程序類加載器:負責加載用戶類路徑上所指定的類庫。
3、雙親委派模型
如果一個類加載器收到類加載的請求,它會先把這個請求委派給父加載器去完成,而不會自己去嘗試加載這個類。只有父加載器無法完成這個加載請求時,子加載器纔會嘗試自己去加載。

六、虛擬機字節碼執行引擎
1、運行時棧幀結構
棧幀是用於支持虛擬機進行方法調用和方法執行的數據結構。棧幀存儲了方法的局部變量表、操作數棧、動態連接、方法返回地址和一些額外的附加信息。每一個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機裏面從入棧到出棧的過程。

局部變量表:是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。
操作數棧:也稱爲操作棧,它是一個後入先出的棧。操作數棧的每一個元素可以是任意的 Java 數據類型。
動態連接:每個棧幀都包含一個指向運行時常量池中,該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接。
方法返回地址:方法退出後需要返回到方法被調用的位置,程序才能繼續執行。
附加信息:虛擬機規範允許具體的虛擬機實現增加一些規範裏沒有描述的信息到棧幀中,例如與調試相關的信息。
2、方法調用
方法調用並不等於方法執行,方法調用階段唯一的任務就是確定被調用方法的版本(即調用哪一個方法)。此時,在 Class 文件裏存儲的只是符號引用,而不是直接引用,只有在類加載期間,甚至是運行期間才能確定目標方法的直接引用。

解析:在類加載的解析階段,將方法的符號引用轉化爲直接引用,這類方法調用稱爲解析。這種解析能成立的前提是:方法在程序執行之前有一個可確定的調用版本,並且這個方法的調用版本在運行期不可改變,即“編譯期可知,運行期不可變”。
分派:
靜態分派:在編譯期依賴靜態類型(又稱外觀類型)來定位方法執行版本的分派動作,稱爲靜態分派。靜態分派的典型應用是方法重載。
動態分派:在運行期根據實際類型確定方法執行版本的分派過程,稱爲動態分派。動態分派的典型應用是方法重寫。
七、早期(編譯期)優化
1、Javac 編譯過程
(1)解析與填充符號表
詞法分析:將源代碼的字符流轉變爲標記(Token)集合,標記是編譯過程的最小元素,關鍵字、變量名、字面量、運算符都可以成爲標記。
語法分析:根據 Token 序列構造抽象語法樹。
填充符號表:符號表是由一組符號地址和符號信息構成的表格,可以把它想象成哈希表中 K-V 值對的形式。
(2)註解處理
在編譯期間對註解進行處理。可以讀取、修改、添加抽象語法樹中的任何元素。

(3)語義分析與字節碼生成
語義分析:對結構上正確的源程序進行上下文邏輯審查。
標註檢查:包括變量使用前是否已被聲明、變量與賦值之間的數據類型是否能夠匹配等。
數據及控制流分析:對程序上下文邏輯進行更進一步的驗證,包括局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理等。
解語法糖:虛擬機運行時並不支持語法糖的語法,因此,需要在編譯階段還原回簡單的基礎語法結構。
字節碼生成:把前面各個步驟所生成的信息(語法樹、符號表)轉化成字節碼寫到磁盤中,同時還進行了少量的代碼添加和轉換工作。
2、Java 語法糖
泛型與類型擦除:泛型的本質是參數化類型的應用,即將所操作的數據類型指定爲一個參數。
自動裝箱與拆箱、遍歷循環、變長參數。
條件編譯:編譯器在編譯時只對滿足條件的代碼進行編譯,而將不滿足條件的代碼捨棄。Java 語言可以使用條件爲布爾常量值的 if 語句進行條件編譯。
八、晚期(運行期)優化
1、HotSpot 虛擬機內的即時編譯器
(1)解釋器與編譯器
當程序需要迅速啓動和執行時,解釋器可以首先發揮作用,省去編譯的時間,立即執行。
在程序運行後,隨着時間的推移,編譯器把越來越多的代碼編譯成本地代碼後,可以獲取更高的執行效率。
(2)C1、C2 編譯器
C1 編譯器(Client Compiler):運行在 Client 模式。
C2 編譯器(Server Compiler):運行在 Server 模式。
(3)混合模式、解釋模式與編譯模式
混合模式:解釋器與編譯器搭配使用的方式。
解釋模式:全部代碼都使用解釋方式執行,編譯器完全不介入工作。
編譯模式:優先採用編譯方式執行,但是解釋器仍會在編譯無法進行時介入執行過程。
(4)分層編譯
分層編譯根據編譯器編譯、優化的規模與耗時,劃分出不同的編譯層次。

第 0 層:程序解釋執行,解釋器不開啓性能監控功能,可觸發第 1 層編譯。
第 1 層:也稱 C1 編譯,將字節碼編譯爲本地代碼,進行簡單、可靠的優化,必要時加入性能監控的邏輯。
第 2 層(或 2 層以上):也稱 C2 編譯,也是將字節碼編譯爲本地代碼,但會啓用一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化。
2、即時編譯觸發條件
(1)熱點代碼
被多次調用的方法。
被多次執行的循環體。
(2)熱點探測
判斷一段代碼是不是熱點代碼,是不是需要觸發即時編譯,這樣的行爲稱爲熱點探測。

基於採樣的熱點探測:虛擬機週期性地檢查各個線程的棧頂,如果發現某個方法經常出現在棧頂,那這個方法就是“熱點代碼”。
基於計數器的熱點探測:虛擬機爲每個方法(甚至是代碼塊)建立計數器,統計方法的執行次數,如果執行次數超過一定閾值就認爲它是“熱點代碼”。
HotSpot 虛擬機使用的是基於計數器的熱點探測方法,它爲每個方法準備了兩類計數器。

方法調用計數器:統計方法被調用的次數。
回邊計數器:統計一個方法中循環體代碼執行的次數。
3、編譯優化技術
公共子表達式消除:如果一個表達式 E 已經計算過了,並且從先前計算到現在 E 中所有變量的值都沒有變化,那麼 E 的這次出現就成了公共子表達式。對於這種表達式,沒有必要再次進行計算,直接用前面計算過的表達式結果代替 E 即可。
數組邊界檢查消除:編譯器通過數據流分析判定數組下標是否會越界,如果分析後確定不會越界,那麼可以把數組的上下界檢查消除。
方法內聯:把目標方法的代碼“複製”到發起調用的方法之中,避免發生真實的方法調用。
逃逸分析:當一個對象在方法中定義後,如果它被外部方法所引用或被外部線程訪問到,那麼就說這個對象發生了逃逸。如果一個對象不會逃逸到方法或線程之外,那麼可以爲這個變量進行一些高效的優化,比如棧上分配、同步消除、標量替換等。
九、Java 內存模型與線程
1、Java 內存模型
(1)主內存與工作內存
所有的變量都存儲在主內存中。每條線程有自己的工作內存,工作內存中保存了被該線程使用到的變量的主內存副本拷貝。
線程對變量的操作必須在工作內存中進行,而不能直接讀寫主內存中的變量。
不同的線程之間無法直接訪問對方工作內存中的變量,線程間變量值的傳遞需要通過主內存來完成。
(2)內存間交互操作
lock(鎖定):把一個主內存變量標識爲一條線程獨佔的狀態。
unlock(解鎖):把一個處於鎖定狀態的主內存變量釋放出來。
read(讀取):把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的 load 動作使用。
load(載入):把 read 操作從主內存中得到的變量值放入工作內存的變量副本中。
use(使用):把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作。
assign(賦值):把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
store(存儲):把工作內存中一個變量的值傳送到主內存中,以便隨後的 write 操作使用。
write(寫入):把 store 操作從工作內存中得到的變量的值放入主內存的變量中。
(3)volatile 的作用
保證變量對所有線程的可見性。
禁止指令重排序優化。
(4)原子性、可見性與有序性
原子性:
基本數據類型的訪問讀寫具備原子性: Java 內存模型直接保證了 read、load、assign、use、store 和 write 操作的原子性。
synchronized 代碼塊之間的操作具備原子性:底層通過 lock 和 unlock 操作實現。
可見性:當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。Java 內存模型通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作爲傳遞媒介的方式來實現可見性。
有序性:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指“線程內表現爲串行的語義”,後半句是指“指令重排序”現象和“工作內存與主內存同步延遲”現象。
(5)先行發生原則
程序次序規則:在一個線程內,按照程序代碼順序,書寫在前的操作先行發生於書寫在後的操作。準確地說,是控制流順序而不是程序代碼順序,因爲要考慮分支、循環等結構。
管程鎖定規則:一個 unlock 操作先行發生於後面(時間上的先後順序)對同一個鎖的 lock 操作。
volatile 變量規則:對一個 volatile 變量的寫操作先行發生於後面(時間上的先後順序)對這個變量的讀操作。
線程啓動規則:Thread 對象的 start() 方法先行發生於此線程的每一個動作。
線程終止規則:線程中的所有操作都先行發生於對此線程的終止檢測。
線程中斷規則:對線程 interrupt() 方法的調用先行發生於被中斷線程檢測到中斷事件的發生。
對象終結規則:一個對象的初始化完成(構造函數執行結束)先行發生於它的 finalize() 方法的開始。
傳遞性:如果操作 A 先行發生於操作 B,操作 B 先行發生於操作 C,那麼可以得出操作 A 先行發生於操作 C。
2、Java 與線程
(1)線程的實現
使用內核線程實現:內核線程就是直接由操作系統內核支持的線程。
使用用戶線程實現:用戶線程完全建立在用戶空間的線程庫上,系統內核不能感知線程的存在。
使用用戶線程加輕量級進程混合實現:用戶線程還是完全建立在用戶空間中,而操作系統提供支持的輕量級進程則作爲用戶線程和內核線程之間的橋樑。
(2)Java 線程調度
協同式線程調度:線程的執行時間由線程本身來控制,線程執行完之後,主動通知系統切換到另外一個線程上。
搶佔式線程調度:每個線程由系統來分配執行時間,線程的切換不由線程本身來決定。
Java 使用的線程調度方式就是搶佔式調度。

(3)線程狀態
新建(New):線程創建後尚未啓動。
運行(Runable):包括了操作系統線程狀態中的 Running 和 Ready,處於此狀態的線程有可能正在執行,也有可能正在等待着 CPU 爲它分配執行時間。
無限期等待(Waiting):不會被分配 CPU 執行時間,等待着被其他線程顯式地喚醒。
限期等待(Timed Waiting):不會被分配 CPU 執行時間,無須等待被其他線程顯式地喚醒,在一定時間之後會由系統自動喚醒。
阻塞(Blocked):線程被阻塞了,在等待着獲取到一個排他鎖。在程序等待進入同步區域的時候,線程將進入這種狀態。
結束(Terminated):已終止線程的線程狀態,線程已經結束執行。
十、線程安全與鎖優化
1、Java 語言中的線程安全
按線程安全的“安全程度”由強至弱排序,可以將多個線程的共享數據分爲 5 類:不可變、絕對線程安全、相對線程安全、線程兼容和線程對立。

不可變:不可變的對象一定是線程安全的,無論是對象的方法實現還是方法的調用者,都不需要再採取任何的線程安全保障措施。
絕對線程安全:必須滿足“不管運行時環境如何,調用者都不需要任何額外的同步措施”。
相對線程安全:就是我們通常意義上所講的線程安全,它需要保證對一個對象單獨的操作是線程安全的,但是對於一些特定順序的連續調用,則需要在調用端使用額外的同步手段來保證調用的正確性。
線程兼容:對象本身並不是線程安全的,但可以通過在調用端正確地使用同步手段來保證對象在併發環境中可以安全地使用。
線程對立:無論調用端是否採取了同步措施,都無法在多線程環境中併發使用的代碼。
2、線程安全的實現方法
互斥同步(阻塞同步):同步是指在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個線程使用,而互斥是實現同步的一種手段。
非阻塞同步:在進行同步操作時,不需要把線程掛起,而是先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了;如果共享數據有爭用,產生了衝突,那就採取其他的補償措施。
無同步方案:
可重入代碼(純代碼):如果一個方法的返回結果是可以預測的,只要輸入了相同的數據,就都能返回相同的結果,那它就滿足可重入性的要求,當然也就是線程安全的。
線程本地存儲:如果能保證使用共享數據的代碼在同一個線程中執行,那麼就可以把共享數據的可見範圍限制在同一個線程之內。這樣,無須同步也能保證線程之間不出現數據爭用的問題。
3、鎖優化
自旋鎖:如果物理機有多個處理器,能讓多個線程同時並行執行,那麼可以讓後面請求鎖的線程“稍等一下”,但不放棄處理器的執行時間,然後看看持有鎖的線程是否很快就會釋放鎖。爲了讓線程等待,只需讓線程執行一個忙循環(自旋),這就是所謂的自旋鎖。
鎖消除:鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。
鎖粗化:如果一系列的連續操作都對同一個對象反覆加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那麼虛擬機將會把加鎖同步的範圍擴展(粗化)到整個操作序列的外部,這樣只需要加鎖一次就可以了。
輕量級鎖:輕量級鎖並不是用來代替重量級鎖的,而是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。對象頭的 Mark Word 有個鎖標誌位,用於標識同步對象的鎖狀態。
偏向鎖:偏向鎖是指這個鎖會偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。
原文地址https://www.cnblogs.com/jingqueyimu/p/12716260.html

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