JVM對于是每個Java程序員掌握一定Java基礎(chǔ)后棉浸,都需要學(xué)習(xí)的揖曾。因為很多代碼問題贩毕,只能了解了JVM底層原理后才能解決悯许。大多數(shù)Java后端開發(fā)者都知道堆(Heap)和棧(Stack)的概念,卻沒有真正理解其原理辉阶。推薦《深入理解Java虛擬機(第二版)》---周志明著學(xué)習(xí)JVM岸晦。
進程和線程
學(xué)習(xí)JVM前要了解進程和線程的概念。
以下是一個類比睛藻,來自阮一峰---進程與線程的一個簡單解釋。
- 計算機的CPU是像一座工廠邢隧,時刻在運行店印。
- 因工廠電力有限,一次只能供給一個車間使用倒慧。也就是說按摘,一個車間開工的時候,其他車間都必須停工纫谅。背后的含義就是炫贤,單個CPU一次只能運行一個任務(wù)。
- 進程就好比工廠的車間付秕,它代表CPU所能處理的單個任務(wù)兰珍。任一時刻,CPU總是運行一個進程询吴,其他進程處于非運行狀態(tài)掠河。
- 一個車間里亮元,可以有很多工人。他們協(xié)同完成一個任務(wù)唠摹。
- 線程就好比車間里的工人爆捞。一個進程可以包括多個線程。
進程和線程里面還涉及到“鎖”的知識勾拉,請在參考網(wǎng)址中學(xué)習(xí)煮甥。
下圖是:Java虛擬機運行時數(shù)據(jù)區(qū)
程序計數(shù)器(Program Counter Resiger)
首先程序計數(shù)器是一塊較小的內(nèi)存空間,“決定”當(dāng)前線程字節(jié)碼的執(zhí)行順序藕赞,因為它存儲字節(jié)碼的行號成肘。而在多線程中,每個線程都具有一個程序計數(shù)器找默,各條線程之間獨立艇劫,計數(shù)器互不影響,獨立存儲惩激。程序計數(shù)器是“線程私有”的內(nèi)存店煞。
如果線程執(zhí)行Java方法,計數(shù)器記錄的是虛擬機字節(jié)碼指令的地址风钻。而如果執(zhí)行Native方法顷蟀,值為空(Undefined),理解這一段文字需要理解Native方法是什么骡技!Native方法是非Java代碼(比如C)編寫的方法鸣个。程序計數(shù)器是Java虛擬機規(guī)范中唯一沒有OutOfMemoryError的內(nèi)存區(qū)域。
Java虛擬機棧(Java Virtual Machine Stacks)
虛擬機棧是Java方法(也就是字節(jié)碼)運行時的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)[幀是一種數(shù)據(jù)結(jié)構(gòu)]用于存儲局部變量表等布朦。它是“線程私有”的囤萤,生命周期和線程相同。
我們常說的棧(Stacks)其中一種含義就是Java虛擬機棧是趴,確切的說是虛擬機棧中局部變量表部分涛舍。局部變量表存放原始(基本)數(shù)據(jù)類型,其中Long和double占兩個局部變量空間(Slot)-32位唆途。也存放對象引用和ruturnAddress類型(指向了一條字節(jié)碼指令的地址富雅?)。
局部變量表所需的內(nèi)存空間在編譯期間完成分配肛搬,當(dāng)進入(運行)一個方法時没佑,這個方法需要在幀中分配多大的局部變量空間是完全確定的,這是因為局部變量表是有結(jié)構(gòu)的温赔,每個區(qū)塊按照一定次序存放蛤奢,所以可以明確知道每個區(qū)塊的大小。局部變量就是在方法運行期間不會改變局部變量表的大小。
Java虛擬機規(guī)范中規(guī)定此區(qū)域有兩種異常:
- 每次方法調(diào)用都會有一個棧幀壓入虛擬機棧远剩,JVM分配給虛擬機棧的內(nèi)存是有限的扣溺。如果方法調(diào)用過多,導(dǎo)致虛擬機棧滿了就會溢出瓜晤。如果線程請求的棧深度(棧幀的數(shù)量)大于虛擬機所允許的深度(虛擬機棧的內(nèi)存)锥余,將拋出StackOverflowError異常
- 虛擬機棧可以動態(tài)擴展痢掠,如果擴展時無法申請到足夠的內(nèi)存驱犹,就會拋出OutOfMemoryError異常。
本地方法棧(Native Method Stack)
本地方法棧與Java虛擬機棧類似足画,是虛擬機使用到的Native方法服務(wù)雄驹。Sun HotSpot虛擬機直接本地方法棧和虛擬機棧合二為一。與虛擬機棧一樣淹辞,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常医舆。
Java堆(Java Heap)
Java堆(Java Heap)是Java虛擬機所管理的內(nèi)存中最大的一塊。是被所有線程共享的一塊內(nèi)存區(qū)域象缀。存放對象實例和數(shù)組(數(shù)組也是對象)蔬将,現(xiàn)在不是那么“絕對”了。
Java堆是垃圾收集器管理的主要區(qū)域央星,因此很多時候也被稱做“GC堆”(Garbage Collected Heap)霞怀。
根據(jù)Java虛擬機規(guī)范,Java堆只要邏輯上是連續(xù)的即可莉给,就像磁盤空間毙石。可以實現(xiàn)成固定大小的颓遏,也可以是可擴展的(大部分虛擬機都可動態(tài)擴展)徐矩。如果堆沒有內(nèi)存完成實例分配,且堆也無法再擴展時叁幢,會拋出OutOfMemoryError異常丧蘸。
方法區(qū)(Method Area)
方法區(qū)(Method Area)不是存儲方法,而是用于存儲已被虛擬機加載的類信息遥皂、常量、靜態(tài)變量刽漂、即時編譯器編譯后的代碼等數(shù)據(jù)演训。Java虛擬機規(guī)范將方法區(qū)描述為堆的一個邏輯部分,是邏輯區(qū)贝咙,但為區(qū)分堆样悟,別名叫Non-Heap(非堆)。
方法區(qū)(Method Area)一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴展外,還可以選擇不實現(xiàn)垃圾收集窟她。此區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收和(對類型的卸載陈症?)。也可能拋出OutOfMemoryError異常震糖。
運行時常量池(Runtime Constant Pool)
運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分录肯。Class文件中除了有類的版本、字段吊说、方法论咏、接口等描述信息外,還有一項信息是常量池(Constant Pool Table)颁井,用于存放編譯期生成的各種字面量(Literal)和符號引用(Symbolic References)厅贪,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放。
字面量比較接近于Java語言層面的常量概念雅宾,如文本字符串养涮、 聲明為final的常量值等。 而符號引用則屬于編譯原理方面的概念眉抬,包括了下面三類常量:
- 類和接口的全限定名(Fully Qualified Name)
- 字段的名稱和描述符(Descriptor)
- 方法的名稱和描述符
對于運行時常量池贯吓,Java虛擬機規(guī)范沒有做任何細(xì)節(jié)的要求。運行時常量池不同于Class文件常量池吐辙,運行期間也可能將新的常量放入池中宣决。內(nèi)存不足時會拋出OutOfMemoryError異常。
直接內(nèi)存(Direct Memory)
直接內(nèi)存通俗來說就是I/O方式使用Native堆直接分配堆外內(nèi)存昏苏。
總結(jié)
一般來說尊沸,每個進程分配一個"Heap",每個線程分配一個"Stack"贤惯。因為一個進程中有很多線程洼专,所以Java堆等是線程共享的;而"Stack"的生命周期跟線程相同孵构,即"Stack"是線程獨占的屁商,所以程序計數(shù)器、Java虛擬機棧等是線程私有的颈墅。
"Stack"的另外兩種含義有:
- 一種數(shù)據(jù)結(jié)構(gòu)蜡镶,即一組數(shù)據(jù)的存放方式,特點是FILO---First In,Last Out(先進后出)
- 一種代碼運行方式恤筛,即"調(diào)用棧"(call Stack)官还,表示函數(shù)或子例程像堆積木一樣存放,以實現(xiàn)層層調(diào)用毒坛。
"Stack"和Heap的主要區(qū)別是:"Stack"是有結(jié)構(gòu)的望伦,"Heap"是沒有結(jié)構(gòu)的林说。因此,"Stack"的尋址速度要快于"Heap"屯伞。
線程共享的好處:任何時候一個線程改變一些數(shù)據(jù)腿箩,其他線程可以看到它。
劣摇?號部分通讀全文理解后解釋珠移。