JDK和JRE和JVM的關系
JDK(Java Development Kit)是程序開發者用來來編譯、調試java程序用的開發工具包
JRE(JavaRuntimeEnvironment,Java運行環境),也就是Java平台。所有的Java 程序都要在JRE下才能運行。普通用戶只需要運行已開發好的java程序,安裝JRE即可
JVM(JavaVirtualMachine,Java虛擬機)是JRE的一部分。它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。JVM有自己完善的硬件架構,如處理器、堆棧、寄存器等,還具有相應的指令系統
JVM內存區域
本文的講解都從這個圖一一開始,你腦海里先試着回憶一下這個幾個區域的概念,是獨享的還是共享的?每個區域都存儲了什么?哪些區域會被垃圾回收?哪些區域會拋出OOM?哪些區域會拋出SOF?如何避免
什么是JVM運行時數據區域?
Java虛擬機定義了在程序執行期間使用的各種運行時數據區域。其中一些數據區域是在Java虛擬機啟動時創建的,僅在Java虛擬機退出時才被銷毀。其他數據區域是每個線程的。創建線程時創建每個線程的數據區域,並在線程退出時銷毀每個數據區域。
堆內存
堆內存中存儲的是所有類實例和數組的內存,在虛擬機啟動時創建,虛擬機結束時銷毀,歸還給操作系統,堆內存中對象的銷毀都JVM自行管理(垃圾收集器),當程序創建對象的越來越多時並且這些對象都無法被回收時,這個區域會拋出OOM異常,並且堆內存是所有線程共享的,所以當多個線程操作堆內存的數據時會有並發問題,要加鎖。
棧內存
棧分為虛擬機棧和本地方法棧,首先棧是線程安全的,棧內存隨線程創建而創建,隨線程銷毀而銷毀,棧內存是不需要垃圾回收器進行回收的。線程棧的大小可以是在虛擬機啟動時指定固定大小,也可以是自行計算動態擴容的。當指定大小時,線程棧的內存隨着使用而不足時JVM拋出StackOverFlowError,當不指定大小時,線程棧動態擴容時如果沒有足夠的內存不足,JVM將會拋出OOM錯誤。
虛擬機棧描述的是Java方法執行的內存模型,每個方法在執行時會創建一個棧幀用於存儲放法局部變量表,操作數棧,動態鏈接,出口信息,如下圖,整個棧幀是先入后出。
局部變量表存放了編譯器可知的各種基本數據類型,對象引用(不包含成員變量)每個局部變量表占用32位(4個字節),所以long和double會占用兩個局部變量表,其它類型占用一個,哪怕byte雖然只有8位,也占用一個局部變量表,局部變量表所需的內存在編譯期就已經確定了也就是進入這個方法時就已經確定了,運行期間不會更改.
操作數棧則存儲方法內一些進行了運算操作后的結果.
動態鏈接,在方法內調用接口,通過字面量鏈接到具體的實現類,實現Java的動態特性.
出口地址(返回地址),return或者發生Exception等。
本地方法棧虛擬機棧相似,都是線程私有的,安全的,區別就是虛擬機為虛擬機棧執行Java服務(字節碼服務),而本地方法棧為虛擬機使用到的Native方法服務,本地方法棧中使用的語言,使用方式,數據結構沒有強制要求。
程序計數器
Java程序是多線程執行的,當一個線程執行字節碼時,突然CPU切換到另一個線程,那么上一個線程執行的上下文信息怎么保存呢?等到下次再切換到這個線程,從哪里開始執行呢?這些信息都需要在線程切換時記錄,這就是程序計數器的職責,是每個線程私有的,線程安全的,因線程創建而創建,因線程銷毀而銷毀,程序計數器其實就是一小塊內存。
程序計數器指向當前線程所執行的字節碼所在的行號,記錄着當前程序運行到哪了字節碼解釋器的工作就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。分支,循環,跳轉,異常處理,線程回復等都需要依賴這個計數器來完成
如果一個線程執行一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是一個本地方法,這個計數器的值則為undefine,此內存區域是唯一一個在Java的虛擬機規范中沒有規定任何OutOfMemoryError異常情況的區域
元數據區
默認情況下,類元數據只受可用的本地內存限制。新參數(MaxMetaspaceSize)用於限制本地內存分配給類元數據的大小。如果沒有指定這個參數,元空間會在運行時根據需要動態調整,
這個區域也是會發生GC的,垃圾回收將在元數據使用達到“MaxMetaspaceSize”參數的設定值時進行,適時地監控和調整元空間對於減小垃圾回收頻率和減少延時是很有必要的。如果的元空間持續的發生GC說明可能存在類、類加載器導致的內存泄漏或是大小設置不合適,如果這個空間使用達到了MaxMetaspaceSize,但GC無法回收(所有的類信息都是有用的,所以無法回收),也會發生OOM錯誤。
String常量池已經從方法區(jdk8以前的叫法)中的運行時常量池分離到堆中了,不在元數據中。
Metaspace由兩部分組成:Klass MetaSpace 和 NoKlass MetaSpace,Klass代表的是
class文件在jvm中運行時的數據結構,NoKlass專門用來存儲Klass相關的其它數據,比如Method和ConstantPool。
回答剛開始的問題
用一段代碼分析JVM內存的存儲
new Thread(new Runnable() {
@Override
public void run() {
test();
}
public void test(){
Object obj = new Object();
}
}).start();
上面這段代碼很簡單,啟動了一個線程,線程的run方法中調用了test方法,test方法中創建一個Objet對象,一起來看一下這段代碼涉及的JVM內存哪些區域,分別存儲了什么。
首先創建了一個線程,那么這個線程對應的私有的虛擬機棧內存肯定被分配,這個線程的代碼執行中對應的程序計數器內存肯定被分配,因為沒有涉及到本地方法,所有本地棧內存不會分配,而且虛擬機棧內存是在編譯器就確定的。
Test方法執行時,創建一個Object對象,我們知道obj是一個引用(reference)類型,所以obj保存在Java棧的本地變量表中,而在Java堆中會保存該引用的實例化對象,Java堆中還必須包含能查找到此對象類型數據的地址信息(如對象類型、父類、實現的接口,方法等)這些類型數據則保存在元數據區域中。一般對象引用到對象實例和對象類型指向有兩種方法,一種是句柄池方式,一種是直接指針方式。這兩種對象的訪問方式各有優勢,使用句柄訪問方式的最大好處就是reference中存放的是穩定的句柄地址,在對象的移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要修改。使用直接指針訪問方式的最大好處是速度快,它節省了一次指針定位的時間開銷。目前Java默認使用的HotSpot虛擬機采用的便是是第二種方式進行對象訪問的,下面用兩張圖來表述一下這兩種方式。
這張圖是句柄池方式
這張圖是直接指針方式
關於基本數據類型和引用類型的分配
基本數據類型包括 int short long bolean等,引用類型就是我們常見的對象,那么這兩種數據類型內存中是怎么分配的呢?這個得區別看待,我們根據下面代碼來分析
class Dog {
private int age;
}
class Test{
public void test(){
Dog dog = new Dog();
dog.age = 2;
int age = 1;
Integer age = new Integer(3);
}
}
在Test類中的test方法中,我們創建了一個Dog對象,這個對象實例是分配在堆上的,dog這個引用是在棧上的,dog中的age在哪里呢?因為Dog對象實例是在堆上的,所有他的成員變量也是在堆上的。 int age這個變量是棧上的,因為它是局部變量,並且是基本數據類型,Integer age實例是在堆上的,引用是在棧上的,根據這個例子,可以總結下面兩條基本黃金法則
- 引用類型總是被分配到“堆”上。
- 值類型總是分配到它聲明的地方:
a. 作為引用類型的成員變量分配到“堆”上
b. 作為方法的局部變量時分配到“棧”上
總結
本文詳細介紹了JVM內存區域的各個情況,也就是JVM內存模型,也解答了一些常見的面試題和內存分配相關的一些問題,希望能夠幫助到讀者更好的了解到JVM,可能會有人有些疑問,為什么不說堆內存的分代(年輕代,年老代)問題呢?我認為這個屬於JVM垃圾回收的方位,分代思想只是解決垃圾收回問題的一種方法,同理,Java8中G1的region也是一樣,都是為了解決垃圾回收效率和性能問題,會放在JVM垃圾回收一文來說。
