做者:原子彈大俠,高級技術專家java
1. 打怪升級,你繞不開JVM 程序員
2. JVM 區域劃分 面試
3. 程序計數器 性能優化
4. Java虛擬機棧 bash
5. Java堆內存 網絡
6. 方法區 / Metaspace 架構
7. 本地方法棧 併發
8. 堆外內存 jvm
9. 全文總結 高併發
JVM,對Java程序員進階而言,是一個絕對繞不開,也不能繞開的話題。
在你打怪升級、進階蛻變的路上,勢必會遇到項目上線中各類OOM、GC等問題,此時JVM的功底就相當重要了。
這篇文章,咱們將從本身寫的代碼運行角度出發,將JVM「開膛破肚」。看看咱們寫的代碼,在JVM的各區域都幹了些啥?
多說一句,對於Java工程師的面試,JVM也是必問的一環,所以不管從面試仍是實際工做,你都頗有必要夯實本身的JVM功底。
扯得有點遠,趕忙拉回來,立刻進入正題!
jvm的區域,大體有如下幾塊:
程序計數器
虛擬機棧
堆
方法區
本地方法棧
接下來咱們將JVM當成一個生物體,上述部分就是其不一樣器官。咱們將從本身寫的Java代碼如何經過JVM來運行這一角度,來分析JVM裏這些「器官」是如何支撐咱們的Java代碼跑起來的。
假設咱們有以下的一個類,就是最最基本的一個HelloWorld而已:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
複製代碼
上面那段代碼首先會存在於 「.java」 後綴的文件裏,這個文件就是java源代碼文件。
可是這個文件是面向咱們程序員的,計算機是看不懂這段代碼的。
因此此時就得經過編譯器,把「.java」後綴的源代碼文件編譯爲「.class」後綴的字節碼文件。
這個「.class」後綴的字節碼文件裏,存放的就是對你寫出來的代碼編譯好的字節碼了。
字節碼纔是計算器能夠理解的一種語言,而不是咱們寫出來的那一堆代碼。這個字節碼看起來大概是下面這樣的:
注:這段字節碼並非徹底對照着HelloWorld那個類來寫的,就是給一段示例,讓你們知道「.java」翻譯成的「.class」是大概什麼樣子的。
大概給各位解釋一下,圖中好比「0: aload_0」這樣的就是「字節碼指令」,他對應了一條條機器指令,計算機只有讀到這種機器碼指令,才知道具體應該要幹什麼。
好比字節碼指令可能會讓計算機從內存裏讀取某個數據,或者把某個數據寫入到內存裏去。各類各樣的指令,會指示計算機去幹各類各樣的事情。
因此到這裏,你們首先明白的第一點:Java代碼是會被翻譯成字節碼的,不一樣字節碼指令指揮計算機幹不一樣的事情。
那麼在執行字節碼指令的時候,JVM裏的程序計數器做用是啥呢?
答案是:用來記錄每一個線程當前執行的字節碼指令的位置,即記錄當前線程目前執行到了哪一條字節碼指令。
在實際中,會有多個線程併發執行各類不一樣的代碼,因此每一個線程都有本身的程序計數器,專門記錄當前線程目前執行到了哪一條字節碼指令。
下圖更加清晰的展現出了他們之間的關係。
好,咱們接着來看。你們都清楚,Java代碼執行時,必定是線程來執行某個方法中的代碼。就算是最基礎的 HelloWorld ,也會有一個main線程來執行main方法裏的代碼。
在方法裏,常常會定義一些方法內的局部變量,好比下面這樣,在方法裏定義了一個局部變量「name」。
public void sayHello() {
String name = "hello";
}複製代碼
因此我們JVM的這個「器官」就要出場了,JVM必須有一塊區域是來保存每一個方法內的局部變量等等數據的,這個區域就是Java虛擬機棧
爲何須要這個區域?由於每一個線程都會去執行各類方法的代碼,方法內還會嵌套調用其餘的方法,因此每一個線程都要有本身的Java虛擬機棧。
若是線程執行了一個方法,那麼就會爲這個方法調用建立對應的一個棧幀
棧幀裏就有這個方法的局部變量表 、操做數棧、動態連接、方法出口等東西。這裏別的東西不太好理解,後面咱們再經過其餘文章詳細闡述,這裏先理解一個局部變量就能夠。
回到上面的例子,好比一個線程調用了上面寫的「sayHello」方法,那麼就會爲「sayHello」方法建立一個棧幀,壓入線程本身的Java虛擬機棧裏面去。
在棧幀的局部變量表裏就會有「name」這個局部變量,下圖展現了這個過程。
接着若是「sayHello」方法調用了另一個「greeting」方法 ,好比下面那樣的代碼:
這時會給「greeting」方法又建立一個棧幀,壓入線程的Java虛擬機棧。
想一想爲啥會這樣?由於sayHello方法裏開始執行greeting方法了,並且greeting方法的棧幀的局部變量表裏有一個「greet」變量,它是greeting方法的局部變量。
下圖展現了這個過程:
接着若是「greeting」方法執行完畢了,就會把「greeting」方法對應的棧幀從Java虛擬機棧裏給出棧,而後若是「sayHello」方法也執行完畢了,就會把「sayHello」方法也從Java虛擬機棧裏出棧。
這就是JVM中的Java虛擬機棧這個組件的做用。
這塊你們須要記住的是:調用執行任何方法時,都會給方法建立棧幀,而後入棧。
在棧幀裏存放了這個方法對應的局部變量之類的數據,包括這個方法執行的其餘相關的信息,方法執行完畢以後就出棧。
JVM中有另一個很是關鍵的區域,就是Java堆,用來存放咱們在代碼中建立的各類對象的,好比下面的代碼:
public void teach(String name) {
Student student = new Student(name);
student.study();
}複製代碼
上面的 「new Student(name)」 就建立了一個Student類型的對象實例,這個對象實例裏面會包含一些數據。相似Student這樣的對象,就會存放在Java堆內存裏。
而後方法的棧幀的局部變量表裏,這個引用類型的「student」局部變量就會存放Student對象的地址。你能夠認爲局部變量表裏的「student」指向了Java堆裏的Student對象。
下圖展現了這個過程:
這個方法區是在JDK 1.8之前的版本里,表明JVM中的一塊區域,主要是放相似Student類本身的信息的,平時用到的各類類的信息,都是放在這個區域裏的,還會有一些相似常量池的東西放在這個區域裏。
可是在JDK 1.8之後,這塊區域的名字改了,叫作「Metaspace」,能夠認爲是「元數據空間」這樣的意思,固然主要仍是存放咱們本身寫的各類類相關的信息。
在JDK不少底層API裏,好比IO相關的,NIO相關的,網絡Socket相關的,若是你們去看他內部的源碼,會發現不少地方都不是Java代碼。
不少地方都會去走native方法,去調用本地操做系統裏面的一些方法,可能調用的都是c語言寫的方法,或者一些底層類庫,好比下面這樣的:
public native int hashCode();
在調用這種native方法的時候,就會有線程對應的本地方法棧,這個裏面也是跟Java虛擬機棧相似的,也是存放各類native方法的局部變量表之類的信息。
關於這塊,這裏就不展開講了,後續有機會咱們再寫文章專門闡述。
還有一個區域,不屬於JVM,經過NIO中的allocateDirect這種API,能夠在Java堆外分配內存空間,而後經過Java虛擬機裏的 DirectByteBuffer 來引用和操做堆外內存空間。
不少技術都會用這種方式,由於有一些場景下,堆外內存分配能夠提高性能。
最後作一點總結:
Java代碼經過JVM運行時,首先必定會一行一行執行編譯好的字節碼指令
而後在執行的過程當中,對於方法的調用,會經過Java虛擬機棧來爲每一個方法建立棧幀,入棧和出棧,並且棧幀裏有方法的局部變量。
對於對象的建立,會分配到Java堆內存裏去
對於類信息的存儲,會放在方法區 / Metaspace這樣的區域裏
另外有兩塊特殊的區域:
本地方法棧:執行native方法時候用的棧,跟Java虛擬機棧是相似的
堆外內存:能夠在Java堆外分配內存空間來存儲一些對象。
做者簡介:
原子彈大俠,阿里巴巴高級技術專家
經歷過每日百億流量的互聯網系統架構,尤爲對上億用戶場景下的高併發系統架構設計以及性能優化相關領域有深刻的研究。
END
長按下圖二維碼,即刻關注【狸貓技術窩】 阿里、京東、美團、字節跳動 頂尖技術專家坐鎮 爲IT人打造一個 「有溫度」 的技術窩!