C++與java之間有一堵由內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)所圍成的“高墻”耀怜,墻外的人想進(jìn)去袜茧,墻里的人卻想出來(lái)……
與C爪瓜、C++程序員時(shí)刻要關(guān)注著內(nèi)存的分配與釋放,會(huì)不會(huì)又有哪里出現(xiàn)了內(nèi)存泄露不同是梯捕,java程序員可以“高枕無(wú)憂”厢呵。因?yàn)檫@一切都已經(jīng)有jvm來(lái)幫我們管理了,java程序員只需要關(guān)注具體的業(yè)務(wù)邏輯就可以了科阎,至于內(nèi)存分配與回收述吸,交給jvm去干吧忿族。但這樣也帶來(lái)一個(gè)問(wèn)題锣笨,我們不再去關(guān)注內(nèi)存分配了,不再去關(guān)注內(nèi)存回收了道批。一旦出現(xiàn)內(nèi)存泄露就束手無(wú)策了错英,在不同的應(yīng)用場(chǎng)景,怎么樣去做性能調(diào)優(yōu)就成了一個(gè)問(wèn)題隆豹。所以椭岩,對(duì)于java程序員來(lái)說(shuō),這些是必須了解的一部分璃赡。
沒(méi)有對(duì)象怎么辦判哥?new一個(gè)啊。單身狗程序員每次提到new對(duì)象都激動(dòng)不已碉考,可是你的對(duì)象是怎么new出來(lái)的塌计?new出來(lái)又放在哪里?怎么引用的侯谁?你的對(duì)象被別人動(dòng)了怎么辦锌仅?使用完成之后又是如何釋放的章钾?何時(shí)釋放的?等等等等這些問(wèn)題热芹,如果你不能很輕松的回答出來(lái)贱傀,那么在本系列文章中你可能會(huì)找到一些答案。當(dāng)然伊脓,本人才疏學(xué)淺府寒,文筆拙劣,只是拋磚引玉报腔,理解不周到或者有誤的地方椰棘,歡迎拍磚。
JVM內(nèi)存區(qū)域可以大致劃分為“線程隔離區(qū)域”和“線程共享區(qū)域”榄笙。所謂“線程隔離區(qū)域”即線程非共享區(qū)域邪狞,每個(gè)線程獨(dú)享的,執(zhí)行指令操作機(jī)存放私有數(shù)據(jù)茅撞。不管做什么操作帆卓,不會(huì)影響到其他線程∶浊穑可以想象成剑令,你個(gè)人電腦硬盤中的蒼老師,只能你一個(gè)人在夜深人靜的時(shí)候拉上窗簾獨(dú)自享受拄查,別人無(wú)法同你分享吁津,你刪除或者新下載也不會(huì)對(duì)別人造成影響。而“線程共享區(qū)域”則是所有的線程共同擁有的堕扶,主要存放對(duì)象實(shí)例數(shù)據(jù)碍脏。如果A線程對(duì)這塊區(qū)域的某個(gè)數(shù)據(jù)進(jìn)行了修改,而剛好B線程正在使用或者需要使用該數(shù)據(jù)稍算,則A線程對(duì)數(shù)據(jù)的修改在B線程中也會(huì)得到體現(xiàn)典尾。可以想象成你把蒼老師傳到了某社區(qū)糊探,這時(shí)候網(wǎng)上其他人都能共享你的蒼老師了钾埂。當(dāng)大家看得正興奮的時(shí)候,你突然刪掉了你上傳的老師科平,這時(shí)候大家都只能去尋找新的素材了………褥紫,不知道你是否對(duì)“線程隔離區(qū)域”和“線程共享區(qū)域”的概念有了個(gè)大致了解。在jvm中瞪慧,線程隔離區(qū)域包含程序計(jì)數(shù)器髓考、本地方法棧、虛擬機(jī)棧汞贸。線程共享區(qū)域包含堆區(qū)绳军、永久代(jdk1.8中廢除永久代)印机、直接內(nèi)存(jdk1.8中新增)(看下圖)
一、這是我的私人住所门驾,我不同意射赛,你們別來(lái)!-線程隔離區(qū)域
線程隔離區(qū)域存放什么數(shù)據(jù)呢奶是?局部變量楣责、方法調(diào)用的壓棧操作等。線程隔離區(qū)域包含巴拉巴拉……(看下圖)
1聂沙、睡了一覺(jué)秆麸,剛剛我做到哪了?-程序計(jì)數(shù)器
我們都知道在多線程的場(chǎng)景下及汉,會(huì)發(fā)生線程切換沮趣,如果當(dāng)前執(zhí)行的線程讓出執(zhí)行權(quán),則線程會(huì)被掛起坷随,當(dāng)線程再次被喚醒的時(shí)候房铭,如果沒(méi)有程序計(jì)數(shù)器線程可能就懵逼了,我是誰(shuí)温眉?我在哪缸匪?我要做什么?类溢。但是如果有了程序計(jì)數(shù)器凌蔬,線程就能找到上次執(zhí)行到的字節(jié)碼的位置繼續(xù)往下執(zhí)行。程序計(jì)數(shù)器可以理解為當(dāng)前線程正在執(zhí)行的字節(jié)碼指令的行號(hào)指示器闯冷。分支砂心、循環(huán)、跳轉(zhuǎn)窃躲、異常處理计贰、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來(lái)完成钦睡。
查閱了一些資料蒂窒,列出了程序計(jì)數(shù)器的三個(gè)特點(diǎn),這里也列舉一下
1)荞怒、如果線程正在執(zhí)行的是Java 方法洒琢,則這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址
2)、如果正在執(zhí)行的是Native 方法褐桌,則這個(gè)計(jì)數(shù)器值為空(Undefined)衰抑。因?yàn)镹ative方法大多是通過(guò)C實(shí)現(xiàn)并未編譯成需要執(zhí)行的字節(jié)碼指令。那native 方法的多線程是如何實(shí)現(xiàn)的呢荧嵌? native 方法是通過(guò)調(diào)用系統(tǒng)指令來(lái)實(shí)現(xiàn)的呛踊,那系統(tǒng)是如何實(shí)現(xiàn)多線程的則 native 就是如何實(shí)現(xiàn)的砾淌。Java線程總是需要以某種形式映射到OS線程上,映射模型可以是1:1(原生線程模型)谭网、n:1(綠色線程 / 用戶態(tài)線程模型)汪厨、m:n(混合模型)。以HotSpot VM的實(shí)現(xiàn)為例愉择,它目前在大多數(shù)平臺(tái)上都使用1:1模型劫乱,也就是每個(gè)Java線程都直接映射到一個(gè)OS線程上執(zhí)行宫静。此時(shí)侮叮,native方法就由原生平臺(tái)直接執(zhí)行驹饺,并不需要理會(huì)抽象的JVM層面上的“pc寄存器”概念——原生的CPU上真正的PC寄存器是怎樣就是怎樣邓梅。就像一個(gè)用C或C++寫的多線程程序弄唧,它在線程切換的時(shí)候是怎樣的退盯,Java的native方法也就是怎樣的狱从。
3)改橘、此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError情況的區(qū)域(程序運(yùn)行過(guò)程中計(jì)數(shù)器中改變的只是值破花,而不會(huì)隨著程序的運(yùn)行需要更大的空間)
2拉一、自己的事情自己做!-虛擬機(jī)棧
這個(gè)區(qū)域就是我們經(jīng)常所說(shuō)的棧旧乞,是java方法執(zhí)行的內(nèi)存模型蔚润,也是我們?cè)陂_(kāi)發(fā)中接觸得很多的一塊區(qū)域。虛擬機(jī)棧存放當(dāng)前正在執(zhí)行方法的時(shí)候所需要的數(shù)據(jù)尺栖、地址嫡纠、指令。每個(gè)線程都會(huì)獨(dú)享一塊椦佣模空間除盏,每次方法調(diào)用都會(huì)創(chuàng)建一個(gè)棧幀,棧幀保存了方法的局部局部變量挫以、操作數(shù)棧者蠕、動(dòng)態(tài)鏈接、出口等信息掐松。棧幀的深度也是有限制的踱侣,超過(guò)限制會(huì)拋出StackOverflowError異常。
我們結(jié)合一個(gè)例子來(lái)了解一下虛擬機(jī)棧和棧幀大磺,我們有如下代碼:
public class myProgram {
public static void main(String[] args) {
String str = "my String";
methodOne(1);
}
public static void methodOne(int i) {
int j = 2;
int sum = i + j;
// ......
methodTwo();
// .....
}
public static void methodTwo() {
if (true) {
int j = 0;
}
if (true) {
int k = 1;
}
return;
}
}
代碼很簡(jiǎn)單抡句,main調(diào)用methodOne,methodOne調(diào)用methodTwo杠愧,如果當(dāng)前正在執(zhí)行methodTwo方法待榔,則虛擬機(jī)棧中棧幀的情況應(yīng)該是如下圖情況,棧頂為正在執(zhí)行的方法流济。
我們能看到锐锣,每個(gè)棧幀都包含局部變量表腌闯,操作數(shù)棧、動(dòng)態(tài)鏈接雕憔、返回地址等……
1)绑嘹、局部變量表
顧名思義,局部變量表就是存放局部變量的表橘茉,局部變量包括方法形參工腋、方法內(nèi)部定義的局部變量。局部變量表由多個(gè)變量槽(slot)組成畅卓,每個(gè)槽位都有個(gè)索引號(hào)擅腰,索引的范圍是從0開(kāi)始至局部變量最大的slot空間,虛擬機(jī)就是通過(guò)索引定位的方式使用局部變量表翁潘。比如在methodOne方法中趁冈,形參i就是在0號(hào)索引的slot中,局部變量j就放在1號(hào)索引的slot中拜马,我們看看結(jié)合methodOne方法的字節(jié)碼進(jìn)行分析(通過(guò)javap -verbose myProgram查看字節(jié)碼文件)渗勘。
public static void methodOne(int);
descriptor: (I)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_2
1: istore_1
2: iload_0
3: iload_1
4: iadd
5: istore_2
6: invokestatic #4 // Method methodTwo:()V
9: return
LineNumberTable:
line 8: 0
line 9: 2
line 12: 6
line 14: 9
0:加載int類型常量2
1:存儲(chǔ)到索引為1的變量中(這里指源程序中的j)
2:加載索引為0的變量(這里指源程序中的i)
3:加載索引為1的變量(這里指源程序中的j)
4:執(zhí)行add指令
5:將執(zhí)行結(jié)果存儲(chǔ)到索引為2的變量中(這里指源程序中的sum)
6:靜態(tài)調(diào)用
需要注意的一點(diǎn)是,為了盡可能節(jié)省棧幀的空間俩莽,局部變量表中的slot是可以重用的旺坠,方法體重定義的變量,其作用域不一定會(huì)覆蓋整個(gè)方法體扮超,我們看看methodTwo的源碼取刃,第一個(gè)if和第二個(gè)if的作用域不一樣,所以內(nèi)部變量可能是用的同一個(gè)slot出刷,我們可以通過(guò)methodTwo方法的字節(jié)碼來(lái)驗(yàn)證一下
public static void methodTwo();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iconst_1
3: istore_0
4: return
LineNumberTable:
line 19: 0
line 23: 2
line 26: 4
你看璧疗,我沒(méi)騙你吧,methodTwo方法兩個(gè)if中的變量j和k馁龟,使用的都是索引為0的slot崩侠。這樣的設(shè)計(jì)可以節(jié)省棧幀的空間,同時(shí)也會(huì)影響jvm的垃圾回收坷檩,因?yàn)榫植孔兞勘硎荊C Root的一部分却音,局部變量表slot中當(dāng)前存放的變量關(guān)聯(lián)的對(duì)象為可達(dá)對(duì)象(后面講到垃圾回收時(shí)候再詳細(xì)講)。
2)淌喻、操作數(shù)棧
操作數(shù)棧也是一個(gè)棧僧家,也看可以成為表達(dá)式棧。操作數(shù)棧和局部變量表在訪問(wèn)方式上有著較大的差異裸删,它不是通過(guò)索引來(lái)訪問(wèn),而是通過(guò)標(biāo)準(zhǔn)的棧操作—壓棧和出椪笤—來(lái)訪問(wèn)的涯塔。我們對(duì)變量的操作都是在操作數(shù)棧中完成的肌稻,我們依然拿methodOne方法來(lái)舉例。再看一下methodOne方法的字節(jié)碼:
public static void methodOne(int);
descriptor: (I)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_2
1: istore_1
2: iload_0
3: iload_1
4: iadd
5: istore_2
6: invokestatic #4 // Method methodTwo:()V
9: return
LineNumberTable:
line 8: 0
line 9: 2
line 12: 6
line 14: 9
下圖為每一行字節(jié)碼對(duì)應(yīng)操作數(shù)棧和本地變量表之間的關(guān)系匕荸,具體看圖爹谭,不用多做描述了。
3)榛搔、動(dòng)態(tài)鏈接
每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用诺凡,持有這個(gè)引用是為了支持方法調(diào)用過(guò)程中的動(dòng)態(tài)連接。剛開(kāi)始看這一段的時(shí)候總是覺(jué)得很生澀践惑,比較拗口腹泌。我們還是繼續(xù)看那段代碼的字節(jié)碼文件,其中有一段叫做“Constant pool”尔觉,里面存儲(chǔ)了該Class文件里的大部分常量的內(nèi)容(包括類和接口的全限定名凉袱、字段的名稱和描述符以及方法的名稱和描述符)。
不知道你有沒(méi)有注意我們字節(jié)碼中是怎么處理menthodOne方法的調(diào)用的侦铜?在main方法中調(diào)用methodone方法的字節(jié)碼為invokestatic #3专甩,這里的#3就是一個(gè)” 符號(hào)引用”,我們發(fā)現(xiàn)#3還引用著另外的常量池項(xiàng)目钉稍,順著這條線把能傳遞到的常量池項(xiàng)都找出來(lái)(標(biāo)記為Utf8的常量池項(xiàng))涤躲。由此我們可以看出,invokestatic 指令就是以常量池中指向方法的符號(hào)引用作為參數(shù)贡未,完成方法的調(diào)用篓叶。這些符號(hào)引用一部分在類的加載階段(解析)或第一次使用的時(shí)候就轉(zhuǎn)化為了直接引用(指向數(shù)據(jù)所存地址的指針或句柄等),這種轉(zhuǎn)化稱為靜態(tài)鏈接羞秤。而相反的缸托,另一部分在運(yùn)行期間轉(zhuǎn)化為直接引用,就稱為動(dòng)態(tài)鏈接瘾蛋。我們看一下字節(jié)碼中的常量池和符號(hào)引用俐镐,注意main方法中的#2 #3:
Constant pool:
#1 = Methodref #6.#18 // java/lang/Object."<init>":()V
#2 = String #19 // my String
#3 = Methodref #5.#20 // myProgram.methodOne:(I)V
#4 = Methodref #5.#21 // myProgram.methodTwo:()V
#5 = Class #22 // myProgram
#6 = Class #23 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;
)V
#13 = Utf8 methodOne
#14 = Utf8 (I)V
#15 = Utf8 methodTwo
#16 = Utf8 SourceFile
#17 = Utf8 myProgram.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = Utf8 my String
#20 = NameAndType #13:#14 // methodOne:(I)V
#21 = NameAndType #15:#8 // methodTwo:()V
#22 = Utf8 myProgram
#23 = Utf8 java/lang/Object
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;
)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String my String
2: astore_1
3: iconst_1
4: invokestatic #3 // Method methodOne:(I)V
7: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 7
4)、返回地址
我們的經(jīng)常使用return x;來(lái)使方法返回一個(gè)值給方法調(diào)用者哺哼,如果沒(méi)有返回值的方法也可以在方法的方法需要返回的地方加上return;當(dāng)然佩抹,這不是必須的,因?yàn)樵创a在轉(zhuǎn)化為字節(jié)碼的時(shí)候取董,總是會(huì)在方法的最后加上return指令棍苹,不信你看上面methodTwo方法的字節(jié)碼那張圖片。
正常情況下茵汰,方法遇到返回指令退出枢里,這種退出方法的方式稱為正常完成出口。如果方法正常返回,則當(dāng)前棧幀從java棧中彈出栏豺,恢復(fù)發(fā)起調(diào)用者的方法的棧幀彬碱,如果方法有返回值,jvm會(huì)把返回值壓入到發(fā)起調(diào)用方法的操作數(shù)棧奥洼。但是在異常情況下巷疼,方法執(zhí)行遇到了異常,且這個(gè)異常在方法體內(nèi)未得到處理灵奖,方法則會(huì)異常退出嚼沿,這種退出方式稱為異常完成出口。當(dāng)異常拋出且沒(méi)有被捕捉時(shí)瓷患,則方法立即終止骡尽,然后JVM恢復(fù)發(fā)起調(diào)用的方法的棧幀,如果在調(diào)用者中也未對(duì)異常進(jìn)行捕捉尉尾,則調(diào)用者也會(huì)立即終止爆阶,層層向上,直到最外層拋出異常沙咏。
3辨图、樓上做不了的事情,來(lái)我這做肢藐!-本地方法棧
本地方法是什么故河?本地方法就是在jdk中(也可以自定義)那些被Native關(guān)鍵字修飾的方法(下圖)。這類方法有點(diǎn)類似java中的接口吆豹,沒(méi)有實(shí)現(xiàn)體鱼的,但實(shí)際上是由jvm在加載時(shí)調(diào)用底層實(shí)現(xiàn)的,實(shí)現(xiàn)體是由非java語(yǔ)言(如C痘煤、C++)實(shí)現(xiàn)的凑阶,所以本地方法可以理解為連接java代碼和其他語(yǔ)言實(shí)現(xiàn)的代碼的入口。而本地方法棧的功能就類似于虛擬機(jī)棧衷快,只是一個(gè)服務(wù)于java方法執(zhí)行宙橱,一個(gè)服務(wù)于執(zhí)行本地方法執(zhí)行。
二蘸拔、來(lái)啊师郑,快活啊调窍!反正有大把空間宝冕!-線程共享區(qū)域
1、 喂邓萨,你的對(duì)象都在這里地梨!-堆
堆區(qū)域在jvm中是非常重要的一塊區(qū)域菊卷,因?yàn)槲覀兤匠?chuàng)建的對(duì)象的實(shí)例就存在在這個(gè)區(qū)域,這個(gè)區(qū)域的幾乎是被所有線程共享湿刽。同時(shí)也是java虛擬機(jī)管理的內(nèi)存中最大的一塊的烁。由于目前主流的垃圾收集器都采用分代收集算法褐耳,所以通常將堆細(xì)分為新生代诈闺、老年代,新生代又分為兩塊Eden區(qū)铃芦、From Survivor區(qū)雅镊、To Survivor區(qū)(這里主要針對(duì)通常使用的分代收集器,G1收集器采用不同的劃分策略刃滓,后面有機(jī)會(huì)再講)仁烹。不過(guò)不管怎么劃分,目的都是為了更合理的利用內(nèi)存咧虎,提高內(nèi)存空間使用率卓缰,提高垃圾回收的效率和回收質(zhì)量。下圖展示了堆區(qū)域的劃分
我們?cè)谶@篇文章里只談堆區(qū)內(nèi)存的劃分砰诵,關(guān)于內(nèi)存分配征唬、內(nèi)存回收等會(huì)在下篇文章細(xì)講,因?yàn)樯婕暗膬?nèi)容太多了……不過(guò)我們可以先思考幾個(gè)問(wèn)題1茁彭、為什么需要區(qū)分新生代总寒、老年代?2理肺、為什么將新生代分為Eden摄闸、Survivor區(qū)?各區(qū)大小怎么分配妹萨?有什么分配依據(jù)年枕?
2、 治不了你乎完?那我就廢了你熏兄!-方法區(qū)
看標(biāo)題可能會(huì)有些誤解,其實(shí)這里廢除的是永久代的概念囱怕,而不是方法區(qū)霍弹。剛開(kāi)始總是搞不清這兩者的關(guān)系,后來(lái)就去查閱了一些資料總算是搞清楚了一些娃弓,書(shū)上是這么說(shuō)的:“JVM的虛擬機(jī)規(guī)范只是規(guī)定了有方法區(qū)這么個(gè)概念和它的作用典格,并沒(méi)有規(guī)定如何去實(shí)現(xiàn)它。不同JVM的方法區(qū)的實(shí)現(xiàn)會(huì)不一樣台丛,比如在HotSpot中使用永久代實(shí)現(xiàn)方法區(qū)耍缴,其他JVM并沒(méi)有永久代的概念砾肺。方法區(qū)是一種規(guī)范,永久代是一種實(shí)現(xiàn)防嗡”渫簦”
所以,我們常說(shuō)的新生代蚁趁、老年代裙盾、永久代中的永久代就是方法區(qū)的一種實(shí)現(xiàn),且只存在于HotSpot虛擬機(jī)中有這種概念他嫡。用過(guò)jdk1.8之前的版本(HotSpot虛擬機(jī))的同學(xué)應(yīng)該經(jīng)常能碰到永久代溢出的異撤伲“java.lang.OutOfMemoryError: PermGen space”,這里的PermGen space指的是永久代钢属。在jdk6中徘熔,永久代包含方法區(qū)和常量池,但是在jdk1.7的版本中規(guī)劃去除永久代淆党,于是在1.7中將常量池移到了老年代中酷师。在jdk1.8中徹底廢除了永久代,取而代之的是元空間染乌。
3山孔、 會(huì)有天使替我去愛(ài)你!-直接內(nèi)存
永久代設(shè)置太大吧慕匠,浪費(fèi)資源饱须!永久代設(shè)置太小吧,溢出了台谊!于是讓人惱火的永久代溢出的異常時(shí)常發(fā)生蓉媳,并且永久代的GC效率低下,于是锅铅,在jdk1.8中徹底廢除了永久區(qū)酪呻,放到了直接內(nèi)存的元空間中!元空間的本質(zhì)和永久代類似盐须,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)玩荠。元空間相比永久代有什特性呢?永久代在物理上是堆的一部分贼邓,與新生代老年代的地址是連續(xù)的阶冈,而元空間屬于本地內(nèi)存,不受JVM控制塑径,也不會(huì)發(fā)生永久代溢出的異常女坑。
直接內(nèi)存也可以稱為堆外內(nèi)存,為什么要將方法區(qū)放入到直接內(nèi)存呢统舀?
1匆骗、 永久代會(huì)為 GC 帶來(lái)不必要的復(fù)雜度劳景,并且回收效率偏低。
2碉就、 類及方法的信息等比較難確定其大小盟广,因此永久代調(diào)優(yōu)較為困難,容易發(fā)生內(nèi)存溢出瓮钥。
3筋量、 加快了復(fù)制的速度。因?yàn)槎褍?nèi)在flush到遠(yuǎn)程時(shí)骏庸,會(huì)先復(fù)制到直接內(nèi)存(非堆內(nèi)存)毛甲,然后再發(fā)送年叮,而堆外內(nèi)存相當(dāng)于省略掉了這個(gè)工作具被。
4、 Oracle 可能會(huì)將HotSpot 與 JRockit 合二為一
結(jié)尾
終于迎來(lái)了這篇文章的“殺青”只损!剛開(kāi)始搭建站點(diǎn)的時(shí)候的計(jì)劃是做到站點(diǎn)周更一姿,但是僅僅這篇文章,前前后后就花了一個(gè)多月(當(dāng)然跃惫,也只是有空的時(shí)候才來(lái)寫寫)叮叹!才發(fā)現(xiàn)要寫一篇技術(shù)文章還是真的要花很多功夫的,既要引經(jīng)還要據(jù)典爆存!后續(xù)會(huì)持續(xù)記錄蛉顽、分享!