天天看點

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”字尾的位元組碼檔案裡,存放的就是對你寫出來的代碼編譯好的位元組碼了。

這個位元組碼才是電腦可以了解的一種語言,而不是我們寫出來的那一堆代碼。

這個位元組碼看起來大概是下面這樣的:

JVM的區域劃分以及工作原理

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

比如這裡的“0: aload_0”這樣的東西,就是“位元組碼指令”,他對應了一條一條的機器指令,計算機隻有讀到這種機器碼指令,才知道具體應該要幹什麼。

比如說位元組碼指令可能會讓計算機從記憶體裡讀取某個資料,或者把某個資料寫入到記憶體裡去。

總之,各種各樣的指令,就會訓示計算機去幹各種各樣的事情。

是以現在首先明白一點:我們寫好的Java代碼是會被翻譯成位元組碼的,對應各種位元組碼指令。那麼Java代碼通過JVM跑起來的第一件事情就明确了。

接下來,在執行位元組碼指令時,JVM裡的程式計數器就是用來記錄每個線程目前執行的位元組碼指令的位置的,記錄目前線程目前執行到了哪一條位元組碼指令。

因為會有多個線程來并發的執行各種不同的代碼,是以每個線程都有自己的一個程式計數器,專門記錄目前這個線程目前執行到了哪一條位元組碼指令了

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

JVM的區域劃分以及工作原理

Java虛拟機棧

Java代碼在執行的時候,一定是線程來執行某個方法中的代碼,哪怕上面那個最基礎的HelloWorld代碼,也會有一個main線程來執行main方法裡的代碼。

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

public void sayHello() {

String name = “hello”;

}

是以JVM必須有一塊區域來儲存每個方法内的局部變量等資料,這個區域就是Java虛拟機棧

每個線程都會去執行各種方法的代碼,方法内還會嵌套調用其他的方法,是以每個線程都有自己的Java虛拟機棧。

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

棧幀裡有這個方法的局部變量表 、操作數棧、動态連結、方法出口等東西,但是這裡别的不太好了解,先了解一個局部變量就可以。

舉例,比如一個線程調用了上面寫的 “sayHello” 方法,那麼就會為“sayHello”方法建立一個棧幀,壓入線程自己的Java虛拟機棧裡面去。

在棧幀的局部變量表裡就會有“name”這個局部變量。

下圖展示了這個過程。

JVM的區域劃分以及工作原理

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

JVM的區域劃分以及工作原理
這時會給“greeting”方法又建立一個棧幀,壓入線程的Java虛拟機棧裡,因為開始執行“greeting”方法了。
           

而且“greeting”方法的棧幀的局部變量表裡會有一個“greet”變量,這是“greeting”方法的局部變量。

JVM的區域劃分以及工作原理

接着如果“greeting”方法執行完畢了,就會把“greeting”方法對應的棧幀從Java虛拟機棧裡給出棧。

然後接下來如果“sayHello”方法也執行完畢了,就會把“sayHello”方法也從Java虛拟機棧裡出棧。

這就是JVM中的 “ Java虛拟機棧 ” 這個元件的作用:調用執行任何方法的時候,都會給方法建立棧幀,然後入棧。

而在棧幀裡存放了這個方法對應的局部變量之類的資料,包括這個方法執行的其他相關的資訊,方法執行完畢之後就出棧。

Java堆記憶體

JVM中有另外一個非常關鍵的區域,就是Java堆,這裡就是存放我們在代碼中建立的各種對象的,比如說下面的代碼:

public void sayHello(String name) {

Student student = new Student(name);

student.study();

}

上面的 “new Student(name)” 這個代碼,就是建立了一個Student類型的對象執行個體,這個對象執行個體裡面會包含一些資料。

比如說這個Student的“name”就是屬于這個對象執行個體的資料,類似Student這樣的對象,就會存放在Java堆記憶體裡。

Java堆記憶體區域裡會放入類似Student的對象,然後方法的棧幀的局部變量表裡,會存放這個引用類型的“student”局部變量,即存放Student對象的位址。

相當于你可以認為局部變量表裡的“student”指向了Java堆裡的Student對象。

看下圖會更加清晰一些。

JVM的區域劃分以及工作原理

若代碼中new了兩個對象,比如:

{

Student a= new Student(“name1”)

Student b= new Student(“name2”)

}

那麼在堆記憶體中,會存放兩份獨立的Student對象,每個對象儲存不同的具體資訊,比如a的堆記憶體中對象儲存的name=name1,b的堆記憶體中對象儲存的name=name2

方法區 / Metaspace

這個方法區是在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堆外配置設定記憶體空間來存儲一些對象