深入理解 JVM 的內(nèi)存區(qū)域劃分
本文轉(zhuǎn)載自微信公眾號(hào)「碼蟲甲」,作者碼蟲甲 。轉(zhuǎn)載本文請(qǐng)聯(lián)系碼蟲甲公眾號(hào)。
一、Java文件是如何運(yùn)行起來的?
執(zhí)行流程:
1.Java文件經(jīng)過編譯后變成 .class 字節(jié)碼文件
2.字節(jié)碼文件通過類加載器加載到 JVM 虛擬機(jī)中
3.加載完畢之后,交由JVM執(zhí)行引擎執(zhí)行
在整個(gè)程序執(zhí)行過程中,JVM會(huì)用一段空間來存儲(chǔ)程序執(zhí)行期間需要用到的數(shù)據(jù)和相關(guān)信息,這段空間一般被稱作為Runtime Data Area(運(yùn)行時(shí)數(shù)據(jù)區(qū)),也就是我們常說的JVM內(nèi)存。
二、運(yùn)行時(shí)數(shù)據(jù)區(qū)包括哪幾部分?
運(yùn)行時(shí)數(shù)據(jù)區(qū)主要包含以下5大塊:方法區(qū),堆都為線程共享區(qū)域,有線程安全問題,Java棧、本地方法棧和程序計(jì)數(shù)器都是獨(dú)享區(qū)域,不存在線程安全問題,其中 JVM 的調(diào)優(yōu)主要就是圍繞堆,棧兩大塊進(jìn)行。
1.方法區(qū)
存儲(chǔ)了每個(gè)類的信息(包括類的名稱、方法信息、字段信息)、靜態(tài)變量、常量以及編譯器編譯后的代碼等,類加載器將 .class 文件加載到運(yùn)行時(shí)數(shù)據(jù)區(qū)時(shí)就是先丟到這一塊上面。
在Class文件中除了類的字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池,用來存儲(chǔ)編譯期間生成的字面量和符號(hào)引用。
在方法區(qū)中有一個(gè)非常重要的部分就是運(yùn)行時(shí)常量池,它是每一個(gè)類或接口的常量池的運(yùn)行時(shí)表示形式,在類和接口被加載到JVM后,對(duì)應(yīng)的運(yùn)行時(shí)常量池就被創(chuàng)建出來。當(dāng)然并非Class文件常量池中的內(nèi)容才能進(jìn)入運(yùn)行時(shí)常量池,在運(yùn)行期間也可將新的常量放入運(yùn)行時(shí)常量池中,比如String的intern方法。
2.堆
和方法區(qū)同屬線程共享區(qū)域,它主要放了一些存儲(chǔ)的數(shù)據(jù),比如對(duì)象實(shí)例,數(shù)組···等。
堆是Java垃圾收集器管理的主要區(qū)域,在JVM中只有一個(gè)堆。
3.Java棧
Java棧也稱作虛擬機(jī)棧(Java Vitual Machine Stack),Java棧是Java方法執(zhí)行的內(nèi)存模型。
Java棧中存放的是一個(gè)個(gè)的棧幀,每個(gè)棧幀對(duì)應(yīng)一個(gè)被調(diào)用的方法,在棧幀中包括局部變量表(Local Variables)、操作數(shù)棧(Operand Stack)、指向當(dāng)前方法所屬的類的運(yùn)行時(shí)常量池(運(yùn)行時(shí)常量池的概念在方法區(qū)部分會(huì)談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。當(dāng)線程執(zhí)行一個(gè)方法時(shí),就會(huì)隨之創(chuàng)建一個(gè)對(duì)應(yīng)的棧幀,并將建立的棧幀壓棧。當(dāng)方法執(zhí)行完畢之后,便會(huì)將棧幀出棧。因此可知,線程當(dāng)前執(zhí)行的方法所對(duì)應(yīng)的棧幀必定位于Java棧的頂部。講到這里,大家就應(yīng)該會(huì)明白為什么 在 使用 遞歸方法的時(shí)候容易導(dǎo)致棧內(nèi)存溢出的現(xiàn)象了以及為什么棧區(qū)的空間不用程序員去管理了(當(dāng)然在Java中,程序員基本不用關(guān)系到內(nèi)存分配和釋放的事情,因?yàn)镴ava有自己的垃圾回收機(jī)制),這部分空間的分配和釋放都是由系統(tǒng)自動(dòng)實(shí)施的。對(duì)于所有的程序設(shè)計(jì)語言來說,棧這部分空間對(duì)程序員來說是不透明的。下圖表示了一個(gè)Java棧的模型:
局部變量表,顧名思義,想必不用解釋大家應(yīng)該明白它的作用了吧。就是用來存儲(chǔ)方法中的局部變量(包括在方法中聲明的非靜態(tài)變量以及函數(shù)形參)。對(duì)于基本數(shù)據(jù)類型的變量,則直接存儲(chǔ)它的值,對(duì)于引用類型的變量,則存的是指向?qū)ο蟮囊?。局部變量表的大小在編譯器就可以確定其大小了,因此在程序執(zhí)行期間局部變量表的大小是不會(huì)改變的。
操作數(shù)棧,想必學(xué)過數(shù)據(jù)結(jié)構(gòu)中的棧的朋友想必對(duì)表達(dá)式求值問題不會(huì)陌生,棧最典型的一個(gè)應(yīng)用就是用來對(duì)表達(dá)式求值。想想一個(gè)線程執(zhí)行方法的過程中,實(shí)際上就是不斷執(zhí)行語句的過程,而歸根到底就是進(jìn)行計(jì)算的過程。因此可以這么說,程序中的所有計(jì)算過程都是在借助于操作數(shù)棧來完成的。
指向運(yùn)行時(shí)常量池的引用,因?yàn)樵诜椒▓?zhí)行的過程中有可能需要用到類中的常量,所以必須要有一個(gè)引用指向運(yùn)行時(shí)常量。
方法返回地址,當(dāng)一個(gè)方法執(zhí)行完畢之后,要返回之前調(diào)用它的地方,因此在棧幀中必須保存一個(gè)方法返回地址。
由于每個(gè)線程正在執(zhí)行的方法可能不同,因此每個(gè)線程都會(huì)有一個(gè)自己的Java棧,互不干擾。
4.本地方法棧
我們點(diǎn)開Thread類的源碼,可以看到它的start()方法帶有一個(gè)native關(guān)鍵字修飾,而且不存在方法體,這種用native修飾的方法就是本地方法。
- private native void start0();
本地方法棧與Java棧的作用和原理非常相似,區(qū)別只不過是Java棧是為執(zhí)行Java方法服務(wù)的,而本地方法棧則是為執(zhí)行本地方法(Native Method)服務(wù)的。在JVM規(guī)范中,并沒有對(duì)本地方發(fā)展的具體實(shí)現(xiàn)方法以及數(shù)據(jù)結(jié)構(gòu)作強(qiáng)制規(guī)定,虛擬機(jī)可以自由實(shí)現(xiàn)它。在HotSopt虛擬機(jī)中直接就把本地方法棧和Java棧合二為一。
5.程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register),也有稱為PC寄存器的。學(xué)過匯編語言的同學(xué)對(duì)程序計(jì)數(shù)器這個(gè)概念并不陌生,在匯編語言中,程序計(jì)數(shù)器是指CPU中的寄存器,它保存的是程序當(dāng)前執(zhí)行的指令的地址(也可以說保存下一條指令的所在存儲(chǔ)單元的地址),當(dāng)CPU需要執(zhí)行指令時(shí),需要從程序計(jì)數(shù)器中得到當(dāng)前需要執(zhí)行的指令所在存儲(chǔ)單元的地址,然后根據(jù)得到的地址獲取到指令,在得到指令之后,程序計(jì)數(shù)器便自動(dòng)加1或者根據(jù)轉(zhuǎn)移指針得到下一條指令的地址,如此循環(huán),直至執(zhí)行完所有的指令。
雖然JVM中的程序計(jì)數(shù)器并不像匯編語言中的程序計(jì)數(shù)器一樣是物理概念上的CPU寄存器,但是JVM中的程序計(jì)數(shù)器的功能跟匯編語言中的程序計(jì)數(shù)器的功能在邏輯上是等同的,也就是說是用來指示執(zhí)行哪條指令的。
由于在JVM中,多線程是通過線程輪流切換來獲得CPU執(zhí)行時(shí)間的,因此,在任一具體時(shí)刻,一個(gè)CPU的內(nèi)核只會(huì)執(zhí)行一條線程中的指令,因此,為了能夠使得每個(gè)線程都在線程切換后能夠恢復(fù)在切換之前的程序執(zhí)行位置,每個(gè)線程都需要有自己獨(dú)立的程序計(jì)數(shù)器,并且不能互相被干擾,否則就會(huì)影響到程序的正常執(zhí)行次序。因此,可以這么說,程序計(jì)數(shù)器是每個(gè)線程所私有的。
在JVM規(guī)范中規(guī)定,如果線程執(zhí)行的是非native方法,則程序計(jì)數(shù)器中保存的是當(dāng)前需要執(zhí)行的指令的地址;如果線程執(zhí)行的是native方法,則程序計(jì)數(shù)器中的值是undefined。
由于程序計(jì)數(shù)器中存儲(chǔ)的數(shù)據(jù)所占空間的大小不會(huì)隨程序的執(zhí)行而發(fā)生改變,所以它是內(nèi)存區(qū)域中唯一一個(gè)不會(huì)出現(xiàn)OutOfMemoryError的區(qū)域,而且占用內(nèi)存空間小到基本可以忽略不計(jì)。