該讀書筆記用於記錄在學習《深刻理解Java虛擬機——JVM高級特性與最佳實踐》一書中的一些重要知識點,對其中的部份內容進行概括,主要是方便以後進行復習。java
Java虛擬機在執行過程當中會將其管理的內存劃分爲多個不一樣的數據區域。其中一些區域隨着虛擬機啓動而建立,一些區域生命週期則依賴用戶線程的啓動和結束。算法
下面是JDK1.7 數組
是一塊較小的內存空間,用於記錄當前線程所執行的字節碼的行號,在執行過程當中經過改變計數器的值來選擇下一條被執行的指令。分支、循環、異常處理等都經過程序計數器實現。安全
在多線程環境下,CPU在不一樣線程間進行切換時,爲了保證CPU下一次切換到線程時能繼續以前的執行軌跡進行,須要時用程序計數器記錄下切換前執行到哪一步。該區域爲各個線程私有,互不干擾。該區域不會發生OOM。數據結構
若是線程在執行一個java方法,那麼程序計數器將會記錄正在執行的虛擬機字節碼的指令地址。而若是正在執行的是一個native方法,計數器的值則爲空。多線程
Java虛擬機棧由線程私有,其生命週期同線程同樣。Java虛擬機棧主要是用於描述Java程序中方法執行時的內存模型。併發
每個方法在執行的時候都會建立一個棧幀,棧幀裏存儲局部變量表、操做數棧,動態連接等信息。每個方法從開始調用到執行結束的過程就對應着一個棧幀在java虛擬機棧的入棧到出棧的過程。函數
棧幀是支持虛擬機進行方法調用和方法執行的數據結構。棧幀存儲了方法的局部變量表,操做數棧,動態連接和方法返回地址等信息。每個方法從調用開始到執行結束都對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。oop
本地方法棧和虛擬機棧的做用同樣,不一樣之處在於java虛擬機棧服務於虛擬機中的字節碼(java方法)。而本地方法棧則是爲虛擬機使用native修飾的方法服務。佈局
java堆是Java虛擬機管理的內存中最大的一塊,同時堆被全部的線程所共享,堆內存在虛擬機啓動是被建立。Java堆的做用在於存放對象的實例,幾乎全部的對象實例都在Java堆上分配內存。同時Java堆也是垃圾收集器管理的主要區域,根據分代收集算法還能夠將堆分爲新生代和老年代,更細緻的能夠分爲Eden區、from區、to區。
方法區和堆同樣被各個線程所共享。方法區用於存儲已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。方法區也被稱爲「永久代」(PermGen)。
運行時常量池在JDK1.7以前是方法區的一部分,在JDK1.7的時候運行時常量池就被移到堆中。常量池用於保存編譯期生成的各類字面量和符號引用,但並非說只有編譯期才能生成常量。在運行時也能夠將新的常量放入常量池中(String的intern()方法)。
直接內存並非Java虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域,因此直接內存的大小不受Java堆大小的影響,可是仍是受宿主機內存大小影響,也會發生OOM。在Java中使用該區域的典型表明就是NIO。
在JDK1.8以後,HotSpot JVM移除了方法區,而使用本地化內存來代替,這塊區域被稱爲「元空間」。「永久代」被移除使得JVM參數PermSize 和MaxPermSize會被忽略,當前在啓動時會有警告信息。添加了一個MaxMetaspaceSize對元數據區大小進行調整。默認狀況下,類元數據分配受到可用的本機內存容量的限制。
使用永久代來存儲類信息、常量、靜態變量等數據不是個好主意, 很容易遇到內存溢出的問題.JDK8的實現中將類的元數據放入 本地內存, 將字符串池和類的靜態變量放入java堆中。同時對永久代進行調優是很困難的,同時將元空間與堆的垃圾回收進行了隔離,避免永久代引起的Full GC和OOM等問題。
JDK1.6
JDK1.7
JDK1.8
主要發生的異常分爲兩種:OutOfMemeryError
和StackOverFlowError
。其中OutOfMemeryError
是當Java虛擬機因爲內存不足而沒法分配對象時拋出,而且垃圾收集器再也不有可用的內存。而StackOverFlowError
是當堆棧溢出發生時拋出的。
在內存區域的各部分中,程序計數器不會發生內存溢出的狀況。
虛擬機棧和本地方法棧用於存儲方法執行的順序,當方法的調用層次過深(遞歸)時,可能會致使分配的棧內存不足時將會拋出StackOverFlowError
的異常。從上圖咱們能夠看出這一塊區域還會拋出OutOfMemeryError
。這是由於擋在多線程狀況下,虛擬機中大量的線程進行方法調用致使建立的棧幀建立過多使得Java虛擬機因爲內存不足而沒法分配對象。
對於Java堆和方法區可能會因爲程序中建立的實例過多而致使OutOfMemeryError
。
若是線程請求的棧深度大於虛擬機鎖容許的最大深度,將拋出StackOverFlowError,StackOverFlowError通常是函數調用層級過多致使,好比死遞歸、死循環。這類異常通常須要咱們檢查代碼是否存在邏輯上的問題。
若是虛擬機在擴展棧時沒法申請到足夠的內存空間或者堆中存在着大量沒法被gc的類信息,將拋出OutOfMemeoryError,前者通常是在多線程環境纔會產生,通常用「減小內存的方法」,既減小最大堆和減小棧容量來換取更多的線程支持。減小最大堆使得棧可被分配的內存變大,能夠容納更多的線程,減小棧容量使得可建立的棧幀數量變大。後者須要咱們dump出堆異常模型檢查問題。若是是內存溢出則調整堆的大小,內存泄露則須要檢查相關代碼。
方法區和元數據區用於存放class的相關信息,當工程中類比較多,而方法區或者元數據區過小,在啓動時容易拋出OOM異常。
JDK1.7以前,經過-XX:PermSize,-XX:MaxPerSize,調整方法區的大小;
JDK1.8後,經過-XX:MetaspaceSize ,-XX:MaxMetaspaceSize,調整元數據區的大小;
jdk自己不多操做直接內存,而直接內存(DirectMemory)致使溢出最大的特徵是:Heap Dump文件不會看到明顯異常,而程序中直接或者間接的用到了NIO。
直接內存不受java堆大小限制,但受本機總內存的限制,能夠經過MaxDirectMemorySize來設置。
在咱們平時使用Java語言建立對象時,使用最多的確定是經過new
關鍵字完成,固然還有其餘的方式如反射,克隆等。咱們使用很簡單,可是實際上關於建立一個對象在虛擬機中是作了大量的事情的。
當虛擬機檢測到一條new
指令時,首先到常量池中檢測new指令所攜帶的參數是否能和常量池中的某一符號引用所對應,若是找到了對應的符號引用還須要檢查其所表明的類是否已經完成了加載、解析、初始化等過程。若是以上條件不知足那麼還須要先進行類加載過程。
當完成了類加載過程後就須要對對象進行內存分配了,爲一個對象分配內存的大小實際上在類加載過程當中就已經肯定下來,這裏的內存分配過程就是講一塊肯定大小的內存從Java堆中劃分出來。
假設堆中的內存是一塊絕對規整的,即被使用的內存和沒有被使用的內存有着明確的劃分,一邊是使用過的,一邊是沒有使用的,存在着一個指針做爲分界的標誌。那麼分配過程實際上就是講指針像沒有被使用的區域移動肯定大小的距離。這種分配方式被稱爲「指針碰撞」。
實際上不少狀況下內存區域並不是像上面那種狀況,而是已經使用過的內存和沒使用過的相互交錯,這時沒有辦法使用指針碰撞了。虛擬機會維護一個用於記錄那些內存是可用的的列表。在內存分配時找出一塊足夠大小的地方劃分給對象。這種方式被稱爲「空閒列表」。
從上面的描述可知使用哪一種方式是由Java堆是否規整來決定,而Java堆是否規整則由使用的垃圾收集器是否帶有壓縮整理功能決定。
在程序運行過程當中,對象建立時很是頻繁的事情,在多線程狀況下這個過程就變得很是危險。
經過對對象建立過程進行同步保證在併發下的安全性,在虛擬機中經過CAS+失敗重試的方式保證原子性。
經過預先在Java堆中爲不一樣的線程分配一塊內存將線程間的內存分配過程隔離開來,這塊內存區域被稱爲線程本地緩衝(thread location allocation buffer)。線程進行內存分配時在TLAB上進行。只有當TLAB不足須要從新分配時才須要同步操做。虛擬機是否使用TLAB能夠經過參數-XX:+/-UseTLAB
來控制。
上面的內存分配過程完成後,就須要將分配到的內存都初始化爲零值。同時設置對象信息,例如該對象是哪一個類的實例、對象的哈希碼、對象的GC分代年齡等信息。這些信息都是存放在對象的對象頭中。
以上過程完成後會調用方法對對象的信息進行初始化,就是調用構造方法。
在HotSpot Jvm中,對象的內存佈局主要分爲三個區域:
對象頭中主要包含兩部分,第一部分用於存儲對象自身的運行時數據如哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。這一部分的數據在32位和64位虛擬機中的長度分別爲32bit和64bit。
對象的另外一部分是類型指針,也便是一個對象指向它所屬類的指針。虛擬機能夠經過這個指針肯定對象屬於具體哪個類,固然這一過程並非必定得,查找對象的所屬類並非必定要經過對象自己。若是存儲的是一個數組,那麼對象頭中還會存儲該數組的長度。
實例數據是用於存儲程序代碼中定義的各個字段的內容,包括從父類繼承的和子類從新定義的。該部分的存儲順序受到虛擬機分配策略參數和字段在源代碼中定義的數據順序影響。HotSpot Jvm中默認的分配策略爲longs/doubles,ints,shorts/chars,bytes/booleans,oops(Ordinary Object Points)
,即相同大小的字段會被分配在一塊兒。在知足分配策略的條件下父類中的字段會被分配在子類的前面。
若是CompactFields爲true(默認),那麼子類中較小的變量會被插入到父類的空隙中取。
這一部分並非必要的,僅僅起到佔位符的做用,除此外沒有其餘的做用。由於HotSpot虛擬機的自動內存管理系統要求對象的大小要求時8字節的倍數,對象頭部分固定爲8的倍數,而實例數據若是大小不到8的倍數就由對其填充來補充。
對象是在堆區域建立,爲了使用對象,咱們須要經過棧上的reference數據來操做堆上的對象。目前主流的訪問方式有兩種:
使用句柄訪問的方式須要在堆上額外分配出一塊區域來做爲句柄池,在棧上的reference數據則存儲對象的句柄地址而句柄則包括對象實例數據和類型數據的地址。
//圖來自《深刻理解java虛擬機》
直接指針訪問的方式中reference存儲的是對象的實例地址,而對象類型數據的地址則是存儲在對象的實例中。
//圖來自《深刻理解java虛擬機》
使用句柄方式是reference數據不存儲對象的具體地址而是經過句柄來指向,當對象移動(垃圾回收)後也不須要修改reference中的值。
使用直接指針訪問好處在於直接定位到對象實例數據,不須要通過句柄這一次定位,在頻繁的對象定位過程當中能有效提高效率,HotSpot使用直接指針定位方式。