1.幾個計算機的概念
為以后寫文章考慮,也為鞏固自己的知識和一些基本概念,這里要理清楚幾個計算機中的概念奏黑。
1炊邦、計算機存儲單位
從小到大依次為位Bit、字節(jié)Byte熟史、千字節(jié)KB馁害、兆M、千兆GB蹂匹、TB碘菜,相鄰單位之間都是1024倍,1024為2的10次方怒详,即:
- 1Byte = 8bit
- 1K = 1024Byte
- 1M = 1024K
- 1G = 1024M
- 1T = 1024G
2炉媒、計算機存儲元件
寄存器:中央處理器CPU的一部分,是計算機中讀寫速度最快的存儲元件昆烁,但是容量很少
內(nèi)存:屬于獨立的一個部件吊骤,是和CPU溝通的橋梁,用于存放CPU中的運算數(shù)據(jù)以及與外部存儲器交換的數(shù)據(jù)静尼。盡管在今天白粉,對內(nèi)存的讀寫速度已經(jīng)很快了,但是由于寄存器是在CPU上的鼠渺,所以對于內(nèi)存的讀寫速度和對于寄存器的讀寫速度上還是有幾個數(shù)量級的差距鸭巴。但是沒辦法,對于內(nèi)存的讀寫I/O操作是很難消除的拦盹,寄存器數(shù)量有限鹃祖,不可能通過寄存器來完成所有的運算任務(wù)
3、內(nèi)核空間和用戶空間
連接內(nèi)存和寄存器的是地址總線普舆,地址總線的寬度影響了物理地址的索引范圍恬口,因為總線寬度決定了處理器一次可以從寄存器或內(nèi)存中獲取多少個Bit,同時也決定了處理器最大可以尋址的地址空間沼侣。比如32位CPU的系統(tǒng)祖能,可尋址范圍為0x00000000~0xFFFFFFFF,即232=4294967296個內(nèi)存位置蛾洛,每個內(nèi)存位置1個字節(jié)养铸,即32位CPU系統(tǒng)可以有4GB的內(nèi)存空間。不過應(yīng)用程序是不可以完全使用這些地址空間的轧膘,因為這些地址空間被劃分為了內(nèi)核空間和用戶空間钞螟,程序只能使用用戶空間的內(nèi)存。內(nèi)核空間主要是指操作系統(tǒng)運行時所使用的用于程序調(diào)度扶供、虛擬內(nèi)存的使用或者鏈接硬件資源的程序邏輯筛圆。區(qū)分內(nèi)核空間和用戶空間的目的主要是從系統(tǒng)的穩(wěn)定性的角度考慮的。Windows 32操作系統(tǒng)默認內(nèi)核空間和用戶空間的比例是1:1椿浓,即2G內(nèi)核空間太援、2G內(nèi)存空間,32位Linux系統(tǒng)中默認比例則是1:3扳碍,即1G內(nèi)核空間提岔,3G內(nèi)存空間。
4笋敞、字長
CPU的主要技術(shù)指標之一碱蒙,指的是CPU一次能并行處理二進制的位數(shù)(Bit)。通常稱處理字長為8位數(shù)據(jù)的CPU為8位CPU夯巷,32位CPU就是在同一時間內(nèi)處理字長為32位的二進制數(shù)據(jù)赛惩。不過目前雖然CPU大多是64位的,但還是以32位字長運行
2.前言
說到Java內(nèi)存區(qū)域趁餐,可能很多人第一反應(yīng)是“堆椗缂妫”。首先堆棧不是一個概念后雷,而是兩個概念季惯,堆和棧是兩塊不同的內(nèi)存區(qū)域,簡單理解的話臀突,堆是用來存放對象而棧是用來執(zhí)行程序的勉抓。其次,堆內(nèi)存和棧內(nèi)存的這種劃分方式比較粗糙候学,這種劃分方式只能說明大多數(shù)程序員最關(guān)注的藕筋、與對象內(nèi)存分配關(guān)系最密切的內(nèi)存區(qū)域是這兩塊,Java內(nèi)存區(qū)域的劃分實際上遠比這復(fù)雜梳码。對于Java程序員來說隐圾,在虛擬機自動內(nèi)存管理機制的幫助下,不再需要為每一個new操作去配對delete/free代碼边翁,不容易出現(xiàn)內(nèi)存泄露和內(nèi)存溢出問題翎承。但是,也正是因為Java把內(nèi)存控制權(quán)交給了虛擬機符匾,一旦出現(xiàn)內(nèi)存泄露和內(nèi)存溢出的問題叨咖,就難以排查,因此一個好的Java程序員應(yīng)該去了解虛擬機的內(nèi)存區(qū)域以及會引起內(nèi)存泄露和內(nèi)存溢出的場景啊胶。
3.運行時數(shù)據(jù)區(qū)域
Java虛擬機(JVM)內(nèi)部定義了程序在運行時需要使用到的內(nèi)存區(qū)域:
之所以要劃分這么多區(qū)域出來是因為這些區(qū)域都有自己的用途甸各,以及創(chuàng)建和銷毀的時間。有些區(qū)域隨著虛擬機進程的啟動而存在焰坪,有的區(qū)域則依賴用戶線程的啟動和結(jié)束而銷毀和建立趣倾。
線程共享內(nèi)存區(qū): 方法區(qū)和堆,
線程私有內(nèi)存區(qū): 虛擬機棧某饰、本地方法棧儒恋、程序技術(shù)器善绎,基本上隨著線程產(chǎn)生和消亡,也就是說生命周期和線程相同诫尽,因此基本不需要考慮內(nèi)存回收的問題禀酱,編譯時確定所需內(nèi)存大小。
從這個分類角度來看一下這幾個數(shù)據(jù)區(qū)牧嫉。
3.1剂跟、線程獨有的內(nèi)存區(qū)域
(1)PROGRAM COUNTER REGISTER,程序計數(shù)器
這塊內(nèi)存區(qū)域很小酣藻,它是當前線程所執(zhí)行的字節(jié)碼的行號指示器曹洽,字節(jié)碼解釋器通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支辽剧,跳轉(zhuǎn)送淆,循環(huán)等基礎(chǔ)功能都要依賴它來實現(xiàn)。
這里需要注意三點內(nèi)容:
- 每條線程都有一個獨立的程序計數(shù)器抖仅,各線程間的計數(shù)器互不影響坊夫,因此該區(qū)域是線程私有的。
- 當線程在執(zhí)行一個Java方法時撤卢,該計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址环凿,也就是說有值,當線程在執(zhí)行的是Native方法(調(diào)用本地操作系統(tǒng)方法)時放吩,該計數(shù)器的值為空智听。
- 另外,該內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OOM(內(nèi)存溢出:OutOfMemoryError)情況的區(qū)域渡紫,也就是說此塊區(qū)域不會拋出內(nèi)存溢出的異常到推。
(2)JAVA STACK,虛擬機棧
該區(qū)域也是線程私有的惕澎,它的生命周期也與線程相同莉测。
虛擬機棧也就是我們平常所稱的棧內(nèi)存,它是為java方法服務(wù)的唧喉,描述了java方法執(zhí)行的內(nèi)存模型捣卤。
每一個方法從調(diào)用直至執(zhí)行完畢的過程,就對應(yīng)著一個棧幀在虛擬機中入棧到出棧的過程八孝。
Java方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會同時創(chuàng)建一個棧幀董朝,棧幀用于存儲局部變量表、操作數(shù)棧干跛、動態(tài)鏈接子姜、方法返回地址和一些額外的附加信息。棧它是用于支持續(xù)虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)楼入。對于執(zhí)行引擎來講哥捕,活動線程中牧抽,只有棧頂?shù)臈怯行У模Q為當前棧幀扭弧,這個棧幀所關(guān)聯(lián)的方法稱為當前方法阎姥,執(zhí)行引擎所運行的所有字節(jié)碼指令都只針對當前棧幀進行操作记舆。在編譯程序代碼時鸽捻,棧幀中需要多大的局部變量表、多深的操作數(shù)棧都已經(jīng)完全確定了泽腮,并且寫入了方法表的Code屬性之中御蒲。因此,一個棧幀需要分配多少內(nèi)存诊赊,不會受到程序運行期變量數(shù)據(jù)的影響厚满,而僅僅取決于具體的虛擬機實現(xiàn)。
棧的大小通常在256K~756K之間碧磅,具體和JVM的實現(xiàn)有關(guān)碘箍。
-
在Java虛擬機規(guī)范中,對這個區(qū)域規(guī)定了兩種異常情況:
1鲸郊、如果線程請求的棧深度大于虛擬機所允許的深度丰榴,將拋出StackOverflowError異常。 2秆撮、如果虛擬機在動態(tài)擴展棧時無法申請到足夠的內(nèi)存空間四濒,則拋出OutOfMemoryError異常。
這兩種情況存在著一些互相重疊的地方:當椫氨妫空間無法繼續(xù)分配時盗蟆,到底是內(nèi)存太小,還是已使用的検婵悖空間太大喳资,其本質(zhì)上只是對同一件事情的兩種描述而已。在單線程的操作中腾供,無論是由于棧幀太大仆邓,還是虛擬機棧空間太小台腥,當椇曜福空間無法分配時,虛擬機拋出的都是StackOverflowError異常黎侈,而不會得到OutOfMemoryError異常察署。而在多線程環(huán)境下,則會拋出OutOfMemoryError異常峻汉。
下面詳細說明棧幀中所存放的各部分信息的作用和數(shù)據(jù)結(jié)構(gòu)贴汪。
** 1脐往、局部變量表**
局部變量表是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量扳埂,其中存放的數(shù)據(jù)的類型是編譯期可知的各種基本數(shù)據(jù)類型业簿、對象引用(reference(不等同于對象本身,可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔樠舳部赡苁侵赶蛞粋€代表對象的句柄或其他與此對象相關(guān)的位置))和returnAddress類型(它指向了一條字節(jié)碼指令的地址)梅尤。
局部變量表所需的內(nèi)存空間在編譯期間完成分配,即在Java程序被編譯成Class文件時岩调,就確定了所需分配的最大局部變量表的容量巷燥。當進入一個方法時,這個方法需要在棧中分配多大的局部變量空間是完全確定的号枕,在方法運行期間不會改變局部變量表的大小缰揪。
局部變量表的容量以變量槽(Slot)為最小單位。在虛擬機規(guī)范中并沒有明確指明一個Slot應(yīng)占用的內(nèi)存空間大写写尽(允許其隨著處理器钝腺、操作系統(tǒng)或虛擬機的不同而發(fā)生變化),一個Slot可以存放一個32位以內(nèi)的數(shù)據(jù)類型:boolean赞厕、byte艳狐、char、short坑傅、int僵驰、float、reference和returnAddresss唁毒。reference是對象的引用類型叫潦,returnAddress是為字節(jié)指令服務(wù)的饰剥,它執(zhí)行了一條字節(jié)碼指令的地址贬媒。對于64位的數(shù)據(jù)類型(long和double)惫谤,虛擬機會以高位在前的方式為其分配兩個連續(xù)的Slot空間。
虛擬機通過索引定位的方式使用局部變量表近零,索引值的范圍是從0開始到局部變量表最大的Slot數(shù)量诺核,對于32位數(shù)據(jù)類型的變量,索引n代表第n個Slot久信,對于64位的窖杀,索引n代表第n和第n+1兩個Slot。
在方法執(zhí)行時裙士,虛擬機是使用局部變量表來完成參數(shù)值到參數(shù)變量列表的傳遞過程的入客,如果是實例方法(非static),則局部變量表中的第0位索引的Slot默認是用于傳遞方法所屬對象實例的引用,在方法中可以通過關(guān)鍵字“this”來訪問這個隱含的參數(shù)桌硫。其余參數(shù)則按照參數(shù)表的順序來排列夭咬,占用從1開始的局部變量Slot,參數(shù)表分配完畢后铆隘,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的Slot卓舵。
局部變量表中的Slot是可重用的,方法體中定義的變量膀钠,作用域并不一定會覆蓋整個方法體掏湾,如果當前字節(jié)碼PC計數(shù)器的值已經(jīng)超過了某個變量的作用域,那么這個變量對應(yīng)的Slot就可以交給其他變量使用托修。這樣的設(shè)計不僅僅是為了節(jié)省空間忘巧,在某些情況下Slot的復(fù)用會直接影響到系統(tǒng)的而垃圾收集行為。
** 2睦刃、操作數(shù)棧**
- 操作數(shù)棧又常被稱為操作棧,主要用來存儲運算結(jié)果以及運算的操作數(shù)十酣。
- 操作數(shù)棧的最大深度也是在編譯的時候就確定了涩拙。32位數(shù)據(jù)類型所占的棧容量為1,64為數(shù)據(jù)類型所占的棧容量為2。
- 它不同于局部變量表通過索引來訪問耸采,而是通過壓棧和出棧的方式:當一個方法開始執(zhí)行時兴泥,它的操作棧是空的,在方法的執(zhí)行過程中虾宇,會有各種字節(jié)碼指令(比如:加操作搓彻、賦值等)向操作棧中寫入和提取內(nèi)容。
- Java虛擬機的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”嘱朽,其中所指的“椥癖幔”就是操作數(shù)棧。因此我們也稱Java虛擬機是基于棧的搪泳,這點不同于Android虛擬機稀轨,Android虛擬機是基于寄存器的“毒基于棧的指令集最主要的優(yōu)點是可移植性強奋刽,主要的缺點是執(zhí)行速度相對會慢些;而由于寄存器由硬件直接提供艰赞,所以基于寄存器指令集最主要的優(yōu)點是執(zhí)行速度快佣谐,主要的缺點是可移植性差。
** 3方妖、動態(tài)連接**
每個棧幀都包含一個指向運行時常量池(在方法區(qū)中狭魂,后面介紹)中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接。Class文件的常量池中存在有大量的符號引用趁蕊,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用為參數(shù)坞生。這些符號引用,一部分會在類加載階段或第一次使用的時候轉(zhuǎn)化為直接引用(如final掷伙、static域等)是己,稱為靜態(tài)解析,另一部分將在每一次的運行期間轉(zhuǎn)化為直接引用任柜,這部分稱為動態(tài)連接卒废。
** 4、方法返回地址**
當一個方法被執(zhí)行后宙地,有兩種方式退出該方法:執(zhí)行引擎遇到了任意一個方法返回的字節(jié)碼指令或遇到了異常摔认,并且該異常沒有在方法體內(nèi)得到處理。無論采用何種退出方式宅粥,在方法退出之后参袱,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行秽梅。方法返回時可能需要在棧幀中保存一些信息抹蚀,用來幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)。一般來說企垦,方法正常退出時环壤,調(diào)用者的PC計數(shù)器的值就可以作為返回地址,棧幀中很可能保存了這個計數(shù)器值钞诡,而方法異常退出時郑现,返回地址是要通過異常處理器來確定的,棧幀中一般不會保存這部分信息荧降。
方法退出的過程實際上等同于把當前棧幀出站接箫,因此退出時可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,如果有返回值誊抛,則把它壓入調(diào)用者棧幀的操作數(shù)棧中列牺,調(diào)整PC計數(shù)器的值以指向方法調(diào)用指令后面的一條指令。
(3)NATIVE METHOD STACK拗窃,本地方法棧
和虛擬機棧起的作用一樣瞎领,只不過方法棧為虛擬機使用到的Native方法服務(wù)。虛擬機規(guī)范并沒有對這個區(qū)域有什么強制規(guī)定随夸,因此我們使用的HotSpot虛擬機九默,就干脆沒有這塊區(qū)域了,它和虛擬機棧是一起的宾毒。
3.2驼修、線程間共享的內(nèi)存區(qū)域
(1)HEAP,堆
- Java Heap是虛擬機所管理的內(nèi)存中最大的一塊,它是所有線程共享的一塊內(nèi)存區(qū)域乙各,JVM啟動時創(chuàng)建墨礁。
- 此內(nèi)存區(qū)域的唯一目的就是存放對象實例和數(shù)組,幾乎所有的對象實例都要在這里分配內(nèi)存耳峦。
- Java Heap 是垃圾收集器管理的主要區(qū)域恩静,因此 很多 時候也被稱為"GC堆"。由于現(xiàn)在垃圾收集器采用的基本都是分代收集算法蹲坷,所以堆還可以細分為新生代和老年代驶乾,再細致一點還有Eden區(qū)、From Survivior區(qū)循签、To Survivor區(qū)等级乐。(這個后面講)
- 根據(jù)Java虛擬機規(guī)范的規(guī)定,堆可以處在物理上不連續(xù)的內(nèi)存空間中县匠,只要邏輯上是連續(xù)的即可风科。如果在堆中沒有內(nèi)存可分配時,并且堆也無法擴展時聚唐,將會拋出OutOfMemoryError異常丐重。
(2)METHOD AREA,方法區(qū)
這塊區(qū)域用于存儲虛擬機加載的類信息杆查、常量、靜態(tài)變量臀蛛、即時編譯器編譯后的代碼等數(shù)據(jù)亲桦,虛擬機規(guī)范是把這塊區(qū)域描述為堆的一個邏輯部分的,但實際它應(yīng)該是要和堆區(qū)分開的浊仆。從上面提到的分代收集算法的角度看客峭,HotSpot中,方法區(qū)≈永久代抡柿。不過JDK 7之后舔琅,我們使用的HotSpot應(yīng)該就沒有永久代這個概念了,會采用Native Memory來實現(xiàn)方法區(qū)的規(guī)劃了洲劣。默認最小值為16MB备蚓,最大值為64MB,可以通過-XX:PermSize 和 -XX:MaxPermSize 參數(shù)限制方法區(qū)的大小囱稽。
- 方法區(qū)也是各個線程共享的內(nèi)存區(qū)域郊尝,它用于存儲已經(jīng)被虛擬機加載的類信息、常量战惊、靜態(tài)變量流昏、即時編譯器編譯后的代碼等數(shù)據(jù)。
- 方法區(qū)又被稱為“永久代”,不過JDK7, JRockit和IBM J9等JVM已經(jīng)沒有永久代的概念了况凉。
- Java虛擬機規(guī)范把方法區(qū)描述為Java堆的一個邏輯部分谚鄙,而且它和Java Heap一樣不需要連續(xù)的內(nèi)存,對于方法區(qū)的分配會采用Native Memory來實現(xiàn)刁绒。
- 垃圾收集行為在這個區(qū)域比較少出現(xiàn)闷营,該區(qū)域內(nèi)存的回收目標是對廢棄常量池和無用類的回收。
- 運行時常量池是方法區(qū)的一部分膛锭,用于存放編譯時期生成的各種字面量和符號引用粮坞,該常量池具有動態(tài)性。
- 根據(jù)Java虛擬機規(guī)范的規(guī)定初狰,當方法區(qū)無法滿足內(nèi)存分配需求時莫杈,將拋出OutOfMemoryError異常(OOM)。
(3)RUNTIME CONSTANT POOL奢入,運行時常量池
Class文件中除了有類的版本信息筝闹、字段、方法腥光、接口等描述信息外关顷,還有一項信息就是常量池,用于存放編譯期間生成的各種字面量和符號引用武福,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中议双,另外翻譯出來的直接引用也會存儲在這個區(qū)域中。這個區(qū)域另外一個特點就是動態(tài)性捉片,Java并不要求常量就一定要在編譯期間才能產(chǎn)生平痰,運行期間產(chǎn)生的常量也會存在這個常量池中,String.intern()方法就是這個特性的應(yīng)用伍纫。
關(guān)于字面量宗雇、符號引用和直接引用:
字面量相當于Java語言層面常量的概念,如文本字符串莹规,聲明為final的常量值等赔蒲。
符號引用則屬于編譯原理方面的概念,包括了如下三種類型的常量:1良漱、類和接口的全限定名 2舞虱、字段名稱和描述符 3、方法名稱和描述符债热。
直接引用可以是直接指向引用目標的指針砾嫉、相對偏移量或者是一個能夠間接定位到目標的句柄。直接引用是和虛擬機的內(nèi)存布局有關(guān)的窒篱,同一個符號引用在不同的虛擬機上翻譯的直接引用一般是不同的焕刮。如果有了直接引用舶沿,那么引用的目標必定是存在內(nèi)存中的。
4配并、直接內(nèi)存
直接內(nèi)存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分括荡,也不是Java虛擬機規(guī)范中定義的內(nèi)存區(qū)域。它直接從操作系統(tǒng)中分配溉旋,因此不受Java堆大小的限制畸冲,但是還是會受到本機總內(nèi)存(包括RAM、SWAP區(qū))大小以及處理器尋址空間的限制观腊,因此它也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)邑闲。
JDK1.4中新增加了NIO,引入了一種基于通道與緩沖區(qū)的I/O方式梧油,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存苫耸,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作。這樣能在一些場景中顯著提高性能儡陨,因為避免了在Java堆和Native堆中來回復(fù)制數(shù)據(jù)褪子。
5、總結(jié)
簡單的總結(jié)一下:
- 程序計數(shù)器(PC):Java線程私有骗村,類似于操作系統(tǒng)里的PC計數(shù)器嫌褪,用于指定下一條需要執(zhí)行的字節(jié)碼的地址;
- Java虛擬機棧:Java線程私有胚股,虛擬機展描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的時候笼痛,都會創(chuàng)建一個棧幀用于存儲局部變量、操作數(shù)琅拌、動態(tài)鏈接晃痴、方法出口等信息;每個方法調(diào)用都意味著一個棧幀在虛擬機棧中入棧到出棧的過程财忽;
- 本地方法棧:和Java虛擬機棧的作用類似,區(qū)別是該該區(qū)域為JVM調(diào)用到的本地方法服務(wù)泣侮;
- 堆(Heap):所有線程共享的一塊區(qū)域即彪,垃圾收集器管理的主要區(qū)域。目前主要的垃圾回收算法都是分代收集活尊,因此該區(qū)域還可以細分為如下區(qū)域: – 年輕代 – Eden空間 – From Survivor空間1隶校,F(xiàn)rom Survivor空間2,用于存儲在Young gc過程中幸存的對象蛹锰; – 老年代
- 方法區(qū):各個線程共享的一個區(qū)域深胳,用于存儲虛擬機加載的類信息、常量铜犬、靜態(tài)變量等信息舞终;
- 運行時常量池:方法區(qū)的一部分轻庆,用于存放編譯器生成的各種字面量和符號引用;