JVM加載回顧
上次我解釋了JVM是如何加載class的,以及有哪些加載器。請看下圖我們在進行一次簡單的回憶!
一個類從加載到使用,一般會經歷下面的這個過程 看圖:
加載->驗證->準備->初始化->使用->卸載銷燬
我們寫的字節碼如何運行的
我們寫的代碼被編譯成字節碼,實際上還是java虛擬機調用執行引擎來執行的。這裏我就不詳細說了,可以看我以前寫的文章。
JVM到底是怎麼劃分內存區域的
1,首先我們寫的java代碼是不是要被加載到JVM進行運行,我給大家畫了一個整體的架構圖。
通過圖解我們知道java代碼被編譯成了class,然後由我們的加載器加載到jvm,然後由java執行器執行編譯好的字節碼,在程序運行的時期就會用到數據和相關的信息,這個時候就需要java運行時區域,這個區域也是我們通常說的jvm內存,內存的管理也是對這個區域進行管理,比如分配內存和回收內存。
java運行時區域圖解:
這幾個區域我不想像別人那樣寫一大堆文字,然後你們看了一點都不知道是什麼東西。這個很難理解,接下來我將結合一些代碼來講解。
我們思考一個問題,我們假設我們的代碼要存儲哪些東西在運行時區域裏面呢?我大致歸納了一下,看是否和你們想的一樣哦。
首先我們寫的所有的class是不是都要有一個地方存儲啊?我們代碼在運行的時候是不是要執行我們一個個的方法?在運行某個方法的時候,如果要創建對象是不是要有一個存放對象的地方啊?你有這些疑問的時候設計的人同樣是這樣的!
這個就是JVM爲什麼要劃分不同的區域來存放了,它是爲了我們寫好代碼在運行的過程中根據需要來使用。
存放我們類的方法區
JDK1.8以前叫做方法區,我現在用的是1.8的JDK,JDK8把這塊區域叫做“元數據空間”。這個就專門存放我們寫的各種類的信息。
結合我們代碼來看看
package cn.changhong.conf.client.utils;
/**
* @author 獨秀天狼
*/
public class Luancher {
public static void main(String[] args) {
Message message=new Message();
SendMsg sender=new DingdingSendMsg();
sender.send(message);
}
}
根據我原來說的只要用到了class就會加載到JVM看圖理解比較好!
存放執行指令的程序計數器
/**
* @author 獨秀天狼
*/
public class Luancher {
public static void main(String[] args) {
Message message=new Message();
SendMsg sender=new DingdingSendMsg();
sender.send(message);
}
}
當前我們這個代碼會被編譯成字節碼,字節碼就是計算器可以理解的語言,我們字節碼的執行指令大概是如下,給大家看一下:
想看到代碼的一些指令,可以用javap 來看我們的class轉換到指令是什麼樣的。
public class cn.changhong.conf.client.utils.Luancher {
public cn.changhong.conf.client.utils.Luancher();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class cn/changhong/conf/client/utils/Message
3: dup
4: invokespecial #3 // Method cn/changhong/conf/client/utils/Message."<init>":()V
7: astore_1
8: new #4 // class cn/changhong/conf/client/utils/DingdingSendMsg
11: dup
12: invokespecial #5 // Method cn/changhong/conf/client/utils/DingdingSendMsg."<init>":()V
15: astore_2
16: aload_2
17: aload_1
18: invokeinterface #6, 2 // InterfaceMethod cn/changhong/conf/client/utils/SendMsg.send:(Lcn/changhong/conf/client/utils/Message;)V
23: return
}
E:\apollo_conf\target\classes\cn\changhong\conf\client\utils>
比如“0: aload_0”就是字節碼指令,計算機讀到這條指令就知道要做什麼了。
所以在這裏大家要明白:我們寫的代碼最終會被轉換成字節碼指令。
在字節碼執行的時候就需要一個內存區域來記錄我們執行指令的位置。如圖
我們的程序基本上都是多線程執行指令的,每個線程執行指令都會有一個獨立的計數器,專門記錄當前的線程執行指令到那個位置了。如圖:
通過這樣的圖解是不是比較清晰了。
Java虛擬機棧
當java代碼在執行的時候,一定是線程執行某個方法,比如下面我寫出的這些代碼,這個會有一個main線程去執行main方法的代碼,當線程main執行main方法的時候,就會有對於的程序計數器來記錄當前main線程執行的指令位置。
/**
* @author 獨秀天狼
*/
public class Luancher {
public static void main(String[] args) {
Message message=new Message();
SendMsg sender=new DingdingSendMsg();
sender.send(message);
}
}
方法裏面我們會有一些局部的成員變量,就當前這個代碼就有message,sender因此我們JVM就必須要有一個區域來保存當前的局部變量等數據。這個就是java虛擬機棧。
每個線程都有自己的虛擬機棧,這個代碼裏面就會有main線程的虛擬機棧,用來存放自己執行的局部變量。當執行方法就會創建一個棧幀
這個棧幀就存儲了局部變量,操作數棧,方法出口等,這個時候就會把main方法壓入到main線程創建的虛擬機棧中,同時 message ,sender就會依次存放。如圖所示!
緊接着 sender.send()方法就來到了 DingdingSendMsg 裏面的這個send方法,這個方法裏面也有局部變量
public class DingdingSendMsg implements SendMsg {
@Override
public void send(Message message) {
String title=message.getTitle();
System.out.println(title);
//TODO 發送釘釘消息
}
}
那這個時候也會爲send方法創建一個棧幀,壓入到線程的虛擬機棧裏面。如圖:
上述就是JVM中的java虛擬機棧整個圖解過程,調用任何方法都會爲這個方法創建一個棧幀然後入棧,這個棧幀裏面就保存了局部變量的數據,和方法執行的相關的信息,執行完了就出棧。
最後我們把整個過程圖解一下:
以上就java虛擬機棧的整個圖解,希望大家已經明白了。
java對象的存儲 堆
我們現在已經知道了main線程執行main()方法,會有自己的程序計數器,還會把main方法的局部變量依次壓入java虛擬機棧中存放。
在上面的圖中還有一個非常關鍵的區域沒有說明,那就是java堆內存,這個就是存放我們代碼創建的各種對象的。
比如剛剛那些代碼裏面:
/**
* @author 獨秀天狼
*/
public class Luancher {
public static void main(String[] args) {
Message message=new Message();
SendMsg sender=new DingdingSendMsg();
sender.send(message);
}
}
這個代碼裏面就有 new Message()和 new DingdingSendMsg()這2個對象創建,所以我們創建的這部分數據就會存儲在堆中。
然後局部變量 message,sender引用了對象 Mesage,DingdingSendMsg,我用一個圖來說明。
總結
最後我將畫一個完整的圖給大家體會一下,這樣就更加的清晰和明瞭。
謝謝大家的閱讀,希望能給你們梳理清楚了這個難懂的JVM內存結構。面試再也不怕了,也真正的理解了,java工作的原理了。
喜歡就給我點贊。
github代碼案例 後續有大量的springboot代碼案例
本文中版權歸獨秀天狼團隊所有,轉載請標註清楚。