
jvm組成
借用Java虛擬機(jī)(JVM)面試題的圖來看jvm

jvm由兩個(gè)子系統(tǒng)兩個(gè)組件組成。兩個(gè)子系統(tǒng)分別是類加載子系統(tǒng),執(zhí)行引擎。兩個(gè)組件是運(yùn)行時(shí)數(shù)據(jù)區(qū)域,本地接口。
類加載子系統(tǒng):根據(jù)給定的類名,加載進(jìn)運(yùn)行時(shí)數(shù)據(jù)區(qū)域的方法區(qū)中
執(zhí)行引擎:執(zhí)行classes的指令
本地接口:與其他編程語言交互
運(yùn)行時(shí)數(shù)據(jù)區(qū)域:jvm的內(nèi)存
作用:編譯器把java代碼變成字節(jié)碼,類加載器去加載字節(jié)碼到內(nèi)存中,即到運(yùn)行時(shí)數(shù)據(jù)區(qū)域的方法區(qū)中,然后字節(jié)碼通過執(zhí)行引擎變成底層的指令集,指令集交給cpu的過程中要調(diào)用到本地接口來實(shí)現(xiàn)整個(gè)程序的功能。
jvm內(nèi)存區(qū)域
java虛擬機(jī)再執(zhí)行java程序的時(shí)候會把他管理的內(nèi)存劃分成不同的區(qū)域,有線程私有的,有線程共享的。這里借用公眾號JavaGuide的圖片


可以看到虛擬機(jī)棧,本地方法棧,程序計(jì)數(shù)器是線程私有的。堆,方法區(qū),直接內(nèi)存是線程共享。下面一個(gè)個(gè)介紹
程序計(jì)數(shù)器
程序計(jì)數(shù)器占一塊較小內(nèi)存,字節(jié)碼解釋器工作時(shí)通過改變程序計(jì)數(shù)器的值來選取下一條要執(zhí)行的字節(jié)碼指令,同時(shí)每個(gè)線程有自己的程序計(jì)數(shù)器,意味著線程之間互不影響,切換線程后可以恢復(fù)到上次運(yùn)行的位置。
程序計(jì)數(shù)器是唯一一個(gè)不會出現(xiàn)OOM的內(nèi)存區(qū)域,因?yàn)樗纳芷陔S著線程創(chuàng)建而創(chuàng)建,結(jié)束而死亡。
虛擬機(jī)棧
虛擬機(jī)棧是線程私有的,生命周期和線程相同。描述的java方法執(zhí)行的內(nèi)存模型,每次方法調(diào)用的數(shù)據(jù)都是通過棧來傳遞。
java內(nèi)存大概分為棧內(nèi)存和堆內(nèi)存,棧指的是虛擬機(jī)棧中的局部變量表,主要存放編譯器可知的各種數(shù)據(jù)類型以及對象引用。
java虛擬機(jī)棧會出現(xiàn)兩種錯(cuò)誤StackOverFlowError與OutOfMemoryError。
本地方法棧
基本與虛擬機(jī)棧類似,虛擬機(jī)棧為虛擬機(jī)執(zhí)行java方法,本地方法棧為虛擬機(jī)使用的native方法服務(wù)。
堆
堆是虛擬機(jī)中占內(nèi)存最大的一塊,負(fù)責(zé)存放對象實(shí)例,幾乎所有對象實(shí)例和數(shù)組都在這分配內(nèi)存。如果某些方法中的對象引用沒有被返回或者未被外面使用,就會分配內(nèi)存在棧上。
java堆是垃圾收集器管理的主要區(qū)域,由于現(xiàn)在都采用分代垃圾收集算法,所以堆細(xì)分成新生代,老年代。

可以看到j(luò)vm分為堆內(nèi)存和非堆內(nèi)存,非堆內(nèi)存就是永久代,又稱方法區(qū)。堆內(nèi)存存放的是對象,同時(shí)垃圾收集器就是判斷處理這些對象的。非堆內(nèi)存存放的是永久代,放的是程序運(yùn)行時(shí)長期存在的對象,如類的方法,常量。
JDK8的時(shí)候廢除了永久代,然后在直接內(nèi)存里面弄了個(gè)元空間,都是方法區(qū)的實(shí)現(xiàn)。
方法區(qū)
方法區(qū)是各線程共享的一個(gè)區(qū)域,存儲類的常量,靜態(tài)變量等。方法區(qū)與永久代的關(guān)系引用文獻(xiàn)
《Java 虛擬機(jī)規(guī)范》只是規(guī)定了有?法區(qū)這么個(gè)概念和它的作?,并沒有規(guī)定如何去實(shí)現(xiàn)它。那么,在不同的 JVM 上?法區(qū)的實(shí)現(xiàn)肯定是不同的了。 ?法區(qū)和永久代的關(guān)系很像Java 中接?和類的關(guān)系,類實(shí)現(xiàn)了接?,?永久代就是 HotSpot 虛擬機(jī)對虛擬機(jī)規(guī)范中?法區(qū)的?種實(shí)現(xiàn)?式。 也就是說,永久代是 HotSpot 的概念,?法區(qū)是 Java 虛擬機(jī)規(guī)范中的定義,是?種規(guī)范,?永久代是?種實(shí)現(xiàn),?個(gè)是標(biāo)準(zhǔn)?個(gè)是實(shí)現(xiàn),其他的虛擬機(jī)實(shí)現(xiàn)并沒有永久代這?說法。
為什么要用元空間來代替永久代,永久代有jvm設(shè)置的固定大小,不能調(diào)整,所以經(jīng)常發(fā)生空間溢出的錯(cuò)誤,而元空間用的是直接內(nèi)存,就是你機(jī)器可用內(nèi)存的限制,出現(xiàn)錯(cuò)誤的概率比較小。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池在方法區(qū)里面,一開始運(yùn)行時(shí)常量池邏輯包括字符串常量池在永久代里面,jdk7后就把字符串常量池移到了堆中,運(yùn)行池常量池則還在方法區(qū),不過方法區(qū)從永久代變成了元空間。
堆和棧的區(qū)別
- 棧是線程之間私有的,堆是所有線程共享的。
- 棧的存取速度比堆快,
java類加載的過程
分為三個(gè)步驟,加載,連接,初始化。其中連接可以分為驗(yàn)證,準(zhǔn)備,解析。
加載:將class文件加載進(jìn)內(nèi)存,并創(chuàng)建一個(gè)class對象。類的加載有類加載器來完成。
驗(yàn)證:確保加載類的信息符合規(guī)范,無安全問題。
準(zhǔn)備:為類的靜態(tài)Field分配內(nèi)存,設(shè)置初始值(不是代碼設(shè)置的初始值,是java虛擬機(jī)的默認(rèn)初始值)
解析:將類的二進(jìn)制數(shù)據(jù)中的符號引用替換成直接引用
初始化:對類的變量初始化,對static修飾的變量或者代碼塊進(jìn)行初始化。
類加載器有 啟動類加載器,擴(kuò)展類加載器,系統(tǒng)類加載器,用戶自定義類加載器
類加載的機(jī)制
雙親委托機(jī)制:首先,每個(gè)加載器都有對應(yīng)的父加載器,除了啟動類加載器。
類加載器收到加載的請求,不會自己立馬加載,而是去把請求轉(zhuǎn)給父類,如果父類還有父類,就繼續(xù)轉(zhuǎn)。當(dāng)轉(zhuǎn)到啟動類加載器(即沒有父類了),判斷啟動類加載器有沒有加載過,有就加載成功,不能就回退給啟動類加載器的子類,嘗試是否被加載過,不能就繼續(xù)回退,一直到第一個(gè)類加載器自己加載為止。
優(yōu)點(diǎn):防止重復(fù)加載一個(gè)類,保證數(shù)據(jù)安全。
java對象的創(chuàng)建過程
分為類加載檢查,分配內(nèi)存,把內(nèi)存區(qū)域初始化零值,設(shè)置對象頭,執(zhí)行init方法。
類加載檢查:檢查new后面的參數(shù)是否能在常量池中定位到這個(gè)類的符號引用,檢查是否被加載過,沒有就按步驟去加載類。
分配內(nèi)存:為新的對象分配內(nèi)存,內(nèi)存大小在第一步就已經(jīng)知道了。分配方式有“指針碰撞”和“空閑列表”兩種。
初始化零值:虛擬機(jī)將分配到的內(nèi)存都初始化為零值。(注意:不包括對象頭)
設(shè)置對象頭:把一些必要信息存放到對象頭中。
執(zhí)行init方法:執(zhí)行完new之后,已經(jīng)生成了一個(gè)可用的對象了,然后要按照程序員的意愿執(zhí)行init方法,就是把它設(shè)置成任意值這種,人為的初始化。
對象的訪問方式
訪問方式由虛擬機(jī)來實(shí)現(xiàn),一般由兩種,句柄訪問,直接指針。
內(nèi)存分配
堆內(nèi)存分為新生代,老年代。新生代又分為eden區(qū),survivor from(s0),survivor to(s1),eden區(qū)域最大。一般對象會在eden區(qū)分配,當(dāng)eden沒有足夠空間去分配,就發(fā)起一次Minor GC(新生代垃圾收集)。大對象(需要大量連續(xù)內(nèi)存空間)直接進(jìn)入老年代,長期存活的對象進(jìn)入老年代。每個(gè)對象都有一個(gè)age計(jì)數(shù)器,對象在survivor區(qū)每經(jīng)過一次Minor GC就增加一歲,如果到了默認(rèn)值(一般15),就會變成老年代。
判斷對象死亡的兩種方法
堆進(jìn)行回收要判斷對象是否已經(jīng)死亡(不再被任何途徑使用的對象)。
引用計(jì)數(shù)法:每個(gè)對象添加一個(gè)引用計(jì)數(shù)器,有被引用就+1,失效就-1。如果計(jì)數(shù)器為0就是不可能再被使用的了。
可達(dá)性分析算法:把名字是GC Roots的對象作為起點(diǎn),通過這些起點(diǎn)開始往下搜索,節(jié)點(diǎn)走過的路徑就是引用鏈,如果一個(gè)對象到GC Roots沒有任何引用鏈,就代表對象不可用了,可以被回收了
圖片出自JavaGuide的復(fù)習(xí)資料

垃圾收集算法
- 標(biāo)記-清理算法
先標(biāo)記出所有不需要回收的對象,然后把沒有標(biāo)記的對象都給回收了。有效率問題和回收后存在大量不連續(xù)的碎片問題。 - 復(fù)制算法
先把內(nèi)存分為兩塊相同大小的內(nèi)存塊,每次使用都只在一個(gè)上面使用,當(dāng)這塊內(nèi)存使用完后,回收掉不需要的對象,然后把剩下的對象復(fù)制到另一個(gè)內(nèi)存上面,再把這個(gè)內(nèi)存塊全部清理掉,下次再使用。 - 標(biāo)記-整理算法
先進(jìn)行標(biāo)記不需要回收的對象,然后把所有對象移動到另一端,然后直接清理掉邊界以外的內(nèi)存。 - 分代收集算法
根據(jù)存活周期分為不同代的對象,如新生代,老年代一樣。根據(jù)每個(gè)年代的特點(diǎn)執(zhí)行相對應(yīng)的算法。如新生代使用復(fù)制算法,老年代使用標(biāo)記清理,標(biāo)記-整理算法。這樣子就可以提高gc的效率。
四個(gè)引用
引用計(jì)數(shù)法和可達(dá)性分析算法都是判斷對象是否被引用。引用又可以分為強(qiáng)引用,軟引用,弱引用,虛引用。
強(qiáng)引用:大部分引用都是強(qiáng)引用,垃圾回收器決定不會回收強(qiáng)引用,即使拋出OOM錯(cuò)誤,終止程序都不會隨意回收強(qiáng)引用對象??赡軙?dǎo)致內(nèi)存泄露。
在安卓中一般就是new一個(gè)對象就是強(qiáng)引用了。
軟引用:軟引用如果內(nèi)存足夠就不管他,如果內(nèi)存不足了,就會開始回收他。只要沒被回收就可以使用。
從網(wǎng)絡(luò)上獲取照片并顯示時(shí),用軟引用緩存下來。下次再去網(wǎng)絡(luò)上獲取的時(shí)候就可以先判斷該圖片有沒有被緩存,有的話就顯示。
弱引用:如果垃圾回收器發(fā)現(xiàn)了弱引用的對象,不管內(nèi)存是否充足,都會進(jìn)行回收。但是垃圾回收機(jī)制優(yōu)先級低,所以一般不會立馬回收掉。
安卓中也可以緩存一些數(shù)據(jù),防止內(nèi)存泄露。
虛引用:如果一個(gè)對象有虛引用,和沒有引用一樣。任何時(shí)候都可能被回收。虛引用主要用于跟蹤對象被垃圾回收的活動。