深入理解JVM内存区域与内存分配
前言:這是一篇關(guān)于JVM內(nèi)存區(qū)域的文章,由網(wǎng)上一些有關(guān)這方面的文章和《深入理解Java虛擬機(jī)》整理而來,所以會有些類同的地方,也不能保證我自己寫的比其他網(wǎng)上的和書本上的要好,也不可能會這樣。寫博客的目的是為了個人對這方面自己理解的分享與個人的積累,所以有寫錯的地方多多指教。
看到深入兩字,相信很多的JAVA初學(xué)者都會直接忽略這樣的文章,其實關(guān)于JVM內(nèi)存區(qū)域的知識對于初學(xué)者來說其實是很重要的,了解Java內(nèi)存分配的原理,這對于以后JAVA的學(xué)習(xí)會有更深刻的理解,這是我個人的看法。
先來看看JVM運行時候的內(nèi)存區(qū)域
大多數(shù) JVM 將內(nèi)存區(qū)域劃分為?Method Area(Non-Heap)(方法區(qū)),Heap(堆),Program Counter Register(程序計數(shù)器),?VM Stack(虛擬機(jī)棧,也有翻譯成JAVA 方法棧的),Native Method Stack?(本地方法棧),其中Method Area和Heap是線程共享的,VM?Stack,Native Method Stack?和Program Counter Register是非線程共享的。為什么分為線程共享和非線程共享的呢?請繼續(xù)往下看。
首先我們熟悉一下一個一般性的 Java 程序的工作過程。一個 Java 源程序文件,會被編譯為字節(jié)碼文件(以 class 為擴(kuò)展名),每個java程序都需要運行在自己的JVM上,然后告知 JVM 程序的運行入口,再被 JVM 通過字節(jié)碼解釋器加載運行。那么程序開始運行后,都是如何涉及到各內(nèi)存區(qū)域的呢?
概括地說來,JVM初始運行的時候都會分配好Method Area(方法區(qū))和Heap(堆),而JVM 每遇到一個線程,就為其分配一個Program Counter Register(程序計數(shù)器),?VM Stack(虛擬機(jī)棧)和Native Method Stack?(本地方法棧),當(dāng)線程終止時,三者(虛擬機(jī)棧,本地方法棧和程序計數(shù)器)所占用的內(nèi)存空間也會被釋放掉。這也是為什么我把內(nèi)存區(qū)域分為線程共享和非線程共享的原因,非線程共享的那三個區(qū)域的生命周期與所屬線程相同,而線程共享的區(qū)域與JAVA程序運行的生命周期相同,所以這也是系統(tǒng)垃圾回收的場所只發(fā)生在線程共享的區(qū)域(實際上對大部分虛擬機(jī)來說知發(fā)生在Heap上)的原因。
1. 程序計數(shù)器
程序計數(shù)器是一塊較小的內(nèi)存區(qū)域,作用可以看做是當(dāng)前線程執(zhí)行的字節(jié)碼的位置指示器。分支、循環(huán)、跳轉(zhuǎn)、異常處理和線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計算器來完成,不多說。
2.VM Strack
先來了解下JAVA指令的構(gòu)成:
JAVA指令由?操作碼?(方法本身)和?操作數(shù)?(方法內(nèi)部變量)?組成。
1)方法本身是指令的操作碼部分,保存在Stack中; 2)方法內(nèi)部變量(局部變量)作為指令的操作數(shù)部分,跟在指令的操作碼之后,保存在Stack中(實際上是簡單類型(int,byte,short 等)保存在Stack中,對象類型在Stack中保存地址,在Heap 中保存值);虛擬機(jī)棧也叫棧內(nèi)存,是在線程創(chuàng)建時創(chuàng)建,它的生命期是跟隨線程的生命期,線程結(jié)束棧內(nèi)存也就釋放,對于棧來說不存在垃圾回收問題,只要線程一結(jié)束,該棧就 Over,所以不存在垃圾回收。也有一些資料翻譯成JAVA方法棧,大概是因為它所描述的是java方法執(zhí)行的內(nèi)存模型,每個方法執(zhí)行的同時創(chuàng)建幀棧(Strack Frame)用于存儲局部變量表(包含了對應(yīng)的方法參數(shù)和局部變量),操作棧(Operand Stack,記錄出棧、入棧的操作),動態(tài)鏈接、方法出口等信息,每個方法被調(diào)用直到執(zhí)行完畢的過程,對應(yīng)這幀棧在虛擬機(jī)棧的入棧和出棧的過程。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對象的引用(reference類型,不等同于對象本身,根據(jù)不同的虛擬機(jī)實現(xiàn),可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔?#xff0c;也可能是一個代表對象的句柄或者其他與對象相關(guān)的位置)和 returnAdress類型(指向下一條字節(jié)碼指令的地址)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,在方法在運行之前,該局部變量表所需要的內(nèi)存空間是固定的,運行期間也不會改變。
棧幀是一個內(nèi)存區(qū)塊,是一個數(shù)據(jù)集,是一個有關(guān)方法(Method)和運行期數(shù)據(jù)的數(shù)據(jù)集,當(dāng)一個方法 A 被調(diào)用時就產(chǎn)生了一個棧幀 F1,并被壓入到棧中,A 方法又調(diào)用了 B 方法,于是產(chǎn)生棧幀 F2 也被壓入棧,執(zhí)行完畢后,先彈出 F2棧幀,再彈出 F1 棧幀,遵循“先進(jìn)后出”原則。光說比較枯燥,我們看一個圖來理解一下 Java棧,如下圖所示:
?3.Heap
Heap(堆)是JVM的內(nèi)存數(shù)據(jù)區(qū)。Heap 的管理很復(fù)雜,是被所有線程共享的內(nèi)存區(qū)域,在JVM啟動時候創(chuàng)建,專門用來保存對象的實例。在Heap 中分配一定的內(nèi)存來保存對象實例,實際上也只是保存對象實例的屬性值,屬性的類型和對象本身的類型標(biāo)記等,并不保存對象的方法(以幀棧的形式保存在Stack中),在Heap 中分配一定的內(nèi)存保存對象實例。而對象實例在Heap 中分配好以后,需要在Stack中保存一個4字節(jié)的Heap 內(nèi)存地址,用來定位該對象實例在Heap 中的位置,便于找到該對象實例,是垃圾回收的主要場所。java堆處于物理不連續(xù)的內(nèi)存空間中,只要邏輯上連續(xù)即可。
4.Method Area
Object Class Data(加載類的類定義數(shù)據(jù))?是存儲在方法區(qū)的。除此之外,常量、靜態(tài)變量、JIT(即時編譯器)編譯后的代碼也都在方法區(qū)。正因為方法區(qū)所存儲的數(shù)據(jù)與堆有一種類比關(guān)系,所以它還被稱為 Non-Heap。方法區(qū)也可以是內(nèi)存不連續(xù)的區(qū)域組成的,并且可設(shè)置為固定大小,也可以設(shè)置為可擴(kuò)展的,這點與堆一樣。 垃圾回收在這個區(qū)域會比較少出現(xiàn),這個區(qū)域內(nèi)存回收的目的主要針對常量池的回收和類的卸載。 5.運行時常量池(Runtime Constant Pool) 方法區(qū)內(nèi)部有一個非常重要的區(qū)域,叫做運行時常量池(Runtime Constant Pool,簡稱 RCP)。在字節(jié)碼文件(Class文件)中,除了有類的版本、字段、方法、接口等先關(guān)信息描述外,還有常量池(Constant Pool Table)信息,用于存儲編譯器產(chǎn)生的字面量和符號引用。這部分內(nèi)容在類被加載后,都會存儲到方法區(qū)中的RCP。值得注意的是,運行時產(chǎn)生的新常量也可以被放入常量池中,比如 String 類中的 intern() 方法產(chǎn)生的常量。 常量池就是這個類型用到的常量的一個有序集合。包括直接常量(基本類型,String)和對其他類型、方法、字段的符號引用.例如: ◆類和接口的全限定名; ◆字段的名稱和描述符; ◆方法和名稱和描述符。 池中的數(shù)據(jù)和數(shù)組一樣通過索引訪問。由于常量池包含了一個類型所有的對其他類型、方法、字段的符號引用,所以常量池在Java的動態(tài)鏈接中起了核心作用. 很有用且重要關(guān)于常量池的擴(kuò)展:Java常量池詳解http://www.cnblogs.com/DreamSea/archive/2011/11/20/2256396.html 6.Native Method Stack 與VM Strack相似,VM Strack為JVM提供執(zhí)行JAVA方法的服務(wù),Native Method Stack則為JVM提供使用native 方法的服務(wù)。 7.直接內(nèi)存區(qū) 直接內(nèi)存區(qū)并不是 JVM 管理的內(nèi)存區(qū)域的一部分,而是其之外的。該區(qū)域也會在 Java 開發(fā)中使用到,并且存在導(dǎo)致內(nèi)存溢出的隱患。如果你對 NIO 有所了解,可能會知道 NIO 是可以使用 Native Methods 來使用直接內(nèi)存區(qū)的。 小結(jié):- 在此,你對JVM的內(nèi)存區(qū)域有了一定的理解,JVM內(nèi)存區(qū)域可以分為線程共享和非線程共享兩部分,線程共享的有堆和方法區(qū),非線程共享的有虛擬機(jī)棧,本地方法棧和程序計數(shù)器。
8.JVM運行原理 例子
以上都是純理論,我們舉個例子來說明 JVM 的運行原理,我們來寫一個簡單的類,代碼如下: 1 public class JVMShowcase { 2 //靜態(tài)類常量, 3 public final static String ClASS_CONST = "I'm a Const"; 4 //私有實例變量 5 private int instanceVar=15; 6 public static void main(String[] args) { 7 //調(diào)用靜態(tài)方法 8 runStaticMethod(); 9 //調(diào)用非靜態(tài)方法 10 JVMShowcase showcase=new JVMShowcase(); 11 showcase.runNonStaticMethod(100); 12 } 13 //常規(guī)靜態(tài)方法 14 public static String runStaticMethod(){ 15 return ClASS_CONST; 16 } 17 //非靜態(tài)方法 18 public int runNonStaticMethod(int parameter){ 19 int methodVar=this.instanceVar * parameter; 20 return methodVar; 21 } 22 } 這個類沒有任何意義,不用猜測這個類是做什么用,只是寫一個比較典型的類,然后我們來看 看 JVM 是如何運行的,也就是輸入 java JVMShow 后,我們來看 JVM 是如何處理的: ?第 1 步?、向操作系統(tǒng)申請空閑內(nèi)存。JVM 對操作系統(tǒng)說“給我 64M(隨便模擬數(shù)據(jù),并不是真實數(shù)據(jù)) 空閑內(nèi)存”,于是,JVM 向操作系統(tǒng)申請空閑內(nèi)存作系統(tǒng)就查找自己的內(nèi)存分配表,找了段 64M 的內(nèi)存寫上“Java 占用”標(biāo)簽,然后把內(nèi)存段的起始地址和終止地址給 JVM,JVM 準(zhǔn)備加載類文件。 ?第 2 步,分配內(nèi)存內(nèi)存。JVM 分配內(nèi)存。JVM 獲得到 64M 內(nèi)存,就開始得瑟了,首先給 heap 分個內(nèi)存,然后給棧內(nèi)存也分配好。 ? ? ?第 3 步,文件檢查和分析class 文件。若發(fā)現(xiàn)有錯誤即返回錯誤。 ?第 4 步,加載類。加載類。由于沒有指定加載器,JVM 默認(rèn)使用 bootstrap 加載器,就把 rt.jar 下的所有類都加載到了堆類存的Method Area,JVMShow 也被加載到內(nèi)存中。我們來看看Method Area區(qū)域,如下圖:(這時候包含了 main 方法和 runStaticMethod方法的符號引用,因為它們都是靜態(tài)方法,在類加載的時候就會加載) Heap 是空,Stack 是空,因為還沒有對象的新建和線程被執(zhí)行。 第 5 步、執(zhí)行方法。執(zhí)行 main 方法。執(zhí)行啟動一個線程,開始執(zhí)行 main 方法,在 main 執(zhí)行完畢前,方法區(qū)如下圖所示: (public?final?static?String?ClASS_CONST?=?"I'm?a?Const"; ?) 在 Method Area 加入了 CLASS_CONST 常量,它是在第一次被訪問時產(chǎn)生的(runStaticMethod方法內(nèi)部)。 堆內(nèi)存中有兩個對象 object 和 showcase 對象,如下圖所示:(執(zhí)行了JVMShowcase?showcase=new?JVMShowcase(); ?) 為什么會有 Object 對象呢?是因為它是 JVMShowcase 的父類,JVM 是先初始化父類,然后再初始化子類,甭管有多少個父類都初始化。 在棧內(nèi)存中有三個棧幀,如下圖所示: 于此同時,還創(chuàng)建了一個程序計數(shù)器指向下一條要執(zhí)行的語句。 第 6 步,釋放內(nèi)存。釋放內(nèi)存。運行結(jié)束,JVM 向操作系統(tǒng)發(fā)送消息,說“內(nèi)存用完了,我還給你”,運行結(jié)束。 -------------------------------------------------------------------------------------------- 現(xiàn)在來看JVM內(nèi)存是如何分配的,該部分轉(zhuǎn)載來自?http://blog.csdn.net/shimiso/article/details/8595564預(yù)備知識:
1.一個Java文件,只要有main入口方法,我們就認(rèn)為這是一個Java程序,可以單獨編譯運行。
2.無論是普通類型的變量還是引用類型的變量(俗稱實例),都可以作為局部變量,他們都可以出現(xiàn)在棧中。只不過普通類型的變量在棧中直接保存它所對應(yīng)的值,而引用類型的變量保存的是一個指向堆區(qū)的指針,通過這個指針,就可以找到這個實例在堆區(qū)對應(yīng)的對象。因此,普通類型變量只在棧區(qū)占用一塊內(nèi)存,而引用類型變量要在棧區(qū)和堆區(qū)各占一塊內(nèi)存。
?
示例:(以下所有實例中,是根據(jù)需要對于棧內(nèi)存中的幀棧簡化成了只有局部變量表,實際上由上面對幀棧的介紹知道不僅僅只有這些信息,同理堆內(nèi)存也一樣)
?
?
1.JVM自動尋找main方法,執(zhí)行第一句代碼,創(chuàng)建一個Test類的實例,在棧中分配一塊內(nèi)存,存放一個指向堆區(qū)對象的指針110925。
2.創(chuàng)建一個int型的變量date,由于是基本類型,直接在棧中存放date對應(yīng)的值9。
3.創(chuàng)建兩個BirthDate類的實例d1、d2,在棧中分別存放了對應(yīng)的指針指向各自的對象。他們在實例化時調(diào)用了有參數(shù)的構(gòu)造方法,因此對象中有自定義初始值。
?
?
調(diào)用test對象的change1方法,并且以date為參數(shù)。JVM讀到這段代碼時,檢測到i是局部變量,因此會把i放在棧中,并且把date的值賦給i。
?
?
把1234賦給i。很簡單的一步。
?
?
change1方法執(zhí)行完畢,立即釋放局部變量i所占用的棧空間。
?
調(diào)用test對象的change2方法,以實例d1為參數(shù)。JVM檢測到change2方法中的b參數(shù)為局部變量,立即加入到棧中,由于是引用類型的變量,所以b中保存的是d1中的指針,此時b和d1指向同一個堆中的對象。在b和d1之間傳遞是指針。
?
change2方法中又實例化了一個BirthDate對象,并且賦給b。在內(nèi)部執(zhí)行過程是:在堆區(qū)new了一個對象,并且把該對象的指針保存在棧中的b對應(yīng)空間,此時實例b不再指向?qū)嵗齞1所指向的對象,但是實例d1所指向的對象并無變化,這樣無法對d1造成任何影響。
?
?
change2方法執(zhí)行完畢,立即釋放局部引用變量b所占的棧空間,注意只是釋放了棧空間,堆空間要等待自動回收。
?
?
調(diào)用test實例的change3方法,以實例d2為參數(shù)。同理,JVM會在棧中為局部引用變量b分配空間,并且把d2中的指針存放在b中,此時d2和b指向同一個對象。再調(diào)用實例b的setDay方法,其實就是調(diào)用d2指向的對象的setDay方法。
?
?
調(diào)用實例b的setDay方法會影響d2,因為二者指向的是同一個對象。
?
?
change3方法執(zhí)行完畢,立即釋放局部引用變量b。
?
以上就是Java程序運行時內(nèi)存分配的大致情況。其實也沒什么,掌握了思想就很簡單了。無非就是兩種類型的變量:基本類型和引用類型。二者作為局部變量,都放在棧中,基本類型直接在棧中保存值,引用類型只保存一個指向堆區(qū)的指針,真正的對象在堆里。作為參數(shù)時基本類型就直接傳值,引用類型傳指針。
小結(jié):
1.分清什么是實例什么是對象。Class a= new Class();此時a叫實例,而不能說a是對象。實例在棧中,對象在堆中,操作實例實際上是通過實例的指針間接操作對象。多個實例可以指向同一個對象。
2.棧中的數(shù)據(jù)和堆中的數(shù)據(jù)銷毀并不是同步的。方法一旦結(jié)束,棧中的局部變量立即銷毀,但是堆中對象不一定銷毀。因為可能有其他變量也指向了這個對象,直到棧中沒有變量指向堆中的對象時,它才銷毀,而且還不是馬上銷毀,要等垃圾回收掃描時才可以被銷毀。
3.以上的棧、堆、代碼段、數(shù)據(jù)段等等都是相對于應(yīng)用程序而言的。每一個應(yīng)用程序都對應(yīng)唯一的一個JVM實例,每一個JVM實例都有自己的內(nèi)存區(qū)域,互不影響。并且這些內(nèi)存區(qū)域是所有線程共享的。這里提到的棧和堆都是整體上的概念,這些堆棧還可以細(xì)分。
4.類的成員變量在不同對象中各不相同,都有自己的存儲空間(成員變量在堆中的對象中)。而類的方法卻是該類的所有對象共享的,只有一套,對象使用方法的時候方法才被壓入棧,方法不使用則不占用內(nèi)存。
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的深入理解JVM内存区域与内存分配的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql单列去重复group by分组
- 下一篇: Codeforses 185 A Pla