【死磕JVM】給同事講了一遍GC後,他要去面試,年輕人,就是容易衝動!

牧小農發表於2021-04-03

前言

在一個風和日麗的中午,和同事小勇一起走在公司樓下的小公園裡面,看到很多的小姐姐,心想什麼時候能夠和這些小姐姐一起討論人生呀,美滋滋,嘿嘿嘿。

收起你的哈喇子好不好,小勇總是在這個時候發出聲音,挺讓人喜(fu)歡(ck)的。
小勇:小農,現在不是推崇垃圾分類嗎,你說到底什麼是垃圾?小勇總是在我和他散步的時候,問這麼讓人深思的問題!
我:什麼是垃圾啊,你不就是垃圾嗎?
小勇:去你大爺的,正經的。
我:小勇啊,答應我以後散步的時候我們討論點輕鬆點的問題好嘛?垃圾是啥,垃圾就是沒有引用的物件就是垃圾啊
小勇:。。。。,我們還是去午休吧

我:別啊,都講到這裡了,給你普及一下,你難道不想以後你的簡歷上出現——熟悉GC常用演算法,熟悉常見垃圾收集器,具有實際JVM調優實戰經驗嗎?保證讓你豁然開朗,等你以後去面試的時候,給面試官講這些保證妥妥的。

小勇:你這麼說我倒是有點興趣,但是如果講不明白,那你就浪費了我時間了,晚飯就你請吧。
我是沒問題,但是我的三個粉絲不會答應你的
小勇:你沒問題就行了,請開始你的表演吧~

什麼是垃圾

什麼是垃圾,就是沒有任何引用指向的一個物件或者多個物件(迴圈引用),但是他們卻依然佔據著記憶體空間。

GC是一種自動的儲存管理機制。當一些被佔用的記憶體不再需要時,就應該予以釋放。這種儲存資源管理,稱為垃圾回收。

就像我們的衣櫃一樣,我們裡面可能存放這很多衣服,有可能幾個月或者幾年都不會穿過一次,但是這些我們不穿的衣服一直霸佔著我們的衣櫃(記憶體),我們把這些不會穿的衣服扔掉的或者捐贈出去,這樣我們就可以放更多可以穿的衣服,這個就類似於“垃圾回收”。

在GC裡面,只分為可回收和不可回收,如下圖所示:
在這裡插入圖片描述

1.1 Java 和 C++ 垃圾回收的區別

Java是你只管扔垃圾就可以,Java會自動幫你處理,而C++要手動處理,但是容易造成一個問題就是忘記回收或者回收多次

  • java

    1. GC處理垃圾
    2. 開發效率高,執行效率低
  • C++

    1. 手工處理垃圾
    2. 忘記回收,會導致記憶體洩漏
    3. 回收多次,非法訪問
    4. 開發效率地,執行效率高

怎麼找垃圾?

上面我們知道了什麼是垃圾,那麼我們如何去找到垃圾呢?

在堆裡面存放這Java中幾乎所有的物件例項,垃圾收集器在對堆進行回收前,首先要做的事情就是確定這些物件哪些還 “存活”,哪些是需要進行回收的(即不再被引用的物件)

找到垃圾有兩種演算法

  • reference count (引用計數演算法)
  • Root Searching (根可達演算法)

1. 引用計數法

會給物件中新增一個引用計數器,每當有一個地方引用它的時候,計數器的值就 +1 ,當引用失效時,計數器值就 -1 ,計數器的值為 0 的物件不可能在被使用,這個時候就可以判定這個物件是垃圾。

在這裡插入圖片描述

當圖中的數值變成0時,這個時候使用引用計數演算法就可以判定它是垃圾了,但是引用計數法不能解決一個問題,就是當物件是迴圈引用的時候,計數器值都不為0,這個時候引用計數器無法通知GC收集器來回收他們,如下圖所示:

在這裡插入圖片描述
這個時候就需要使用到我們的根可達演算法

2. 根可達演算法

根可達演算法的意思是說從根上開始搜尋,當一個程式啟動後,馬上需要的那些個物件就叫做根物件,所謂的根可達演算法就是首先找到根物件,然後跟著這根線一直往外找到那些有用的,例如我們Java程式 main() 方法執行,一個main() 方法會啟動一個執行緒。

執行緒棧變數: 執行緒裡面會有執行緒棧和main棧幀,從這個main() 裡面開始的這些物件都是我們的根物件。

靜態變數: 一個class 它有一個靜態的變數,load到記憶體之後馬上就得對靜態變數進行初始化,所以靜態變數到的物件這個叫做根物件。

常量池: 如果你這個class會用到其他的class的那些個類的物件,這些就是根物件。

JNI: 如果我們呼叫了 C和C++ 寫的那些本地方法所用到的那些個類或者物件

在這裡插入圖片描述

圖中的 object5 和object6 雖然他們之間互相引用了,但是從根找不到它,所以就是垃圾,而object8沒有任何引用自然而然也是垃圾,其他的Object物件都有可以從根找到的,所以是有用的,不會被垃圾回收掉。

3. 區別

演算法 思想 優點 缺點
引用計數法 給物件新增一個引用計數器,每當一個地方引用這個物件的時候,計數器值就+1;當引用失效時,計數器值-1 判定效率高 不能解決物件之間相互引用的情況,開銷比較大,頻繁且大量的引用變化,帶來大量的額外運算
可達性分析 通過一系列稱為 "GC Roots" 的物件作為起始點,從這些節點向下搜尋,當GC Roots到某個物件不可達時,這個物件就是可回收的 更加精確和嚴謹,可以分析出迴圈資料結構相互引用的情況 實現比較複雜,需要分析大量的資料,消耗大量時間

如何清理垃圾

我們找到對應的垃圾之後,我們如果去清理垃圾呢?GC常用的演算法有三種:

  • Mark-Sweep(標記清除)
  • Copying(拷貝)
  • Mark-Compact(標記壓縮)

1. 標記 - 清除演算法

就和它的名字一樣 ,演算法分為 “標記” 和 “清除” 兩個階段,首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件,這個是最基礎的收集演算法,為什麼說它是最基礎的,因為後續的收集器都是基礎這種思路並對其不足進行改進而得到的。

在這裡插入圖片描述
標記清除演算法它有自己的小問題,大家可以看到上面這張圖,我們從GC的根找到那些不可回收的,綠色是不可回收的,紫色是可以回收的,我們把它回收之後就變成空閒的了,這種演算法相對比較簡單,在存活物件比較多的情況下效率比較高,它需要經歷兩次掃描,第一遍掃描是找到那些有用的,第二遍掃描是把那些沒用的找出來清理掉,這裡會有兩個問題:一個是效率問題,標記和清除兩個過程的效率都不高,另一個是空間問題,標記清除之後會產生大量不連續的空間碎片,如果空間碎片太多會導致以後的程式在執行過程中需要分配較大物件的時候,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

2. 複製演算法

為了解決效率的問題,所以有了複製(Copying)演算法的出現,它將可用的記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的記憶體用完了,就將還存活著的物件賦值到另一塊上面,然後再把已使用過的記憶體空間一次清理掉,這樣使得每次都對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只需要移動堆頂的指標,這種適用於存活物件較少的情況,所以比較適合eden區,只掃描一次,效率提高了沒有碎片,但是會造成空間的浪費,將記憶體縮小為原來的一半,未免太高了一點,而且移動複製物件,需要調整物件的引用

在這裡插入圖片描述

3. 標記 壓縮演算法

標記壓縮就是把所有的東西整理的過程,清理的過程同時壓縮到頭上去。回收之前,有用的往前面走,將剩下的清理出來,但是標記壓縮演算法依然有它的問題,我們都是通過GC Roots 找到那些不可回收的物件,然後把不可回收的往前挪,這個時候我們需要掃描兩次而且需要移動物件,第一遍掃描出有用的物件,第二遍進行移動,而且移動如果是多執行緒還需要進行同步,所以這個效率會低很多,但是它不會產生碎片,分配物件也不會產生記憶體減半。

在這裡插入圖片描述

4. 總結

  • Mark-Sweep(標記清除): 標記為垃圾之後就給清理掉,別的空間還是固定的,效率還可以,就是容易產生碎片
  • Copying(拷貝): 將記憶體一分為二,只使用一半,如果垃圾太多了,拷貝有用的到另外一邊,剩下的清理就直接整個記憶體進行清理,效率比較高
  • Mark-Compact(標記壓縮): 將所有的物件湊在一起,把垃圾全部清理走,接下來剩下的這個空間還是連續的,在裡面分配任何內容的時候直接往裡面分配就行了

堆記憶體邏輯分割槽

JVM中的Hot Spot 用的是分代演算法

在這裡插入圖片描述
新生代分為:eden、survivor

eden(伊甸): 預設比例8:是我們剛剛新 new出來物件之後往裡扔的那塊區域
survivor: 預設比例是1:是回收一次之後跑到這個區域,這裡面由於裝的物件不同,所以採取的演算法也不同

由於新生代存活物件特別少,死去物件特別多所以使用的演算法是 複製演算法

old 老年代:tenured(終身)
老年代活著的物件特別多適用於:標記清除和標記壓縮演算法

一個物件從出生到消亡

在這裡插入圖片描述
一個物件產生之後首先進行棧上分配,棧上如果分配不下會進入伊甸區,伊甸區經過一次垃圾回收之後進入surivivor區,survivor區在經過一次垃圾回收之後又進入另外一個survivor,與此同時伊甸區的某些物件也跟著進入另外一個survivot,什麼時候年齡夠了就會進入old區,這是整個物件的一個邏輯上的移動過程。

那什麼時候會在棧上分配,什麼時候會在伊甸區分配?

1 棧上分配

棧上分配:

  • 執行緒私有小物件:小物件、執行緒私有的
  • 無逃逸:就在某一段程式碼中使用,除了這段程式碼就沒有人認識它了
  • 支援標量替換:意思是用普通的屬性、把普通的型別代替物件就叫標量替換

棧上分配會比在堆上分配快一點,如果在棧上分配不下,會優先進行本地分配,也就是 執行緒本地分配TLAB(Thread local Allocation Buffer): 在伊甸區很多執行緒都會往裡面分配物件,但是分配物件的時候我們一定會進行空間的徵用,誰搶到算誰的,多執行緒的同步,效率就會降低,所以設計了TLAB機制

  • 佔用eden,預設為1%,在伊甸區取用百分之一的空間,這塊空間叫做執行緒獨有,分配物件的時候首先往執行緒獨有的這塊空間進行分配
  • 多執行緒的時候不用競爭eden就可以申請空間,提高效率

2 老年代

物件什麼時候進入老年代?

回收了多少次進入老年代?

  • 超過 XX:MaxTenuringThreshold指定次數(YGC)
    1. Parallel Scavenge 15次進入老年代
    2. CMS 6次進入老年代
    3. G1 15次進入老年代

網上有說可以次數往上調大,這個是不可能的

動態年齡判斷

為了能夠適用不同程式的記憶體狀況,虛擬機器並不是永遠的要求物件的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Surivivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。

兩個Survivor之間拷貝來拷貝去只要超過百分之50的時候把年齡最大的直接放入到old區,也就是不一定非得到15歲。

在s1裡面有這麼多物件拷貝到了s2裡面超過百分之50的話,s1裡面在加上伊甸區裡面,整個一個物件一下子拷貝到s2裡面,經過一次垃圾回收,過去之後,這個時候整個加起來物件已經超過s2的一半了,這裡面年齡最大的一些物件直接進入老年區,這個就叫做動態年輕判斷

在這裡插入圖片描述

大物件直接進入老年代 ,所謂的大物件是指,需要連續大量記憶體空間的Java物件,最典型的大物件就是那種很長的字串以及陣列,經常出現大物件容易導致記憶體還有不少空間的時候就提前觸發了垃圾收集來獲得足夠的連續記憶體空間

在這裡插入圖片描述
start 先是new一個物件,然後在棧上進行分配,如果在棧上能夠分配,就分配到棧上,棧直接彈出,彈出結束,如果在棧上分配不下,判斷物件是否為大物件,如果是大物件,直接進入老年代,FGC後結束如果不是,進入執行緒本地分配(TLAB),不管怎麼樣都會到伊甸區進行GC清除,如果清除完畢,直接結束,如果沒有清除完畢,進入S1,S1繼續GC清除,如果年齡到了進入old區,如果年齡不夠進入S2,然後S2再繼續GC的清除,要麼年齡到了,要麼動態年齡達到

MinorGC/YGC: 年輕代空間耗盡時觸發
MajorGC/FullGC: 在老年代無法繼續分配空間時觸發,新生代老年代同時進行回收

常見的垃圾回收器

新生代收集器: Serial、ParNew、Parallel Scavenge

老年代收集器: Serial Old、CMS、Parallel Old

新生代和老年代收集器: G1、ZGC、Shenandoah

每種垃圾回收器之間不是獨立操作的,下圖表示垃圾回收器之間有連線表示,可以協作使用:
在這裡插入圖片描述

新生代垃圾收集器

1. Serial收集器

Serial 收集器是最基礎、歷史最悠久的收集器,是一個單執行緒工作的收集器,它的“單執行緒”的意義不是說他只會使用一個處理器或者一條收集執行緒去完成垃圾收集的工作,更重要的是強調在它進行垃圾收集的時候,會暫停其他所有工作執行緒,直到它收集結束

在這裡插入圖片描述

根據上圖中我們可以知道,當Serial收集器執行的時候,會暫停所有執行緒,“Stop The World” ,等到GC完成後,應用執行緒繼續執行,就類似於 你有三個女朋友,他們同時讓你陪他們去逛街,你只能陪完其中一個才能去陪另外一個,陪其中一個的時候,其他女朋友就要等待,但是垃圾收集這項工作要比這種情況要複雜的多!

優勢: 因為使用的是單執行緒的方式,所以對於單個CPU來說,是其他型別收集器中效率最高的一個
缺點: 在使用者不可知、不可控的情況下,暫停所有執行緒,風險性和體驗感不好,讓人比較難接受

使用命令:可以開啟Serial 作為新生代收集器

-XX:+UserSerialGC #選擇Serial作為新生代垃圾收集器

2. ParNew收集器

ParNew收集器實質上是Serial收集器的多執行緒並行版本,除了同時使用多條執行緒進行垃圾收集器之外,其餘的比如Serial收集器可用的控制引數、收集演算法、Stop The Wrold 、物件分配規則等等都和Serial收集器完全一樣,在多核機器上,預設開啟的手機執行緒數和CPU數量一樣,但是可以通過引數進行修改

-XX:ParallelGCThreads #設定JVM垃圾收集的執行緒數

在這裡插入圖片描述
ParNew收集器除了支援多執行緒並行收集之外,其他與Serial收集器相比並沒有太多創新之處,但它 卻是不少執行在服務端模式下的HotSpot虛擬機器,尤其是JDK 7之前的遺留系統中首選的新生代收集 器,其中有一個與功能、效能無關但其實很重要的原因是:除了Serial收集器外,目前只有它能與CMS 收集器配合工作。

優點:隨著CPU的有效利用,對於GC時系統資源的有效利用有好處
缺點:同Serial一樣的毛病
使用場景:ParNew是許多執行在Server模式下的虛擬機器中首選的新生代收集器,因為CMS只能與Serial 或者 ParNew 配合使用,在如今的多核環境下,首選的是多執行緒並行的ParNew,ParNew收集器是啟用CMS後(使用-XX:+UseConcMarkSweepGC選項)的預設新生代收集器,也可以使用-XX:+/-UseParNewGC選項來強制指定或者禁用它

3. Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代的收集器,它同樣是基於標記-複製演算法那實現的收集器,也是能夠並行收集器的多執行緒收集器,Parallel Scavenge收集器關注點與其他收集器的不用處在於,CMS等收集器的關注點是儘可能地縮短垃圾收集時使用者執行緒的停頓時間,而Parallel Scavenge收集器的目標則是一個可控制的吞吐量,所謂的吞吐量就是處理器用於執行使用者程式碼的時間與處理器總消耗的比值,如下圖所示:

在這裡插入圖片描述
如果說虛擬機器完成某個任務,使用者程式碼加上垃圾收集總共耗費了100分鐘,其中垃圾收集花掉1分鐘,那麼吞吐量就是99%。停頓時間越短就越適合需要與使用者互動或者需要保證服務響應質量的程式,良好的響應速度能提升使用者體驗。

垃圾收集器每100秒收集一次,每次停頓10秒,和垃圾收集器每50秒收集一次,每次停頓時間7秒,雖然後者停頓時間變短了,但是總體吞吐量變低了,CPU總體利用率變低了。

收集頻率 每次停頓時間 吞吐量
100秒收集一次 10秒 91%
每50秒收集一次 7秒 88%

可以通過 -XX:MaxGCPauseMillis來設定收集器儘可能在多長時間內完成記憶體回收,可以通過 -XX:GCTimeRatio來精確控制吞吐量。

如下是 Parallel 收集器和 Parallel Old 收集器結合進行垃圾收集的示意圖,在新生代,當使用者執行緒都執行到安全點時,所有執行緒暫停執行,ParNew 收集器以多執行緒,採用複製演算法進行垃圾收集工作,收集完之後,使用者執行緒繼續開始執行;在老年代,當使用者執行緒都執行到安全點時,所有執行緒暫停執行,Parallel Old 收集器以多執行緒,採用標記整理演算法進行垃圾收集工作。
在這裡插入圖片描述

老年代垃圾收集器

1. Serial Old 收集器

Serial Old 是 Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用標記-整理演算法,這個收集器的主要意義也是供客戶端模式下HotSpot虛擬機器使用。如果在服務端一種是與Parallel Scavenge收集器搭配使用,另外一種是作為CMS 收集器發生失敗時的後備預案。

Serial收集器與Serial Old收集器的執行示意圖:

在這裡插入圖片描述
適用場景: Client模式;單核伺服器;與Parallel Scavenge收集器搭配;作為CMS收集器的後備方案,在併發收集發生Concurrent Mode Failure時使用

2. Parallel Old收集器

Parallel Old 是 Parallel Scavenge收集器的老年代版本,支援多執行緒併發收集,基於標記-整理演算法實現,可以充分利用多核CPU的計算能力,慮Parallel Scavenge/Parallel Old收集器執行示意圖:

在這裡插入圖片描述

2. CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。CMS收集器是基於標記-清楚演算法實現的,這個收集器的運作過程比前面的幾個收集器更復雜一點,整個過程分為四個步驟:

1) 初始標記(CMS initial mark): 只是標記 GC Roots能夠直接關聯到的物件,速度很快

2) 併發標記(CMS concurrent mark): 從GC Roots 的直接關聯物件開始遍歷整個物件圖的過程,這個過程耗時較長但是不需要停頓使用者執行緒,可以和垃圾收集執行緒一起併發執行

3) 重新標記(CMS remark): 修正併發標記期間,因使用者程式繼續運作導致標記產生物件的標記記錄,這個階段的停頓時間會比初始標記階段稍長一些

4) 併發清除(CMS concurrent sweep): 清理刪除掉標記階段判斷的已經死亡的物件,由於不需要移動存活物件,這個階段也是可以與使用者執行緒同時併發的。

其中 初始標記、重新標記這兩個步驟仍然需要 “Stop The World” 暫停所有使用者執行緒,由於在整個過程中耗時最長的併發標記和併發清理階段中,垃圾收集器執行緒都可以與使用者執行緒一起工作,總體來說,CMS收集器的記憶體回收過程是和使用者執行緒一起併發執行的,如下圖所示:

在這裡插入圖片描述
優點: CMS收集器是一款優秀的收集器,它主要體現為:併發收集、低停頓。

缺點:

CMS收集器對處理器資源非常敏感,在併發階段,雖然不會導致使用者執行緒停頓,但也會因為佔用一部分執行緒導致應用程式變慢,降級總的吞吐量。CMS預設啟動回收執行緒數是(處理器核心數量+3)/4,也就是說如果處理器核心數大於等於四個,併發回收時垃圾收集執行緒只佔用不超過25%的處理器運算資源,處理器資源會隨著CPU數量的增加而下降,但是當CPU數量不足四個的時候,CMS對使用者程式的影響就可能變的很大。
CMS收集器無法處理 “浮動垃圾” ,有可能出現 “Concurrent Mode Failure” 失敗進而導致另一次完全“Stop The World” 的Full GC 的產生,在CMS的併發標記和併發清理階段,使用者執行緒是還在繼續進行的,程式在執行自然就還會伴隨有新的垃圾物件不斷產生,但這一部分垃圾物件是出現在標記過程結束以後,CMS無法在當次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉。這一部分的垃圾就稱為“浮動垃圾”
因為CMS是一款基於 “標記-清除”演算法實現的收集器,因此收集結束時會有大量的空間碎片產生,空間碎片過多的時,將對給大物件帶來很大的麻煩,有可能不得不提前進行Full GC的操作,不過通過引數:-XX:+UseCMS-CompactAtFullCollection進行優化

新生代和老年代垃圾收集器

G1收集器

Garbage First (簡稱G1)收集器是垃圾收集器技術發展歷史上的里程碑式的成果,它開創了收集器面向區域性收集的設計思路和基於Region的記憶體佈局形式。

G1收集器是一款面向伺服器端應用的垃圾收集器,在JDK9釋出的時候成為服務端模式下的預設垃圾收集器,而CMS則淪為不被推薦使用的收集器

特點:

在G1收集器出現之前所有的其他收集器,目標範圍要麼是新生代要麼是老年代,要麼就是Java堆,但是G1做了全面性,它可以面向堆記憶體任何部分來組成回收集進行回收,衡量標準不再是它屬於哪個分代,而是那塊記憶體中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式。而G1開創的基於Region的堆記憶體佈局是它能夠實現這個目標的關鍵。

雖然G1仍然保留了新生代和老年代的概念,但新生代和老年代不再是固定的,他們都是一系列區域的動態集合,G1可以建立可預測的停頓時間模型,是因為它將Region作為單次回收最小單元

G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域,每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間或者老年代空間,收集器能夠對扮演不同的角色的Region採用不同的策略去處理。

Region中海油一類特殊的Humongous區域,專門用來儲存大物件。G1認為只要大小超過一個Region容量一半的物件即可判定為大物件。

G1收集器的執行過程:

  • 初始標記(Initial Marking): 標記GC Roots 能直接關聯到的物件,並且修改TAMS指標的值,讓下一階段使用者執行緒併發執行時,能正確在可用的Region中分配新物件,需要耗時較短的停頓執行緒,但是是借用Minor GC的時候同步完成的,所以在這個階段實際沒有額外的停頓

  • 併發標記(Concurrent Marking): 從GC Roots 開始對堆中物件進行可達性分析,遞迴掃描整個堆裡面的物件圖,找出要回收的物件,這個階段耗時較長,但可以和使用者程式併發執行。

  • 最終標記(Final Marking): 對使用者執行緒做另一個短暫的暫停,使用者處理併發階段結束後仍遺留下來的最後那少量的SATB記錄

  • 篩選回收(Live Data Counting and Evacuation): 負責更新Region的統計資料,對各個Region的回收價值和成本進行排序,根據使用者鎖期望的停頓時間來制定回收計劃,可以只有選擇任意多個Region構成回收集,然後把決定回收的那一部分Region的存活物件賦值到空的Region中,再清理整個Region的全部空間。

在這裡插入圖片描述

總結

小勇你懂了嗎?小勇小勇,你別睡著了啊,我還沒講完呢!小勇醒醒啊!!!
小勇迷迷糊糊的說:怎麼了,下班了嗎?
。。。。,下班啥,我講的GC你聽懂了嗎?
小勇:聽懂了,我明天就去面試,你講的太棒了!
。。。。。敷衍,算了我已經把東西都放在筆記裡面了,你要是感興趣就可以來看看,今天就到這裡了,我們上去吧

end....

我是牧小農,怕什麼真理無窮,進一步有進一步的歡喜,大家加油!

相關文章