本文已經收錄到個人Github我的博客,歡迎大佬們光臨寒舍:html
個人GIthub博客java
Java
與C++
之間有一堵由內存動態分配和垃圾回收機制所圍成的高牆,牆外面的人想進去,牆裏面的人出不來git
對於Java
程序員來講,JVM
給咱們提供了自動內存管理機制,不須要既當「皇帝」,又當「人民」,不須要人爲地給每個new
操做寫配對的delete/free
代碼,不容易出現內存泄漏和內存溢出問題。然而一旦出現內存泄漏和溢出方面的問題,若是不清楚JVM
內存的內存管理機制,那麼將很難定位與解決問題。並且,JVM
的內存管理機制在面試中也是很是重要的考點之一。程序員
綜上,想要更加深刻了解JVM
的奧祕,探究JVM
內存管理機制是必不可少的!!!github
JVM
運行時數據區域
JVM
執行Java
程序的過程:Java
源代碼文件 (.java
) 會被Java
編譯器編譯爲字節碼文件(.class
),而後由JVM
中的類加載器加載各個類的字節碼文件,加載完畢以後,交由JVM
執行引擎執行面試
在上述過程當中,JVM
會用一段空間來存儲執行程序期間須要用到的數據和相關信息,這段空間就是運行時數據區,也就是常說的JVM
內存算法
JVM
會將它所管理的內存劃分爲若干個不一樣的數據區域,劃分結果如圖:數組
可見,運行時數據區被分爲線程私有數據區和線程共享數據區兩大類:緩存
Java
堆、方法區(內部包含運行時常量池)下面將爲您詳細介紹各個數據區的內容安全
- 若是線程正在執行的是一個
Java
方法,那麼計數器記錄的是正在執行的虛擬機字節碼指令的地址- 若是線程正在執行的是一個
Native
方法,那麼計數器的值則爲空
字節碼解釋器工做時,就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
Java
虛擬機規範》中,是惟一一個沒有規定任何 OutOfMemoryError
狀況的區域Java
虛擬機棧Java
方法執行的內存模型
每一個方法在執行的同時都會建立一個棧幀,用於存儲局部變量表、操做數棧、動態連接、方法出口等信息
每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程
局部變量表存放了編譯期可知的各類基本數據類型、對象引用類型和 returnAddress
類型,它所需的內存空間在編譯期間完成分配
Java
內存區分爲堆內存(Heap
)和棧內存(Stack
),其中『棧』指的是虛擬機棧,『堆』指的是 Java
堆Java
虛擬機規範中,對這個區域規定了兩種異常情況:
- 若是線程請求的棧深度大於虛擬機所容許的深度,將拋出
StackOverflowError
異常- 若是虛擬機棧可動態擴展且擴展時沒法申請到足夠的內存,將拋出
OutOfMemoryError
異常
Native
方法服務想要了解
Native
方法的讀者,能夠看下這篇文章:Java中native方法
StackOverflowError
和 OutOfMemoryError
異常在
Java
堆中,可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB
),但不管哪一個區域,存儲的都仍然是對象實例,進一步劃分的目的是爲了更好地回收內存,或者更快地分配內存
GC 堆
」(可別叫作垃圾堆orz)Java
虛擬機所管理的內存中最大的一塊Java
虛擬機規範中,若是在堆中沒有內存完成實例分配,且堆也沒法再擴展時,將會拋出 OutOfMemoryError
異常定義:與 Java
堆同樣,是各個線程共享的內存區域
做用:用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據
人們更願意把這個區域稱爲 「永久代」,它還有個別名叫作 Non-Heap
(非堆)
在 JDK7
的 HotSpot
中,已經把本來放在永久代的字符串常量池,靜態變量移出;
在JDK8
中,廢棄永久代的概念,改用元空間;
對用元空間替換永久代的緣由感興趣的話,能夠看下這篇文章:一文讀懂 - 元空間和永久代
永久代/元空間
和方法區的區別:
永久代/元空間
可看做是方法區的實現
Java
堆同樣不須要連續的內存和能夠選擇固定大小或可擴展外,還可選擇不實現 GC
Java
虛擬機規範中,當方法區沒法知足內存分配需求時,將拋出 OutOfMemoryError
異常
Class
文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表,用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放
Q1:字面量是什麼
能夠理解爲字面意思的常量。
int a; //變量
const int b = 10; //b爲常量,10爲字面量
string str = 「hello world!」; // str 爲變量,hello world!爲字面量
複製代碼
由例子可知,字面量就是如此容易理解
Q2:符號引用是什麼
能夠是任意類型的字面量。只要能無歧義的定位到目標。在編譯期間因爲暫時不知道類的直接引用,所以先使用符號引用代替。最終仍是會轉換爲直接引用訪問目標
好比:java/lang/StringBuilder
Q3:運行時常量池是什麼
Class
文件常量池的一個重要特徵是具有動態性,體如今並不是只有預置入 Class
文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中Java
虛擬機規範中,當常量池沒法再申請到內存時會拋出 OutOfMemoryError
異常Java
虛擬機規範》中定義的內存區域,可是這部份內存也被頻繁地調用JAVA
堆和Native
堆中來回複製數據,所以在一些場景下能顯著提升性能
JDK1.4
中新加入了NIO
類,引入了基於通道與緩衝區的IO
方式,可使用Native
函數庫直接分配直接內存(堆外內存),而後經過DirectByteBuffer
做爲這塊內存的引用進行操做
HotSpot
虛擬機內存對象探祕在熟悉虛擬機內存劃分及其具體內容以後,爲詳細瞭解虛擬機內存中數據的其餘細節,以經常使用的虛擬機
HotSpot
和經常使用的內存區域Java
堆爲例,探討HotSpot
虛擬機在Java
堆中對象分配、佈局和訪問的全過程
遇到一個
new
指令後建立過程分三步
1.類加載檢查
檢查 new
指令的參數是否能在常量池中定位到一個類的符號引用且該符號引用表明的類是否已被加載、解析和初始化,若沒有則需先執行相應的類加載,反之下一步
2.分配內存
- 由
Java
堆中的內存是否規整決定如何給新生對象分配可用空間- 由堆所採用的垃圾收集器是否帶有空間壓縮整理的能力決定
Java
堆中的內存是否規整
- 過程:將用過和空閒的內存放在兩邊,中間以一個指針做爲分界指示器。當分配內存時,就把指針向空閒一邊挪動與對象大小相等的距離便可
- 應用:Serial、ParNew 等帶 壓縮過程的收集器
- 過程:維護一個記錄可用內存塊的列表。當分配內存時,就從列表中找到一塊足夠大的空間劃分給對象實例並更新記錄
- 應用:基於
Mark-Sweep
算法的CMS
收集器
保證內存分配是線程安全的解決方案:
- 對內存分配的動做進行同步處理
- 每一個線程在
Java
堆中預先分配一塊內存(本地線程分配緩衝TLAB
),在本線程的TLAB
上進行分配,當TLAB
用完須要分配新的TLAB
時再同步鎖定
3.設置對象頭
將對象的所屬類、找到類的元數據信息的方式、對象的哈希碼、對象的 GC
分代年齡等信息存放在對象的對象頭中
分爲三塊區域
Mark Word
:用於存儲對象自身的運行時數據,如哈希碼、GC
分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID
、偏向時間戳等- 類型指針:用於肯定這個對象的所屬類
Java
源碼中定義順序這兩個因素影響。兩種主流的訪問方式
經過句柄訪問對象
在 Java
堆中劃分出一塊內存來做爲句柄池,reference
存儲的是對象的句柄地址,在句柄中包含了對象實例數據與類型數據各自的具體地址信息
好處:reference
中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference
自己不須要修改
經過直接指針訪問對象
在 Java
堆對象的佈局中考慮如何放置訪問類型數據的相關信息,reference
存儲的直接就是對象地址
好處:速度更快,節省了一次指針定位的時間開銷
OutOfMemoryError
異常這部分的內容能夠看下這篇文章:JVM內存溢出詳解(棧溢出,堆溢出,持久代溢出、沒法建立本地線程)
恭喜你!已經看完了前面的文章,相信你對
JVM
內存管理機制已經有必定深度的瞭解,下面,進行一下課堂小測試,驗證一下本身的學習成果吧!
Q1:在JVM
中,爲何要把堆與棧分離?棧不是也能夠存儲數據嗎?
從軟件設計的角度看,棧表明了處理邏輯,而堆表明了數據,分工明確,處理邏輯更爲清晰體現了「分而治之」以及「隔離」的思想。
堆與棧的分離,使得堆中的內容能夠被多個棧共享(也能夠理解爲多個線程訪問同一個對象)。這樣共享的方式有不少收益:提供了一種有效的數據交互方式(如:共享內存);堆中的共享常量和緩存能夠被全部棧訪問,節省了空間。
棧由於運行時的須要,好比保存系統運行的上下文,須要進行地址段的劃分。因爲棧只能向上增加,所以就會限制住棧存儲內容的能力。而堆不一樣,堆中的對象是能夠根據須要動態增加的,所以棧和堆的拆分,使得動態增加成爲可能,相應棧中只需記錄堆中的一個地址便可。
堆和棧的結合完美體現了面向對象的設計。當咱們將對象拆開,你會發現,對象的屬性便是數據,存放在堆中;而對象的行爲(方法)便是運行邏輯,放在棧中。所以編寫對象的時候,其實即編寫了數據結構,也編寫的處理數據的邏輯。
Q2:爲啥說堆和JVM
棧是程序運行的關鍵
若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力
本文參考連接: