對(duì)于從事 C/C++ 程序開(kāi)發(fā)的開(kāi)發(fā)人員來(lái)說(shuō)挽荡,在內(nèi)存管理領(lǐng)域,他們既是擁有最高權(quán)力的帝皇,又是從事最基礎(chǔ)工作的勞動(dòng)人民——既擁有每一個(gè)對(duì)象的“所有權(quán)”腋逆,又擔(dān)負(fù)著每一個(gè)對(duì)象生命從開(kāi)始到終結(jié)的維護(hù)責(zé)任旁壮。
對(duì)于 Java 程序員來(lái)說(shuō)监嗜,在虛擬機(jī)的自動(dòng)內(nèi)存管理機(jī)制的幫助下,不在需要為每一個(gè) new 操作去寫配對(duì)的 delete/free 代碼抡谐,而且不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出問(wèn)題裁奇,看起來(lái)由虛擬機(jī)管理內(nèi)存一切都很美好。不過(guò)也正是因?yàn)?Java 程序員把內(nèi)存控制權(quán)交給了 Java 虛擬機(jī)麦撵,一旦出現(xiàn)內(nèi)存泄漏和溢出的問(wèn)題刽肠,如果不了解虛擬機(jī)怎樣使用內(nèi)存的,那排查錯(cuò)誤將會(huì)成為一項(xiàng)異常艱難的工作免胃。
1. 什么是 JVM音五?
JVM(Java 虛擬機(jī))是 Java Virtual Machine 的縮寫,它是一個(gè)虛構(gòu)出來(lái)的計(jì)算機(jī)杜秸,通過(guò)在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來(lái)實(shí)現(xiàn)的放仗。
JVM 有自己的硬件架構(gòu),如處理器撬碟、堆棧诞挨、寄存器等,還有對(duì)應(yīng)分指令系統(tǒng)呢蛤。
假如一個(gè)程序使用的內(nèi)存區(qū)域是一個(gè)貨架惶傻,那 JVM 就相當(dāng)于是一個(gè)淘寶店鋪,它不是真實(shí)存在的貨架其障,但它和真實(shí)貨架一樣可以上架和下架商品银室,而且上架的商品數(shù)量也是有限的。
假如貨架是在深圳励翼,那 JVM 的平臺(tái)無(wú)關(guān)性就相當(dāng)于是客人可以在各個(gè)地方購(gòu)買你在淘寶上發(fā)布的商品蜈敢,不是只有在深圳才能購(gòu)買貨架上的商品。
2. 什么是 Java 內(nèi)存模型汽抚?
Java 內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問(wèn)規(guī)則抓狭,也就是在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存,以及從內(nèi)存中取出變量這樣的底層細(xì)節(jié)造烁。
下面我們就來(lái)看下 Java 內(nèi)存模型的具體介紹否过。
2.1 主內(nèi)存與工作內(nèi)存
Java 內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)阿紫主內(nèi)存(Main Memory)中午笛,每條線程有自己的工作內(nèi)存(Working Memory),線程的工作內(nèi)存中保存了該線程所使用到的變量的內(nèi)存副本苗桂。
不同線程之間無(wú)法直接訪問(wèn)其他線程工作內(nèi)存中的變量药磺,線程間變量的傳遞都要通過(guò)主內(nèi)存來(lái)完成(如下圖 2-1所有)。
圖2-1 Java 內(nèi)存模型
2.2 執(zhí)行引擎
所謂執(zhí)行引擎煤伟,就是一個(gè)運(yùn)算器癌佩,能夠識(shí)別輸入的指令,并根據(jù)輸入的指令執(zhí)行一套特定的邏輯便锨,最終輸入特定的結(jié)果執(zhí)行引擎對(duì)于 JVM 的作用就像是 CPU 對(duì)于實(shí)體機(jī)器的作用驼卖,都可以識(shí)別指令,并且根據(jù)指令完成特定的運(yùn)算鸿秆。
2.3 主內(nèi)存與工作內(nèi)存的交互模型
Java 內(nèi)存模型中定義了 8 種操作完成主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,虛擬機(jī)實(shí)現(xiàn)時(shí)必須保證 每一種操作都是原子級(jí)怎囚、不可再分的卿叽。
這 8 種操作又可分為作用于主內(nèi)存和作用于工作內(nèi)存的操作。
2.3.1 作用于主內(nèi)存的操作
① lock(鎖定)
作用于主內(nèi)存的變量恳守,它把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)考婴。
② unlock(解鎖)
作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái)催烘,釋放后的變量才能被其他線程鎖定沥阱。
③ read(讀取)
作用于主內(nèi)存的變量伊群,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中考杉,以便 laod 時(shí)使用。
④ write(寫入)
作用于主內(nèi)存的變量舰始,它把 store 操作從工作內(nèi)存中得到的變量值存入到主內(nèi)存變量中崇棠。
2.3.2 作用于工作內(nèi)存的操作
① load(載入)
作用于工作內(nèi)存的變量,它把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中丸卷。
② use(使用)
作用于工作內(nèi)存的變量枕稀,它把一個(gè)工作內(nèi)存中的一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用的變量的值的字節(jié)碼執(zhí)行時(shí)會(huì)執(zhí)行這個(gè)操作谜嫉。
③ assign(賦值)
作用于工作內(nèi)存的變量萎坷,它把一個(gè)執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)變量賦值的字節(jié)碼執(zhí)行時(shí)執(zhí)行這個(gè)操作沐兰。
④ store(存儲(chǔ))
作用于工作內(nèi)存的變量哆档,它把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的 write 操作僧鲁。
3. JVM 是怎么劃分內(nèi)存的虐呻?
Java 虛擬機(jī)在執(zhí)行 Java 程序過(guò)程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域象泵。這些區(qū)域都有各自的用途,以及創(chuàng)建和銷毀的時(shí)間斟叼,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而存在偶惠,有些區(qū)域則是以來(lái)用戶線程的啟動(dòng)和結(jié)束而建立和銷毀。Java 虛擬機(jī)所管理的內(nèi)存將會(huì)包括以下幾個(gè)運(yùn)行區(qū)域朗涩,如圖4-1所示忽孽。
圖4-1 Java 虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)
3.1 線程私有的數(shù)據(jù)區(qū)
3.1.1 程序計(jì)數(shù)器
程序計(jì)數(shù)器有以下 三個(gè)特點(diǎn):
- 較小
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它的作用可以看做是當(dāng)前線程所指向的字節(jié)碼的行號(hào)指示器谢床。在虛擬機(jī)的概念模型里兄一,字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要指向字節(jié)碼指令,分支识腿、循環(huán)出革、跳轉(zhuǎn)、異常處理渡讼、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來(lái)完成骂束。
- 線程私有
由于 Java 虛擬機(jī)的多線程是通過(guò)線程輪流 切換并分配處理器指向時(shí)間的方式來(lái)實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻成箫,一個(gè)處理器(對(duì)于多核處理器來(lái)說(shuō)是一個(gè)內(nèi)核)只會(huì)執(zhí)行一條線程的指令展箱。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置蹬昌,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器混驰,各條線程之間的計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ)皂贩,我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存栖榨。
- 無(wú)異常
如果線程正在執(zhí)行的是一個(gè) Java 的方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址先紫;如果正在執(zhí)行的是 Native 方法治泥,這個(gè)計(jì)數(shù)器值則為空(Undefined)。此內(nèi)存區(qū)域是唯一一個(gè)在 Java 虛擬機(jī)規(guī)范中沒(méi)有任何 OutOfMemoryError 情況的區(qū)域遮精。
3.1.2 虛擬機(jī)棧
- 線程私有
與程序計(jì)數(shù)器一樣居夹,Java 虛擬機(jī)棧也是線程私有的,它的生命周期與線程相同本冲。
- 描述 Java 方法執(zhí)行的內(nèi)存模型
虛擬機(jī)棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀(Stack Frame准脂,棧幀是方法運(yùn)行期的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu))用于存儲(chǔ)局部變量表、操作棧檬洞、動(dòng)態(tài)鏈接狸膏、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完成的過(guò)程添怔,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧棧從入棧到出棧的過(guò)程湾戳。
- 異常
在 Java 虛擬中贤旷,對(duì)虛擬機(jī)規(guī)定了下面兩種異常:
① StockOverflowError
當(dāng)執(zhí)行 Java 方法是會(huì)進(jìn)行壓棧的操作,在棧棧會(huì)保存局部變量砾脑、操作棧和方法出口等信息幼驶。
JVM 規(guī)定了棧的最大勝讀,如果線程請(qǐng)求執(zhí)行方法時(shí)棧的深度大于規(guī)定的深度韧衣,就會(huì)拋出棧溢出異常 StockOverflowError盅藻。
② OutOfMemoryError
如果虛擬機(jī)在擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出內(nèi)存溢出異常 OutOfMemoryError畅铭。
3.1.3 本地方法棧
本地方法棧的作用于虛擬機(jī)非常相似氏淑,它有下面兩個(gè)特點(diǎn)。
① 為 native 服務(wù)
本地方法棧與虛擬機(jī)棧的區(qū)別是虛擬機(jī)棧為 Java 服務(wù)硕噩,而本地方法棧為 native 方法服務(wù)假残。
② 異常
與虛擬機(jī)棧一樣,本地方法棧也會(huì)拋出 StackOverflowError 和 OutOfMemoryError 異常炉擅。
3.2 所有線程共享的數(shù)據(jù)區(qū)域
3.2.1 Java 堆
Java 堆(Java Heap)也就是實(shí)例堆守问,它有以下四個(gè)特點(diǎn):
① 最大
對(duì)于大多數(shù)應(yīng)用來(lái)說(shuō),Java 堆(Java Heap)是 Java 虛擬機(jī)所管理的內(nèi)存中最大的一塊坑资。
② 線程共享
Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建穆端。
③ 存放實(shí)例
此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象的實(shí)例袱贮,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存。這一點(diǎn)在 Java 虛擬機(jī)規(guī)范中的描述是:所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配体啰,但是隨著 JIT 編譯器的發(fā)展與逃逸分析技術(shù)的逐漸成熟攒巍,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化發(fā)送荒勇,所有的對(duì)象都分配在堆上也漸漸變得不那么“絕對(duì)”了 柒莉。
④ GC
Java 堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱為“GC堆”(Carbage Collected Heap)沽翔。如果從內(nèi)存回收的角度看兢孝,由于現(xiàn)在收集器基本都是采用的分代收集算法(詳見(jiàn)XXX),所以 Java 堆中還可以細(xì)分為:新生代和老年代仅偎。如果從內(nèi)存分配的角度看跨蟹,線程共享的 Java 堆中可能劃分多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)橘沥。不過(guò)窗轩,無(wú)論如何劃分,都與存放內(nèi)容無(wú)關(guān)座咆,無(wú)論哪個(gè)區(qū)域痢艺,存儲(chǔ)的都依然是對(duì)象實(shí)例仓洼,進(jìn)一步劃分的目的是為了更好地回收內(nèi)存,或者更快地分配內(nèi)存堤舒。
3.2.2 方法區(qū)
方法區(qū)存儲(chǔ)的是已經(jīng)被虛擬機(jī)加載的數(shù)據(jù)色建,它有以下三個(gè)特點(diǎn):
① 線程共享
方法區(qū)域 Java 堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域植酥,它用于存儲(chǔ)已經(jīng)被虛擬機(jī)加載的等數(shù)據(jù)镀岛。
② 存儲(chǔ)的數(shù)據(jù)類型
◇ 類的信息;
◇ 常量友驮;
**◇ **靜態(tài)變量漂羊;
◇ 即時(shí)編譯器編譯后的代碼,等卸留。
③ 異常
方法區(qū)的大小決定履歷系統(tǒng)可以保存多少個(gè)類走越,如果系統(tǒng)定義了太多的類,導(dǎo)致方法區(qū)溢出耻瑟,虛擬機(jī)同樣拋出內(nèi)存溢出異常 OutOfMemoryError旨指。
方法區(qū)又可以分為運(yùn)行時(shí)常量池和直接內(nèi)存兩部分。
① 運(yùn)行常量池
運(yùn)行時(shí)常量池(Running Constant Pool)是方法區(qū)的一部分喳整。
Class 文件中處了有類的 版本谆构、字段、方法和接口等描述信息框都,還有一項(xiàng)信息就是常量池(Constant Pool Table)搬素。
常量池用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
運(yùn)行時(shí)常量池受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)就會(huì)拋出 OutOfMemoryError 異常夺欲。
② 直接內(nèi)存
直接內(nèi)存(Direct Memory)有以下四個(gè)特點(diǎn):
a)在虛擬機(jī)數(shù)據(jù)區(qū)外
直接內(nèi)存不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是 Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域粱哼。
b)直接分配
在 JDL1.4 中新加入的 NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的 I/O 方式檩咱,它可以使用 native 函數(shù)庫(kù)直接分配堆外內(nèi)存揭措,然后通過(guò)一個(gè)存儲(chǔ)在 Java 堆中的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用操作,這樣能避免在 Java 堆和 native 堆中來(lái)回復(fù)制數(shù)據(jù)刻蚯。
c)受設(shè)備內(nèi)存大小限制
直接內(nèi)存的分配不會(huì)受到 Java 堆大小的限制蜂筹,但是會(huì)受到設(shè)備總內(nèi)存(RAM 以及 SWAP 區(qū))大小以及處理器尋址空間的限制。
d)異常
直接內(nèi)存的容量默認(rèn)與 Java 對(duì)的最大值一樣芦倒,如果超額申請(qǐng)內(nèi)存艺挪,也可能導(dǎo)致 OOM 異常出現(xiàn)。
4. 對(duì)象訪問(wèn)
介紹完 Java 虛擬機(jī)的運(yùn)行時(shí)數(shù)據(jù)區(qū)之后,我們可以探討一個(gè)問(wèn)題:在 Java 語(yǔ)言中麻裳,對(duì)象訪問(wèn)是如何進(jìn)行的口蝠?對(duì)象訪問(wèn)在 Java 中中悟出不在,是最普通的程序行為津坑,但即使是最簡(jiǎn)單的訪問(wèn)妙蔗,也會(huì)涉及 Java 棧、Java 堆疆瑰、方法區(qū)這三個(gè)最重要內(nèi)存區(qū)域之間的關(guān)聯(lián)關(guān)系眉反,如下面這段代碼:
<pre style="margin: 0px; padding: 0px; overflow: auto; font-family: "Courier New"; font-size: 12px; overflow-wrap: break-word; white-space: pre-wrap;">Object obj = new Object();</pre>
假如這句代碼出現(xiàn)在方法體中,那“Object obj”這部分的寓意兼顧會(huì)反映到 Java 棧的本地變量表中穆役,作為一個(gè) reference 類型數(shù)據(jù)出現(xiàn)寸五。而“new Object()”這部分的語(yǔ)義將會(huì)反映 到 Java 堆中,形成一塊存儲(chǔ)了 Object 類型所有實(shí)例數(shù)據(jù)值(Instance Data耿币,對(duì)象中各個(gè)實(shí)例字段的數(shù)據(jù))的結(jié)構(gòu)化內(nèi)存梳杏,根據(jù)具體類型以及虛擬機(jī)實(shí)現(xiàn)的對(duì)象內(nèi)存布局(Object Memory Layout)的不同,這塊內(nèi)存的長(zhǎng)度是不固定的淹接。另外十性,在 Java 堆中還必須包含能查找到此對(duì)象類型數(shù)據(jù)(如對(duì)象類型、父類塑悼、實(shí)現(xiàn)的接口劲适、方法等)的地址信息,這些類型數(shù)據(jù)則存儲(chǔ)在方法區(qū)中厢蒜。
由于 reference 類型在 Java 虛擬機(jī)規(guī)范里面只規(guī)定了一個(gè)指向?qū)ο蟮囊眉跸欤](méi)有定義這個(gè)引用應(yīng)該通過(guò)哪些方法區(qū)定位,以及訪問(wèn)到 Java 堆中的對(duì)象的具體位置郭怪,因此不同虛擬機(jī)的對(duì)訪問(wèn)方式會(huì)有所不同,主流的訪問(wèn)方法有兩種:使用句柄和直接指針刊橘。
- 如果使用句柄訪問(wèn)方式鄙才,Java 堆中將會(huì)劃分出一塊內(nèi)存作為句柄池,reference 中存儲(chǔ)的就是對(duì)象的句柄地址促绵,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)和類型數(shù)據(jù)各自的具體地址信息攒庵,如圖4-1所示。
圖4-1 通過(guò)句柄訪問(wèn)對(duì)象
- 如果使用直接指針訪問(wèn)方式败晴,Java 堆對(duì)象的布局中就必須考慮如何放置訪問(wèn)類型數(shù)據(jù)的相關(guān)信息浓冒,reference 中直接存儲(chǔ)的就是對(duì)象地址,如圖4-2所示尖坤。
圖4-2 通過(guò)直接指針獲取對(duì)象
兩種對(duì)象的訪問(wèn)方式各有優(yōu)勢(shì)稳懒,使用句柄訪問(wèn)方式的最大好處就是 reference 中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針慢味,而 reference 本身不需要被修改场梆。
使用直接指針訪問(wèn)方式的最大好處就是速度更快墅冷,它節(jié)省了一次指針定位的時(shí)間開(kāi)銷,由于對(duì)象的訪問(wèn)在 Java 中非常頻繁或油,因此這類開(kāi)銷積少成多也是一項(xiàng)非衬蓿可觀的執(zhí)行成本。
5. 棧幀中的數(shù)據(jù)有什么用顶岸?
當(dāng) Java 程序出現(xiàn)異常時(shí)腔彰,程序會(huì)打印出對(duì)于的異常堆棧,通過(guò)這個(gè)堆棧我們可以知道方法的調(diào)用鏈路辖佣,而這個(gè)調(diào)用鏈路就是有一個(gè)個(gè) Java 方法棧幀組成霹抛。
下面來(lái)看下棧幀包含的局部變量表、操作數(shù)棧凌简、動(dòng)態(tài)連接和返回地址分別有著什么作用:
圖5-1 棧幀
5.1 局部變量表
局部變量表(Local Variable Table)中的變量只在當(dāng)前函數(shù)調(diào)用中有效上炎,當(dāng)函數(shù)調(diào)用結(jié)束后,隨著函數(shù)棧幀的銷毀雏搂,局部變量表也會(huì)隨之銷毀藕施。
局部變量表中存放著編譯期可知的各種數(shù)據(jù),如以下三種:
① 基本數(shù)據(jù)類型
如 boolean凸郑、char裳食、int等。
② 對(duì)象引用
reference 類型芙沥,可能是一個(gè)執(zhí)行對(duì)象起始地址的引用指針诲祸,也可能是指向一個(gè)代表對(duì)象的句柄或其他與此對(duì)象相關(guān)的位置。
③ returnAddress 類型
指向了一條字節(jié)碼指令的地址而昨。
5.2 操作數(shù)棧
操作數(shù)棧(Operand Stack)也叫操作棧救氯,它主要用于保存計(jì)算過(guò)程的中間結(jié)果,同時(shí)作為計(jì)算過(guò)程中臨時(shí)變量的存儲(chǔ)空間歌憨。
操作數(shù)棧也是一個(gè)先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu)着憨,只支持入棧和出棧兩種操作。
當(dāng)一個(gè)方法剛開(kāi)執(zhí)行時(shí)务嫡,操作數(shù)是空的甲抖,在方法執(zhí)行的過(guò)程中,會(huì)有各種字節(jié)碼執(zhí)行往操作數(shù)棧中寫入和提取內(nèi)容心铃,也就是出棧/入棧操作准谚。
如下面的這張圖(如圖5-2所示)中,當(dāng)調(diào)用了虛擬機(jī)額 iadd 指令后去扣,它就會(huì)在操作數(shù)棧彈出兩個(gè)整數(shù)并進(jìn)行加法計(jì)算柱衔,并將計(jì)算結(jié)果入棧。
圖5-2 iadd 指令與操作數(shù)棧的變化
5.3 動(dòng)態(tài)連接
每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬非法的引用,持有這個(gè)引用是為了支持方法調(diào)用過(guò)程中的動(dòng)態(tài)連接(Dynamic Linking)秀存。
5.4 方法返回地址
當(dāng)一個(gè)方法開(kāi)始執(zhí)行后捶码,只有兩種方式可以退出這個(gè)方法,一種是正常調(diào)用完成或链,另一種是異常調(diào)用完成惫恼。
① 正常調(diào)用完成
執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,這時(shí)候可能會(huì)有返回值傳遞給上層方法調(diào)用者澳盐。
是否有返回值和返回值的類型將根據(jù)遇到哪種方法返回指令來(lái)決定祈纯,這種退出方法的方式成為正常調(diào)用完成(Normal Method Invocation Completion)。
**② 異常調(diào)用完成 **
在方法執(zhí)行過(guò)程中遇到異常叼耙,并且這個(gè)異常沒(méi)有在方法體內(nèi)得到處理腕窥,就會(huì)導(dǎo)致方法退出,這種退出方式成為異常調(diào)用完成(Abrupt Method Invocation Completion)
一個(gè)方法使用異常調(diào)用完成 的方法退出筛婉,任何值都不會(huì)返回給它的調(diào)用者簇爆。
無(wú)論采用哪種退出方式,在方法退出后爽撒,都需要返回到方法被調(diào)用的位置入蛆,程序才能繼續(xù)執(zhí)行。
6. 什么是可達(dá)性算法硕勿?
在主流的商用程序語(yǔ)言(Java哨毁、C#等)的主流實(shí)現(xiàn)中,都是通過(guò)可達(dá)性分析(Reachability Analysis)判定對(duì)象是否存活源武。
這個(gè)算法的基本思路就是通過(guò)一系列“GC Roots”對(duì)象作為起始點(diǎn)扼褪,從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索走過(guò)的路徑就叫引用鏈粱栖。
當(dāng)一個(gè)對(duì)象到 GC Roots 沒(méi)有與任何引用鏈相連時(shí)话浇,則證明此對(duì)象是無(wú)用的。
比如闹究,下圖(圖6-1所示)中的 object6幔崖, object7, object8跋核, 雖然它們相互關(guān)聯(lián),但是衙門到 GC Roots 是不可達(dá)的所以它們會(huì)被判定為可回收對(duì)象叛买。
圖6-1 可達(dá)性算法判斷對(duì)象是否可回收
在 Java 中砂代,不同內(nèi)存區(qū)域可作為 GC Roots 的對(duì)象的包括以下三種:
① 虛擬機(jī)棧
虛擬機(jī)棧的棧幀中的局部變量表中引用的對(duì)象,比如某個(gè)方法正在使用的類字段率挣。
② 方法區(qū)
◆ 類靜態(tài)屬性引用的對(duì)象刻伊;
◆ 常量引用的對(duì)象。
③ 本地方法棧
本地方法棧棧 native 方法引用的對(duì)象。
7. Java 中有哪幾種引用捶箱?
無(wú)論是通過(guò)引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量智什,還是通過(guò)可達(dá)性分析算法判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá),判斷對(duì)象是否存活都與引用有關(guān)丁屎。
在 JDK 1.2 之后荠锭,Java 對(duì)引用的改良進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用晨川、軟引用证九、弱引用、虛引用四種共虑,這四種引用強(qiáng)度依次減弱愧怜。
7.1 強(qiáng)引用
強(qiáng)引用有以下四個(gè)特點(diǎn):
① 普遍存在
強(qiáng)引用是指代碼中普遍存在的,比如“Object obj = new Object()”這類引用妈拌。
② 直接引用
強(qiáng)引用可以直接訪問(wèn)目標(biāo)對(duì)象拥坛。
③ 不會(huì)回收
強(qiáng)引用指向的對(duì)象在任何時(shí)候都不會(huì)被系統(tǒng)回收,虛擬機(jī)及時(shí)拋出 OOM 異常尘分,也不會(huì)回收強(qiáng)引用指向的對(duì)象猜惋。
使用 obj=null 不會(huì)觸發(fā) GC,但是在下次GC的時(shí)候音诫,這個(gè)強(qiáng)引用對(duì)象就可以被回收了惨奕。
④ OOM 隱患
強(qiáng)引用可能導(dǎo)致內(nèi)存泄漏。
7.2 軟引用
軟引用有以下四個(gè)特點(diǎn):
① 有用但非必需
軟引用用于描述一些還有用但非必需的對(duì)象竭钝。
② 二次回收
對(duì)于軟引用關(guān)聯(lián)的對(duì)象梨撞,在系統(tǒng)即將發(fā)生內(nèi)存溢出前,會(huì)把這些對(duì)象列入回收范圍中進(jìn)行二次回收香罐。
③ OOM 隱患
如果二次回收后還沒(méi)有足夠的內(nèi)存卧波,就會(huì)拋出內(nèi)存溢出異常。
④ SoftReference
在 JDK 1.2 后庇茫,Java 提供了 SoftReference 類來(lái)實(shí)現(xiàn)軟引用港粱。
7.3 弱引用
軟引用有以下四個(gè)特點(diǎn)
① 比軟引用更弱
弱引用的強(qiáng)度比軟引用一些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次 GC 前旦签。
② 發(fā)現(xiàn)即回收
在 GC 時(shí)查坪,只要發(fā)現(xiàn)弱引用,不管系統(tǒng)堆空間使用情況如何宁炫,都會(huì)將對(duì)象進(jìn)行回收偿曙。
③ 可有可無(wú)
軟引用、弱引用適合保存可有可無(wú)的緩存數(shù)據(jù)羔巢。
④ WeakReference
JDK 1.2 后望忆,提供了 WeakReference 類來(lái)實(shí)現(xiàn)弱引用。
7.4 虛引用
虛引用是最弱的一種引用關(guān)系启摄,它有以下三個(gè)特點(diǎn):
① 無(wú)法獲取
一個(gè)對(duì)象無(wú)論是否有虛引用的存在,都不會(huì)對(duì)其生成時(shí)間構(gòu)成影響歉备,也無(wú)法通過(guò)虛引用取得一個(gè)對(duì)象實(shí)例。
② 收到通知
為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象被垃圾回收器回收時(shí)收到一個(gè)系統(tǒng)通知落午。
③ PhantomReference
在 JDK 1.2 后,提供了 PhantomReference 類來(lái)實(shí)現(xiàn)虛引用溃斋。
8. 什么是垃圾回收器?
如果說(shuō)收集算法是內(nèi)存回收的方法論梗劫,那么垃圾回收器就是內(nèi)存回收的具體實(shí)現(xiàn)。我們 Java 開(kāi)發(fā)者不用像 C++ 開(kāi)發(fā)者那樣關(guān)心內(nèi)存釋放的問(wèn)題截碴,但我們也不能胡亂操作梳侨。當(dāng)我們操作不當(dāng)導(dǎo)致某塊內(nèi)存泄漏時(shí),GC 就不能對(duì)這塊內(nèi)存進(jìn)行回收日丹。
GC 可不是好伺候的主走哺,如果你讓“GC 很忙”,那它就會(huì)讓你“應(yīng)用很卡”哲虾。
拿 Android 來(lái)說(shuō)丙躏,進(jìn)行 GC 時(shí),所有線程都要暫停束凑,包括主線程晒旅,16ms 是Android 要求的每幀繪制時(shí)間,而當(dāng) GC 的時(shí)間超過(guò) 16ms汪诉,就會(huì)造成丟幀的情況废恋,也就是界面卡頓。
垃圾回收器回收資源的方法就是垃圾回收算法扒寄,下面我們來(lái)看下四個(gè)主要的垃圾回收算法鱼鼓。
8.1 標(biāo)記-清除算法
標(biāo)記-清除算法(Mark-Sweep)相當(dāng)于是先把貨架上有人買的、沒(méi)人買的该编、空著的商品和位置都記錄下來(lái)迄本,然后再把沒(méi)人買的商品統(tǒng)一進(jìn)行下架。
圖8-1 標(biāo)記-清除算法
- 工作原理
◇ 第一步:標(biāo)記所有需要回收的對(duì)象上渴;
◇ 第二步:標(biāo)記完成后岸梨,統(tǒng)一回收所有被標(biāo)記的對(duì)象。
- 缺點(diǎn)
◇ 效率低
標(biāo)記和清除的效率都不高稠氮。
◇ 內(nèi)存碎片
標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片曹阔,內(nèi)存碎片大多會(huì)導(dǎo)致當(dāng)程序需要分配較大的對(duì)象時(shí),無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā) GC隔披。
8.2 復(fù)制算法
為了解決效率問(wèn)題赃份,復(fù)制(Copying)收集算法出現(xiàn)了抓韩。
圖8-2 復(fù)制算法
- 工作原理
復(fù)制算法把可用內(nèi)存按容量劃分為大小相同的兩塊谒拴,每次只使用其中的一塊英上。
當(dāng)使用中的這塊內(nèi)存用完了啤覆,就把存活的對(duì)象復(fù)制到另一塊內(nèi)存上窗声,然后把已使用的控件一次清理掉。
這樣每次都是對(duì)半個(gè)內(nèi)存區(qū)域進(jìn)行回收拦耐,內(nèi)存分配時(shí)也不用考慮內(nèi)存碎片等復(fù)雜問(wèn)題揩魂。
- 優(yōu)點(diǎn)
復(fù)制算法的優(yōu)點(diǎn)是每次只對(duì)半個(gè)內(nèi)存區(qū)域進(jìn)行內(nèi)存回收炮温,分配內(nèi)存時(shí)也不用考慮內(nèi)存碎片等復(fù)雜情況柒啤,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可方援。
- 缺點(diǎn)
◇ 浪費(fèi)空間
把內(nèi)存縮小一半用太浪費(fèi)空間犯戏。
◇ 有時(shí)效率低
在對(duì)象存活率高時(shí),要進(jìn)行較多的復(fù)制操作先匪,這時(shí)效率就低了呀非。
8.3 標(biāo)記-整理算法
在復(fù)制算法中,如果不想浪費(fèi) 50% 的空間猖败,就需要有額外的空間進(jìn)行分配擔(dān)保恩闻,以應(yīng)對(duì)被使用內(nèi)存中所有對(duì)象都存活的低端情況所以養(yǎng)老區(qū)不能用這種算法剧董。
根據(jù)養(yǎng)老區(qū)的特點(diǎn),有人提出了一種標(biāo)記-整理(Mark-Compact)算法侠草。
圖8-3 標(biāo)記-整理算法
- 工作原理
標(biāo)記-整理算法的標(biāo)記過(guò)程與標(biāo)記-清除算法一樣边涕,但后續(xù)步驟是讓所有存活的對(duì)象向一端移動(dòng)功蜓,然后直接清除掉邊界外的內(nèi)存宠蚂。
8.4 分代收集算法
現(xiàn)代商業(yè)虛擬機(jī)的垃圾回收機(jī)制都是采用分代收集算法(Generational Collection)算法求厕,這種算法會(huì)根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊,這樣就可以根據(jù)各個(gè)區(qū)域的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/p>
在新生區(qū)美浦,每次垃圾收集都有大批對(duì)象死去项栏,只有少了存活沼沈,所以可以用復(fù)制算法币厕。
養(yǎng)老區(qū)中因?yàn)閷?duì)象存活率高劈榨、沒(méi)有額外空間對(duì)它進(jìn)行擔(dān)保,就必須使用標(biāo)記-清除或標(biāo)記-整理算法進(jìn)行回收拷姿。
對(duì)內(nèi)存可分為新生區(qū)、養(yǎng)老區(qū)永久存儲(chǔ)區(qū)三個(gè)區(qū)域描滔。
圖8-4 堆存儲(chǔ)的三個(gè)區(qū)域
8.4.1 新生區(qū)(Young Generation Space)
新生區(qū)是類的誕生含长、成長(zhǎng)拘泞、消亡的區(qū)域枕扫。
新生區(qū)有分為伊甸區(qū)、幸存者區(qū)兩部分诗鸭。
- 伊甸區(qū)(Eden Space)
大多數(shù)情況下强岸,對(duì)象都是在伊甸區(qū)中分配的砾赔,當(dāng)伊甸區(qū)沒(méi)有足夠的空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次 Minor GC十绑。
Minor GC 是指發(fā)生在新生區(qū)的垃圾回收動(dòng)作本橙,Minor GC 非常繁忙脆诉,回收速度也比較快贷币。
當(dāng)伊甸區(qū)的空間用完時(shí)役纹,GC 會(huì)對(duì)伊甸區(qū)進(jìn)行垃圾回收促脉,然后把伊甸區(qū)剩下的對(duì)象移動(dòng)到幸存0區(qū)策州。
- 幸存0區(qū)(Survivor 0 Space)
如果幸存0區(qū)滿了,GC 會(huì)對(duì)該區(qū)域進(jìn)行垃圾回收旁仿,然后把該 區(qū)域剩下的對(duì)象移動(dòng)到幸存1區(qū)枯冈。
- 幸存1區(qū)(Survivor 1 Space)
如果幸存1區(qū)滿了办悟,GC 會(huì)對(duì)該區(qū)域進(jìn)行垃圾回收,然后把幸存1區(qū)中的對(duì)象移動(dòng)到養(yǎng)老區(qū)罪既。
8.4.2 養(yǎng)老區(qū)(Tenure Generation Space)
養(yǎng)老區(qū)用于保存從新生區(qū)篩選出來(lái)的 Java 對(duì)象琢感。
當(dāng)幸存1區(qū)嘗試移動(dòng)對(duì)象到養(yǎng)老區(qū)探熔,但是發(fā)現(xiàn)空間不足時(shí)诀艰,虛擬機(jī)會(huì)發(fā)起一次 Major GC。
Major GC 的速度一般比 Minor GC 慢 10 倍以上苛蒲。
大對(duì)象直接進(jìn)入養(yǎng)老區(qū)臂外,比如很大的數(shù)字和很長(zhǎng)的字符串。
8.4.3 永久存儲(chǔ)區(qū)(Permanent Space)
永久存儲(chǔ)區(qū)是一個(gè)常駐內(nèi)存區(qū)域嚎货,用于存放 JDK 自身攜帶的 Class Interface 元數(shù)據(jù)蔫浆。
永久存儲(chǔ)區(qū)存儲(chǔ)的是運(yùn)行環(huán)境必需的類信息瓦盛,被裝載進(jìn)該區(qū)域的數(shù)據(jù)是不會(huì)被垃圾回收器回收掉的,只有 JVM 關(guān)閉時(shí)才會(huì)釋放此區(qū)域的內(nèi)存挠唆。
--轉(zhuǎn)自:https://www.cnblogs.com/steffen/p/11368018.html扮念,感謝原作者寫出這么優(yōu)秀的技術(shù)文章柜与,甚是喜愛(ài)弄匕,所以轉(zhuǎn)載沽瞭!如有侵權(quán)驹溃,給我留言,馬上刪除亡哄!