前言
其實有很多Android開發(fā)者不明白老赤,為什么我們需要去學(xué)習(xí)jvm,在我們實際的開發(fā)工作中哪些地方用到了這方面的知識制市,或者學(xué)完這些知識我們在哪些地方能用到抬旺。我相信這是困擾很多普通android開發(fā)者的一個問題。今天我將通過這篇文章帶領(lǐng)大家去學(xué)習(xí)jvm息堂,同時通過知識點的講解來告訴大家主要學(xué)習(xí)jvm有什么用嚷狞。
一、JVM的內(nèi)存布局圖
主要組成:
從上圖我們能很清楚的看到荣堰,JVM主要包含兩個子系統(tǒng)和兩個組件
兩個子系統(tǒng):(1)類裝載子系統(tǒng)床未。(2)執(zhí)行引擎
兩個組件:(1)運行時數(shù)據(jù)區(qū)。(2)本地接口
(1)類裝載子系統(tǒng):它的主要作用是根據(jù)全限定名類名(如:java.lang.object)來裝載到運行時數(shù)據(jù)區(qū)中振坚。
(2)執(zhí)行引擎:執(zhí)行classes中的指令
(3)運行時數(shù)據(jù)區(qū)域:這就是我們常說的JVM的內(nèi)存(這里也是我們后面需要講的重點部分)
(4)本地接口:與native libraries交互薇搁,是其他編程語言交互的接口。
二渡八、內(nèi)存模型的工作機制
如上圖所示:
1啃洋、首先是利用開發(fā)工具編寫得到Java源碼,也就是我們的.java文件
2屎鳍、再利用Java源碼編譯器宏娄,將java文件轉(zhuǎn)變成.class 文件
3、最后通過類裝載子系統(tǒng)講.class文件逮壁,裝載到運行時數(shù)據(jù)去中去孵坚。
所以接下來我們講重點講解我們的jvm運行時數(shù)據(jù)區(qū)。
三窥淆、JVM運行時數(shù)據(jù)區(qū)
JVM運行時數(shù)據(jù)區(qū)主要分為兩個部分:
1卖宠、線程共享區(qū)
2、線程私有區(qū)
線程共享區(qū)分為:方法區(qū)忧饭、堆
線程私有區(qū)分為:虛擬機棧扛伍、本地方法棧、程序計數(shù)器
這里有一個要注意的點:JDK 1.8之后(包括1.8)將線程共享區(qū)中的方法區(qū)改成了元空間词裤,并放入直接內(nèi)存中
下面會逐一為大家講解刺洒,每個區(qū)中的每部分都有神馬作用,并在其中穿插講解部分面試問題吼砂。
1作媚、程序計數(shù)器
程序計數(shù)器主要有兩個作用:
(1)字節(jié)碼解釋器通過改變程序計數(shù)器來依次讀取指令,從而實現(xiàn)代碼劉的控制帅刊。
(2)在多線程的情況下纸泡,程序計數(shù)器勇于記錄當(dāng)前線程執(zhí)行的位置,從而當(dāng)線程被切回來的時候赖瞒,能夠知道該線程上次運行到哪了女揭。
這里給大家解釋一下上面兩個是什么意思,如下圖:
如上圖:當(dāng)系統(tǒng)空出了1微妙栏饮,1微妙被分成很多個時間片段吧兔。這時候有兩個線程,線程1和線程2會去爭奪這個時間資源袍嬉,如果第一個時間片被線程1搶占之后境蔼,在第一個時間片的時間內(nèi)灶平,線程1執(zhí)行到了如果A點。這時候還沒執(zhí)行完箍土。第二個時間片被線程2搶到了逢享。這時候系統(tǒng)回去執(zhí)行線程2的程序指令,當(dāng)?shù)谌齻€時間片被線程1搶到之后吴藻,線程1不會從開始的地方執(zhí)行瞒爬,而是從A(即上次執(zhí)行到的地方)去執(zhí)行。所以這里就用到了程序計數(shù)器沟堡,相當(dāng)于確認代碼執(zhí)行位置的指示器侧但。上圖的流程也被稱為:時間片輪轉(zhuǎn)機制。
這里要注意航罗,程序計數(shù)器是在多線程的情況下去干事情的禀横。
注意:程序計數(shù)器是唯一一個不會出現(xiàn) OutOfMemoryError 的內(nèi)存區(qū)域,它的生命周期隨著線程的創(chuàng)建而創(chuàng)建粥血,隨著線程的結(jié)束而死亡燕侠。
2、虛擬機棧
在上面我們講過立莉,虛擬機棧這個是屬于線程私有區(qū)绢彤,所以這里我們需要了解,每個線程所包含的內(nèi)容蜓耻,如下圖:
從上圖我們可以看出:
1茫舶、虛擬機棧是線程私有的。
2刹淌、在線程中對應(yīng)的有幾個方法饶氏,就有幾個方法棧
3、每個方法棧幀中有勾,都有對應(yīng)的局部變量表疹启,操作數(shù)棧,動態(tài)了解蔼卡,以及方法出口
第一個線程私有的這個就不用解釋了喊崖,第二個就很好的解釋了為什么遞歸會出現(xiàn)OutOfMemoryError,因為在一個線程中如果循環(huán)的去執(zhí)行方法雇逞,每個方法都會去虛擬機棧中開辟一定的空間荤懂,如果方法一直進棧而不出棧,如果當(dāng)虛擬機棧中的空間無法在被申請(也就是滿了)那么就會出現(xiàn):OutOfMemoryError
第三個當(dāng)我們執(zhí)行一個方法的時候塘砸,如下:
左側(cè)java代碼节仿,右側(cè)為字節(jié)碼。我們在分析虛擬機工作的時候掉蔬,我們做好還是從右邊字節(jié)碼分析廊宪。
首先在操作數(shù)棧中開辟一個空間存放常量1(a在操作數(shù)棧中入棧)矾瘾,讓后將常量1 放入到局部變量表中(a在操作舒展中出棧,在局部變量表中入棧)箭启,常量2 如常量1一樣壕翩。
左邊第55行開始:從剛剛?cè)霔5骄植孔兞勘淼某A?和常量2分別入棧到操作數(shù)棧,然后做ADD加法册烈;開辟操作舒椄昶茫控件存放計算的結(jié)果3婿禽,再將計算結(jié)果3放入到局部變量表中入棧赏僧。最后通過方法出口,講結(jié)果3從局部變量表中出站扭倾。
從上面的學(xué)習(xí)中我們要注意:在虛擬機棧中一般會出現(xiàn)兩種錯誤:StackOverFlowError淀零、OutOfMemoryError,其中OutOfMemoryError這個上面已經(jīng)講過了膛壹。StackOverFlowError出現(xiàn)主要是驾中,Java 虛擬機棧的內(nèi)存大小不允許動態(tài)擴展,當(dāng)線程請求棧的深度超過當(dāng)前 Java 虛擬機棧的最大深度的時候模聋,就拋出 StackOverFlowError 錯誤肩民。
3、本地方法棧
其實是在講虛擬機棧的時候链方,第二張圖已經(jīng)講本地方法棧講過了持痰。其實本地房發(fā)展和虛擬機棧所發(fā)揮的作用非常相似,區(qū)別是:虛擬機棧為虛擬機執(zhí)行 Java 方法 (也就是字節(jié)碼祟蚀,如上圖)服務(wù)工窍,而本地方法棧則為虛擬機使用到的 Native 方法服務(wù)。
4前酿、線程私有---堆
上面主要講了Java虛擬機運行時區(qū)域中的線程私有部分患雏,下面我們將展開對線程公共部分的講解,也就是我們的堆和方法區(qū)罢维,以及垃圾回收機制淹仑,性能調(diào)優(yōu)等。
其實在java虛擬機中堆才是虛擬機所管理的內(nèi)存中最大一塊肺孵,并且線程共享攻人。這個內(nèi)存區(qū)域主要是用來存放對象實例(注意這里是實例,不是引用)幾乎所有的對象實例以及數(shù)組都是在這里分配的內(nèi)存悬槽。
堆也是垃圾回收器管理的主要區(qū)域怀吻,又稱為GC 堆(Garbage Collected Heap)。
從上圖可以看出初婆,堆的內(nèi)存劃分蓬坡,主要分為:年輕代和老年代(這里還有個永久代沒有畫出)猿棉。很多小伙伴會疑問為什么要這么劃分,主要是java虛擬機根據(jù)對象存活的周期不同來劃分的屑咳。因為堆內(nèi)存是垃圾回收最頻繁的一塊區(qū)域萨赁,如果不進行區(qū)域劃分,那么新的對象和生命周期長的對象都放在一起兆龙,那么堆內(nèi)存每次進行頻繁的垃圾回收的時候杖爽,都需要遍歷所有的對象,那么這個將會耗費大量的時間紫皇,會影響我們的GC效率慰安。總結(jié)一句分區(qū)是為了提高GC效率聪铺。
1化焕、年輕代
年輕代也是mirror gc 區(qū)域,年輕代主要分為:Eden,Survivor(From,To)
新生的對象有限放在新生代中(這里指得是新生的小對象铃剔,如果是大對象會直接放入老年代),新生的對象生命周期比較短暫撒桨,存活率很低,常規(guī)的應(yīng)用在進行一次垃圾回收的時候,會回收70%~95%的空間。
1.1 年輕代MirrorGC流程:
在了解年輕代的MirrorGC流程之前破喻,我們先來了解對象的組成部分煌张,為什么要先了解這部分,因為我們MirrorGc在回收的過程中和對象是息息相關(guān)的。
從上圖可以看出,一個普通對象主要包括以下部分:
對象頭:對象頭主要由Markword,和class pointer組成茎截,markword 主要是用來存放對象的hashCode,鎖信息或者分代年齡GC等標志信息。
實例數(shù)據(jù):存放類的屬性數(shù)據(jù)信息赶盔,如果有數(shù)組實例則還需要包括數(shù)組的長度企锌,以及4自己的對齊。
對齊:由于虛擬機要求對象的其實地址必須是8字節(jié)的整數(shù)倍于未。(注意這個對齊數(shù)據(jù)不是必須存在的撕攒,如果你的數(shù)據(jù)剛剛好是8字節(jié)的整數(shù)倍,那就不需要對齊操作了)
補充一點為什么要這個對齊操作:為了提高對象的訪問效率烘浦。
GC流程:對象(這里指的是小對象抖坪,上面說了大對象直接去老年代了)new出來首先先去Eden區(qū),當(dāng)Eden區(qū)放滿之后闷叉,系統(tǒng)會開啟GC進程(這里指的是Mirror GC)去分析年輕代(Eden擦俐、From、To)握侧,回收可回收的對象蚯瞧,不能回收的移動到To 區(qū)嘿期,同時在對象頭中標記(上面說了對象頭的markword 存放了GC標記信息。)如果下次Eden區(qū)又放滿了埋合,同樣GC掃描备徐,能回收的回收,不能回收的則放入From區(qū)甚颂,以此類推蜜猾,所以總是(Eden +To)和 (Eden+From) 來回數(shù)據(jù)改變,同時改變對象頭振诬。當(dāng)對象頭中的信息到達6歲(并發(fā)的伐值蹭睡,并行是15歲,因為對象頭age只占4個字節(jié))贷揽,則到老年代中去棠笑。如果老年代中的數(shù)據(jù)也滿了的話梦碗,那么系統(tǒng)會啟動GC進程(這里是Full GC)禽绪。Full GC 會去掃描整個堆內(nèi)存區(qū),把能回收的都回收掉洪规,這里注意CMS收集器在運行的時候印屁,會消耗時間并停止所有線程,所以要盡可能的減少Full GC斩例,所以調(diào)優(yōu)主要就是調(diào)其它區(qū)的大小雄人,減少GC的可能。
注意:堆區(qū)用來存對象念赶,棧區(qū)用來存函數(shù)進行中的變量础钠,棧中存的對象,實際上是對對象的引用叉谜。
從上面我們會延伸兩個問題:1旗吁、什么是CMS收集器 ?2停局、GC的回收策略是什么很钓?下面我們一一展開。
5董栽、GC如何回收
1)標記清除算法:
該算法分為兩個階段码倦,標記、和清除階段:首先比較出所有需要回收的對象打上標記锭碳,在標記完成后統(tǒng)一對有標記過進行回收袁稽,這個是最基礎(chǔ)的算法,如下圖:
從上圖我們能看出:整理之后會產(chǎn)生大量的內(nèi)存碎片擒抛。
還有一個問題就是:效率問題推汽。這種清除方式效率不高蝗柔,為了解決效率問題推出了復(fù)制清除法。
2)復(fù)制算法
為了解決效率問題民泵,“復(fù)制”收集算法出現(xiàn)了癣丧。它可以將內(nèi)存分為大小相同的兩塊,每次使用其中的一塊栈妆。當(dāng)這一塊的內(nèi)存使用完后胁编,就將還存活的對象復(fù)制到另一塊去,然后再把使用的空間一次清理掉鳞尔。這樣就使每次的內(nèi)存回收都是對內(nèi)存區(qū)間的一半進行回收嬉橙。
3)標記整理法
跟標記清除法一樣,只是在清楚的時候不一樣寥假,標記整理法是為了解決標記清除法產(chǎn)生內(nèi)存碎片而生的市框。對可回收對象回收,而是讓所有存活的對象向一端移動糕韧,然后直接清理掉端邊界以外的內(nèi)存枫振。
4)分代收集法
虛擬機的垃圾收集都采用分代收集算法,這種算法沒有什么新的思想萤彩,只是根據(jù)對象存活周期的不同將內(nèi)存分為幾塊粪滤。一般將 java 堆分為新生代和老年代,這樣我們就可以根據(jù)各個年代的特點選擇合適的垃圾收集算法雀扶。
6杖小、垃圾收集器
關(guān)于垃圾收集器,這里我只講一個CMS 收集器愚墓,其他收集器有感興趣的小伙伴們可以自行去了解一下予权。
如果說收集算法是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實現(xiàn)
1)CMS 收集器:
CMS(Concurrent Mark Sweep)收集器(標記-清除算法):老年代并行收集器浪册,它非常符合在注重用戶體驗的應(yīng)用上使用扫腺,以獲取最短回收停頓時間為目標的收集器,具有高并發(fā)议经、低停頓的特點斧账,追求最短GC回收停頓時間。
從上面的一句話我們可以看出他的幾個特點:1煞肾、標記清除法咧织,2、并發(fā)收集器籍救, 3习绢、最短GC回收停頓時間(低停頓)
具體收集分四步:
初始標記(暫停所有其他的線程,記錄下直接和root相連的對象)
并發(fā)標記(同時開啟GC和用戶線程,用一個閉包去記錄可達對象闪萄。)
重新標記(重新標記或者修正并發(fā)期間用戶繼續(xù)運行導(dǎo)致的標記變動的那一部分對象)
并發(fā)清除 (開啟用戶線程梧却,同時GC線程開始對為標記的區(qū)域回收)
因為上面是采用的是標記清除算法,所以也具有標記清除算法的缺點:會產(chǎn)生打量空間碎片败去、同時對CPU資源敏感放航,無法處理浮動垃圾。
總結(jié)
其實關(guān)于JVM的內(nèi)容還有很多圆裕,就比如:上面沒有提到的广鳍,是如何去判斷這個對象是可以回收的還是不可以回收(可達性分析法,引用計數(shù)法吓妆,強引用赊时,弱應(yīng)用等),如何進行JVM調(diào)優(yōu)等一些列這樣的問題行拢。這里不做深入討論祖秒,感興趣的小伙伴留言,我到時候再專門出一期關(guān)于GC 是如何去判斷對象是可回收還是不可回收的問題舟奠,和JVM調(diào)優(yōu)竭缝。希望通過本次學(xué)習(xí)能為大家在了解JVM打開一個窗口,從這個窗口對應(yīng)的再去深入了解java虛擬機鸭栖。