在高并發(fā)下糙捺,Java程序的GC問(wèn)題屬于很典型的一類(lèi)問(wèn)題诫咱,帶來(lái)的影響往往會(huì)被進(jìn)一步放大。不管是「GC頻率過(guò)快」還是「GC耗時(shí)太長(zhǎng)」洪灯,由于GC期間都存在Stop The World問(wèn)題坎缭,因此很容易導(dǎo)致服務(wù)超時(shí),引發(fā)性能問(wèn)題签钩。
我們團(tuán)隊(duì)負(fù)責(zé)的廣告系統(tǒng)承接了比較大的C端流量掏呼,平峰期間的請(qǐng)求量基本達(dá)到了上千QPS,過(guò)去也遇到了很多次GC相關(guān)的線(xiàn)上問(wèn)題边臼。
這篇文章哄尔,我再分享一個(gè)更棘手的Young GC耗時(shí)過(guò)長(zhǎng)的線(xiàn)上案例,同時(shí)會(huì)整理下YGC相關(guān)的知識(shí)點(diǎn)柠并,希望讓你有所收獲岭接。內(nèi)容分成以下2個(gè)部分:
- 從一次YGC耗時(shí)過(guò)長(zhǎng)的案例說(shuō)起
- YGC的相關(guān)知識(shí)點(diǎn)總結(jié)
從一次YGC耗時(shí)過(guò)長(zhǎng)的案例說(shuō)起
今年4月份,我們的廣告服務(wù)在新版本上線(xiàn)后臼予,收到了大量的服務(wù)超時(shí)告警鸣戴,通過(guò)下面的監(jiān)控圖可以看到:超時(shí)量突然大面積增加,1分鐘內(nèi)甚至達(dá)到了上千次接口超時(shí)粘拾。下面詳細(xì)介紹下該問(wèn)題的排查過(guò)程窄锅。
檢查監(jiān)控
收到告警后,我們第一時(shí)間查看了監(jiān)控系統(tǒng)缰雇,立馬發(fā)現(xiàn)了YoungGC耗時(shí)過(guò)長(zhǎng)的異常入偷。我們的程序大概在21點(diǎn)50左右上線(xiàn)追驴,通過(guò)下圖可以看出:在上線(xiàn)之前,YGC基本幾十毫秒內(nèi)完成疏之,而上線(xiàn)后YGC耗時(shí)明顯變長(zhǎng)殿雪,最長(zhǎng)甚至達(dá)到了3秒多。
由于YGC期間程序會(huì)Stop The World锋爪,而我們上游系統(tǒng)設(shè)置的服務(wù)超時(shí)時(shí)間都在幾百毫秒丙曙,因此推斷:是因?yàn)閅GC耗時(shí)過(guò)長(zhǎng)引發(fā)了服務(wù)大面積超時(shí)。
按照GC問(wèn)題的常規(guī)排查流程其骄,我們立刻摘掉了一個(gè)節(jié)點(diǎn)亏镰,然后通過(guò)以下命令dump了堆內(nèi)存文件用來(lái)保留現(xiàn)場(chǎng)。
jmap -dump:format=b,file=heap pid
最后對(duì)線(xiàn)上服務(wù)做了回滾處理拯爽,回滾后服務(wù)立馬恢復(fù)了正常索抓,接下來(lái)就是長(zhǎng)達(dá)1天的問(wèn)題排查和修復(fù)過(guò)程。
確認(rèn)JVM配置
用下面的命令某抓,我們?cè)俅螜z查了JVM的參數(shù)
ps aux | grep "applicationName=adsearch"
-Xms4g -Xmx4g -Xmn2g -Xss1024K
-XX:ParallelGCThreads=5
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=80
可以看到堆內(nèi)存為4G纸兔,新生代和老年代均為2G,新生代采用ParNew收集器否副。
再通過(guò)命令 jmap -heap pid 查到:新生代的Eden區(qū)為1.6G汉矿,S0和S1區(qū)均為0.2G。
本次上線(xiàn)并未修改JVM相關(guān)的任何參數(shù)备禀,同時(shí)我們服務(wù)的請(qǐng)求量基本和往常持平洲拇。因此猜測(cè):此問(wèn)題大概率和上線(xiàn)的代碼相關(guān)。
檢查代碼
再回到Y(jié)GC的原理來(lái)思考這個(gè)問(wèn)題曲尸,一次YGC的過(guò)程主要包括以下兩個(gè)步驟:
1赋续、從GC Root掃描對(duì)象,對(duì)存活對(duì)象進(jìn)行標(biāo)注
2另患、將存活對(duì)象復(fù)制到S1區(qū)或者晉升到Old區(qū)
根據(jù)下面的監(jiān)控圖可以看出:正常情況下纽乱,Survivor區(qū)的使用率一直維持在很低的水平(大概30M左右),但是上線(xiàn)后昆箕,Survivor區(qū)的使用率開(kāi)始波動(dòng)鸦列,最多的時(shí)候快占滿(mǎn)0.2G了。而且鹏倘,YGC耗時(shí)和Survivor區(qū)的使用率基本成正相關(guān)薯嗤。因此,我們推測(cè):應(yīng)該是長(zhǎng)生命周期的對(duì)象越來(lái)越多纤泵,導(dǎo)致標(biāo)注和復(fù)制過(guò)程的耗時(shí)增加骆姐。
再回到服務(wù)的整體表現(xiàn):上游流量并沒(méi)有出現(xiàn)明顯變化,正常情況下,核心接口的響應(yīng)時(shí)間也基本在200ms以?xún)?nèi)玻褪,YGC的頻率大概每8秒進(jìn)行1次肉渴。
很顯然,對(duì)于局部變量來(lái)說(shuō)归园,在每次YGC后就能夠馬上被回收了黄虱。那為什么還會(huì)有如此多的對(duì)象在YGC后存活下來(lái)呢?
我們進(jìn)一步將懷疑對(duì)象鎖定在:程序的全局變量或者類(lèi)靜態(tài)變量上庸诱。但是diff了本次上線(xiàn)的代碼,我們并未發(fā)現(xiàn)代碼中有引入此類(lèi)變量晤揣。
對(duì)dump的堆內(nèi)存文件進(jìn)行分析
代碼排查沒(méi)有進(jìn)展后桥爽,我們開(kāi)始從堆內(nèi)存文件中尋找線(xiàn)索,使用MAT工具導(dǎo)入了第1步dump出來(lái)的堆文件后昧识,然后通過(guò)Dominator Tree視圖查看到了當(dāng)前堆中的所有大對(duì)象钠四。
立馬發(fā)現(xiàn)NewOldMappingService這個(gè)類(lèi)所占的空間很大,通過(guò)代碼定位到:這個(gè)類(lèi)位于第三方的client包中跪楞,由我們公司的商品團(tuán)隊(duì)提供缀去,用于實(shí)現(xiàn)新舊類(lèi)目轉(zhuǎn)換(最近商品團(tuán)隊(duì)在對(duì)類(lèi)目體系進(jìn)行改造,為了兼容舊業(yè)務(wù)甸祭,需要進(jìn)行新舊類(lèi)目映射)缕碎。
進(jìn)一步查看代碼,發(fā)現(xiàn)這個(gè)類(lèi)中存在大量的靜態(tài)HashMap池户,用于緩存新舊類(lèi)目轉(zhuǎn)換時(shí)需要用到的各種數(shù)據(jù)咏雌,以減少RPC調(diào)用,提高轉(zhuǎn)換性能校焦。
原本以為赊抖,非常接近問(wèn)題的真相了,但是深入排查發(fā)現(xiàn):這個(gè)類(lèi)的所有靜態(tài)變量全部在類(lèi)加載時(shí)就初始化完數(shù)據(jù)了寨典,雖然會(huì)占到100多M的內(nèi)存氛雪,但是之后基本不會(huì)再新增數(shù)據(jù)。并且耸成,這個(gè)類(lèi)早在3月份就上線(xiàn)使用了报亩,client包的版本也一直沒(méi)變過(guò)。
經(jīng)過(guò)上面種種分析墓猎,這個(gè)類(lèi)的靜態(tài)HashMap會(huì)一直存活捆昏,經(jīng)過(guò)多輪YGC后,最終晉升到老年代中毙沾,它不應(yīng)該是YGC持續(xù)耗時(shí)過(guò)長(zhǎng)的原因骗卜。因此,我們暫時(shí)排除了這個(gè)可疑點(diǎn)。
分析YGC處理Reference的耗時(shí)
團(tuán)隊(duì)對(duì)于YGC問(wèn)題的排查經(jīng)驗(yàn)很少寇仓,不知道再往下該如何分析了举户。基本掃光了網(wǎng)上可查到的所有案例遍烦,發(fā)現(xiàn)原因集中在這兩類(lèi)上:
1俭嘁、對(duì)存活對(duì)象標(biāo)注時(shí)間過(guò)長(zhǎng):比如重載了Object類(lèi)的Finalize方法,導(dǎo)致標(biāo)注Final Reference耗時(shí)過(guò)長(zhǎng)服猪;或者String.intern方法使用不當(dāng)供填,導(dǎo)致YGC掃描StringTable時(shí)間過(guò)長(zhǎng)。
2罢猪、長(zhǎng)周期對(duì)象積累過(guò)多:比如本地緩存使用不當(dāng)近她,積累了太多存活對(duì)象;或者鎖競(jìng)爭(zhēng)嚴(yán)重導(dǎo)致線(xiàn)程阻塞膳帕,局部變量的生命周期變長(zhǎng)粘捎。
針對(duì)第1類(lèi)問(wèn)題,可以通過(guò)以下參數(shù)顯示GC處理Reference的耗時(shí)-XX:+PrintReferenceGC危彩。添加此參數(shù)后攒磨,可以看到不同類(lèi)型的 reference 處理耗時(shí)都很短,因此又排除了此項(xiàng)因素汤徽。
再回到長(zhǎng)周期對(duì)象進(jìn)行分析
再往后娩缰,我們添加了各種GC參數(shù)試圖尋找線(xiàn)索都沒(méi)有結(jié)果,似乎要黔驢技窮泻骤,沒(méi)有思路了漆羔。綜合監(jiān)控和種種分析來(lái)看:應(yīng)該只有長(zhǎng)周期對(duì)象才會(huì)引發(fā)我們這個(gè)問(wèn)題。
折騰了好幾個(gè)小時(shí)狱掂,最終峰回路轉(zhuǎn)演痒,一個(gè)小伙伴重新從MAT堆內(nèi)存中找到了第二個(gè)懷疑點(diǎn)。
從上面的截圖可以看到:大對(duì)象中排在第3位的ConfigService類(lèi)進(jìn)入了我們的視野趋惨,該類(lèi)的一個(gè)ArrayList變量中竟然包含了270W個(gè)對(duì)象鸟顺,而且大部分都是相同的元素。
ConfigService這個(gè)類(lèi)在第三方Apollo的包中器虾,不過(guò)源代碼被公司架構(gòu)部進(jìn)行了二次改造讯嫂,通過(guò)代碼可以看出:問(wèn)題出在了第11行,每次調(diào)用getConfig方法時(shí)都會(huì)往List中添加元素兆沙,并且未做去重處理欧芽。
我們的廣告服務(wù)在apollo中存儲(chǔ)了大量的廣告策略配置,而且大部分請(qǐng)求都會(huì)調(diào)用ConfigService的getConfig方法來(lái)獲取配置葛圃,因此會(huì)不斷地往靜態(tài)變量namespaces中添加新對(duì)象千扔,從而引發(fā)此問(wèn)題憎妙。
至此,整個(gè)問(wèn)題終于水落石出了曲楚。這個(gè)BUG是因?yàn)榧軜?gòu)部在對(duì)apollo client包進(jìn)行定制化開(kāi)發(fā)時(shí)不小心引入的厘唾,很顯然沒(méi)有經(jīng)過(guò)仔細(xì)測(cè)試,并且剛好在我們上線(xiàn)前一天發(fā)布到了中央倉(cāng)庫(kù)中龙誊,而公司基礎(chǔ)組件庫(kù)的版本是通過(guò)super-pom方式統(tǒng)一維護(hù)的抚垃,業(yè)務(wù)無(wú)感知。
解決方案
為了快速驗(yàn)證YGC耗時(shí)過(guò)長(zhǎng)是因?yàn)榇藛?wèn)題導(dǎo)致的趟大,我們?cè)谝慌_(tái)服務(wù)器上直接用舊版本的apollo client 包進(jìn)行了替換鹤树,然后重啟了服務(wù),觀察了將近20分鐘逊朽,YGC恢復(fù)正常魂迄。
最后,我們通知架構(gòu)部修復(fù)BUG惋耙,重新發(fā)布了super-pom,徹底解決了這個(gè)問(wèn)題熊昌。
02 YGC的相關(guān)知識(shí)點(diǎn)總結(jié)
通過(guò)上面這個(gè)案例绽榛,可以看到Y(jié)GC問(wèn)題其實(shí)比較難排查。相比FGC或者OOM婿屹,YGC的日志很簡(jiǎn)單灭美,只知道新生代內(nèi)存的變化和耗時(shí),同時(shí)dump出來(lái)的堆內(nèi)存必須要仔細(xì)排查才行昂利。
另外届腐,如果不清楚YGC的流程,排查起來(lái)會(huì)更加困難蜂奸。這里犁苏,我對(duì)YGC相關(guān)的知識(shí)點(diǎn)再做下梳理,方便大家更全面的理解YGC扩所。
YGC的相關(guān)知識(shí)點(diǎn)總結(jié)
5個(gè)問(wèn)題重新認(rèn)識(shí)新生代
YGC 在新生代中進(jìn)行围详,首先要清楚新生代的堆結(jié)構(gòu)劃分。新生代分為Eden區(qū)和兩個(gè)Survivor區(qū)祖屏,其中Eden:from:to = 8:1:1 (比例可以通過(guò)參數(shù) –XX:SurvivorRatio 來(lái)設(shè)定 )助赞,這是最基本的認(rèn)識(shí)。
為什么會(huì)有新生代袁勺?
如果不分代雹食,所有對(duì)象全部在一個(gè)區(qū)域,每次GC都需要對(duì)全堆進(jìn)行掃描期丰,存在效率問(wèn)題群叶。分代后吃挑,可分別控制回收頻率,并采用不同的回收算法盖呼,確保GC性能全局最優(yōu)儒鹿。
為什么新生代會(huì)采用復(fù)制算法?
新生代的對(duì)象朝生夕死几晤,大約90%的新建對(duì)象可以被很快回收约炎,復(fù)制算法成本低,同時(shí)還能保證空間沒(méi)有碎片蟹瘾。雖然標(biāo)記整理算法也可以保證沒(méi)有碎片圾浅,但是由于新生代要清理的對(duì)象數(shù)量很大,將存活的對(duì)象整理到待清理對(duì)象之前憾朴,需要大量的移動(dòng)操作狸捕,時(shí)間復(fù)雜度比復(fù)制算法高。
為什么新生代需要兩個(gè)Survivor區(qū)众雷?
為了節(jié)省空間考慮灸拍,如果采用傳統(tǒng)的復(fù)制算法,只有一個(gè)Survivor區(qū)砾省,則Survivor區(qū)大小需要等于Eden區(qū)大小鸡岗,此時(shí)空間消耗是8 * 2,而兩塊Survivor可以保持新對(duì)象始終在Eden區(qū)創(chuàng)建编兄,存活對(duì)象在Survivor之間轉(zhuǎn)移即可轩性,空間消耗是8+1+1,明顯后者的空間利用率更高狠鸳。
新生代的實(shí)際可用空間是多少揣苏?
YGC后,總有一塊Survivor區(qū)是空閑的件舵,因此新生代的可用內(nèi)存空間是90%卸察。在YGC的log中或者通過(guò) jmap -heap pid 命令查看新生代的空間時(shí),如果發(fā)現(xiàn)capacity只有90%芦圾,不要覺(jué)得奇怪蛾派。
Eden區(qū)是如何加速內(nèi)存分配的?
HotSpot虛擬機(jī)使用了兩種技術(shù)來(lái)加快內(nèi)存分配个少。分別是bump-the-pointer和TLAB(Thread Local Allocation Buffers)洪乍。
由于Eden區(qū)是連續(xù)的,因此bump-the-pointer在對(duì)象創(chuàng)建時(shí)夜焦,只需要檢查最后一個(gè)對(duì)象后面是否有足夠的內(nèi)存即可壳澳,從而加快內(nèi)存分配速度。
TLAB技術(shù)是對(duì)于多線(xiàn)程而言的茫经,在Eden中為每個(gè)線(xiàn)程分配一塊區(qū)域巷波,減少內(nèi)存分配時(shí)的鎖沖突萎津,加快內(nèi)存分配速度,提升吞吐量抹镊。
新生代的4種回收器
SerialGC(串行回收器)锉屈,最古老的一種,單線(xiàn)程執(zhí)行垮耳,適合單CPU場(chǎng)景颈渊。
ParNew(并行回收器),將串行回收器多線(xiàn)程化终佛,適合多CPU場(chǎng)景俊嗽,需要搭配老年代CMS回收器一起使用。
ParallelGC(并行回收器)铃彰,和ParNew不同點(diǎn)在于它關(guān)注吞吐量绍豁,可設(shè)置期望的停頓時(shí)間,它在工作時(shí)會(huì)自動(dòng)調(diào)整堆大小和其他參數(shù)牙捉。
G1(Garage-First回收器)竹揍,JDK 9及以后版本的默認(rèn)回收器,兼顧新生代和老年代邪铲,將堆拆成一系列Region鬼佣,不要求內(nèi)存塊連續(xù),新生代仍然是并行收集霜浴。
上述回收器均采用復(fù)制算法,都是獨(dú)占式的蓝纲,執(zhí)行期間都會(huì)Stop The World.
YGC的觸發(fā)時(shí)機(jī)
當(dāng)Eden區(qū)空間不足時(shí)阴孟,就會(huì)觸發(fā)YGC。結(jié)合新生代對(duì)象的內(nèi)存分配看下詳細(xì)過(guò)程:
1税迷、新對(duì)象會(huì)先嘗試在棧上分配永丝,如果不行則嘗試在TLAB分配,否則再看是否滿(mǎn)足大對(duì)象條件要在老年代分配箭养,最后才考慮在Eden區(qū)申請(qǐng)空間慕嚷。
2、如果Eden區(qū)沒(méi)有合適的空間毕泌,則觸發(fā)YGC喝检。
3、YGC時(shí)撼泛,對(duì)Eden區(qū)和From Survivor區(qū)的存活對(duì)象進(jìn)行處理挠说,如果滿(mǎn)足動(dòng)態(tài)年齡判斷的條件或者To Survivor區(qū)空間不夠則直接進(jìn)入老年代,如果老年代空間也不夠了愿题,則會(huì)發(fā)生promotion failed损俭,觸發(fā)老年代的回收蛙奖。否則將存活對(duì)象復(fù)制到To Survivor區(qū)。
4杆兵、此時(shí)Eden區(qū)和From Survivor區(qū)的剩余對(duì)象均為垃圾對(duì)象雁仲,可直接抹掉回收。
此外琐脏,老年代如果采用的是CMS回收器攒砖,為了減少CMS Remark階段的耗時(shí),也有可能會(huì)觸發(fā)一次YGC骆膝,這里不作展開(kāi)祭衩。
YGC的執(zhí)行過(guò)程
YGC采用的復(fù)制算法,主要分成以下兩個(gè)步驟:
1阅签、查找GC Roots掐暮,將其引用的對(duì)象拷貝到S1區(qū)
2、遞歸遍歷第1步的對(duì)象政钟,拷貝其引用的對(duì)象到S1區(qū)或者晉升到Old區(qū)
上述整個(gè)過(guò)程都是需要暫停業(yè)務(wù)線(xiàn)程的(STW)路克,不過(guò)ParNew等新生代回收器可以多線(xiàn)程并行執(zhí)行,提高處理效率养交。
YGC通過(guò)可達(dá)性分析算法精算,從GC Root(可達(dá)對(duì)象的起點(diǎn))開(kāi)始向下搜索,標(biāo)記出當(dāng)前存活的對(duì)象碎连,那么剩下未被標(biāo)記的對(duì)象就是需要回收的對(duì)象灰羽。
可作為YGC時(shí)GC Root的對(duì)象包括以下幾種:
1、虛擬機(jī)棧中引用的對(duì)象
2鱼辙、方法區(qū)中靜態(tài)屬性廉嚼、常量引用的對(duì)象
3、本地方法棧中引用的對(duì)象
4倒戏、被Synchronized鎖持有的對(duì)象
5怠噪、記錄當(dāng)前被加載類(lèi)的SystemDictionary
6、記錄字符串常量引用的StringTable
7杜跷、存在跨代引用的對(duì)象
8傍念、和GC Root處于同一CardTable的對(duì)象
其中1-3是大家容易想到的,而4-8很容易被忽視葛闷,卻極有可能是分析YGC問(wèn)題時(shí)的線(xiàn)索入口憋槐。
另外需要注意的是,針對(duì)下圖中跨代引用的情況淑趾,老年代的對(duì)象A也必須作為GC Root的一部分秦陋,但是如果每次YGC時(shí)都去掃描老年代,肯定存在效率問(wèn)題治笨。在HotSpot JVM驳概,引入卡表(Card Table)來(lái)對(duì)跨代引用的標(biāo)記進(jìn)行加速赤嚼。
Card Table,簡(jiǎn)單理解是一種空間換時(shí)間的思路顺又,因?yàn)榇嬖诳绱玫膶?duì)象大概占比不到1%更卒,因此可將堆空間劃分成大小為512字節(jié)的卡頁(yè),如果卡頁(yè)中有一個(gè)對(duì)象存在跨代引用稚照,則可以用1個(gè)字節(jié)來(lái)標(biāo)識(shí)該卡頁(yè)是dirty狀態(tài)蹂空,卡頁(yè)狀態(tài)進(jìn)一步通過(guò)寫(xiě)屏障技術(shù)進(jìn)行維護(hù)。
遍歷完GC Roots后果录,便能夠找出第一批存活的對(duì)象上枕,然后將其拷貝到S1區(qū)。接下來(lái)弱恒,就是一個(gè)遞歸查找和拷貝存活對(duì)象的過(guò)程辨萍。
S1區(qū)為了方便維護(hù)內(nèi)存區(qū)域,引入了兩個(gè)指針變量:_saved_mark_word和_top返弹,其中_saved_mark_word表示當(dāng)前遍歷對(duì)象的位置锈玉,_top表示當(dāng)前可分配內(nèi)存的位置,很顯然义起,_saved_mark_word到_top之間的對(duì)象都是已拷貝但未掃描的對(duì)象拉背。
貝到S1區(qū),_top也會(huì)往前移動(dòng)默终,直到_saved_mark_word追上_top椅棺,說(shuō)明S1區(qū)所有對(duì)象都已經(jīng)遍歷完成。
有一個(gè)細(xì)節(jié)點(diǎn)需要注意的是:拷貝對(duì)象的目標(biāo)空間不一定是S1區(qū)齐蔽,也可能是老年代土陪。如果一個(gè)對(duì)象的年齡(經(jīng)歷的YGC次數(shù))滿(mǎn)足動(dòng)態(tài)年齡判定條件便直接晉升到老年代中。對(duì)象的年齡保存在Java對(duì)象頭的mark word數(shù)據(jù)結(jié)構(gòu)中(如果大家對(duì)Java并發(fā)鎖熟悉肴熏,肯定了解這個(gè)數(shù)據(jù)結(jié)構(gòu),不熟悉的建議查閱資料了解下顷窒,這里不做展開(kāi))蛙吏。
最后的話(huà)
這篇文章通過(guò)線(xiàn)上案例分析并結(jié)合原理講解,詳細(xì)介紹了YGC的相關(guān)知識(shí)鞋吉。從YGC實(shí)戰(zhàn)角度出發(fā)鸦做,再簡(jiǎn)單總結(jié)一下:
1、首先要清楚YGC的執(zhí)行原理谓着,比如年輕代的堆內(nèi)存結(jié)構(gòu)泼诱、Eden區(qū)的內(nèi)存分配機(jī)制、GC Roots掃描赊锚、對(duì)象拷貝過(guò)程等治筒。
2屉栓、YGC的核心步驟是標(biāo)注和復(fù)制,絕部分YGC問(wèn)題都集中在這兩步耸袜,因此可以結(jié)合YGC日志和堆內(nèi)存變化情況逐一排查友多,同時(shí)dump的堆內(nèi)存文件需要仔細(xì)分析。
看完三件事??
如果你覺(jué)得這篇內(nèi)容對(duì)你還蠻有幫助堤框,我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)贊域滥,轉(zhuǎn)發(fā),有你們的 『點(diǎn)贊和評(píng)論』蜈抓,才是我創(chuàng)造的動(dòng)力启绰。
關(guān)注公眾號(hào) 『 java爛豬皮 』,不定期分享原創(chuàng)知識(shí)沟使。
同時(shí)可以期待后續(xù)文章ing??