Java G1深入理解(轉(zhuǎn))

#refer:?http://blog.jobbole.com/109170/#comment-159599

一、什么是垃圾回收

首先,在了解G1之前,我們需要清楚的知道,垃圾回收是什么?簡單的說垃圾回收就是回收內(nèi)存中不再使用的對象。

垃圾回收的基本步驟有2步:1.查找內(nèi)存中不再使用的對象;2.釋放這些對象占用的內(nèi)存。

#1.查找內(nèi)存中不再使用的對象

那么問題來了,如何判斷哪些對象不再被使用呢?我們也有2個(gè)方法:

1.1 引用計(jì)數(shù)法:引用計(jì)數(shù)法就是如果一個(gè)對象沒有被任何引用指向,則可視之為垃圾。這種方法的缺點(diǎn)就是不能檢測到環(huán)的存在。

1.2 根搜索算法:根搜索算法的基本思路就是通過一系列名為”GC Roots”的對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈相連時(shí),則證明此對象是不可用的。補(bǔ)充:所有對象都是JVM管理的,都是JVM可以到達(dá)的。

現(xiàn)在我們已經(jīng)知道如何找出垃圾對象了,如何把這些對象清理掉呢?

#2. 釋放這些對象占用的內(nèi)存

常見方式有復(fù)制、清理和整理,但直接清理會存在內(nèi)存碎片,于是就會又了清理再壓縮的方式。

總得來說就產(chǎn)生了三種類型的回收算法。

2.1 標(biāo)記-復(fù)制式算法:它將可用內(nèi)存容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊用完之后,就將還存活的對象復(fù)制到另外一塊上面,然后在把已使用過的內(nèi)存空間一次理掉。它的優(yōu)點(diǎn)是實(shí)現(xiàn)簡單,效率高,不會存在內(nèi)存碎片。缺點(diǎn)就是需要2倍的內(nèi)存來管理。

2.2 標(biāo)記-清理式算法:標(biāo)記清除算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出需要回收的對象,標(biāo)記完成之后統(tǒng)一清除對象。它的優(yōu)點(diǎn)是效率高,缺點(diǎn)是容易產(chǎn)生內(nèi)存碎片。

2.3 標(biāo)記-整理式算法:標(biāo)記操作和“標(biāo)記-清理”算法一致,后續(xù)操作不只是直接清理對象,而是在清理無用對象完成后讓所有存活的對象都向一端移動,并更新引用其對象的指針。因?yàn)橐苿訉ο?,所以它的效率要比“?biāo)記-清理”效率低,但是不會產(chǎn)生內(nèi)存碎片。

基于分代的假設(shè)

由于對象的存活時(shí)間有長有短,對于存活時(shí)間長的對象減少被GC的次數(shù)可以避免不必要的開銷。這樣我們就把內(nèi)存分成新生代和老年代,新生代存放剛創(chuàng)建的和存活時(shí)間比較短的對象,老年代存放存活時(shí)間比較長的對象。這樣每次僅僅清理年輕代,老年代僅在必要時(shí)時(shí)再做清理,可以極大的提高GC效率,節(jié)省GC時(shí)間。

Java垃圾收集器的歷史

第一階段:Serial(串行)收集器

在jdk1.3.1之前,java虛擬機(jī)僅僅能使用Serial收集器。 Serial收集器是一個(gè)單線程的收集器,但它的“單線程”的意義并不僅僅是說明它只會使用一個(gè)CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進(jìn)行垃圾收集時(shí),必須暫停其他所有的工作線程(Stop the World),直到它收集結(jié)束。

#開啟Serial收集器的方式:-XX:+UseSerialGC

第二階段:Parallel(并行)收集器

Parallel收集器也稱吞吐量收集器,相比Serial收集器,Parallel最主要的優(yōu)勢在于使用多線程去完成垃圾清理工作,這樣可以充分利用多核的特性,大幅降低gc時(shí)間。

#開啟Parallel收集器的方式:-XX:+UseParallelGC -XX:+UseParallelOldGC

第三階段:CMS(并發(fā))收集器

CMS收集器在Minor GC時(shí)會暫停所有的應(yīng)用線程,并以多線程的方式進(jìn)行垃圾回收。在Full GC時(shí)不再暫停應(yīng)用線程,而是使用若干個(gè)后臺線程定期的對老年代空間進(jìn)行掃描,及時(shí)回收其中不再使用的對象。

#開啟CMS收集器的方式:-XX:+UseParNewGC -XX:+UseConcMarkSweepGC

第四階段:G1(并發(fā))收集器

G1收集器(或者垃圾優(yōu)先收集器)的設(shè)計(jì)初衷是為了盡量縮短處理超大堆(大于4GB)時(shí)產(chǎn)生的停頓。相對于CMS的優(yōu)勢而言是內(nèi)存碎片的產(chǎn)生率大大降低。

#開啟G1收集器的方式:-XX:+UseG1GC

補(bǔ)充:MinorGC一般指年輕代GC,MajorGC一般指年老代GC

二、了解G1-GC

G1的第一篇paper(附錄1)發(fā)表于2004年,在2012年才在jdk1.7u4中可用。Oracle官方計(jì)劃在jdk9中將G1變成默認(rèn)的垃圾收集器,以替代CMS。為何Oracle要極力推薦G1呢,G1有哪些優(yōu)點(diǎn)?

首先,G1的設(shè)計(jì)原則就是簡單可行的性能調(diào)優(yōu)。

僅需聲明以下參數(shù)即可:-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

其中-XX:+UseG1GC為開啟G1垃圾收集器,-Xmx32g 設(shè)計(jì)堆內(nèi)存的最大內(nèi)存為32G,-XX:MaxGCPauseMillis=200設(shè)置GC的最大暫停時(shí)間為200ms。如果我們需要調(diào)優(yōu),在內(nèi)存大小一定的情況下,我們只需要修改最大暫停時(shí)間即可。

其次,G1將新生代,老年代的物理空間劃分取消了,但是新生代和老年代并沒有取消。

這樣我們再也不用單獨(dú)的空間對每個(gè)代進(jìn)行設(shè)置了,不用擔(dān)心每個(gè)代內(nèi)存是否足夠。

舊GC分代

取而代之的是,G1算法將堆劃分為若干個(gè)區(qū)域(Region),它仍然屬于分代收集器。不過,這些區(qū)域的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應(yīng)用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區(qū)域,G1收集器通過將對象從一個(gè)區(qū)域復(fù)制到另外一個(gè)區(qū)域,完成了清理和整理的工作。這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有CMS內(nèi)存碎片的問題存在了。

新GC分代

在G1中,還有一種特殊的區(qū)域,叫Humongous區(qū)域。 如果一個(gè)對象占用的空間超過了分區(qū)容量50%以上,G1收集器就認(rèn)為這是一個(gè)巨型對象。這些巨型對象,默認(rèn)直接會被分配在年老代,但是如果它是一個(gè)短期存在的巨型對象,就會對垃圾收集器造成負(fù)面影響。為了解決這個(gè)問題,G1劃分了一個(gè)Humongous區(qū),它用來專門存放巨型對象。如果一個(gè)H區(qū)裝不下一個(gè)巨型對象,那么G1會尋找連續(xù)的H分區(qū)來存儲。為了能找到連續(xù)的H區(qū),有時(shí)候不得不啟動Full GC。

補(bǔ)充:在Java8中,永久代移動到了普通的內(nèi)存空間中(堆外內(nèi)存),改為元空間(MetaSpace)。

對象分配策略(Humongous:巨大無比的,極大的)

說起大對象的分配,我們不得不談?wù)剬ο蟮姆峙洳呗?。它分?個(gè)階段:

1. TLAB(Thread Local Allocation Buffer) 線程本地分配緩沖區(qū)
2. Eden區(qū)中分配
3. Humongous區(qū)分配

TLAB為線程本地分配緩沖區(qū),它的目的為了使對象盡可能快的分配出來。如果對象在一個(gè)共享的空間中分配,我們需要采用一些同步機(jī)制來管理這些空間內(nèi)的空閑空間指針。在Eden空間中,每一個(gè)線程都有一個(gè)固定的分區(qū)用于分配對象,即一個(gè)TLAB。分配對象時(shí),線程之間不再需要進(jìn)行任何的同步。

對TLAB空間中無法分配的對象,JVM會嘗試在Eden空間中進(jìn)行分配。如果Eden空間無法容納該對象,就只能在老年代中進(jìn)行分配空間。

最后,G1提供了兩種GC模式,1.Young GC和2.Mixed GC,兩種都是Stop The World(STW)的。下面我們將分別介紹一下這2種模式。

三、G1 Young GC

Young GC主要是對Eden區(qū)進(jìn)行GC,它在Eden空間耗盡時(shí)會被觸發(fā)。在這種情況下,Eden空間的數(shù)據(jù)移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分?jǐn)?shù)據(jù)會直接晉升到年老代空間。Survivor區(qū)的數(shù)據(jù)移動到新的Survivor區(qū)中,也有部分?jǐn)?shù)據(jù)晉升到老年代空間中。最終Eden空間的數(shù)據(jù)為空,GC停止工作,應(yīng)用線程繼續(xù)執(zhí)行。

G1-GC
G1-GC

這時(shí),我們需要考慮一個(gè)問題,如果僅僅GC 新生代對象,我們?nèi)绾握业剿械母鶎ο竽兀?老年代的所有對象都是根么?那這樣掃描下來會耗費(fèi)大量的時(shí)間。于是,G1引進(jìn)了RSet的概念。它的全稱是Remembered Set,作用是跟蹤指向某個(gè)heap區(qū)內(nèi)的對象引用。

G1-GC

在CMS中,也有RSet的概念,在老年代中有一塊區(qū)域用來記錄指向新生代的引用。這是一種point-out,在進(jìn)行Young GC時(shí),掃描根時(shí),僅僅需要掃描這一塊區(qū)域,而不需要掃描整個(gè)老年代(CMS中)。

但在G1中,并沒有使用point-out,這是由于一個(gè)分區(qū)太小,分區(qū)數(shù)量太多,如果是用point-out的話,會造成大量的掃描浪費(fèi),有些根本不需要GC的分區(qū)引用也掃描了。于是G1中使用point-in來解決。point-in的意思是哪些分區(qū)引用了當(dāng)前分區(qū)中的對象。這樣,僅僅將這些對象當(dāng)做根來掃描就避免了無效的掃描。由于新生代有多個(gè),那么我們需要在新生代之間記錄引用嗎?這是不必要的,原因在于每次GC時(shí),所有新生代都會被掃描,所以只需要記錄老年代到新生代之間的引用即可。

需要注意的是,如果引用的對象很多,賦值器需要對每個(gè)引用做處理,賦值器開銷會很大,為了解決賦值器開銷這個(gè)問題,在G1 中又引入了另外一個(gè)概念,卡表(Card Table)。一個(gè)Card Table將一個(gè)分區(qū)在邏輯上劃分為固定大小的連續(xù)區(qū)域,每個(gè)區(qū)域稱之為卡??ㄍǔ]^小,介于128到512字節(jié)之間。Card Table通常為字節(jié)數(shù)組,由Card的索引(即數(shù)組下標(biāo))來標(biāo)識每個(gè)分區(qū)的空間地址。默認(rèn)情況下,每個(gè)卡都未被引用。當(dāng)一個(gè)地址空間被引用時(shí),這個(gè)地址空間對應(yīng)的數(shù)組索引的值被標(biāo)記為”0″,即標(biāo)記為臟被引用,此外RSet也將這個(gè)數(shù)組下標(biāo)記錄下來。一般情況下,這個(gè)RSet其實(shí)是一個(gè)Hash Table,Key是別的Region的起始地址,Value是一個(gè)集合,里面的元素是Card Table的Index。

#Young GC 階段:

階段1:根掃描靜態(tài)和本地對象被掃描
階段2:更新RS處理dirty card隊(duì)列更新RS
階段3:處理RS檢測從年輕代指向年老代的對象
階段4:對象拷貝拷貝存活的對象到survivor/old區(qū)域
階段5:處理引用隊(duì)列軟引用,弱引用,虛引用處理

四、G1 Mixed GC

Mixed GC不僅進(jìn)行正常的新生代垃圾收集,同時(shí)也回收部分后臺掃描線程標(biāo)記的老年代分區(qū)。

#它的GC步驟分2步:

1.全局并發(fā)標(biāo)記(global concurrent marking)
2.拷貝存活對象(evacuation)

在進(jìn)行Mixed GC之前,會先進(jìn)行g(shù)lobal concurrent marking(全局并發(fā)標(biāo)記)。 global concurrent marking的執(zhí)行過程是怎樣的呢?

在G1 GC中,它主要是為Mixed GC提供標(biāo)記服務(wù)的,并不是一次GC過程的一個(gè)必須環(huán)節(jié)。global concurrent marking的執(zhí)行過程分為五個(gè)步驟:

1.初始標(biāo)記(initial mark,STW):在此階段,G1 GC 對根進(jìn)行標(biāo)記。該階段與常規(guī)的 (STW) 年輕代垃圾回收密切相關(guān)。

2.根區(qū)域掃描(root region scan):G1 GC 在初始標(biāo)記的存活區(qū)掃描對老年代的引用,并標(biāo)記被引用的對象。該階段與應(yīng)用程序(非 STW)同時(shí)運(yùn)行,并且只有完成該階段后,才能開始下一次 STW 年輕代垃圾回收。

3.并發(fā)標(biāo)記(Concurrent Marking):G1 GC 在整個(gè)堆中查找可訪問的(存活的)對象。該階段與應(yīng)用程序同時(shí)運(yùn)行,可以被 STW 年輕代垃圾回收中斷

4.最終標(biāo)記(Remark,STW):該階段是 STW 回收,幫助完成標(biāo)記周期。G1 GC 清空 SATB 緩沖區(qū),跟蹤未被訪問的存活對象,并執(zhí)行引用處理。

5.清除垃圾(Cleanup,STW):在這個(gè)最后階段,G1 GC 執(zhí)行統(tǒng)計(jì)和 RSet 凈化的 STW 操作。在統(tǒng)計(jì)期間,G1 GC 會識別完全空閑的區(qū)域和可供進(jìn)行混合垃圾回收的區(qū)域。清理階段在將空白區(qū)域重置并返回到空閑列表時(shí)為部分并發(fā)。

三色標(biāo)記算法

提到并發(fā)標(biāo)記,我們不得不了解并發(fā)標(biāo)記的三色標(biāo)記算法。它是描述追蹤式回收器的一種有用的方法,利用它可以推演回收器的正確性。?

首先,我們將對象分成三種類型的。

1.黑色:根對象,或者該對象與它的子對象都被掃描
2.灰色:對象本身被掃描,但還沒掃描完該對象中的子對象
3.白色:未被掃描對象,掃描完成所有對象之后,最終為白色的為不可達(dá)對象,即垃圾對象

當(dāng)GC開始掃描對象時(shí),按照如下圖步驟進(jìn)行對象的掃描:

1.根對象被置為黑色,子對象被置為灰色。

三色標(biāo)記法

2.繼續(xù)由灰色遍歷,將已掃描過的子對象的對象置為黑色。

三色標(biāo)記法

3.遍歷了所有可達(dá)的對象后,所有可達(dá)的對象都變成了黑色。不可達(dá)的對象即為白色,需要被清理。

三色標(biāo)記法

4.這看起來很美好,但是如果在標(biāo)記過程中,應(yīng)用程序也在運(yùn)行,那么對象的指針就有可能改變。這樣的話,我們就會遇到一個(gè)問題:對象丟失問題。

我們看下面一種情況,當(dāng)垃圾收集器掃描到下面情況時(shí):

三色標(biāo)記法

這時(shí)候應(yīng)用程序執(zhí)行了以下操作:A.c=C; B.c=null;

這樣,對象的狀態(tài)圖變成如下情形:

三色標(biāo)記法

這時(shí)候垃圾收集器再標(biāo)記掃描的時(shí)候就會下圖成這樣:

三色標(biāo)記法

很顯然,此時(shí)C是白色,被認(rèn)為是垃圾需要清理掉,顯然這是不合理的。那么我們?nèi)绾伪WC應(yīng)用程序在運(yùn)行的時(shí)候,GC標(biāo)記的對象不丟失呢?有如下2中可行的方式:

1.在插入的時(shí)候記錄對象
2.在刪除的時(shí)候記錄對象

剛好這對應(yīng)CMS和G1的2種不同實(shí)現(xiàn)方式:

在CMS采用的是增量更新(Incremental update),只要在寫屏障(write barrier)里發(fā)現(xiàn)要有一個(gè)白對象的引用被賦值到一個(gè)黑對象 的字段里,那就把這個(gè)白對象變成灰色的。即插入的時(shí)候記錄下來。

G1中使用的是STAB(snapshot-at-the-beginning)方式,刪除的時(shí)候記錄所有的對象,它有3個(gè)步驟:

1.在開始標(biāo)記的時(shí)候生成一個(gè)快照圖標(biāo)記存活對象;
2.在并發(fā)標(biāo)記的時(shí)候所有被改變的對象入隊(duì)(在write barrier里把所有舊的引用所指向的對象都變成非白的);
3.可能存在游離的垃圾,將在下次被收集;

這樣,G1到現(xiàn)在可以知道哪些老的分區(qū)可回收垃圾最多。 當(dāng)全局并發(fā)標(biāo)記完成后,在某個(gè)時(shí)刻,就開始了Mixed GC。這些垃圾回收被稱作“混合式”是因?yàn)樗麄儾粌H僅進(jìn)行正常的新生代垃圾收集,同時(shí)也回收部分后臺掃描線程標(biāo)記的分區(qū)。混合式垃圾收集如下圖:

G1-GC

混合式GC也是采用的復(fù)制的清理策略,當(dāng)GC完成后,會重新釋放空間。

G1-GC

至此,混合式GC告一段落了,下一小節(jié)我們講進(jìn)入調(diào)優(yōu)實(shí)踐。

五、G1-GC調(diào)優(yōu)實(shí)踐

-XX:MaxGCPauseMillis調(diào)優(yōu)

前面介紹過使用G1的最基本的參數(shù):-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

前面2個(gè)參數(shù)都好理解,后面這個(gè)MaxGCPauseMillis參數(shù)該怎么配置呢?這個(gè)參數(shù)從字面的意思上看,就是允許的GC最大的暫停時(shí)間。G1盡量確保每次GC暫停的時(shí)間都在設(shè)置的MaxGCPauseMillis范圍內(nèi)。 那G1是如何做到最大暫停時(shí)間的呢?這涉及到另一個(gè)概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的區(qū)域集合。

#1.Young GC:選定所有新生代里的region。通過控制新生代的region個(gè)數(shù)來控制Young GC的開銷。

#2.Mixed GC:選定所有新生代里的region,外加根據(jù)global concurrent marking統(tǒng)計(jì)得出收集收益高的若干老年代region。在用戶指定的開銷目標(biāo)范圍內(nèi)盡可能選擇收益高的老年代region。

在理解了這些后,我們再設(shè)置最大暫停時(shí)間就好辦了。 首先,我們能容忍的最大暫停時(shí)間是有一個(gè)限度的,我們需要在這個(gè)限度范圍內(nèi)設(shè)置。但是應(yīng)該設(shè)置的值是多少呢?我們需要在吞吐量跟MaxGCPauseMillis之間做一個(gè)平衡。如果MaxGCPauseMillis設(shè)置的過小,那么GC就會頻繁,吞吐量就會下降。如果MaxGCPauseMillis設(shè)置的過大,應(yīng)用程序暫停時(shí)間就會變長。G1的默認(rèn)暫停時(shí)間是200毫秒,我們可以從這里入手,調(diào)整合適的時(shí)間。

G1-GC其他調(diào)優(yōu)參數(shù)

-XX:G1HeapRegionSize=n

設(shè)置的 G1 區(qū)域的大小。值是 2 的冪,范圍是 1 MB 到 32 MB 之間。目標(biāo)是根據(jù)最小的 Java 堆大小劃分出約 2048 個(gè)區(qū)域。

-XX:ParallelGCThreads=n

設(shè)置 STW 工作線程數(shù)的值。將 n 的值設(shè)置為邏輯處理器的數(shù)量。n 的值與邏輯處理器的數(shù)量相同,最多為 8。如果邏輯處理器不止八個(gè),則將 n 的值設(shè)置為邏輯處理器數(shù)的 5/8 左右。這適用于大多數(shù)情況,除非是較大的 SPARC 系統(tǒng),其中 n 的值可以是邏輯處理器數(shù)的 5/16 左右。

-XX:ConcGCThreads=n

設(shè)置并行標(biāo)記的線程數(shù)。將 n 設(shè)置為并行垃圾回收線程數(shù) (ParallelGCThreads) 的 1/4 左右。

-XX:InitiatingHeapOccupancyPercent=45

設(shè)置觸發(fā)標(biāo)記周期的 Java 堆占用率閾值。默認(rèn)占用率是整個(gè) Java 堆的 45%。

###避免使用以下參數(shù):

避免使用 -Xmn 選項(xiàng)或 -XX:NewRatio 等其他相關(guān)選項(xiàng)顯式設(shè)置年輕代大小。固定年輕代的大小會覆蓋暫停時(shí)間目標(biāo)。

###觸發(fā)Full GC

在某些情況下,G1觸發(fā)了Full GC,這時(shí)G1會退化到使用Serial收集器來完成垃圾的清理工作,它僅僅使用單線程來完成GC工作,GC暫停時(shí)間將達(dá)到秒級別的。整個(gè)應(yīng)用處于假死狀態(tài),不能處理任何請求,我們的程序當(dāng)然不希望看到這些。那么導(dǎo)致發(fā)生Full GC的情況有哪些呢?

1.并發(fā)模式失敗

G1啟動標(biāo)記周期,但在Mixed GC之前,老年代就被填滿,這時(shí)候G1會放棄標(biāo)記周期。這種情形下,需要增加堆大小,或者調(diào)整周期(例如增加線程數(shù)-XX:ConcGCThreads等)。

2.晉升失敗或者疏散失敗

G1在進(jìn)行GC的時(shí)候沒有足夠的內(nèi)存供存活對象或晉升對象使用,由此觸發(fā)了Full GC??梢栽谌罩局锌吹?to-space exhausted)或者(to-space overflow)。解決這種問題的方式是:

A,增加 -XX:G1ReservePercent 選項(xiàng)的值(并相應(yīng)增加總的堆大?。?,為“目標(biāo)空間”增加預(yù)留內(nèi)存量。

B,通過減少 -XX:InitiatingHeapOccupancyPercent 提前啟動標(biāo)記周期。

C,也可以通過增加 -XX:ConcGCThreads 選項(xiàng)的值來增加并行標(biāo)記線程的數(shù)目。

3.巨型對象分配失敗

當(dāng)巨型對象找不到合適的空間進(jìn)行分配時(shí),就會啟動Full GC,來釋放空間。這種情況下,應(yīng)該避免分配大量的巨型對象,增加內(nèi)存或者增大-XX:G1HeapRegionSize,使巨型對象不再是巨型對象。

由于篇幅有限,G1還有很多調(diào)優(yōu)實(shí)踐,在此就不一一列出了,大家在平常的實(shí)踐中可以慢慢探索。最后,期待java 9能正式發(fā)布,默認(rèn)使用G1為垃圾收集器的java性能會不會又提高呢?

附錄參考(1),The original G1 paper: Detlefs, D., Flood, C., Heller, S., and Printezis, T. 2004. Garbage-first garbage collection. In Proceedings of the 4th international Symposium on Memory Management (Vancouver, BC, Canada, October 24 – 25, 2004)

#

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容