深扒JVM,對它進行“開膛破肚”式解析!

1. 打怪升級,你繞不開JVM

JVM,對Java程序員進階而言,是一個絕對繞不開,也不能繞開的話題。

在你打怪升級、進階蛻變的路上,勢必會遇到項目上線中各種OOM、GC等問題,此時JVM的功底就至關重要了。

這篇文章,我們將從自己寫的代碼運行角度出發,將JVM“開膛破肚”。看看我們寫的代碼,在JVM的各區域都幹了些啥?

多說一句,對於Java工程師的面試,JVM也是必問的一環,因此無論從面試還是實際工作,你都很有必要夯實自己的JVM功底。

扯得有點遠,趕緊拉回來,馬上進入正題!

2. JVM 區域劃分

jvm的區域,大致有以下幾塊:

  • 程序計數器

  • 虛擬機棧

  • 方法區

  • 本地方法棧

接下來我們將JVM當成一個生物體,上述部分就是其不同器官。我們將從自己寫的Java代碼如何通過JVM來運行這一角度,來分析JVM裏這些“器官”是如何支撐我們的Java代碼跑起來的。

3. 程序計數器

假設我們有如下的一個類,就是最最基本的一個HelloWorld而已:

public class HelloWorld {

public static void main(String[] args) {

System.out.println("Hello World");

}

}

上面那段代碼首先會存在於 “.java” 後綴的文件裏,這個文件就是java源代碼文件。

但是這個文件是面向我們程序員的,計算機是看不懂這段代碼的。

所以此時就得通過編譯器,把“.java”後綴的源代碼文件編譯爲“.class”後綴的字節碼文件。

這個“.class”後綴的字節碼文件裏,存放的就是對你寫出來的代碼編譯好的字節碼了。

字節碼纔是計算器可以理解的一種語言,而不是我們寫出來的那一堆代碼。這個字節碼看起來大概是下面這樣的:


深扒JVM,對它進行“開膛破肚”式解析!



:這段字節碼並不是完全對照着HelloWorld那個類來寫的,就是給一段示例,讓大家知道“.java”翻譯成的“.class”是大概什麼樣子的。

大概給各位解釋一下,圖中比如“0: aload_0”這樣的就是“字節碼指令”,他對應了一條條機器指令,計算機只有讀到這種機器碼指令,才知道具體應該要幹什麼。

比如字節碼指令可能會讓計算機從內存裏讀取某個數據,或者把某個數據寫入到內存裏去。各種各樣的指令,會指示計算機去幹各種各樣的事情。

所以到這裏,大家首先明白的第一點:Java代碼是會被翻譯成字節碼的,不同字節碼指令指揮計算機幹不同的事情。

那麼在執行字節碼指令的時候,JVM裏的程序計數器作用是啥呢?

答案是:用來記錄每個線程當前執行的字節碼指令的位置,即記錄當前線程目前執行到了哪一條字節碼指令。

在實際中,會有多個線程併發執行各種不同的代碼,所以每個線程都有自己的程序計數器,專門記錄當前線程目前執行到了哪一條字節碼指令。

下圖更加清晰的展示出了他們之間的關係。


深扒JVM,對它進行“開膛破肚”式解析!



4. Java虛擬機棧

好,我們接着來看。大家都清楚,Java代碼執行時,一定是線程來執行某個方法中的代碼。就算是最基礎的 HelloWorld ,也會有一個main線程來執行main方法裏的代碼。

在方法裏,經常會定義一些方法內的局部變量,比如下面這樣,在方法裏定義了一個局部變量“name”。

public void sayHello() {

String name = "hello";

}

所以咱們JVM的這個“器官”就要出場了,JVM必須有一塊區域是來保存每個方法內的局部變量等等數據的,這個區域就是Java虛擬機棧

爲什麼需要這個區域?因爲每個線程都會去執行各種方法的代碼,方法內還會嵌套調用其他的方法,所以每個線程都要有自己的Java虛擬機棧

如果線程執行了一個方法,那麼就會爲這個方法調用創建對應的一個棧幀

棧幀裏就有這個方法的局部變量表 、操作數棧、動態鏈接、方法出口等東西。這裏別的東西不太好理解,後面我們再通過其他文章詳細闡述,這裏先理解一個局部變量就可以。

回到上面的例子,比如一個線程調用了上面寫的“sayHello”方法,那麼就會爲“sayHello”方法創建一個棧幀,壓入線程自己的Java虛擬機棧裏面去。

在棧幀的局部變量表裏就會有“name”這個局部變量,下圖展示了這個過程。


深扒JVM,對它進行“開膛破肚”式解析!



接着如果“sayHello”方法調用了另外一個“greeting”方法 ,比如下面那樣的代碼:


深扒JVM,對它進行“開膛破肚”式解析!



這時會給“greeting”方法又創建一個棧幀,壓入線程的Java虛擬機棧。

想想爲啥會這樣?因爲sayHello方法裏開始執行greeting方法了,而且greeting方法的棧幀的局部變量表裏有一個“greet”變量,它是greeting方法的局部變量。

下圖展示了這個過程:


深扒JVM,對它進行“開膛破肚”式解析!



接着如果“greeting”方法執行完畢了,就會把“greeting”方法對應的棧幀從Java虛擬機棧裏給出棧,然後如果“sayHello”方法也執行完畢了,就會把“sayHello”方法也從Java虛擬機棧裏出棧。

這就是JVM中的Java虛擬機棧這個組件的作用。

這塊大家需要記住的是:調用執行任何方法時,都會給方法創建棧幀,然後入棧。

在棧幀裏存放了這個方法對應的局部變量之類的數據,包括這個方法執行的其他相關的信息,方法執行完畢之後就出棧。

5. 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對象。

下圖展示了這個過程:

深扒JVM,對它進行“開膛破肚”式解析!


6. 方法區 / Metaspace

這個方法區是在JDK 1.8以前的版本里,代表JVM中的一塊區域,主要是放類似Student類自己的信息的,平時用到的各種類的信息,都是放在這個區域裏的,還會有一些類似常量池的東西放在這個區域裏。

但是在JDK 1.8以後,這塊區域的名字改了,叫做“Metaspace”,可以認爲是“元數據空間”這樣的意思,當然主要還是存放我們自己寫的各種類相關的信息。

7. 本地方法棧

在JDK很多底層API裏,比如IO相關的,NIO相關的,網絡Socket相關的,如果大家去看他內部的源碼,會發現很多地方都不是Java代碼。

很多地方都會去走native方法,去調用本地操作系統裏面的一些方法,可能調用的都是c語言寫的方法,或者一些底層類庫,比如下面這樣的:

public native int hashCode();

在調用這種native方法的時候,就會有線程對應的本地方法棧,這個裏面也是跟Java虛擬機棧類似的,也是存放各種native方法的局部變量表之類的信息。

關於這塊,這裏就不展開講了,後續有機會我們再寫文章專門闡述。

8. 堆外內存

還有一個區域,不屬於JVM,通過NIO中的allocateDirect這種API,可以在Java堆外分配內存空間,然後通過Java虛擬機裏的 DirectByteBuffer 來引用和操作堆外內存空間。

很多技術都會用這種方式,因爲有一些場景下,堆外內存分配可以提升性能。

9. 全文總結

最後做一點總結:

  • Java代碼通過JVM運行時,首先一定會一行一行執行編譯好的字節碼指令

  • 然後在執行的過程中,對於方法的調用,會通過Java虛擬機棧來爲每個方法創建棧幀,入棧和出棧,而且棧幀裏有方法的局部變量。

  • 對於對象的創建,會分配到Java堆內存裏去

  • 對於類信息的存儲,會放在方法區 / Metaspace這樣的區域裏

  • 另外有兩塊特殊的區域:

  • 本地方法棧:執行native方法時候用的棧,跟Java虛擬機棧是類似的

  • 堆外內存:可以在Java堆外分配內存空間來存儲一些對象。

這裏分享一份我自己整理的【JVM體系結構與GC調優】PPT,希望對大家學習JVM有所幫助。加入羣(Java填坑之路)789337293 即可免費獲取到!

深扒JVM,對它進行“開膛破肚”式解析!




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章