java 的JVM內存詳解和內存溢出異常

說明

更多關於JAVA虛擬機的知識,大家可以參考 《深入理解java虛擬機》 –周志明著 一書,下面的內容大部分都是總結自這本書中的內容。

java的內存管理

對於java 和C++來說,有這這樣一個巨大的差別,這個差別就是由內存動態分配和垃圾回收技術所圍成的高牆,牆裏面的人想出來,牆外面的人想進去。對於java來說,JVM提供了自動管理內存機制,在該機制下,程序員不用向C或者C++一樣再去寫 delete、free去釋放內存空間,因爲內存管理的權力是由java虛擬機控制的,但是一旦發生了內存泄漏或者內存溢出等問題,排查錯誤將十分艱難,所以學習瞭解虛擬機是如何管理內存的,至關重要。

JVM

java最常用的虛擬機,一般是sun公司提供的hotSpot虛擬機, 是基於棧的虛擬機, 而對於Andriod來說,5.0以後默認使用 Art虛擬機,是基於寄存器的虛擬機。eclipse中使用JDK 1.7做開發時,使用的就是HotSpot虛擬機

java內存的幾個模塊: 運行時數據區

這裏寫圖片描述

更加詳細的一個內存佈局,如圖:
這裏寫圖片描述
可以看到內存區域主要分爲這樣幾塊:

1. 程序計數器(線程私有)

程序計數器的作用:
我們知道,java虛擬機的多線程是是通過CPU時間分片來給每一個線程輪流分配時間片來進行線程的執行的,因此爲了每一個線程切換之後能回到正確的執行位置,每個線程都需要一個獨立的程序計數器,各個線程之間的程序計數器互相獨立,互不影響。
異常:
這個內存區域是JVM規範唯一一個沒有規定任何OutofMemoryError異常的區域

2. java虛擬機棧(線程私有,爲每一個線程分配一個虛擬棧)

虛擬機棧的作用:
java虛擬機棧描述的是java方法執行的內存模型, 裏面存儲的內容的基本單位是是一個個的棧幀每一個棧幀對應着一個方法,每一個方法的調用直至完成過程就對應這一個棧幀在虛擬機棧中的入棧出棧過程。
棧幀:
棧幀中存儲的信息包括等:
1.、局部變量表: 存放了編譯器可知的8種基本數據類型對象的引用等,其中64位的long和double會佔據2個局部變量空間。主要用來存放方法的參數和方法內定義的局部變量, 在編譯的時候就已經確定了局部變量表的大小,在垃圾回收的時候能否被回收的條件就是局部變量表中是否還存在着對對象的引用, 所以有一條推薦的規則是不使用的對象應該手動賦值爲null(但是也會帶來問題),不過從編碼角度來將,以恰當的變量作用域來控制變量回收時間纔是最優雅的解決辦法。
2、 操作數棧 用來進行數據的操作的,對應着局部變量的出棧入棧過程
3、動態鏈接 每一個棧幀都包含着一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用的目的是爲了支持方法調用過程中的動態鏈接,
在Class文件的常量池中存在着大量的符號引用,方法調用指令就以常量池中的符號引用(一切方法調用在class文件中存儲的都只是符號引用,而不是方法在實際運行時內存佈局的入口地址)作爲參數
方法調用指令有5種:
invokestatic: 調用靜態方法
invokespecial: 調用實例構造器方法,私有方法,父類方法
invokevirtual:調用所有的虛方法
invokeinterface: 調用接口方法
invokedynamic: 先在運行時動態解析出符號所引用的方法,再執行該方法,分派邏輯由用戶設定的引導方法決定。
解析:方法的符號引用有的可以在類加載的階段就直接轉化爲直接引用,這種轉化爲靜態解析(方法在程序真正運行之前就已經有了一個可確定的版本,並且此版本在運行期是不可改變的,主要包括靜態方法和私有方法,實例構造器,父類方法super這些方法也被稱爲非虛方法),這些方法在加載的時候被加載到方法區之後,地址就被確定了,稱爲爲解析調用。另外final 修飾的方法也稱爲非虛方法,因爲該方法不能被重寫,沒有其他版本,所以無需對其進行多態選擇。
分派: 分派分爲靜態分派和動態分派。靜態分派的典型應用是方法重載:根據參數的類型和參數的個數作爲判定根據,而參數類型在編譯期間的時候就已經確定了,; 動態分派的典型應用是方法重寫,我們把這種,符號引用是在運行期間轉化爲直接引用,成爲動態鏈接(在運行時期才確定調用哪個方法的分派過程稱爲動態分派
java虛擬機動態分派的過程:(invokevirtual指令的多態查找過程)
第一步: 找到操作數棧頂的第一個元素所指向的對象的實際類型,記做C
第二步: 如果在C類中找到與常量池中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,通過則返回方法的直接引用,查找結束,不通過,則拋出異常
第三步: 否則按照繼承關係從下往上對C類的父類進行第2步的搜索和校驗
第四步:如果始終沒有找到相符的方法,拋出異常。
虛擬機動態分派的實現原理:
類會在方法區中建立一個虛方法表,該表一般在類加載的連接階段進行初始化,初始化了類的變量值之後(爲類的靜態變量賦予默認值),虛擬機也會把該方法表初始化完畢。在這個虛方法表中存放的是各個方法的實際入口地址,如果某個方法在子類中沒有被重寫,那麼子類和父類虛方法表中該方法的地址是一致的,如果子類中重寫了這個方法,則子類方法表中的地址將會替換爲子類實現版本的入口地址
4、方法出口/方法返回地址 方法在調用執行完畢之後,肯定要退出,一種是正常退出,一種是異常退出,正常退出時返回地址可以是調用者的程序計數器的值,異常退出的時候,返回地址要通過異常處理器來確定。
方法退出的時候,相當於把當前棧幀出棧,因此如果是在A方法中調用了B方法,B調用結束時,要進行的操作有,恢復上層方法A的局部變量表和操作數棧如果B有返回值,返回值也要壓入A操作數棧調整PC程序計數器指向下一條指令等

異常信息:兩種
1. StackOverflowError 線程請求的棧深度大於虛擬機所允許的深度,將拋出此異常,
在單個線程下,無論是棧幀過大或者虛擬機棧容量過小,當內存無法分配時,拋出的也是StackOverflowError 信息
2. OurofMemoryError 虛擬機棧一般都可以進行動態擴展,如果在擴展的時候無法申請到足夠的內存,則拋出此異常
一般出現在多線程編程之中,如果每一個棧空間過大,那麼線程一多就容易發生內存不足, 可以通過減少最大堆(實際是增加棧的總內存空間)和減少棧大小(就是減少一個棧容量的大小,讓每一個棧只能佔據更少的空間)來換取更多的線程

3、 本地方法棧

主要爲java中的native方法進行服務的,與虛擬機棧的功能類似,也會拋出StackOverflowError 和OurofMemoryError 兩種異常信息。HotSpot中直接將本地方法棧和虛擬機棧合二爲一了。

4. 堆(被所有線程共享的一塊內存區域)

堆的作用:
是JVM內存管理的最大一塊區域,在JVM啓動時創建,主要用來存放對象的實例幾乎所有的對象實例和數組都要在堆上分配空間,java堆是垃圾收集器回收垃圾的主要管理區域,現在的收集器基本上都採用的分代的收集方法(能夠更好的回收內存,因爲一般來講剛生成的對象只有一小部分能夠存活下來,所以分代之後,只需要頻繁的對新生代進行垃圾回收,只有達到一定的去觸發條件才進行一次full gc),所以堆還可以細分爲:
新生代:分爲三個區域,Eden,From Survivor,To Survivor,比例8:1:1, 新生成的對象一般都是放在Eden區域的,存活下來的放在From Survivor,From Survivor滿了觸發一次回收放到To Survivor, To Survivor滿了觸發一次回收,放到年老代,年老代滿了觸發一次full gc
年老代
從內存分配的角度來看,線程共享的java堆中可能劃分出多個線程私有的分配緩衝區,爲了更好的分配內存
異常
OurofMemoryError 堆一般可以指定堆的大小,也可以進行動態擴展,(通過-Xmx和-Xms控制),一般都是可擴展的實現,當無法爲新生成的對象分配內存空間時,就拋出OurofMemoryError 異常
小知識: 如何檢查堆溢出出現的原因? 通過參數-XX:+HeapDumpOnOurofMemoryError 可以讓虛擬機在內存溢出異常時Dump出當前的內存堆轉儲快照以便事後分析。
一般手段是通過內存映像分析工具(如Eclipse Memory Analyzer)對Dump出來的內存堆轉儲快照進行分析,先區分是發生了內存泄漏(Memory Leak: 內存中的對象已經可以銷燬了,但是垃圾回收處理器無法銷燬,如果是這樣,可以進一步通過工具查看泄漏對象到GC root的引用鏈,於是就能清楚泄漏對象是如何與GC root關聯以至於無法進行回收的)還是內存溢出(Memory Overflow,內存中對象都是存活的,但是沒有內存空間了。可以調JVM參數,代碼上檢查減少生命週期過長的對象等)

5. 方法區(是各個線程共享的一塊內存區域)

方法區作用:
用來存儲已經被虛擬機加載的類信息(包括類名、訪問修飾符、常量池、字段描述、方法描述等),常量(final),靜態變量,即時編譯後的代碼等數據, 還有一個別名叫非堆,用來與java的堆區分開。 HotSpot將方法區成爲永久代,是因爲HotSpot將垃圾回收的區域擴展到了方法區,省去了專門爲方法區內存編寫專門的內存管理的diamante,對於其他虛擬機來講是不存在永久代這個概念的, 針對方法區的垃圾回收主要包括針對常量池的回收和對類型的卸載。在JDK 1.7中的HotSpot中已經將原本放在永久代中的字符串常量池移出了
異常:
OurofMemoryError 方法區無法滿足內存分配需求時, 在經常動態生成大量class的應用中,應該特別注意類的回收情況,比如 反射CGlib(開源項目,生成字節碼文件),大量JSP或者動態產生JSP文件的應用(每一個JSP頁面加載爲一個類)基於OSGI的應用(即使是同一個類文件,被不同的類加載器加載被視爲不同的類
運行時常量池:
運行時常量池是方法區的一部分。 Class文件中除了有類的版本,字段,方法,接口等描述信息外,還有一項是常量池,存放的是編譯期間生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區中的運行常量池中,一般來講,除了符號引用之外,編譯期間會將一部分能確定的符號引用轉化爲直接引用,直接引用也會放進運行時常量池中。
與Class文件常量池不同的另外一個特徵是,運行時常量池具有動態性,運行期間也能將新的常量放進池中,用的比較多的是String的intern方法
附加一點複習的知識:

// 直接賦予字符串的方法,首先判斷字符串常量池中是否有aaa, 如果存在,直接返回aaa的地址,如果不存在,會將字符串放在字符串常量池中; 
String str1 = "aaa";
String str2 = "aaa";
// new 的方法,首先檢查字符串常量池,如果字符串常量池中存在aaa,則直接在堆上創建一個對象aaa,返回堆中此對象的地址; 如果字符串常量池中不存在aaa,則先將aaa放進字符串常量池中,然後再在堆上創建一個aaa對象,返回堆中此對象的地址
String str3 = new String("aaa");
// intern()方法,首先先檢查字符串常量池中是否存在aaa,存在則直接返回池中aaa的地址,如果不存在,則將aaa放進字符串常量池中,不會在堆上創建對象
Strign str4 = str3.intern();
// 另外需要注意的一點是,常量池維護的常量都是由範圍限制的,int型是 -128--- 127,超過範圍之後,即使是int a = 128; 也會在堆上創建一個對象,而不是放置在常量池中

6. 直接內存

直接內存不是虛擬機運行時數據區的一部分,也不是卷虛擬機規範中定義的內存區域, 在JDK 1.4之後新加入了NIO類,引入了一種基於通道的和緩衝區的I/O方式,非阻塞的,它可以使用native方法直接分配堆外內存,然後通過一個存儲在堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作,避免了在java堆和Native堆中來回的複製數據,雖然不受java堆的大小的限制,但是也會受到本機內存的限制,因此也會出現OurofMemoryError異常。

JVM對象的創建的過程

  1. 當new一個對象時,首選檢查這個指令的參數能否在方法區的常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被加載、連接(驗證、初始化、解析)和初始化,如果沒有加載,則先必須類執行加載過程;
  2. 爲這個new的對象分配內存空間,一般空間大小在類加載的時候就已經確定了。 分配空間有兩種策略: 第一如果堆中的內存空間絕對規整的,分配內存就是把指針向空閒內存區域移動一段要new的對象大小的距離,稱爲指針碰撞第二如果堆的內存空間不規整,使用的和未使用的交替存在,則必須要維護一個列表記錄哪塊內存地址沒有被分配,從空閒出分得一塊足夠大小的內存空間放置對象,稱爲空閒列表。 內存是否規整要看JVM採用了哪種垃圾回收機制,是否帶壓縮整理功能, 標記-整理,複製就可以得到規整空間,標記-清除得到的是不規整的空間。
    另外還要考慮併發分配內存空間的問題
    如果對象的生成頻繁,則有可能發生分配地址空間衝突,解決辦法有二種: 一是對分配內存空間的操作進行同步處理,實際上虛擬機採用CAS原理(Compare and Swap)配上失敗重試的方式保證分配的原子性另外一種是將內存分配的動作按照線程劃分在不同的內存空間之中進行,即每個線程在java堆中預先分配一小塊內存,成爲本地線程分配緩衝TLAB,只有線程的TLAB分配滿了之後才需要策略以來保證同步
  3. 內存分配完成之後,JVM爲分配到的內存空間初始化爲零值,對對象進行必要的設置,例如是哪個類的實例,hash碼,對象的GC分代年齡等, 之後再執行構造函數init方法,完成一個對象的初始化工作。

對象的內存佈局:包括

一、對象頭第一部分存儲自身的運行數據(如hash碼,分代年齡,鎖,偏向線程ID、時間戳等) 第二部分是類型指針(指向它的類元數據的指針,虛擬機通過這個指針確定這個獨享是哪個類的實例,另外是數組的話,頭裏還包含記錄數組長度的數據)
二、實例數據,對象存儲的真正有效的數據
三、對齊填充,起佔位符的作用,實例數據部分沒有對齊就自動對齊

對象的訪問定位

訪問定位有兩種方式:
1. 使用句柄
這裏寫圖片描述
使用句柄的話,java會在堆中劃分出一塊區域單獨存放句柄池, 使用句柄最大的優勢是 當對象被移動時,只會改變句柄中的指針,而變量表中的reference不用修改。適合垃圾回收密集的場景
2. 使用直接指針(HotSpot使用的就是直接指針定位)
這裏寫圖片描述
使用直接指針的最大優勢就是速度更快,它節省了一次定位指針開銷的時間,適合訪問頻繁的場景
上述兩幅圖片來自於博客:http://blog.csdn.net/zhaoyw2008/article/details/9286471

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章