JVM-01Java内存区域与内存溢出异常(上)【运行时区域数据】
- 思維導圖
- 概述
- 運行時數據區域
- 程序計數器 (Program Counter Register)
- 概念
- 特征
- 可能拋出的異常
- 知識擴展:JIT即時編譯
- Java虛擬機棧 (Java Virtual Machine Stacks)
- 概念
- 特性
- 可能拋出的異常
- 虛擬機棧的StackOverflowError
- 虛擬機棧的OutOfMemoryError
- 本地方法棧
- 和虛擬機棧的區別
- 特性
- Java堆
- 堆的分層
- 新生代
- 老年代 (主要存放應用程序中生命周期長的內存對象。)
- GC 回收機制
- 新生代Minor GC
- 老年代Full GC(或者叫Major GC)
- JVM參數
- 方法區(永久代)
- 運行時常量池(屬于方法區)
- 直接內存
- 程序計數器 (Program Counter Register)
思維導圖
概述
在內存管理領域 ,C/C++內存管理由開發人員管理,既擁有每一個對象的所有權,還必須負責維護每一個對象生命從開始到終結的責任
對于Java開發人員來講,在虛擬機自動內存管理機制的幫助下,Java由虛擬機管理內存,不容易出現內存泄露和內存溢出,一旦出現如果不了解JVM很難排查。
這里我們主要介紹虛擬機內存的各個區域,講解這些區域的作用、服務對象以及可能產生的問題。
Java虛擬機(JVM)在Java程序運行的過程中,會將它所管理的內存劃分為若干個不同的數據區域,這些區域有的隨著JVM的啟動而創建,有的隨著用戶線程的啟動和結束而建立和銷毀。
一個基本的JVM運行時內存模型如下
上圖是基于“JAVA SE7”的JVM虛擬機規范。虛擬機規范并非一成不變,比如在JDK8的版本中,方法區被移除,取而代之的是元數據空間metaspace。
運行時數據區域
程序計數器 (Program Counter Register)
概念
程序計數器 (Program Counter Register)是當前線程所執行的字節碼的行號指示器。
JAVA代碼編譯后的字節碼在未經過JIT(實時編譯器)編譯前,其執行方式是通過“字節碼解釋器”進行解釋執行。簡單的工作原理為解釋器讀取裝載入內存的字節碼,按照順序讀取字節碼指令。讀取一個指令后,將該指令“翻譯”成固定的操作,并根據這些操作進行分支、循環、跳轉等流程。
假設程序永遠只有一個線程,并不需要程序計數器。但實際上程序是通過多個線程協同合作執行的.
Java虛擬機的多線程是通過線程輪流切換并分配處理器執行時間的方式實現的。
JVM的多線程是通過CPU時間片輪轉(即線程輪流切換并分配處理器執行時間)算法來實現的。
也就是說,某個線程在執行過程中可能會因為時間片耗盡而被掛起,而另一個線程獲取到時間片開始執行。
當被掛起的線程重新獲取到時間片的時候,它要想從被掛起的地方繼續執行,就必須知道它上次執行到哪個位置,在JVM中,通過程序計數器來記錄某個線程的字節碼執行位置。
因此,程序計數器是具備線程隔離的特性,也就是說,每個線程工作時都有屬于自己的獨立計數器。
特征
線程隔離性,每個線程工作時都有屬于自己的獨立計數器。
執行java方法時,程序計數器是有值的,且記錄的是正在執行的字節碼指令的地址
執行native本地方法時,程序計數器的值為空(Undefined)。因為native方法是java通過JNI直接調用本地C/C++庫,由于該方法是通過C/C++而不是java進行實現。所以無法產生相應的字節碼,并且C/C++執行時的內存分配是由自己語言決定的,而不是由JVM決定的。
程序計數器占用內存很小,在進行JVM內存計算時,可以忽略不計
- 程序計數器是唯一一個在java虛擬機規范中沒有規定任何OutOfMemoryError的區域。
可能拋出的異常
程序計數器是唯一一個在java虛擬機規范中沒有規定任何OutOfMemoryError的區域。
知識擴展:JIT即時編譯
許多主流的商用虛擬機(如HotSpot),都同時包含解釋器和編譯器。
Java程序最初是僅僅通過解釋器解釋執行的,即對字節碼逐條解釋執行,這種方式的執行速度相對會比較慢,
尤其當某個方法或代碼塊運行的特別頻繁時,這種方式的執行效率就顯得很低。
于是后來在虛擬機中引入了JIT編譯器(即時編譯器),當虛擬機發現某個方法或代碼塊運行特別頻繁時,就會把這些代碼認定為“Hot Spot Code”(熱點代碼),為了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,并進行各層次的優化,完成這項任務的正是JIT編譯器。
- 當程序需要迅速啟動和執行時,解釋器可以首先發揮作用,省去編譯的時間,立即執行;
- 當程序運行后,隨著時間的推移,編譯器逐漸會失去作用,把越來越多的代碼編譯成本地代碼后,可以獲取更高的執行效率。
- 解釋執行可以節約內存,而編譯執行可以提升效率。
Java虛擬機棧 (Java Virtual Machine Stacks)
概念
同程序計數器一樣,虛擬機棧也是線程私有的,生命周期同線程相同。
虛擬機棧描述的是java方法執行的內存模型: 每個java方法在執行時,會創建一個“棧幀(stack frame)”,棧幀的結構分為“局部變量表、操作數棧、動態鏈接、方法出口”幾個部分。
我們常說的“堆內存、棧內存”中的“棧內存”指的便是虛擬機棧,確切地說,指的是虛擬機棧的棧幀中的局部變量表,因為這里存放了一個方法的所有局部變量。
局部變量表所需的內存空間在編譯期間完成分配。在方法運行的階段是不會改變局部變量表的大小的。
方法調用時,創建棧幀,并壓入虛擬機棧;方法執行完畢,棧幀出棧并被銷毀
特性
虛擬機棧是線程隔離的,即每個線程都有自己獨立的虛擬機棧。
后進先出(LIFO)棧
存儲棧幀,支撐java方法的調用執行和退出
可能出現OutOfMemoryError異常和StackOverflowError異常
可能拋出的異常
虛擬機棧的StackOverflowError
若單個線程請求的棧深度大于虛擬機允許的深度,則會拋出StackOverflowError(棧溢出錯誤)。
JVM會為每個線程的虛擬機棧分配一定的內存大小(-Xss參數),因此虛擬機棧能夠容納的棧幀數量是有限的,若棧幀不斷進棧而不出棧,最終會導致當前線程虛擬機棧的內存空間耗盡,典型如一個無結束條件的遞歸函數調用
虛擬機棧的OutOfMemoryError
不同于StackOverflowError,OutOfMemoryError指的是當整個虛擬機棧內存耗盡,并且無法再申請到新的內存時拋出的異常。
JVM未提供設置整個虛擬機棧占用內存的配置參數。虛擬機棧的最大內存大致上等于“JVM進程能占用的最大內存(依賴于具體操作系統) - 最大堆內存 - 最大方法區內存 - 程序計數器內存(可以忽略不計) - JVM進程本身消耗內存”。當虛擬機棧能夠使用的最大內存被耗盡后,便會拋出OutOfMemoryError,可以通過不斷開啟新的線程來模擬這種異常
本地方法棧
和虛擬機棧的區別
本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用非常相似,它們之間的區別是虛擬機棧為虛擬機執行Java方法服務,而本地方法棧則為虛擬機使用到的Native方法服務。
在虛擬機規范中對本地方法棧中方法使用的語言,使用方法與數據結構沒有強制規定,因此具體的虛擬機可以自由的實現它。甚至有的虛擬機(比如Sun HotSpot虛擬機)直接把本地方法棧和虛擬機合二為一。
本地方法:該方法的實現由非java語言實現,比如C語言實現
與虛擬機一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryErrory異常。
特性
線程私有
后進先出(LIFO)棧
存儲棧幀,支撐本地方法的調用執行和退出
可能出現OutOfMemoryError異常和StackOverflowError異常
有一些虛擬機(如HotSpot)將java虛擬機棧和本地方法棧合并實現
Java堆
Java堆是Java虛擬機所管理的內存中最大的一塊數據區域,在虛擬機啟動時創建并被所有線程共享。
此內存區域唯一的目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存,例如對象實例和數組。
但隨著其他技術的成熟(如JIT),對象分配在堆上慢慢地變得又沒那么“絕對”了。
Java堆同樣是垃圾收集器管理的主要區域,由于現在的收集器基本都采用分代收集算法,所以Java堆中還可以細分為新生代和老年代。
當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制)。如果堆中沒有內存完成實例分配,并且對也無法再擴展時,將會拋出OutOfMemoryError異常。
堆的分層
HotSpot JVM中的堆,一般分為:新生代、老年代
默認的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值為 1:2 ( 該值可以通過參數 –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小。老年代 ( Old ) = 2/3 的堆空間大小。
新生代
新生代又分為 Eden區、SurvivorFrom、SurvivorTo三個區
默認的,Edem : from : to = 8 : 1 : 1 ( 可以通過參數 –XX:SurvivorRatio 來設定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。
JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來為對象服務,所以無論什么時候,總是有一塊 Survivor 區域是空閑著的。
因此,新生代實際可用的內存空間為 9/10 ( 即90% )的新生代空間,只有10%的內存被“浪費”,最大限度的節約資源。
老年代 (主要存放應用程序中生命周期長的內存對象。)
老年代的對象比較穩定,所以MajorGC不會頻繁執行。在進行MajorGC前一般都先進行了一次MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次MajorGC進行垃圾回收騰出空間。
MajorGC采用標記—清除算法:首先掃描一次所有老年代,標記出存活的對象,然后回收沒有標記的對象。MajorGC的耗時比較長,因為要掃描再回收。MajorGC會產生內存碎片,為了減少內存損耗,我們一般需要進行合并或者標記出來方便下次直接分配。
當老年代也滿了裝不下的時候,就會拋出OOM(Out of Memory)異常。
GC 回收機制
Java 中的堆也是 GC 收集垃圾的主要區域。GC 分為兩種:Minor GC、Full GC ( 或稱為 Major GC )。
新生代Minor GC
Minor GC 是發生在新生代中的垃圾收集動作,所采用的是復制算法。
新生代幾乎是所有 Java 對象出生的地方,即 Java 對象申請的內存以及存放都是在這個地方。Java 中的大部分對象通常不需長久存活,具有朝生夕滅的性質。
當對象在 Eden ( 包括一個 Survivor 區域,這里假設是 from 區域 ) 出生后,在經過一次 Minor GC 后,如果對象還存活,并且能夠被另外一塊 Survivor 區域所容納( 上面已經假設為 from 區域,這里應為 to 區域,即 to 區域有足夠的內存空間來存儲 Eden 和 from 區域中存活的對象 ),則使用復制算法將這些仍然還存活的對象復制到另外一塊 Survivor 區域 ( 即 to 區域 ) 中,然后清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),并且將這些對象的年齡設置為1,以后對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,可以通過參數 -XX:MaxTenuringThreshold 來設定 ),這些對象就會成為老年代。
但這也不是一定的,對于一些較大的對象 ( 即需要分配一塊較大的連續內存空間 ) 則是直接進入到老年代。
老年代Full GC(或者叫Major GC)
Full GC 是發生在老年代的垃圾收集動作,所采用的是標記-清除算法。
堆內存中的老年代(Old)不同于這個,老年代里面的對象幾乎個個都是在 Survivor 區域中熬過來的,它們是不會那么容易就 “死掉” 了的。因此,Full GC 發生的次數不會有 Minor GC 那么頻繁,并且做一次 Full GC 要比進行一次 Minor GC 的時間更長。
另外,標記-清除算法收集垃圾的時候會產生許多的內存碎片 ( 即不連續的內存空間 ),此后需要為較大的對象分配內存空間時,若無法找到足夠的連續的內存空間,就會提前觸發一次 GC 的收集動作。
JVM參數
官網 :http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
如下僅列舉幾個常用的
| -Xms | 初始堆大小。如:-Xms256m |
| -Xmx | 最大堆大小。如:-Xmx512m |
| -Xmn | 新生代大小。通常為 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間為 = Eden + 1 個 Survivor,即 90% |
| -Xss | JDK1.5+ 每個線程堆棧大小為 1M,一般來說如果棧不是很深的話, 1M 是絕對夠用了的。 |
| -XX:NewRatio | 新生代與老年代的比例,如 –XX:NewRatio=2,則新生代占整個堆空間的1/3,老年代占2/3 |
| -XX:SurvivorRatio | 新生代中 Eden 與 Survivor 的比值。默認值為 8。即 Eden 占新生代空間的 8/10,另外兩個 Survivor 各占 1/10 |
| -XX:PermSize | 永久代(方法區)的初始大小 |
| -XX:MaxPermSize | 永久代(方法區)的最大值 |
| -XX:+PrintGCDetails | 打印 GC 信息 |
| -XX:+HeapDumpOnOutOfMemoryError | 讓虛擬機在發生內存溢出時 Dump 出當前的內存堆轉儲快照,以便分析用 |
方法區(永久代)
方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。
方法區也稱Non-Heap(非堆),目的是與Java堆區分開來,可通過-XX:MaxPermSize設置內存大小。
從JVM運行時區域內存模型來看,堆和方法區是兩塊獨立的內存塊。但從垃圾收集器來看,HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實現方法區,所以很多人都更愿意把方法區稱為“永久代”
指內存的永久保存區域,主要存放Class和Meta(元數據)的信息,Class在被加載的時候被放入永久區域. 它和和存放實例的區域不同,GC不會在主程序運行期對永久區域進行清理。所以這也導致了永久代的區域會隨著加載的Class的增多而脹滿,最終拋出OOM異常。
在Java8中,永久代已經被移除,被一個稱為“元數據區”(元空間)的區域所取代。
元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代之間最大的區別在于:元空間并不在虛擬機中,而是使用本地內存。
因此,默認情況下,元空間的大小僅受本地內存限制。類的元數據放入 native memory, 字符串池和類的靜態變量放入java堆中. 這樣可以加載多少類的元數據就不再由MaxPermSize控制, 而由系統的實際可用空間來控制.
運行時常量池(屬于方法區)
運行時常量池(Runtime Constant Pool)是方法區的一部分,用于存放Class文件在編譯期生成的各種字面量和符號引用.
因為Class文件除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table)。這部分內容將在類加載后進入方法區的運行時常量池中存放。同時運行時常量池具備動態性,并非預置入Class文件中常量池的內存才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,例如String類的inter()方法。既然運行時常量池是方法區的一部分,自然受到方法區內存限制,當常量池無法再申請到內存時會拋出OutOfMemoryError異常。
直接內存
直接內存(Direct Memory)并不是虛擬機運行時數據的一部分,也不是Java虛擬機規范中定義的內存區域。但是這部分內存也被頻繁的使用,而且也可能導致OutOfMemoryError異常出現。
在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(channel)與緩沖區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存區域的引用進行操作。這樣能在一些場景中顯著的提高性能,因為避免了在Java堆和Native堆之間來回復制數據。
本機直接內存不會受Java堆大小的限制,但是,既然是內存,那么還是會受到本機總的內存(包塊RAM以及SWAP區或者分頁文件)大小以及處理器尋址空間的限制。
服務器管理人員在配置虛擬機參數時,會根據實際內存設置-Xmx等信息參數信息,但經常忽略直接內存,使的各個內存區域總和大于物理內存限制從而導致動態擴展時出現OutOfMemoryError異常。
總結
以上是生活随笔為你收集整理的JVM-01Java内存区域与内存溢出异常(上)【运行时区域数据】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Centos6.5安装/运行/启动/登录
- 下一篇: Maven-EclipseEE使用Mav