Java GC 專家系列3:GC調優實踐

本篇是”GC專家系列“的第三篇。在第一篇理解Java垃圾回收中我們學習了幾種不同的GC算法的處理過程,GC的工作方式,新生代與老年代的區別。所以,你應該已經瞭解了JDK 7中的5種GC類型,以及每種GC對性能的影響。

在第二篇Java垃圾回收的監控中介紹了在真實場景中JVM是如何運行GC,如何監控GC數據以及有哪些工具可用來方便進行GC監控。

在本篇中,我將基於真實的案例來介紹一些GC調優的最佳選項。寫本篇文章時,我假設你已經理解了前兩篇的內容。爲了深入理解本部分內容,你最好先瀏覽一下前兩篇的內容——如果你尚未了解的話。

GC調優是必須的嗎

更精確的說, 基於Java的服務是否一定需要GC調優 ?應該說,GC調優並非所有Java服務都必須做的事情。當然這是基於你已經使用了下面的選項或事實:

  • 通過 -Xms 和 -Xmx 選項指定了內存大小

  • 使用了 -server 選項

  • 系統未產生太多超時日誌

也就是說,如果你未設置內存大小並且你的系統產生了過多的超時日誌,恭喜你需要爲你的系統執行GC調優。

但是,請記住: GC調優是不得已時的選擇 

思考一下GC調優的深層原因。垃圾回收器會去清理Java中創建的對象。GC需要清理的對象數據以及GC執行的次數取決於應用創建對象的多少。因此,爲了控制GC的執行,首先你需要 減少對象的創建 

俗話說“積重難返”。所以我們需要從小處着手,否則它們將不斷壯大直到難以管理。

  • 應該多使用 StringBuilder 和 StringBuffer 對象替代 String 。

  • 減少不必要的日誌輸出。

即便如此,面對有些場景我們依然無能爲力。我們知道解析XML和JSON會佔用大量的內存空間。即便我們儘可能少的使用 String ,儘可能好的優化日誌輸出,然而在解析XML和JSON時仍然會有大量的內存開銷,甚至有10~100MB之多,可我們很難杜絕XML和JSON的使用。但是請記住:XML和JSON會帶來很大的內存開銷。

如果應用的內存佔用不斷提升,你就要開始對其進行GC調優了。我把GC調優的目標分爲以下兩類:

  • 降低移動到老年代的對象數量

  • 縮短Full GC的執行時間

降低移動到老年代的對象數量

在Oracle JVM中除了JDK 7及最高版本中引入的G1 GC外,其他的GC都是基於分代回收的。也就是對象會在Eden區中創建,然後不斷在Survivor中來回移動。之後如果該對象依然存活,就會被移到老年代中。有些對象,因爲佔用空間太大以致於在Eden區中創建後就直接移動到了老年代。老年代的GC較新生代會耗時更長,因此減少移動到老年代的對象數量可以降低full GC的頻率。減少對象轉移到老年代可能會被誤解爲把對象保留在新生代,然而這是不可能的,相反你可以 調整新生代的空間大小 

縮短Full GC耗時

Full GC的單次執行與Minor GC相比,耗時有較明顯的增加。如果執行Full GC佔用太長時間(例如超過1秒),在對外服務的連接中就可能會出現超時。

  • 如果企圖通過縮小老年代空間的方式來降低Full GC執行時間,可能會面臨 OutOfMemoryError 或者帶來更頻繁的Full GC。

  • 如果通過增加老年代空間來減少Full GC執行次數,單次Full GC耗時將會增加。

因此,需要 爲老年代空間設置適當的大小 

影響GC性能的選項

在理解Java垃圾回收的結尾,我說過不要有這樣的想法: 別人通過某個GC選項獲得了明顯的性能提升,爲什麼我不直接用這個選項呢 。因爲 不同的服務所擁有的對象數量和對象的生命週期是不同的 

一個簡單場景,如果執行一個任務需要五個條件:A, B, C, D和E,另外一個任務只需要兩個條件A和B,哪個任務會快一些?通常只需要條件A和B的任務會快一些。

Java GC選項的設置也是一樣的道理。設置很多選項未必能提高GC執行速度,相反還可能會更加耗時。 GC調優的基本規則是對兩臺或更多的服務器設置不同的選項,並對比性能表現 ,然後把被證明能提升性能的選項添加到應用服務器上。請記住這一點。

下表列出了與內存相關的且會影響性能的GC選項:

表1: GC調優需要關注的選項

分類選項說明
堆空間-Xms啓動JVM時的初始堆空間大小

-Xmx堆空間最大值
新生代空間-XX:NewRatio新生代與老年代的比例

-XX:NewSize新生代大小

-XX:SurvivorRatioEden區與Survivor區的比例

我經常會使用的選項是: -Xms , -Xmx 和 -XX:NewRatio ,其中 -Xms 和 -Xmx 是必須的。而如何設置 -XX:NewRatio 對性能會有顯著的影響。

可能有人會問 如何設置永久代(Perm)的大小 , 可以使用 -XX:PermSize 和 -XX:MaxPermSize 進行設置,但記住只有發生由Perm空間不足導致的 OutOfMemoryError 時才需要設置。

另外一個會影響GC性能的選項是GC類型,下表列出了JDK 6.0中能使用的相關設置選項:

表2: GC類型選項

分類選項說明
Serial GC-XX:+UseSerialGC
Parallel GC-XX:+UseParallelGC-XX:ParallelGCThreads=<value>
Parallel Compacting GC-XX:+UseParallelOldGC
CMS GC-XX:+UseConcMarkSweepGC
-XX:UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=<value>
-XX:+UseCMSInitiatingOccupancyOnly

G1-XX:+UnlockExperimentalVMOptions-XX:+UseG1GC在JDK6中使用G1時,這兩個選項必須同時設置

除了G1,其他GC類型都是通過每個選行列的第一行選項進行設置。通常最不會使用的是Serial GC,它是爲client應用優化和設計的。

還有很多其他影響GC性能的選項,但不如上面這些對性能的影響明顯。另外設置更多選項未必能優化GC的執行時間。

GC調優過程

GC調優過程與一般的性能改進流程很相似,下面會介紹我在GC調優過程中的流程。

1. 監控GC狀態

首先需要監控GC狀態信息以明確在GC操作過程中對系統的影響。具體方式可以回顧上一篇文章:Java 垃圾回收的監控。

2. 分析監控數據並決定是否需要GC調優

然後通過GC操作狀態,對監控結果進行分析,並判斷是否有必要進行GC調優。如果分析結果顯示GC耗時在0.1-0.3秒以內的話,一般不需要花費額外的時間做GC調優。然而, 如果GC耗時達到1-3秒甚至10秒以上,就需要立即對系統進行GC調優 。

但是如果你的應用分配了10GB的內存,且不能降低內存容量的話,其實是沒辦法進行GC調優的。這種情況下,你首先要去思考爲什麼需要分配這麼大的內存。如果只給應用分配了1GB或者2GB內存,當有 OutOfMemeoryError 發生時,你需要通過堆dump來分析驗證內存溢出的原因並進行修復。

註釋:堆dump是把內存情況按一定格式輸出到文件,可用於檢查Java 內存中的對象和數據情況。可使用JDK中內置的 jmap 命令創建堆dump文件。創建文件過程中,Java進程會中斷,因此不要在正常運行時系統上做此操作。

3. 設置GC類型和內存大小

如果決定做GC調優,就需要考慮如何選擇GC類型、如何設置內存大小。如果你有多臺服務器,可通過爲每臺服務器設置不同的GC選項並對比不同的表現,這一步很重要。

4. 分析GC調優結果

設置GC選項後,至少要收集24小時的GC表現數據,然後就可以着手分析這些數據了。如果足夠幸運,通過分析就剛好找到了最合適的GC選項。否則就需要分析GC日誌,並分析內存的分配情況。然後通過不同的調整GC類型和內存大小來找到系統的最優選項。

5. 如果結果可接受,則對所有服務應用調優選項並停止調優

如果GC結果令人滿意,就可以把相應的選項應用到所有服務器並停止GC調優。

下面的章節會詳細介紹每個步驟中的詳細過程。

監控GC狀態並分析GC結果

監控Web應用(WAS: Web Application Server)GC運行狀態的最好方式是使用 jstat 命令。在Java 垃圾回收的監控部分已經介紹瞭如何使用jstat命令,所以這裏就直接介紹怎麼樣來校驗結果數據。

下面的例子中列出了JVM未做GC調優時的數據:

$ jstat -gcutil 21719 1s
S0    S1    E    O    P    YGC    YGCT    FGC    FGCT GCT48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.67348.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673

看一下表中的YGC和YGCT,YGCT 除以 YGC算出平均單次YGC耗時爲0.05秒。也就是說在新生代執行一次垃圾回收的平均耗時爲50毫秒。通過這份結果,我們可以無須關注新生代的垃圾回收。

然後再看一下FGCT和FGC,FGCT除以FGC算出平均單次FGC耗時爲19.68秒。也就是平均需要消耗19.68秒來執行一次Full GC。上面的結果(共3次Full GC)可能是每次Full GC都耗時19.68秒,也有可能是其中兩次都只耗時1秒,而另外一次卻消耗了58秒。然而不管哪種情況,都迫切需要進行GC調優。

當然也可以通過 jstat 來校驗結果,不過分析GC的最好方式是使用 -verbosegc 選項來啓動JVM。在前面的文章中我已經詳細介紹了生成日誌的方式以及如何進行分析。就分析 -verbosegc 日誌而言, HPJMeter 是我最偏愛的工具,因爲它簡單易用。使用HPJMeter可以輕鬆獲取GC執行時間的開銷以及GC發生的頻率。

如果GC執行時間滿足以下判斷條件,那麼GC調優並沒那麼必須。

  • Minor GC執行迅速(50毫秒以內)

  • Minor GC執行不頻繁(間隔10秒左右一次)

  • Full GC執行迅速(1秒以內)

  • Full GC執行不頻繁(間隔10分鐘左右一次)

括號內的值並非絕對,依據應用的服務狀態會有不同。有些服務可能要求Full GC處理速度不能超過0.9秒,另外一些服務可能會寬鬆些。因此校驗GC結果並根據具體的服務需要,決定是否要進行GC調優。

在校驗GC狀態時,不要只關心Minor GC和Full GC的耗時,也要 GC執行次數也同樣重要 。如果新生代太小,Minor GC就會頻繁執行(甚至每間隔1秒就要執行一次)。另外,新生代太小導致轉移到老年代的對象增多,也會引起Full GC的頻繁執行。因此使用`-gccapacity`配合jstat命令,以檢查內存空間的使用情況。

設置GC類型和內存大小

設置GC類型

Oracle JVM提供了5種GC類型,如果是低於JDK 7的版本,可以使用Parallel GC, Parallel Compacting GC, CMS GC。當然,到底選哪一個並沒有統一的準則或標準。

所以 如何選擇合適的GC類型 ?推薦方案是將這三種GC都應用到應用中進行對比。不過可以明確的是CMS GC肯定比Parallel GCs更快,即然這樣只使用CMS GC便好。然而CMS GC也有出問題的時候,通常Full GC中使用CMS GC會執行更快,如果CMS GC的併發模式失敗,則會出現比Parallel GCs慢的情況。

併發模式失敗

我們來深入看一下併發模式失敗的場景。

Parallel GC與CMS GC最大的區別在於壓縮任務。壓縮任務通過壓縮內存使用來移除內存中的碎片空間,以清理兩塊已分配使用的內存空間中的間隙。

在Parallel GC中,只要執行Full GC便會進行內存壓縮,因此耗時更長。不過Full GC之後,因爲壓縮的原故,可以分配連續的空間,所以內存的分配速度爲更快一些。

與之相反,CMS GC的執行中並不會伴隨內存壓縮,因此GC速度會更快一些。然而,因此未做內存壓縮, GC清理過程中釋放的內存便會成爲空閒空間。因爲空間不連續,可能會導致在創建大對象時空間不足。例如,如果老年代尚有300M空閒,卻不能爲10MB的對象分配足夠的連續空間。這時便會發生 併發模式失敗 的警告,並觸發內存壓縮。如果使用CMS GC,在內存壓縮過程中可能會比Parallel GCs更爲耗時,也可能會帶來其他問題。關於”併發模式失敗”更詳細的介紹可以看Oracle 工程師的文章:理解CMS GC 日誌 

結論就是,要爲你的系統尋找合適的GC類型。

每個系統都有一個最適當的GC類型,所以你需要找到這個GC類型。如果你有6臺服務器,建議你爲每兩組設置相同的選項,並通過 -verbosegc 選項對結果進行分析和比較。

調整內存大小

下面先列出內存大小與GC執行次數、每次GC耗時之間的關係:

  • 大內存

    • 會降低GC執行次數

    • 相應的會增加GC執行耗時

  • 小內存

    • 會縮知單次GC耗時

    • 相應的會增加GC執行次數

當然,關於使用大內存還是小內存並沒有唯一正確的答案。如果服務器資源足夠且Full GC執行耗時能控制在1秒以內,使用10GB的內存也是可以的。但大多數時候如果設置內存爲10GB,GC執行效果並不盡人意,執行一次Full GC可能要消耗10~30秒(具體時長也會根據對象大小情況而不同)。

既然如此, 如何正確設置內存大小 。通常情況下,我會推薦500MB大小。這不是說你要把自己的WAS(Web Application Server)內存選項設置爲 -Xms500 和 -Xmx500m 。基於當前未調優時的場景,檢查Full GC之後內存大小變化。如果Full GC之後尚有300MB空間剩餘,這樣最好把內存設置到1GB(300MB(默認使用) + 500MB(老年代最小容量) + 200MB(空閒空間))。這意味着你應該才老年代至少設置500MB空間。如果你有3臺服務器,可以分別設置1GB、1.5GB和2GB,並檢查每臺機器的執行結果。

理論上,根據內存大小不同單次執行GC速度應該是1GB > 1.5GB > 2GB,所以1GB的內存會中三個之中GC速度最快的。但並不能保證1GB的內存Full GC耗時1秒,2GB的內存Full GC耗時2秒。實際耗時與機器性能和對象大小也有關係。所以最好的度量方式是設置每種可能性並分析他們的監控結果。

有設置內存大小時,還需要設置另外一選項: NewRatio 。 NewRatio 是新生代與老年代的比值的倒數(即老年代與新生代的比值)。如果 XX:NewRatio=1 ,就是說新生代 : 老年代的比值爲1:1。對於1GB內存,就是新生代與老年代各500MB。如果 NewRatio 的值是2,則是新生代 : 老年代的值爲1:2。因此比值設置的越大,老年代的空間就越大,相應的新生代空間會越小。

設置 NewRatio 也不是一件重要的事,但可能會對整個GC性能帶來嚴重影響。如果新生代太小,對象就會轉移到老年代,引起頻繁的Full GC,導致更多的耗時。

你可能簡單的認爲設置 NewRatio=1 會帶來最佳的效果,然而並非如此。把 NewRatio 設置爲2或3更容易帶來好的GC表現。當然我也實際遇到過一些這樣的例子。

完成GC調優的最快途徑是什麼?通過對比性能測試的結果是得到GC調優結果的最快途徑。通過爲每個服務器設置不同的選項並觀察GC狀態,最好能觀察1到2天的數據。如果是通過性能測試來做GC調優的話,要爲每個服務器準備相同的負載和業務操作。請求比例的分配也要與業務條件相一致。然而即便是專業的性能測試人員,準備精確的負載數據也並非易事,通常需要花費很大精力來做準備。所以更簡捷的GC調優方式就是對業務應用準備GC選項,然後通過等待GC結果並進行分析,儘管可能需要更長的等待時間。

分析GC調優結果

在應用GC選項並設置 -verbosegc 後,可以通過 tail 命令檢查日誌是否按期望的方式正常輸出。如果選項未精確的設置或者沒有按期望輸出,你所花費的時間都將白費。如果日誌輸出與期望相符,等待1到2天的運行後便可檢查和分析結果。最簡單的方式是把日誌文件複製到本地PC,並使用 HPJMeter 進行分析。

分析過程中主要關注以下數據,下面列表是按我自己定義的優先級列出的。其中決定GC選項的最重要的數據是Full GC執行時間。

  • Full GC(平均)耗時

  • Minor GC(平均)耗時

  • Full GC執行間隔

  • MinorGC執行間隔

  • Full GC整體耗時

  • Minor GC整體耗時

  • GC整體耗時

  • Full GC執行次數

  • Minor GC執行次數

如果足夠幸運,你能恰好找到合適的GC選項,通常你並沒這麼幸運。執行GC調優時一定要格外小心,因爲如果你試圖一次就完成GC調優,得到的可能會是 OutOfMemoryError 。

調優案例

上面我們對於GC調優的討論還僅是紙上談兵,現在開始我們看一些具體的GC調優的案例。

案例1

這個例子是爲服務S進行的GC優化。對於這個新上線的服務S,在執行Full GC時有些過於耗時。

先看一下 jstat -gcutil 的結果:

S0 S1 E O P YGC YGCT FGC FGCT GCT12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993

在開始進行調優時不用太關心 持久代 空間的設置,相對而言YGC的數值更值得關注。

從上面的結果中我們可算出執行Minor GC和Full GC的平均時間上的開銷,如下表:

表3:服務S執行Minor GC和Full GC的平均耗時

GC類型GC 執行次數GC執行時間平均耗時
Minor GC542.04737 ms
Full GC56.9461389 ms

對於Minor GC來說, 37 ms 還不算壞,而Full GC的平均耗時 1.389 s 對於系統來說在執行Full GC時可能會導致頻繁的超時現象,例如DB超時設置爲1 s的話就會發生超時。所以這個案例中的系統需要進行GC調優。

首先在開始GC調優之前先檢查當前的內存設置。可以使用 jstat -gccapacity 選項查看內存的使用情況。下面是服務S的檢查結果:

NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC PGCMN PGCMX PGC PC YGC FGC212992.0 212992.0 212992.0 21248.0 21248.0 170496.0 1884160.0 1884160.0 1884160.0 1884160.0 262144.0 262144.0 262144.0 262144.0 54 5

其中關鍵的數據如下:

  • 新生代使用:212, 992 KB(約208 MB)

  • 老年代使用:1,884,160 KB(約1.8 GB)

所以除去持久代之外的內存分配爲2 GB,且新生代 : 老年代爲 1:9 (即 NewRatio=9 )。爲了看到更詳細的信息,對系統的三個不同實現均設置了 -verbosegc 並分別設置了 NewRatio 選項,除此之外未添加其他選項。

  • NewRatio = 2

  • NewRatio = 3

  • NewRatio = 4

一天之後檢查GC時日誌時幸運的發生,在設置 NewRatio 之後尚未有Full GC發生。

發生了什麼?因爲大多數對象在創建之後不久就被銷燬,所以新生代裏的對象在移到老年代之前就被銷燬掉了。

既然如此,就沒必要再設置其他選項,只是選擇好最佳的 NewRatio 即可。 如何選取最佳NewRatio ?只能逐個分析設置不同 NewRatio 值時的Minor GC的平均耗時。

上面三個 NewRatio 設置對應的Minor GC平均耗時如下:

  • NewRatio=2: 45ms

  • NewRatio=3: 34ms

  • NewRatio=4: 30ms

因爲 NewRatio=4 時Minor GC具有最小的耗時,所以就是我們選擇的最佳設置,即便此時新生代的空間相對較小。應用此選項後,服務再也沒有Full GC發生。

下面是系統重新設置過選項後,某天通過 jstat -gcutil 查看到的結果:

S0 S1 E O P YGC YGCT FGC FGCT GCT8.61 0.00 30.67 24.62 22.38 2424 30.219 0 0.000 30.219

你可能認爲因爲系統接收的請求太少以致於GC發生頻率較低,然而在Minor GC執行了2,424次的情況下系統未發生Full GC。

案例2

下面介紹的是服務A的例子。我們在公司的應用性能管理平臺(APM: Application Performance Manager)上發現服務A的JVM週期性的出現長時間的停頓(超過8秒未有響應)的現象。所以我們決定對其進行GC調優。經過排查我們發現此係統在執行Full GC時太過耗時,需要進行優化。

在着手優化之前,我們爲系統加上了 -verbosegc 選項,輸出結果如下圖:

[譯]GC專家系列3-GC調優

圖1:GC調優之前的GC耗時

上圖是HPJMeter自動分析結果後提供的系統GC隨着JVM運行的耗時圖。 X-軸 是JVM從啓動後的運行時間軸, Y-軸 是每次GC的響應時間。其中綠色的是Full GC使用的CMS垃圾回收的耗時,藍色的是Minor GC使用的Parallel Scavenge垃圾回收的耗時。

前面我說過CMS GC是最快的,但上圖可看到有場景耗時竟達到15秒之多。 什麼原因導致這種後果?回想一下我前面說過的:當內存壓縮時CMS將會變慢。另外服務A設置了 -Xms1g 和 -Xmx4g 的選項,操作系統爲其分配的內存爲4 GB。

然後我把GC類型由GMS換成了Parallel GC,並把內存大小設置爲2G, NewRatio 設置爲3。一段時間之後通過 jstat -gcutil 查看到的結果如下:

S0 S1 E O P YGC YGCT FGC FGCT GCT0.00 30.48 3.31 26.54 37.01 226 11.131 4 11.758 22.890

Full GC的速度提升了,與4GB內存時的15秒相比,現在平均每次只需要3秒。但3秒仍然不盡人意,所以我設計了以下六組選項:

  • -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=2

  • -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=3

  • -XX:+UseParallelGC -Xms1g -Xmx1g -XX:NewRatio=3

  • -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=2

  • -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=3

  • -XX:+UseParallelOldGC -Xms1g -Xmx1g -XX:NewRatio=3

哪一個會更快呢?結果顯示內存越小,速度越快。下圖是第六組選項的GC持續時長分佈圖,代表了最優的GC性能提升。圖中看到最慢的爲1.7秒,而平均值降低到1秒以內。

[譯]GC專家系列3-GC調優

圖2:使用第六組選項後的GC耗時

因此我把服務A的GC選項調整爲了第六組中的設置,然而每天夜裏卻連續發生了 OutOfMemoryError 。箇中艱辛不再細說,簡而言之就是批量的數據處理任務導致了JVM內存泄露。到此爲止,所有的問題都明瞭了。

如果只對GC日誌做短時間的觀察例把GC調優的結果應用到所有服務器上是一件非常危險的事情。一定要記住,如果GC調優能夠順利執行而無故障只有一條途徑:像分析GC日誌一樣分析系統的每一個服務操作。

上面通過兩個GC調優的案例演示了GC調優的具體處理過程。如我所述,案例中的GC選項可以不做調整的應用到那些具有相同CPU、操作系統和 JDK 版本以及執行相同功能的服務上去。然而不要把這些選項應用到你的系統上,因爲他們未必適用。

總結

我執行GC調優一般基於經驗而無需通過堆dump後對內存進行詳細的分析,儘管精確的內存狀態可能會帶來更好的GC調優結果。在一般情景,如果內存負載較低時,通過分析內存對象可能效果更好,不過如果服務負載較高,內存空間使用較多時,更推薦基於經驗來做GC調優。

我曾經在一些服務上對G1 GC做過性能測試,不過還沒有全面使用。結果證明G1 GC執行速度比其他任何GC都要快,不過需要把JDK升級到 JDK 7 才能享受到G1帶來的性能提升,另外G1的穩定性目前尚不能完全保證,沒有人知道是否會帶來嚴重的bug。所以大範圍使用 G1 還尚待時日。

當 JDK 7 穩定以後(並不是說它當前不穩定),並且WAS針對JDK 7做過優化之後,G1也許會穩定的運行在服務器上,到那時也許就不再需要進行GC調優了。

更多GC調優的細節可以在 Slideshare 上搜索相關材料。我最推薦的是Twitter 工程師 Attila Szegedi寫的這篇 我在Twitter學到的關於JVM調優的一切 ,有時間可以學習一下。

作者:Sangmin Lee, 性能實驗室高級工程師,NHN公司


原文:http://www.codeceo.com/article/java-gc-learn.html


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