原文地址:http://blog.csdn.net/u012152619/article/details/46968883java
JVM定義了若干個程序執行期間使用的數據區域。這個區域裏的一些數據在JVM啓動的時候建立,在JVM退出的時候銷燬。而其餘的數據依賴於每個線程,在線程建立時建立,在線程退出時銷燬。程序員
程序計數器是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器。分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。算法
因爲Java 虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。編程
若是線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Natvie 方法,這個計數器值則爲空(Undefined)。數組
此內存區域是惟一一個在Java 虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。緩存
線程私有,它的生命週期與線程相同。虛擬機棧描述的是Java 方法執行的內存模型:每一個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於存儲局部變量表、操做棧、動態連接、方法出口等信息。安全
動畫是由一幀一幀圖片連續切換結果的結果而產生的,其實虛擬機的運行和動畫也相似,每一個在虛擬機中運行的程序也是由許多的幀的切換產生的結果,只是這些幀裏面存放的是方法的局部變量,操做數棧,動態連接,方法返回地址和一些額外的附加信息組成。每個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。數據結構
對於執行引擎來講,活動線程中,只有棧頂的棧幀是有效的,稱爲當前棧幀,這個棧幀所關聯的方法稱爲當前方法。執行引擎所運行的全部字節碼指令都只針對當前棧幀進行操做。多線程
局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序被編譯成Class文件時,就在方法的Code屬性的max_locals數據項中肯定了該方法所須要分配的最大局部變量表的容量。jvm
局部變量表的容量以變量槽(Slot)爲最小單位,32位虛擬機中一個Slot能夠存放一個32位之內的數據類型(boolean、byte、char、short、int、float、reference和returnAddress八種)。
reference類型虛擬機規範沒有明確說明它的長度,但通常來講,虛擬機實現至少都應當能今後引用中直接或者間接地查找到對象在Java堆中的起始地址索引和方法區中的對象類型數據。
returnAddress類型是爲字節碼指令jsr、jsr_w和ret服務的,它指向了一條字節碼指令的地址。
虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程的,若是是實例方法(非static),那麼局部變量表的第0位索引的Slot默認是用於傳遞方法所屬對象實例的引用,在方法中經過this訪問。
Slot是能夠重用的,當Slot中的變量超出了做用域,那麼下一次分配Slot的時候,將會覆蓋原來的數據。Slot對對象的引用會影響GC(要是被引用,將不會被回收)。
系統不會爲局部變量賦予初始值(實例變量和類變量都會被賦予初始值)。也就是說不存在類變量那樣的準備階段。
和局部變量區同樣,操做數棧也是被組織成一個以字長爲單位的數組。可是和前者不一樣的是,它不是經過索引來訪問,而是經過標準的棧操做——壓棧和出棧—來訪問的。好比,若是某個指令把一個值壓入到操做數棧中,稍後另外一個指令就能夠彈出這個值來使用。
虛擬機在操做數棧中存儲數據的方式和在局部變量區中是同樣的:如int、long、float、double、reference和returnType的存儲。對於byte、short以及char類型的值在壓入到操做數棧以前,也會被轉換爲int。
虛擬機把操做數棧做爲它的工做區——大多數指令都要從這裏彈出數據,執行運算,而後把結果壓回操做數棧。好比,iadd指令就要從操做數棧中彈出兩個整數,執行加法運算,其結果又壓回到操做數棧中,看看下面的示例,它演示了虛擬機是如何把兩個int類型的局部變量相加,再把結果保存到第三個局部變量的:
在這個字節碼序列裏,前兩個指令iload_0和iload_1將存儲在局部變量中索引爲0和1的整數壓入操做數棧中,其後iadd指令從操做數棧中彈出那兩個整數相加,再將結果壓入操做數棧。第四條指令istore_2則從操做數棧中彈出結果,並把它存儲到局部變量區索引爲2的位置。下圖詳細表述了這個過程當中局部變量和操做數棧的狀態變化,圖中沒有使用的局部變量區和操做數棧區域以空白表示。
虛擬機運行的時候,運行時常量池會保存大量的符號引用,這些符號引用能夠當作是每一個方法的間接引用。若是表明棧幀A的方法想調用表明棧幀B的方法,那麼這個虛擬機的方法調用指令就會以B方法的符號引用做爲參數,可是由於符號引用並非直接指向表明B方法的內存位置,因此在調用以前還必需要將符號引用轉換爲直接引用,而後經過直接引用才能夠訪問到真正的方法。
若是符號引用是在類加載階段或者第一次使用的時候轉化爲直接應用,那麼這種轉換成爲靜態解析,若是是在運行期間轉換爲直接引用,那麼這種轉換就成爲動態鏈接。
方法的返回分爲兩種狀況,一種是正常退出,退出後會根據方法的定義來決定是否要傳返回值給上層的調用者,一種是異常致使的方法結束,這種狀況是不會傳返回值給上層的調用方法。
不過不管是那種方式的方法結束,在退出當前方法時都會跳轉到當前方法被調用的位置,若是方法是正常退出的,則調用者的PC計數器的值就能夠做爲返回地址,,果是由於異常退出的,則是須要經過異常處理表來肯定。
方法的的一次調用就對應着棧幀在虛擬機棧中的一次入棧出棧操做,所以方法退出時可能作的事情包括:恢復上層方法的局部變量表以及操做數棧,若是有返回值的話,就把返回值壓入到調用者棧幀的操做數棧中,還會把PC計數器的值調整爲方法調用入口的下一條指令。
在Java 虛擬機規範中,對虛擬機棧規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError 異常;若是虛擬機棧能夠動態擴展(當前大部分的Java 虛擬機均可動態擴展,只不過Java 虛擬機規範中也容許固定長度的虛擬機棧),當擴展時沒法申請到足夠的內存時會拋出OutOfMemoryError 異常。
本地方法棧(Native MethodStacks)與虛擬機棧所發揮的做用是很是類似的,其區別不過是虛擬機棧爲虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native 方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(譬如Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。
與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。
堆是Java 虛擬機所管理的內存中最大的一塊。Java 堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。可是隨着JIT 編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在堆上也漸漸變得不是那麼「絕對」了。
堆是垃圾收集器管理的主要區域,所以不少時候也被稱作「GC 堆」。
堆的大小能夠經過-Xms(最小值)和-Xmx(最大值)參數設置,-Xms爲JVM啓動時申請的最小內存,默認爲操做系統物理內存的1/64但小於1G,-Xmx爲JVM可申請的最大內存,默認爲物理內存的1/4但小於1G,默認當空餘堆內存小於40%時,JVM會增大Heap到-Xmx指定的大小,可經過-XX:MinHeapFreeRation=來指定這個比列;當空餘堆內存大於70%時,JVM會減少heap的大小到-Xms指定的大小,可經過XX:MaxHeapFreeRation=來指定這個比列,對於運行系統,爲避免在運行時頻繁調整Heap的大小,一般-Xms與-Xmx的值設成同樣。
若是從內存回收的角度看,因爲如今收集器基本都是採用的分代收集算法,因此Java 堆中還能夠細分爲:新生代和老年代;
新生代:程序新建立的對象都是重新生代分配內存,新生代由Eden Space和兩塊相同大小的Survivor Space(一般又稱S0和S1或From和To)構成,可經過-Xmn參數來指定新生代的大小,也能夠經過-XX:SurvivorRation來調整Eden Space及SurvivorSpace的大小。
老年代:用於存放通過屢次新生代GC仍然存活的對象,例如緩存對象,新建的對象也有可能直接進入老年代,主要有兩種狀況:一、大對象,可經過啓動參數設置-XX:PretenureSizeThreshold=1024(單位爲字節,默認爲0)來表明超過多大時就不在新生代分配,而是直接在老年代分配。二、大的數組對象,且數組中無引用外部對象。
老年代所佔的內存大小爲-Xmx對應的值減去-Xmn對應的值。
若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出OutOfMemoryError 異常。
方法區在一個jvm實例的內部,類型信息被存儲在一個稱爲方法區的內存邏輯區中。類型信息是由類加載器在類加載時從類文件中提取出來的。類(靜態)變量也存儲在方法區中。
簡單說方法區用來存儲類型的元數據信息,一個.class文件是類被java虛擬機使用以前的表現形式,一旦這個類要被使用,java虛擬機就會對其進行裝載、鏈接(驗證、準備、解析)和初始化。而裝載(後的結果就是由.class文件轉變爲方法區中的一段特定的數據結構。這個數據結構會存儲以下信息:
類型信息
這個類型的全限定名
這個類型的直接超類的全限定名
這個類型是類類型仍是接口類型
這個類型的訪問修飾符
任何直接超接口的全限定名的有序列表
字段信息
字段名
字段類型
字段的修飾符
方法信息
方法名
方法返回類型
方法參數的數量和類型(按照順序)
方法的修飾符
其餘信息
除了常量之外的全部類(靜態)變量
一個指向ClassLoader的指針
一個指向Class對象的指針
常量池(常量數據以及對其餘類型的符號引用)
JVM爲每一個已加載的類型都維護一個常量池。常量池就是這個類型用到的常量的一個有序集合,包括實際的常量(string,integer,和floating point常量)和對類型,域和方法的符號引用。池中的數據項象數組項同樣,是經過索引訪問的。
每一個類的這些元數據,不管是在構建這個類的實例仍是調用這個類某個對象的方法,都會訪問方法區的這些元數據。
構建一個對象時,JVM會在堆中給對象分配空間,這些空間用來存儲當前對象實例屬性以及其父類的實例屬性(而這些屬性信息都是從方法區得到),注意,這裏並非僅僅爲當前對象的實例屬性分配空間,還須要給父類的實例屬性分配,到此其實咱們就能夠回答第一個問題了,即實例化父類的某個子類時,JVM也會同時構建父類的一個對象。從另一個角度也能夠印證這個問題:調用當前類的構造方法時,首先會調用其父類的構造方法直到Object,而構造方法的調用意味着實例的建立,因此子類實例化時,父類確定也會被實例化。
類變量被類的全部實例共享,即便沒有類實例時你也能夠訪問它。這些變量只與類相關,因此在方法區中,它們成爲類數據在邏輯上的一部分。在JVM使用一個類以前,它必須在方法區中爲每一個non-final類變量分配空間。
方法區主要有如下幾個特色:
一、方法區是線程安全的。因爲全部的線程都共享方法區,因此,方法區裏的數據訪問必須被設計成線程安全的。例如,假如同時有兩個線程都企圖訪問方法區中的同一個類,而這個類尚未被裝入JVM,那麼只容許一個線程去裝載它,而其它線程必須等待
二、方法區的大小沒必要是固定的,JVM可根據應用須要動態調整。同時,方法區也不必定是連續的,方法區能夠在一個堆(甚至是JVM本身的堆)中自由分配。
三、方法區也可被垃圾收集,當某個類不在被使用(不可觸及)時,JVM將卸載這個類,進行垃圾收集
能夠經過-XX:PermSize 和 -XX:MaxPermSize 參數限制方法區的大小。
對於習慣在HotSpot 虛擬機上開發和部署程序的開發者來講,不少人願意把方法區稱爲「永久代」(PermanentGeneration),本質上二者並不等價,僅僅是由於HotSpot 虛擬機的設計團隊選擇把GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對於其餘虛擬機(如BEA JRockit、IBM J9 等)來講是不存在永久代的概念的。
相對而言,垃圾收集行爲在這個區域是比較少出現的,但並不是數據進入了方法區就如永久代的名字同樣「永久」存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。
當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。
名稱 |
特徵 |
做用 |
配置參數 |
異常 |
程序計數器 |
佔用內存小,線程私有, 生命週期與線程相同 |
大體爲字節碼行號指示器 |
無 |
無 |
虛擬機棧 |
線程私有,生命週期與線程相同,使用連續的內存空間 |
Java 方法執行的內存模型,存儲局部變量表、操做棧、動態連接、方法出口等信息 |
-Xss |
StackOverflowError OutOfMemoryError |
java堆 |
線程共享,生命週期與虛擬機相同,能夠不使用連續的內存地址 |
保存對象實例,全部對象實例(包括數組)都要在堆上分配 |
-Xms -Xsx -Xmn |
OutOfMemoryError |
方法區 |
線程共享,生命週期與虛擬機相同,能夠不使用連續的內存地址 |
存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據 |
-XX:PermSize: 16M -XX:MaxPermSize 64M |
OutOfMemoryError |
運行時常量池 |
方法區的一部分,具備動態性 |
存放字面量及符號引用 |
|
|
直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,可是這部份內存也被頻繁地使用,並且也可能致使OutOfMemoryError 異常出現,因此咱們放到這裏一塊兒講解。
在JDK 1.4 中新加入了NIO(NewInput/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O 方式,它可使用Native 函數庫直接分配堆外內存,而後經過一個存儲在Java 堆裏面的DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java 堆和Native 堆中來回複製數據。
常常有人把Java 內存區分爲堆內存(Heap)和棧內存(Stack),這種分法比較粗糙,Java內存區域的劃分實際上遠比這複雜。這種劃分方式的流行只能說明大多數程序員最關注的、與對象內存分配關係最密切的內存區域是這兩塊。
堆很靈活,可是不安全。對於對象,咱們要動態地建立、銷燬,不能說後建立的對象沒有銷燬,先前建立的對象就不能銷燬,那樣的話咱們的程序就步履維艱,因此Java中用堆來存儲對象。而一旦堆中的對象被銷燬,咱們繼續引用這個對象的話,就會出現著名的 NullPointerException,這就是堆的缺點——錯誤的引用邏輯只有在運行時纔會被發現。
棧不靈活,可是很嚴格,是安全的,易於管理。由於只要上面的引用沒有銷燬,下面引用就必定還在,在大部分程序中,都是先定義的變量、引用先進棧,後定義的後進棧,同時,區塊內部的變量、引用在進入區塊時壓棧,區塊結束時出棧,理解了這種機制,咱們就能夠很方便地理解各類編程語言的做用域的概念了,同時這也是棧的優勢——錯誤的引用邏輯在編譯時就能夠被發現。
棧--主要存放引用和基本數據類型。
堆--用來存放 new 出來的對象實例。
內存溢出 out of memory,是指程序在申請內存時,沒有足夠的內存空間供其使用,出現out of memory;好比申請了一個integer,但給它存了long才能存下的數,那就是內存溢出。
內存泄露 memory leak,是指程序在申請內存後,沒法釋放已申請的內存空間,一次內存泄露危害能夠忽略,但內存泄露堆積後果很嚴重,不管多少內存,早晚會被佔光。
memory leak會最終會致使out ofmemory。
Java 堆內存的OutOfMemoryError異常是實際應用中最多見的內存溢出異常狀況。出現Java 堆內存溢出時,異常堆棧信息「java.lang.OutOfMemoryError」會跟着進一步提示「Java heapspace」。
要解決這個區域的異常,通常的手段是首先經過內存映像分析工具(如Eclipse Memory Analyzer)對dump 出來的堆轉儲快照進行分析,重點是確認內存中的對象是不是必要的,也就是要先分清楚究竟是出現了內存泄漏(Memory Leak)仍是內存溢出(Memory Overflow)。
若是是內存泄漏,可進一步經過工具查看泄漏對象到GC Roots 的引用鏈。因而就能找到泄漏對象是經過怎樣的路徑與GC Roots 相關聯並致使垃圾收集器沒法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots 引用鏈的信息,就能夠比較準確地定位出泄漏代碼的位置。
若是不存在泄漏,換句話說就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(-Xmx 與-Xms),與機器物理內存對比看是否還能夠調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的狀況,嘗試減小程序運行期的內存消耗。
一、JVM 會試圖爲相關Java對象在Eden Space中初始化一塊內存區域。
二、當Eden空間足夠時,內存申請結束;不然到下一步。
三、JVM 試圖釋放在Eden中全部不活躍的對象(這屬於1或更高級的垃圾回收)。釋放後若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區。
四、Survivor區被用來做爲Eden及Old的中間交換區域,當Old區空間足夠時,Survivor區的對象會被移到Old區,不然會被保留在Survivor區。
五、當Old區空間不夠時,JVM 會在Old區進行徹底的垃圾收集(0級)。
六、徹底垃圾收集後,若Survivor及Old區仍然沒法存放從Eden複製過來的部分對象,致使JVM沒法在Eden區爲新對象建立內存區域,則出現「outofmemory」錯誤。
對象訪問在Java 語言中無處不在,是最普通的程序行爲,但即便是最簡單的訪問,也會卻涉及Java 棧、Java 堆、方法區這三個最重要內存區域之間的關聯關係,以下面的這句代碼:
Object obj = newObject();
假設這句代碼出如今方法體中,那「Object obj」這部分的語義將會反映到Java 棧的本地變量表中,做爲一個reference 類型數據出現。而「new Object()」這部分的語義將會反映到Java 堆中,造成一塊存儲了Object 類型全部實例數據值(Instance Data,對象中各個實例字段的數據)的結構化內存,根據具體類型以及虛擬機實現的對象內存佈局(Object Memory Layout)的不一樣,這塊內存的長度是不固定的。另外,在Java 堆中還必須包含能查找到此對象類型數據(如對象類型、父類、實現的接口、方法等)的地址信息,這些類型數據則存儲在方法區中。
因爲reference 類型在Java 虛擬機規範裏面只規定了一個指向對象的引用,並無定義這個引用應該經過哪一種方式去定位,以及訪問到Java 堆中的對象的具體位置,所以不一樣虛擬機實現的對象訪問方式會有所不一樣,主流的訪問方式有兩種:使用句柄和直接指針。
若是使用句柄訪問方式,Java 堆中將會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息。
JVM定義了若干個程序執行期間使用的數據區域。這個區域裏的一些數據在JVM啓動的時候建立,在JVM退出的時候銷燬。而其餘的數據依賴於每個線程,在線程建立時建立,在線程退出時銷燬。
程序計數器是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器。分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
因爲Java 虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。
若是線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Natvie 方法,這個計數器值則爲空(Undefined)。
此內存區域是惟一一個在Java 虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。
線程私有,它的生命週期與線程相同。虛擬機棧描述的是Java 方法執行的內存模型:每一個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於存儲局部變量表、操做棧、動態連接、方法出口等信息。
動畫是由一幀一幀圖片連續切換結果的結果而產生的,其實虛擬機的運行和動畫也相似,每一個在虛擬機中運行的程序也是由許多的幀的切換產生的結果,只是這些幀裏面存放的是方法的局部變量,操做數棧,動態連接,方法返回地址和一些額外的附加信息組成。每個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
對於執行引擎來講,活動線程中,只有棧頂的棧幀是有效的,稱爲當前棧幀,這個棧幀所關聯的方法稱爲當前方法。執行引擎所運行的全部字節碼指令都只針對當前棧幀進行操做。
局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序被編譯成Class文件時,就在方法的Code屬性的max_locals數據項中肯定了該方法所須要分配的最大局部變量表的容量。
局部變量表的容量以變量槽(Slot)爲最小單位,32位虛擬機中一個Slot能夠存放一個32位之內的數據類型(boolean、byte、char、short、int、float、reference和returnAddress八種)。
reference類型虛擬機規範沒有明確說明它的長度,但通常來講,虛擬機實現至少都應當能今後引用中直接或者間接地查找到對象在Java堆中的起始地址索引和方法區中的對象類型數據。
returnAddress類型是爲字節碼指令jsr、jsr_w和ret服務的,它指向了一條字節碼指令的地址。
虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程的,若是是實例方法(非static),那麼局部變量表的第0位索引的Slot默認是用於傳遞方法所屬對象實例的引用,在方法中經過this訪問。
Slot是能夠重用的,當Slot中的變量超出了做用域,那麼下一次分配Slot的時候,將會覆蓋原來的數據。Slot對對象的引用會影響GC(要是被引用,將不會被回收)。
系統不會爲局部變量賦予初始值(實例變量和類變量都會被賦予初始值)。也就是說不存在類變量那樣的準備階段。
和局部變量區同樣,操做數棧也是被組織成一個以字長爲單位的數組。可是和前者不一樣的是,它不是經過索引來訪問,而是經過標準的棧操做——壓棧和出棧—來訪問的。好比,若是某個指令把一個值壓入到操做數棧中,稍後另外一個指令就能夠彈出這個值來使用。
虛擬機在操做數棧中存儲數據的方式和在局部變量區中是同樣的:如int、long、float、double、reference和returnType的存儲。對於byte、short以及char類型的值在壓入到操做數棧以前,也會被轉換爲int。
虛擬機把操做數棧做爲它的工做區——大多數指令都要從這裏彈出數據,執行運算,而後把結果壓回操做數棧。好比,iadd指令就要從操做數棧中彈出兩個整數,執行加法運算,其結果又壓回到操做數棧中,看看下面的示例,它演示了虛擬機是如何把兩個int類型的局部變量相加,再把結果保存到第三個局部變量的:
在這個字節碼序列裏,前兩個指令iload_0和iload_1將存儲在局部變量中索引爲0和1的整數壓入操做數棧中,其後iadd指令從操做數棧中彈出那兩個整數相加,再將結果壓入操做數棧。第四條指令istore_2則從操做數棧中彈出結果,並把它存儲到局部變量區索引爲2的位置。下圖詳細表述了這個過程當中局部變量和操做數棧的狀態變化,圖中沒有使用的局部變量區和操做數棧區域以空白表示。
虛擬機運行的時候,運行時常量池會保存大量的符號引用,這些符號引用能夠當作是每一個方法的間接引用。若是表明棧幀A的方法想調用表明棧幀B的方法,那麼這個虛擬機的方法調用指令就會以B方法的符號引用做爲參數,可是由於符號引用並非直接指向表明B方法的內存位置,因此在調用以前還必需要將符號引用轉換爲直接引用,而後經過直接引用才能夠訪問到真正的方法。
若是符號引用是在類加載階段或者第一次使用的時候轉化爲直接應用,那麼這種轉換成爲靜態解析,若是是在運行期間轉換爲直接引用,那麼這種轉換就成爲動態鏈接。
方法的返回分爲兩種狀況,一種是正常退出,退出後會根據方法的定義來決定是否要傳返回值給上層的調用者,一種是異常致使的方法結束,這種狀況是不會傳返回值給上層的調用方法。
不過不管是那種方式的方法結束,在退出當前方法時都會跳轉到當前方法被調用的位置,若是方法是正常退出的,則調用者的PC計數器的值就能夠做爲返回地址,,果是由於異常退出的,則是須要經過異常處理表來肯定。
方法的的一次調用就對應着棧幀在虛擬機棧中的一次入棧出棧操做,所以方法退出時可能作的事情包括:恢復上層方法的局部變量表以及操做數棧,若是有返回值的話,就把返回值壓入到調用者棧幀的操做數棧中,還會把PC計數器的值調整爲方法調用入口的下一條指令。
在Java 虛擬機規範中,對虛擬機棧規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError 異常;若是虛擬機棧能夠動態擴展(當前大部分的Java 虛擬機均可動態擴展,只不過Java 虛擬機規範中也容許固定長度的虛擬機棧),當擴展時沒法申請到足夠的內存時會拋出OutOfMemoryError 異常。
本地方法棧(Native MethodStacks)與虛擬機棧所發揮的做用是很是類似的,其區別不過是虛擬機棧爲虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native 方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(譬如Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。
與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。
堆是Java 虛擬機所管理的內存中最大的一塊。Java 堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。可是隨着JIT 編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在堆上也漸漸變得不是那麼「絕對」了。
堆是垃圾收集器管理的主要區域,所以不少時候也被稱作「GC 堆」。
堆的大小能夠經過-Xms(最小值)和-Xmx(最大值)參數設置,-Xms爲JVM啓動時申請的最小內存,默認爲操做系統物理內存的1/64但小於1G,-Xmx爲JVM可申請的最大內存,默認爲物理內存的1/4但小於1G,默認當空餘堆內存小於40%時,JVM會增大Heap到-Xmx指定的大小,可經過-XX:MinHeapFreeRation=來指定這個比列;當空餘堆內存大於70%時,JVM會減少heap的大小到-Xms指定的大小,可經過XX:MaxHeapFreeRation=來指定這個比列,對於運行系統,爲避免在運行時頻繁調整Heap的大小,一般-Xms與-Xmx的值設成同樣。
若是從內存回收的角度看,因爲如今收集器基本都是採用的分代收集算法,因此Java 堆中還能夠細分爲:新生代和老年代;
新生代:程序新建立的對象都是重新生代分配內存,新生代由Eden Space和兩塊相同大小的Survivor Space(一般又稱S0和S1或From和To)構成,可經過-Xmn參數來指定新生代的大小,也能夠經過-XX:SurvivorRation來調整Eden Space及SurvivorSpace的大小。
老年代:用於存放通過屢次新生代GC仍然存活的對象,例如緩存對象,新建的對象也有可能直接進入老年代,主要有兩種狀況:一、大對象,可經過啓動參數設置-XX:PretenureSizeThreshold=1024(單位爲字節,默認爲0)來表明超過多大時就不在新生代分配,而是直接在老年代分配。二、大的數組對象,且數組中無引用外部對象。
老年代所佔的內存大小爲-Xmx對應的值減去-Xmn對應的值。
若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出OutOfMemoryError 異常。
方法區在一個jvm實例的內部,類型信息被存儲在一個稱爲方法區的內存邏輯區中。類型信息是由類加載器在類加載時從類文件中提取出來的。類(靜態)變量也存儲在方法區中。
簡單說方法區用來存儲類型的元數據信息,一個.class文件是類被java虛擬機使用以前的表現形式,一旦這個類要被使用,java虛擬機就會對其進行裝載、鏈接(驗證、準備、解析)和初始化。而裝載(後的結果就是由.class文件轉變爲方法區中的一段特定的數據結構。這個數據結構會存儲以下信息:
類型信息
這個類型的全限定名
這個類型的直接超類的全限定名
這個類型是類類型仍是接口類型
這個類型的訪問修飾符
任何直接超接口的全限定名的有序列表
字段信息
字段名
字段類型
字段的修飾符
方法信息
方法名
方法返回類型
方法參數的數量和類型(按照順序)
方法的修飾符
其餘信息
除了常量之外的全部類(靜態)變量
一個指向ClassLoader的指針
一個指向Class對象的指針
常量池(常量數據以及對其餘類型的符號引用)
JVM爲每一個已加載的類型都維護一個常量池。常量池就是這個類型用到的常量的一個有序集合,包括實際的常量(string,integer,和floating point常量)和對類型,域和方法的符號引用。池中的數據項象數組項同樣,是經過索引訪問的。
每一個類的這些元數據,不管是在構建這個類的實例仍是調用這個類某個對象的方法,都會訪問方法區的這些元數據。
構建一個對象時,JVM會在堆中給對象分配空間,這些空間用來存儲當前對象實例屬性以及其父類的實例屬性(而這些屬性信息都是從方法區得到),注意,這裏並非僅僅爲當前對象的實例屬性分配空間,還須要給父類的實例屬性分配,到此其實咱們就能夠回答第一個問題了,即實例化父類的某個子類時,JVM也會同時構建父類的一個對象。從另一個角度也能夠印證這個問題:調用當前類的構造方法時,首先會調用其父類的構造方法直到Object,而構造方法的調用意味着實例的建立,因此子類實例化時,父類確定也會被實例化。
類變量被類的全部實例共享,即便沒有類實例時你也能夠訪問它。這些變量只與類相關,因此在方法區中,它們成爲類數據在邏輯上的一部分。在JVM使用一個類以前,它必須在方法區中爲每一個non-final類變量分配空間。
方法區主要有如下幾個特色:
一、方法區是線程安全的。因爲全部的線程都共享方法區,因此,方法區裏的數據訪問必須被設計成線程安全的。例如,假如同時有兩個線程都企圖訪問方法區中的同一個類,而這個類尚未被裝入JVM,那麼只容許一個線程去裝載它,而其它線程必須等待
二、方法區的大小沒必要是固定的,JVM可根據應用須要動態調整。同時,方法區也不必定是連續的,方法區能夠在一個堆(甚至是JVM本身的堆)中自由分配。
三、方法區也可被垃圾收集,當某個類不在被使用(不可觸及)時,JVM將卸載這個類,進行垃圾收集
能夠經過-XX:PermSize 和 -XX:MaxPermSize 參數限制方法區的大小。
對於習慣在HotSpot 虛擬機上開發和部署程序的開發者來講,不少人願意把方法區稱爲「永久代」(PermanentGeneration),本質上二者並不等價,僅僅是由於HotSpot 虛擬機的設計團隊選擇把GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對於其餘虛擬機(如BEA JRockit、IBM J9 等)來講是不存在永久代的概念的。
相對而言,垃圾收集行爲在這個區域是比較少出現的,但並不是數據進入了方法區就如永久代的名字同樣「永久」存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。
當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。
名稱 |
特徵 |
做用 |
配置參數 |
異常 |
程序計數器 |
佔用內存小,線程私有, 生命週期與線程相同 |
大體爲字節碼行號指示器 |
無 |
無 |
虛擬機棧 |
線程私有,生命週期與線程相同,使用連續的內存空間 |
Java 方法執行的內存模型,存儲局部變量表、操做棧、動態連接、方法出口等信息 |
-Xss |
StackOverflowError OutOfMemoryError |
java堆 |
線程共享,生命週期與虛擬機相同,能夠不使用連續的內存地址 |
保存對象實例,全部對象實例(包括數組)都要在堆上分配 |
-Xms -Xsx -Xmn |
OutOfMemoryError |
方法區 |
線程共享,生命週期與虛擬機相同,能夠不使用連續的內存地址 |
存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據 |
-XX:PermSize: 16M -XX:MaxPermSize 64M |
OutOfMemoryError |
運行時常量池 |
方法區的一部分,具備動態性 |
存放字面量及符號引用 |
|
|
直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,可是這部份內存也被頻繁地使用,並且也可能致使OutOfMemoryError 異常出現,因此咱們放到這裏一塊兒講解。
在JDK 1.4 中新加入了NIO(NewInput/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O 方式,它可使用Native 函數庫直接分配堆外內存,而後經過一個存儲在Java 堆裏面的DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java 堆和Native 堆中來回複製數據。
常常有人把Java 內存區分爲堆內存(Heap)和棧內存(Stack),這種分法比較粗糙,Java內存區域的劃分實際上遠比這複雜。這種劃分方式的流行只能說明大多數程序員最關注的、與對象內存分配關係最密切的內存區域是這兩塊。
堆很靈活,可是不安全。對於對象,咱們要動態地建立、銷燬,不能說後建立的對象沒有銷燬,先前建立的對象就不能銷燬,那樣的話咱們的程序就步履維艱,因此Java中用堆來存儲對象。而一旦堆中的對象被銷燬,咱們繼續引用這個對象的話,就會出現著名的 NullPointerException,這就是堆的缺點——錯誤的引用邏輯只有在運行時纔會被發現。
棧不靈活,可是很嚴格,是安全的,易於管理。由於只要上面的引用沒有銷燬,下面引用就必定還在,在大部分程序中,都是先定義的變量、引用先進棧,後定義的後進棧,同時,區塊內部的變量、引用在進入區塊時壓棧,區塊結束時出棧,理解了這種機制,咱們就能夠很方便地理解各類編程語言的做用域的概念了,同時這也是棧的優勢——錯誤的引用邏輯在編譯時就能夠被發現。
棧--主要存放引用和基本數據類型。
堆--用來存放 new 出來的對象實例。
內存溢出 out of memory,是指程序在申請內存時,沒有足夠的內存空間供其使用,出現out of memory;好比申請了一個integer,但給它存了long才能存下的數,那就是內存溢出。
內存泄露 memory leak,是指程序在申請內存後,沒法釋放已申請的內存空間,一次內存泄露危害能夠忽略,但內存泄露堆積後果很嚴重,不管多少內存,早晚會被佔光。
memory leak會最終會致使out ofmemory。
Java 堆內存的OutOfMemoryError異常是實際應用中最多見的內存溢出異常狀況。出現Java 堆內存溢出時,異常堆棧信息「java.lang.OutOfMemoryError」會跟着進一步提示「Java heapspace」。
要解決這個區域的異常,通常的手段是首先經過內存映像分析工具(如Eclipse Memory Analyzer)對dump 出來的堆轉儲快照進行分析,重點是確認內存中的對象是不是必要的,也就是要先分清楚究竟是出現了內存泄漏(Memory Leak)仍是內存溢出(Memory Overflow)。
若是是內存泄漏,可進一步經過工具查看泄漏對象到GC Roots 的引用鏈。因而就能找到泄漏對象是經過怎樣的路徑與GC Roots 相關聯並致使垃圾收集器沒法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots 引用鏈的信息,就能夠比較準確地定位出泄漏代碼的位置。
若是不存在泄漏,換句話說就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(-Xmx 與-Xms),與機器物理內存對比看是否還能夠調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的狀況,嘗試減小程序運行期的內存消耗。
一、JVM 會試圖爲相關Java對象在Eden Space中初始化一塊內存區域。
二、當Eden空間足夠時,內存申請結束;不然到下一步。
三、JVM 試圖釋放在Eden中全部不活躍的對象(這屬於1或更高級的垃圾回收)。釋放後若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區。
四、Survivor區被用來做爲Eden及Old的中間交換區域,當Old區空間足夠時,Survivor區的對象會被移到Old區,不然會被保留在Survivor區。
五、當Old區空間不夠時,JVM 會在Old區進行徹底的垃圾收集(0級)。
六、徹底垃圾收集後,若Survivor及Old區仍然沒法存放從Eden複製過來的部分對象,致使JVM沒法在Eden區爲新對象建立內存區域,則出現「outofmemory」錯誤。
對象訪問在Java 語言中無處不在,是最普通的程序行爲,但即便是最簡單的訪問,也會卻涉及Java 棧、Java 堆、方法區這三個最重要內存區域之間的關聯關係,以下面的這句代碼:
Object obj = newObject();
假設這句代碼出如今方法體中,那「Object obj」這部分的語義將會反映到Java 棧的本地變量表中,做爲一個reference 類型數據出現。而「new Object()」這部分的語義將會反映到Java 堆中,造成一塊存儲了Object 類型全部實例數據值(Instance Data,對象中各個實例字段的數據)的結構化內存,根據具體類型以及虛擬機實現的對象內存佈局(Object Memory Layout)的不一樣,這塊內存的長度是不固定的。另外,在Java 堆中還必須包含能查找到此對象類型數據(如對象類型、父類、實現的接口、方法等)的地址信息,這些類型數據則存儲在方法區中。
因爲reference 類型在Java 虛擬機規範裏面只規定了一個指向對象的引用,並無定義這個引用應該經過哪一種方式去定位,以及訪問到Java 堆中的對象的具體位置,所以不一樣虛擬機實現的對象訪問方式會有所不一樣,主流的訪問方式有兩種:使用句柄和直接指針。
若是使用句柄訪問方式,Java 堆中將會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息。