點擊藍色「程序員的時光 」關注我 ,標註「星標」,及時閱讀最新技術文章
寫在前面:
java
小夥伴兒們,你們好!今天來學習Java虛擬機相關內容,做爲面試必問的知識點,來深刻了解一波!程序員
思惟導圖:web

1,JVM是什麼?
1.1,概述
JVM是Java Virtual Machine
(Java虛擬機)的縮寫,JVM是一種用於計算設備的規範。引入Java虛擬機後,Java語言在不一樣平臺上運行時不須要從新編譯。Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就能夠在多種平臺上不加修改地運行。任何平臺只要裝有針對於該平臺的Java虛擬機,字節碼文件(.class)就能夠在該平臺上運行。這就是「一次編譯,屢次運行」。面試
所謂java能實現跨平臺,是由在不一樣平臺上運行不一樣的虛擬機決定的,所以java文件的執行不直接在操做系統上執行,而是經過jvm虛擬機執行,咱們能夠從這張圖看到,JVM並無直接與硬件打交道,而是與操做系統交互用以執行java程序。算法

1.2,JVM運行流程

這個是JVM的組成圖,由四個部分組成:編程
-
類加載器數組
類加載器的做用是加載類文件到內存。好比咱們執行一個.java程序的文件,首先使用javac命令進行編譯,生成.class文件。而後咱們須要用類加載器將字節碼文件加載到內存中去,經過jvm後續的模塊進行加載執行程序。至因而否可以執行,則由執行引擎負責。安全
-
執行引擎服務器
執行引擎也叫解釋器,負責解釋命令,提交操做系統執行。微信
-
本地接口
它的做用是融合不一樣的編程語言爲Java所用,目前該方法使用的是愈來愈少了,除非是與硬件有關的應用,好比經過Java程序驅動打印機,或者Java系統管理生產設備。
-
運行時數據區
運行數據區是整個JVM的重點。咱們全部寫的程序都被加載到這裏,以後纔開始運行,Java生態系統如此的繁榮,得益於該區域的優良自治。整個JVM框架由加載器加載文件,而後執行器在內存中處理數據,須要與異構系統交互是能夠經過本地接口進行!
2,JVM的內存區域
內存區域也就是上面的運行時數據區。對於從事C或者C++的程序員來講,必須對每一個對象的整個生命週期負責。可是對java程序員來講,在jvm的自動內存管理機制下,不須要爲每個對象去寫delete
或者free
代碼,不容易出現內存泄漏或內存溢出的問題。但正由於java程序員將內存管理權力交給了內存管理機制,因此一旦出現內存泄漏或者內存溢出的問題,在對jvm內存結構不清楚的狀況下,排查錯誤將會成爲一項很是複雜且困難的工做。
運行時數據區

2.1,程序計算器
程序計數器是一小塊的內存區域,能夠看作當前線程執行字節碼的行號指示器,在虛擬機的概念模型裏,**字節碼解釋工做就是經過改變這個程序計數器的值來選取下一個要執行的字節碼指令。**好比分支控制,循環控制,跳轉,異常等操做,線程恢復等功能都是經過這個計數器來完成。
因爲jvm的多線程是經過線程的輪流切換並分配處理器執行時間來實現的。所以,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。所以,爲了線程切換後能回到正確的執行位置,每條線程都須要本身獨有的程序計數器,多條線程計數器之間互不影響,獨立存儲。咱們稱這類內存區域爲線程私有的內存區域。
若是線程執行的是Java方法時,程序計數器記錄的是 Java 虛擬機正在執行的字節碼指令的地址,而在線程執行 Native 方法時,程序計數器爲空,由於此時 Java 虛擬機調用是和操做系統相關的接口,接口的實現不是 Java 語言,而是 C語言和 C++。
程序計數器是惟一一個在Java虛擬機中不會出現 OutOfMemoryError 的內存區域,它的生命週期隨着線程的建立而建立,隨着線程的結束而結束。
2.2,Java虛擬機棧
與程序計數器一致,Java虛擬機棧也是線程私有的,生命週期與線程相同。虛擬機棧描述的是Java方法的執行內存模型,每一個方法在執行的時候都會建立一個棧幀(用於存儲局部變量表、操做數棧、動態鏈棧、方法出口等信息)。每個方法從執行到結束的過程,就對應一個棧幀從入棧到出棧的過程。
Java內存能夠粗糙地分爲堆內存(Heap)和棧內存(Stack),固然Java內存區域的劃分實際上遠比這複雜,咱們如今所說的Java虛擬機棧就是這裏的棧內存,或者說是虛擬機棧中局部變量表部分。
局部變量表存放了編譯器可知的四類八種基本數據類型(boolean、byte、char、short、int、float、long、double),對象引用(reference類型,它不一樣於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)。
Java虛擬機會出現兩種異常情況:
若是線程在棧中申請的深度大於虛擬機所容許的深度,將出現StackOverFlowError
異常; 若是虛擬機棧能夠動態擴展,且擴展沒法申請到足夠的內存,就會拋出OutOfMemoryError
異常。
2.3,本地方法棧
本地方法棧與虛擬機棧的做用很是相似,區別是:虛擬機棧爲虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務,在 HotSpot 虛擬機中直接將虛擬機棧和本地方法棧合二爲一。
與Java虛擬機棧同樣,本地方法棧在執行的時候也會建立一個棧幀(用於存儲局部變量表、操做數棧、動態鏈棧、方法出口等信息)。也會拋出StackOverFlowError
異常和OutOfMemoryError
異常。
2.4,Java堆
Java堆是JVM所管理的內存中最大的一塊區域,Java堆是被全部線程所共享的一片區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例以及數組都在這裏分配內存。
Java堆是垃圾收集器管理的主要區域,所以也被稱做GC堆(Garbage Collected Heap)。從內存回收的角度看,因爲如今收集器基本都採用分代垃圾收集算法,因此Java堆還能夠細分爲:新生代和老年代。**進一步劃分的目的是更好地回收內存,或者更快地分配內存。**根據JVM的規範規定,Java堆能夠處於物理上不連續的內存空間,只要邏輯上是連續的便可。若是在堆中沒有完成內存分配,且堆也沒有可擴展的內存空間,則會拋出OutOfMemoryError
異常。
2.5,方法區
方法區與 Java 堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作 Non-Heap(非堆),目的應該是與 Java 堆區分開來。
Java虛擬機相對而言對方法區的限制很是寬鬆,除了和堆同樣不須要連續的空間和能夠選擇固定大小或者可擴展以外,還能夠選擇不實現垃圾回收。相對而言,垃圾回收在這個區域算比較少見了,但並不是數據進入方法區之後就能夠實現永久存活了,這個區域的回收目標主要是常量池的回收和對類型的卸載,通常來講,這個區域的回收成績是比較難以讓人滿意的。尤爲是類型的卸載,條件至關苛刻。根據Java虛擬機規範規定,當方法區沒法知足內存分配時,將拋出OutOfMemoryError
異常。
咱們在這裏舉一個簡單例子來看看,看看上述的哪些信息會存放上方法區中;
靜態變量和常量,在編譯期間就放在方法區中;
//靜態變量,在編譯期間存放在方法區
private static int num=10;
//常量,在編譯期間存放在方法區
private final String name="boy";
咱們先來看看new String時堆中的變化;
String s1="hello";
String s2=new String("hello");
String s3=new String("hello");
System.out.println(s1==s3); // false
System.out.println(s2==s3); // false
這個輸出的結果確定是false,採用new的時候會在堆內存開闢一塊空間存放hello對象,雖然s2和s3指向的內容相同,可是棧種存放的地址不一樣,因此是不相等的。

對於引用類型來講,"=="指的是地址值的比較。
雙引號直接寫的字符串是在常量池之中,而new的對象則不在池之中。
再來看看運行期間添加進常量池的;
String s2=new String("hello");
String s3=new String("hello");
//在運行過程當中添加進常量池中
System.out.println(s2.intern()==s3.intern());

若是常量池中存在當前字符串,那麼直接返回常量池中該對象的引用。
若是常量池中沒有此字符串, 會將此字符串引用保存到常量池中後, 再直接返回該字符串的引用!
2.6,運行時常量池
運行時常量池是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池信息(用於存放編譯期生成的各類字面量和符號引用)。既然運行時常量池時方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存時會拋出 OutOfMemoryError
異常。
2.7,直接內存
直接內存並不屬於Jvm運行時數據區的一部分,可是這部份內存區域被頻繁的調用,也可能發生OutOfMemoryError異常。顯然本機的直接內存不會受到Java堆分配內存的影響,可是既然是內存,確定要受到本機總內存大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,但常常忽略直接內存。使得各個區域的內存總和大於物理內存限制,從而致使動態擴展時出現OutOfMemoryError
異常。
3,Java對象的建立過程
下面這張圖就是Java對象建立的過程,總共來講分爲五部分;

3.1,類加載過程
虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,而且檢查這個符號引用表明的類是否已被加載過、解析和初始化過。若是沒有,那必須先執行相應的類加載過程。
3.2,分配內存
在類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在類加載完成後即可肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從 Java 堆中劃分出來。分配方式有 「指針碰撞」 和 「空閒列表」 兩種,選擇哪一種分配方式由 Java 堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
指針碰撞:
-
場景:Java堆中內存是絕對規整的; -
原理:全部用過的內存都放在一邊,空閒的內存放在另一邊,中間放一個指針做爲分界點的指示器,分配內存時只須要把那個指針向空閒空間那邊挪動一段與對象大小相等的距離就能夠了; -
GC收集器:Serial、ParNew等帶Compact過程的收集器。
空閒列表:
-
場景:Java堆中內存不是規整的; -
原理:虛擬機會維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄; -
GC收集器:CMS基於Mark-Sweep算法的收集器。
內存分配併發的問題
在建立對象的時候還須要考慮的一個問題就是在併發狀況下,線程是否安全的問題。由於建立對象在虛擬機中是很是頻繁的行爲,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的狀況。所以必需要保證線程安全,解決這個問題有兩種方案:
-
**CAS以及失敗重試(比較和交換機制):**對分配內存空間的操做進行同步處理——實際上虛擬機採用CAS配上失敗重試的方式保證更新操做的原子性。CAS操做須要輸入兩個數值,一箇舊值(操做前指望的值)和一個新值,在操做期間先比較舊值有沒有發送變化,若是沒有變化,才交換成新值,不然不進行交換。 -
**TLAB(分配緩衝):**把內存分配的動做按照線程劃分在不一樣的空間之中進行,即每一個線程在Java堆中預先分配一小塊私有內存,也就是本地線程分配緩衝。TLAB的目的是在爲新對象分配內存空間時,讓每一個Java應用線程能在使用本身專屬的分配指針來分配空間,減小同步開銷。
3.3,初始化零值
內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操做保證了對象的實例字段在 Java 代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
3.4,設置對象頭
初始化零值完成以後,虛擬機要對對象進行必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希嗎、對象的 GC 分代年齡等信息。這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不一樣,如是否啓用偏向鎖等,對象頭會有不一樣的設置方式。
3.5,執行Init方法
在上面工做都完成以後,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象建立纔剛開始,<init>
方法尚未執行,全部的字段都還爲零。因此通常來講,執行 new 指令以後會接着執行 <init>
方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底產生出來。
4,對象的訪問定位
創建對象就是爲了使用對象,咱們的Java程序經過棧上的 reference
數據來操做堆上的具體對象。對象的訪問方式由虛擬機實現而定,目前主流的訪問方式有使用句柄和直接指針兩種。
4.1,使用句柄
若是使用句柄的話,那麼Java堆中將會劃分出一塊內存來做爲句柄池,reference
中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。如圖所示:

4.2,直接指針
若是使用直接指針訪問,那麼 Java 堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而reference 中存儲的直接就是對象的地址。如圖所示:

這兩種對象訪問方式各有優點,**使用句柄來訪問的最大好處就是reference 中存儲的是穩定的句柄地址,**在對象被移動時只會改變句柄中的實例數據指針,而 reference 自己不須要修改。**使用直接指針訪問方式最大的好處就是速度更快,**它節省了一次指針定位的時間開銷。因爲對象的訪問在Java中很是頻繁,所以這類開銷聚沙成塔後也是一項很是樂觀的執行成本。
5,OutOfMemoryError(內存溢出)異常
在Java虛擬機規範的描述中,除了程序計算器以外,**虛擬機內存的其餘幾個運行時區域都有發生OutOfMemoryError
異常的可能。**如今咱們經過兩個實例來驗證異常發生的場景,也會初步介紹幾個與內存相關的最基本的虛擬機參數。
5.1,堆內存異常
咱們來演示一下堆內存的異常:
/**
* @author 公衆號:程序員的時光
* @create 2020-11-23 08:54
* @description
*/
public class HeapOOM {
public static void main(String[] args) {
//測試堆內存異常
List<HeapOOM> heapOOMList=new ArrayList<>();
//這裏只添加一個對象,不會發生異常
heapOOMList.add(new HeapOOM());
//添加進死循環,不斷地new對象,堆內存已經耗盡
while (true) {
heapOOMList.add(new HeapOOM());
}
}
}
在運行這個程序以前,咱們先要設置Java虛擬機的參數。因爲IDEA默認設置的堆內存很大,因此咱們須要單個配置;點擊Run >> Edit Configurations
,而後就開始配置,以下,初始化堆內存和最大堆內存都設置爲10m,看看上面的死循環可否在10m內存中完成;

咱們來看運行結果:

能夠看到堆內存發生異常,上面的死循環中咱們不斷地new對象,致使堆內存已經耗盡,沒法爲新生的對象分配內存,從而發生異常。
5.2,棧內存異常
再來看看棧內存異常:
/**
* @author 公衆號:程序員的時光
* @create 2020-11-23 09:14
* @description
*/
public class StackOOM {
public static void main(String[] args) {
test();
}
//咱們設置一個簡單的遞歸方法,沒有跳出遞歸條件的話,就會發生棧內存異常
public static void test(){
test();
}
}
咱們設置一個簡單的遞歸方法,可是不給出跳出遞歸條件,這樣的話就會異。
運行結果以下:

這種是線程請求的棧深度超過虛擬機所容許的最大深度,拋出StackOverflowError
異常,緣由就是使用不合理的遞歸形成的。
咱們再來看看第二種異常狀況:
/**
* @author 公衆號:程序員的時光
* @create 2020-11-23 10:05
* @description
*/
public class StackOOM1 {
//線程任務,每一個線程任務一直在執行
private void WinStop(){
while(true){
System.out.println(System.currentTimeMillis());
}
}
//不斷建立線程
public void StackByThread(){
while(true){
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
WinStop();
}
});
}
}
public static void main(String[] args) {
StackOOM1 stackOOM1=new StackOOM1();
stackOOM1.StackByThread();
}
}
上述代碼的理論上運行結果是:Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
,可是運行這段代碼可能會致使操做系統卡頓,運行須謹慎。
這種是虛擬機在擴展棧時沒法申請到足夠的內存空間,拋出OutOfMemoryError
異常,緣由是不斷建立活躍的線程形成的。
微信搜索公衆號《程序員的時光》
好了,今天就先分享到這裏了,下期繼續給你們帶來JVM垃圾回收面試內容!
更多幹貨、優質文章,歡迎關注個人原創技術公衆號~
參考文獻:
[1].深刻理解Java虛擬機(第2版) .做者 周志明
本文分享自微信公衆號 - 程序員的時光(gh_9211ec727426)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。