由于Java程序是交由JVM執(zhí)行的,所以我們在談Java內(nèi)存區(qū)域劃分的時(shí)候事實(shí)上是指JVM內(nèi)存區(qū)域劃分。在討論JVM內(nèi)存區(qū)域劃分之前,先來看一下Java程序具體執(zhí)行的過程:

這里虛擬機(jī)的其中一個(gè)作用是將字節(jié)碼轉(zhuǎn)為機(jī)器碼,以此來實(shí)現(xiàn)JAVA的跨平臺(tái)作用。但這并不是現(xiàn)在的重點(diǎn)。
在整個(gè)程序執(zhí)行過程中,JVM會(huì)用一段空間來存儲(chǔ)程序執(zhí)行期間需要用到的數(shù)據(jù)和相關(guān)信息,這段空間一般被稱作為Runtime Data Area(運(yùn)行時(shí)數(shù)據(jù)區(qū)),也就是我們常說的JVM內(nèi)存。因此,在Java中我們常常說到的內(nèi)存管理就是針對這段空間進(jìn)行管理(如何分配和回收內(nèi)存空間)。
運(yùn)行時(shí)數(shù)據(jù)區(qū)包括哪幾部分?

程序計(jì)數(shù)器(Program Counter Register)
? 也有稱作為PC寄存器。想必學(xué)過匯編語言的朋友對程序計(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è)線程所私有的。
? 程序計(jì)數(shù)器可以簡單理解為一個(gè)代碼行數(shù)指示器,表示當(dāng)前機(jī)器已經(jīng)讀到了第幾行代碼。
Java棧
? ? ? Java棧也稱作虛擬機(jī)棧(Java Vitual Machine Stack),也就是我們常常所說的棧,跟C語言的數(shù)據(jù)段中的棧類似。事實(shí)上,Java棧是Java方法執(zhí)行的內(nèi)存模型。為什么這么說呢?下面就來解釋一下其中的原因。
Java棧中存放的是一個(gè)個(gè)的棧幀,每個(gè)棧幀對應(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è)對應(yīng)的棧幀,并將建立的棧幀壓棧。當(dāng)方法執(zhí)行完畢之后,便會(huì)將棧幀出棧。因此可知,線程當(dāng)前執(zhí)行的方法所對應(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í)施的。對于所有的程序設(shè)計(jì)語言來說,棧這部分空間對程序員來說是不透明的。下圖表示了一個(gè)Java棧的模型:

簡單來講,執(zhí)行JAVA方法開始時(shí),將方法壓棧,若方法中調(diào)用了其它方法,則繼續(xù)將新方法壓棧,當(dāng)方法全部執(zhí)行完,出棧。
這就解釋了為什么在用遞歸時(shí)會(huì)常常出現(xiàn)stackoverflow的異常。因?yàn)镴AVA棧內(nèi)存也是有限的。
局部變量表,顧名思義,想必不用解釋大家應(yīng)該明白它的作用了吧。就是用來存儲(chǔ)方法中的局部變量(包括在方法中聲明的非靜態(tài)變量以及函數(shù)形參)。對于基本數(shù)據(jù)類型的變量,則直接存儲(chǔ)它的值,對于引用類型的變量,則存的是指向?qū)ο蟮囊?。局部變量表的大小在編譯器就可以確定其大小了,因此在程序執(zhí)行期間局部變量表的大小是不會(huì)改變的。
操作數(shù)棧,想必學(xué)過數(shù)據(jù)結(jié)構(gòu)中的棧的朋友想必對表達(dá)式求值問題不會(huì)陌生,棧最典型的一個(gè)應(yīng)用就是用來對表達(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棧,互不干擾。
本地方法棧
本地方法棧與Java棧的作用和原理非常相似。區(qū)別只不過是Java棧是為執(zhí)行Java方法服務(wù)的,而本地方法棧則是為執(zhí)行本地方法(Native Method)服務(wù)的。在JVM規(guī)范中,并沒有對本地方發(fā)展的具體實(shí)現(xiàn)方法以及數(shù)據(jù)結(jié)構(gòu)作強(qiáng)制規(guī)定,虛擬機(jī)可以自由實(shí)現(xiàn)它。在HotSopt虛擬機(jī)中直接就把本地方法棧和Java棧合二為一。
堆
在C語言中,堆這部分空間是唯一一個(gè)程序員可以管理的內(nèi)存區(qū)域。程序員可以通過malloc函數(shù)和free函數(shù)在堆上申請和釋放空間。那么在Java中是怎么樣的呢?
Java中的堆是用來存儲(chǔ)對象本身的以及數(shù)組(當(dāng)然,數(shù)組引用是存放在Java棧中的)。只不過和C語言中的不同,在Java中,程序員基本不用去關(guān)心空間釋放的問題,Java的垃圾回收機(jī)制會(huì)自動(dòng)進(jìn)行處理。因此這部分空間也是Java垃圾收集器管理的主要區(qū)域。另外,堆是被所有線程共享的,在JVM中只有一個(gè)堆。
方法區(qū)
方法區(qū)在JVM中也是一個(gè)非常重要的區(qū)域,它與堆一樣,是被線程共享的區(qū)域。在方法區(qū)中,存儲(chǔ)了每個(gè)類的信息(包括類的名稱、方法信息、字段信息)、靜態(tài)變量、常量以及編譯器編譯后的代碼等。
在Class文件中除了類的字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池,用來存儲(chǔ)編譯期間生成的字面量和符號(hào)引用。
在方法區(qū)中有一個(gè)非常重要的部分就是運(yùn)行時(shí)常量池,它是每一個(gè)類或接口的常量池的運(yùn)行時(shí)表示形式,在類和接口被加載到JVM后,對應(yīng)的運(yùn)行時(shí)常量池就被創(chuàng)建出來。當(dāng)然并非Class文件常量池中的內(nèi)容才能進(jìn)入運(yùn)行時(shí)常量池,在運(yùn)行期間也可將新的常量放入運(yùn)行時(shí)常量池中,比如String的intern方法。
在JVM規(guī)范中,沒有強(qiáng)制要求方法區(qū)必須實(shí)現(xiàn)垃圾回收。很多人習(xí)慣將方法區(qū)稱為“永久代”,是因?yàn)镠otSpot虛擬機(jī)以永久代來實(shí)現(xiàn)方法區(qū),從而JVM的垃圾收集器可以像管理堆區(qū)一樣管理這部分區(qū)域,從而不需要專門為這部分設(shè)計(jì)垃圾回收機(jī)制。不過自從JDK7之后,Hotspot虛擬機(jī)便將運(yùn)行時(shí)常量池從永久代移除了。
基于上述的說明, 可以很容易的總結(jié)出堆棧內(nèi)存的以下差異
1, 堆內(nèi)存屬于java 應(yīng)用程序所使用, 棧內(nèi)存屬于線程所私有的, 它的生命周期與線程相同?
2, 不論何時(shí)創(chuàng)建一個(gè)對象, 它總是存儲(chǔ)在堆內(nèi)存空間 并且棧內(nèi)存空間包含對它的引用 . 棧內(nèi)存空間只包含方法原始數(shù)據(jù)類型局部變量以及堆空間中對象的引用變量?
3, 在堆中的對象可以全局訪問, 棧內(nèi)存空間屬于線程所私有?
4, jvm 棧內(nèi)存結(jié)構(gòu)管理較為簡單, 遵循LIFO 的原則, 堆空間內(nèi)存管理較為復(fù)雜 , 細(xì)分為:新生代和老年代 etc..?
5, 棧內(nèi)存生命周期短暫, 而堆內(nèi)存伴隨整個(gè)用用程序的生命周期?
6, 二者拋出異常的方式, 如果線程請求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常, 堆內(nèi)存拋出OutOfMemoryError異常
下面以例子來說明一下:

可以看到,MAIN方法中,NEW了兩個(gè)對象,調(diào)用了一個(gè)方法:

可以看出,方法里的基本變量,是存在棧區(qū),方法中的對象,在棧區(qū)只是有引用,真正的對象是在堆區(qū)。
方法中的String基本型,是在常量池中。但如果是 String a = new String("abc");? 那此時(shí)的a是對象! 就不會(huì)在常量池中,而在堆區(qū)!??
我們來看看執(zhí)行程序的步驟。
一旦我們開始運(yùn)行程序, 它會(huì)把所有的運(yùn)行時(shí)類加載到堆內(nèi)存空間, 在 Line 1 行找到main() 方法, Java Runtime 創(chuàng)建由main() 方法線程使用的棧內(nèi)存空間
在第二行 我們創(chuàng)建了原始數(shù)據(jù)類型的局部變量, 所以它將被存儲(chǔ)在main() 方法的棧內(nèi)存空間
在第3行我們創(chuàng)建了一個(gè)Object 類型的對象, 所以它被創(chuàng)建在Heap 堆內(nèi)存空間中 并且 Stack 棧內(nèi)存空間包含對它的引用, 當(dāng)我們在第4行中創(chuàng)建Memory 對象時(shí), 會(huì)發(fā)生類似的過程
現(xiàn)在我們在第5行調(diào)用foo() 方法, 此時(shí)會(huì)在stack 棧創(chuàng)建一個(gè)block 供foo() 方法使用
Java 是通過值傳遞, 在第6行, 會(huì)在foo() 棧中創(chuàng)建一個(gè)對Object 對象的新的引用
在第7行 , 一個(gè)string 類型的對象被創(chuàng)建, 此時(shí) 會(huì)在foo() 棧內(nèi)存中創(chuàng)建它的一個(gè)引用 str
foo() 方法在第8行執(zhí)行完畢, 此時(shí), 程序會(huì)釋放stack 棧內(nèi)存中為foo() 方法分配的棧內(nèi)存空間
在第9行, main() 方法執(zhí)行完畢, 為main()方法創(chuàng)建的堆棧內(nèi)存被銷毀, 此時(shí) 這個(gè)java 程序結(jié)束運(yùn)行, Java Runtime 會(huì)釋放所有的內(nèi)存
現(xiàn)在就可以講講JAVA指針的含義:實(shí)際上,棧中的變量指向堆內(nèi)存中的變量,這就是Java中的指針!