Java運行時環境---內存劃分

背景:據說Java運行時環境的內存劃分是挺進BAT的必經之路。java

 

內存劃分程序員

  Java程序內存的劃分是交由JVM執行的,而不像C語言那樣須要程序員本身買單(C語言須要程序員爲每個new操做去配對delete/free代碼),放權給JVM虛擬機處理有利也有弊,好處是不容易出現內存泄漏和內存溢出問題,壞處就是本身的屁股不能本身擦,萬一有一天JVM罷工不釋放了,仍是自個忘了釋放,So瞭解虛擬機容易引發內存泄漏和溢出的場景對Java程序員來講仍是必不可少的。【內存溢出:Out Of Memmory,系統已經不能再分配空間了,比如你須要50M的空間,系統就只剩下40M;內存泄露:Memmory Leak,開闢了資源空間但用完後忘記釋放,內存還在被佔用,屢次內存泄漏就會致使內存溢出;】瞭解JVM內存劃分要端到端,先從Java程序執行的具體過程來看:算法

從圖1中能夠清楚看到Java程序的執行過程,大體就是Java源代碼(後綴爲.java)首先被Java編譯器編譯成字節碼文件(後綴爲.class),而後交由JVM中的類加載器加載各個類的字節碼文件,加載好字節碼文件後再交由JVM引擎執行。在整個程序執行過程當中,上圖中運行時區域會用一段空間來存儲程序執行期間須要用到的數據和相關信息,也就是咱們弄懂內存劃分要深度研究的區域,即JVM。數組

運行時數據(內存)區:安全

圖2中,梯形形狀的部分是全部線程之間共享的區域,長方形形狀的部分則是線程運行時獨有的數據區域;《Java虛擬機規範》規定了運行時區域包括:程序計算器、Java棧(虛擬機線)、本地方法棧、方法區和堆五大部分。多線程

一、程序計算器(Program Counter Register)
函數

程序計算器又稱爲PC寄存器,這塊內存區域至關小,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器經過改變這個計算器的值來選取下一條須要執行的字節碼命令;在JVM中多線程是經過線程輪流切換來得到CPU執行時間的,So不管何種狀況下一個CPU的內核只會執行一條線程中的指令,而爲了保證每一個線程都在線程切換後可以恢復到切換以前的程序執行位置,每一個線程就必需要有本身獨立的程序計數器,所以程序計數器是每一個線程私有的;在JVM中,若是線程執行的是非native方法,則程序計數器中保存的是當前須要執行的指令的地址,而線程如果執行native方法則程序計數器中的值是undifined;由於程序計數器中存儲的數據所佔內存空間的多少不會隨這程序的執行而改變,因而程序計數器不可能發生內存溢出OOM現象;佈局

特性:spa

a. 是當前線程所執行的字節碼的行號指示器;線程

b. 是當前線程私有的;

c. 不會發生OOMError的錯誤;

二、Java棧(虛擬機Java stack)

簡單的說Java棧就是Java方法執行的內存模型。其內部存放的是一個個的棧幀,每一個棧幀對應着一個被調用的方法;棧幀中包括局部變量表(Local Variables)、操做數棧(Operation Stack)、指向當前方法所屬的類的運行時常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息;因爲每一個線程正在執行的方法可能不一樣,所以每一個線程都會有一個本身的互不干擾的Java棧;噹噹前線程執行了一個方法,隨之就會建立一個與之對應的棧幀,並將創建的棧幀進行壓棧操做,當方法執行完畢後便會將棧幀出棧;So線程當前執行的方法所對應的棧幀一定位於Java棧的頂部,即如隊列的先進後出。下圖表示了一個Java棧的模型:

局部變量:

局部變量表就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態變量以及函數形參);若變量是基本數據類型則直接存儲它的值,若變量是引用類型則存儲的是指向對象的引用;而局部變量表的大小在編譯的時候就已經肯定了,So程序執行期間其大小亦是不會改變的。

操做數棧:

操做數棧就是用於對錶達式求值計算的,當個線程執行過程實際上就是不斷執行語句的過程,也就是不斷計算的過程,So程序中的全部計算過程都是經過操做數棧來完成的。

指向運行時常量池的引用:

指向運行時常量池的引用是因爲在方法的執行過程當中有可能須要用到類中的常量,所以必需要有一個引用指向運行時常量。

方法返回地址:

一個方法執行完畢後,要返回以前調用它的位置,因而在棧中就必須保存一個方法返回的地址。

Java棧的生命週期和線程類似;在每一個方法執行的同時都會建立一個棧幀,用於存儲局部變量表、操做數棧、指向運行時常量池的引用和返回地址(方法出口)等信息,每一個方法從調用到執行完畢的過程都對應着一個棧幀JVM中入棧到出棧到過程。(棧的大小與具體虛擬機的實現有關,通常在256~756之間)

特性:

a. 生命週期與線程類似且線程屬於私有;

b. 當線程請求的棧深度超過了JVM所容許的最大深度就會發生StackOverflowError異常;

c. 如果棧的擴展沒法申請到足夠的內存則會產生OutOfMemmoryError異常;

三、本地方法棧(Native Method Stack)

本地方法棧的原理和做用與Java棧相似,不一樣的是Java棧是爲執行Java方法服務的,而本地方法棧是爲執行本地方法服務的。

四、堆(Heap)

Java中的堆事要來存儲對象自己以及數組(數組引用存儲在Java棧中),Heap事JVM所管理的內存中最大的一塊,它在虛擬機啓動事建立且在JVM中只有一個堆;因爲JVM垃圾收集器採用的基本都是分代收集算法,So堆還能夠劃分爲Young Generation(年輕代)、Old Generation(年老代)以及Perm Generation(永久代)【JDK7以後,Hotspot虛擬機便將永久代這個概念移除了】,其中的Young Generation又分爲Eden、From和To,而From和To又統稱爲Survivor Spaces(倖存區);正常狀況下,一個對象從建立到銷燬,應該是從Eden開始,而後到Survivor Spaces,再到Old Generation,最後在某次GC下消失。

特性:

a. 堆是被全部線程共享的,且JVM中只有一個堆;

b. 存儲對象實例;

c. 當在堆中沒有完成實例的分配且沒法再擴展內存則會有OutOfMemory異常;

五、方法區(Method Area)

方法區主要用於存儲每一個類的信息(類的名稱、方法信息、字段信息)、靜態變量、常量和編譯器編譯後的代碼等;此外,在Class文件中除了類的字段、方法和接口等描述外,還有常量池,用來存儲編譯期間生成的字面量和符號引用;在方法區中還有一個很是重要的部分就是運行時常量池,它是每個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM後對應的運行時常量池就被建立出來,非Class文件常量池的內容也能夠將新的常量放入運行事常量池中,如String的intern方法(若是常量池中存在當前字符串就會直接返回當前字符串,如果常量池中無此字符串則會將其放入常量池中再返回)。

在大概瞭解了Java運行時環境JVM內存的劃分後,我的感受要進入BAT還有必要了解下對象的建立和定位,這應該對本身寫代碼也有莫大的助力。

對象的建立

一、JVM接收到一條new指令後,首先會去檢查這個指令的參數可否再常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已經被加載、解析和初始化,如若沒有,則會先執行類的初始化;

二、類加載檢查經過後,JVM會爲新生對象分配內存,而對象所需內存大小在類加載完成後即可徹底肯定,隨後就在Java堆中劃分出一塊肯定大小的內存爲對象分配空間;

case1: 若內存是規整的,則JVM將採用指針碰撞發來爲對象分配內存;指針碰撞發會將全部用過的內存放在一邊,空閒的內存放置於另外一邊,中間放着一個指針做爲分界點的指示器,分配內存的時候只需把指針向空閒內存的那邊挪動一段與對象大小相等的距離;若是垃圾收集器選擇的是Serial、ParNew這種基於壓縮算法的機制,則虛擬機採用指針碰撞發分配內存;

case2: 若內存時不規整的,已使用的內存和未使用的內存相互交錯,那麼JVM採用的是空閒列表發來爲對象分配內存;就是虛擬機維護了一個列表,記錄下那些內存塊是可用的,而後在分配內存的時候就從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上維護的內容;如果垃圾回收器選擇CMS這種基於標記-清除算法的機制,則JVM採用這種方式分配內存;

case3: 在JVM中可能會出現虛擬機正在給對象A分配內存,但指針尚未來得及修改,此時對象B又同時使用了原來的指針來分配內存的狀況;爲了及時保證new對象時候線程的安全性,JVM採用了CAS配上失敗重試的方式保證更新更新操做的原子性和TLAB兩種方式來解決這個問題;

三、分配內存結束後,JVM將分配到的內存空間都初始化爲零值(不包括對象頭【Object Header第一部是Mark Word用,於存儲對象自身的運行時數據,第二部分是類型指針,用於肯定這個對象是哪一個類的實例】);這一步保證了對象的實例字段在Java代碼中能夠不用賦值就可以直接使用,且程序能訪問到這些字段的數據類型所對應的零值;

四、對Object進行必要的設置,如該對象屬於哪一個類的實例、任何才能訪問到類的元數據信息、對象的哈希值、對象的GC分代年齡等信息,這些信息存放在Object的對象頭中;

五、執行<int>方法,把對象按照程序猿的意願進行初始化,這樣一個真正可用的對象就完徹底全的產生了。 

對象的訪問定位

在Java中須要經過棧上的reference(引用)數據來操做堆上具體對象;好比建立一個對象 String name = new String(); ,其中new String()其實有兩部分,一部分是類數據(如表明類的Class對象),另外一部分則是實例數據;因爲reference在JVM中只是一個指向對象new String()的引用name,並無規定name應該經過何種方式去定位及訪問Heap中對象的具體位置,So對象訪問的最終方式仍是由虛擬機決定的,目前主流方式有兩種:

case1: 指針訪問,java堆對象的佈局中必須考慮如何放置訪問類型數據的相關信息,該訪問方式下reference中存儲的就是對象地址;

case2: 句柄訪問,java堆中將會劃分出一塊內存做爲句柄池,此訪問方式reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息;

相關文章
相關標籤/搜索