一個Java類在運行時候,變量是怎么在JVM中分布的呢?
JVM學習第三篇思考:一個Java類在Jvm內(nèi)存中是怎么存在的
又名:Java虛擬機的內(nèi)存模型(JMM)是什么樣的.
通過前面兩篇文章的學習,我們知道了一個Java類的生命周期及類加載器。我們可以得到如下兩幅圖:
類生命周期:

編輯
父類委托機制:

編輯
思考:

編輯
我們編寫的類中的變量、方法、對象這些都需要內(nèi)存存放的。那么在運行時候這些數(shù)據(jù)在Java虛擬機內(nèi)存中是怎么存放的呢?
本文目標:
凱哥(凱哥Java:kaigejava)希望通過本文學習,大家對Java虛擬機運行時數(shù)據(jù)區(qū)域有更深的了解
我們寫的代碼在JVM中是怎么存在的?
1:我們現(xiàn)在看看總體Java運行時數(shù)據(jù)模型:

編輯
2:我們來看看下面這段代碼,執(zhí)行的時候,在JVM中數(shù)據(jù)存放:

編輯
上面代碼很簡單,那么對應的變量、對象等在內(nèi)存中都是怎么分配的呢?
2.1:方法區(qū)
注:在JDK1.8之后,方法區(qū)被元空間替換了。
方法區(qū):用來存放的是類的信息、常量、靜態(tài)變量等。該區(qū)域也是各個線程共享的內(nèi)存區(qū)域。
根據(jù)Java虛擬機規(guī)范中的規(guī)定,當方法去無法滿足內(nèi)存分配的時候,會拋出:OutOfMemoryError異常的。
根據(jù)上面的 定義,我們可以知道比如我們JvmDemo.class信息、static string str=“jvmDemo”是在方法區(qū)存放的。
對應咱們代碼,方法區(qū)存放的如下圖:

編輯
2.2:堆區(qū)
堆區(qū)是JVM所管理的內(nèi)存中的最大的一塊區(qū)域。該區(qū)域是所有線程共享的一塊內(nèi)存區(qū)域。該區(qū)域空間在虛擬機啟動的時候就被創(chuàng)建了(-Xms的設置。后面凱哥(凱哥Java:kaigejava)也會詳細講解的)。
此區(qū)域的目的是存放對象實例的。幾乎所有的對象實例都是在這里分配的。Java虛擬機規(guī)范中是這么描述的:所有的對象的實例以及數(shù)組都要在堆上分配。
堆區(qū)是垃圾收集器管理的主要區(qū)域(后面凱哥(凱哥Java:kaigejava)也會詳細講解的).
堆區(qū)空間,在物理上可以不是連續(xù)的內(nèi)存空間,只要在邏輯上是連續(xù)的即可。如果堆沒有內(nèi)存完成實例分配,并且堆也無法在擴展的時候,將會拋出異常:OutOfMemoryError。這個大家很熟悉吧。
根據(jù)上面定義的,我們可以知道,上面代碼中Son son = new Son(); 這行代碼創(chuàng)建的實例對象是存放在堆區(qū)的。

編輯
2.3:程序計數(shù)器
程序計數(shù)器的作用可以看做是當前線程所執(zhí)行的字節(jié)碼的行號指示器。字節(jié)碼解釋器在工作的時候,時候通過改變計數(shù)器的值來選擇接下來要執(zhí)行的字節(jié)碼指令的。
同時我們都知道,當多線程的時候,Java虛擬機是通過線程輪流切換分配處理器執(zhí)行時間的方式來實現(xiàn)的。在任何一個確定的時刻一個處理器只會執(zhí)行一條線程中的指令。因此,為了解決多個線程在切換后,能夠迅速恢復到切換前執(zhí)行的位置,每個線程都需要有個獨立的程序計數(shù)器,各個線程直接的計數(shù)器互不影響,獨立存儲的。一般稱這類內(nèi)存區(qū)域為:"線程私有"的內(nèi)存。當線程正在執(zhí)行的一個方法是Native的,這種情況下,計數(shù)器的值就是undefined了。這個區(qū)域也是Java虛擬機內(nèi)存區(qū)域中唯一一個沒有OOM的區(qū)域。
根據(jù)上面描述,我們可以知道,我們自己編寫的*.java文件要想被執(zhí)行,需要被編譯成*.class的字節(jié)碼文件。字節(jié)碼文件對應各種字節(jié)碼指令。比如我們上面JvmDemo的字節(jié)碼文件:

編輯
從上面截圖,我們可以看到,行號是0,3,4,7,8這樣的。程序計數(shù)器就是記錄這些行號的
我們也可以使用idea的插件,來查看我們JvmDemo的相關信息:

編輯
2.4:虛擬機棧
Java虛擬機棧,也是線程私有的。其生命周期與線程相同,當一個線程運行結束后,對應的虛擬機棧也結束。
虛擬機棧是Java方法執(zhí)行的內(nèi)存模型:即每個方法被執(zhí)行的時候,都會被同時創(chuàng)建一個棧幀(Stack Frame),這個棧幀是用來存放方法局部變量表、操作棧、動態(tài)鏈接、方法出口等信息。每一個方法被調(diào)用直到其執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中入棧和出棧的過程。
比如:我們上面代碼執(zhí)行的時候,執(zhí)行main方法的時候,主線程就會把main方法壓入到虛擬機棧中,當執(zhí)行到add方法的時候,add方法就被壓入到棧中了。當執(zhí)行完add方法后,add方法就被從虛擬機棧中彈出,這個時候add對應的棧幀也銷毀。
虛擬機棧幀如下圖:

編輯
局部變量存放:各種基本數(shù)據(jù)類型、對象引用和返回類型
八大基本數(shù)據(jù)類型:boolean、byte、char、short、init、float、long、double;
對象引用:reference類型。這里是存放的是對象在對內(nèi)存中的地址值。不等同于對象自身的。根據(jù)不同的虛擬機的實現(xiàn),這個指向可能是指向了對象起始地址的引用指針,也有可能是指向了對象對象的句柄或其他對象與其他對象的位置;
返回類型:returnAddress類型。指向一條字節(jié)碼指令地址。
擴展:long類型和double類型的數(shù)據(jù)會占用2個局部變量空間。其他6個數(shù)據(jù)類型占用1個。
局部變量表所消耗的內(nèi)存空間在編譯期間就完成了分配,當進入一個方法的時候,這個方法需要在棧幀中分配多大的局部變量空間是完全確定的。在方法的運行期間,不會改變該區(qū)域空間大小的。
在咱們上面代碼中,虛擬機棧存放的就是咱們main方法和add方法相關的
2.5:本地方法棧
本地方法棧的作用和虛擬機棧的作用相似。不同之處在于:虛擬機棧是為了虛擬機執(zhí)行Java方法服務的。而本地方法棧則是為了虛擬機使用到Native方法服務的。此區(qū)域也是方法私有的。比如我們調(diào)用線程的run方法或者CAS的時候,調(diào)用的都是native方法。
總結:
通過本文學習,我們在自己腦海中應該有如下圖的概念:

編輯
其中方法區(qū)、堆區(qū)是所有線程共享的;虛擬機棧、程序計數(shù)器、本地方法棧這三個是線程私有的,其生命周期同線程一致。
方法區(qū):存放類型、常量、靜態(tài)變量等
堆區(qū):用來存放對象實例、數(shù)組
虛擬機棧:局部變量表、動態(tài)鏈接、操作棧等
本地方法棧:用來存放當線程調(diào)用native方法的時候使用的
程序計數(shù)器:用來記錄當前線程執(zhí)行的字節(jié)碼行號的。
好了,本文凱哥(凱哥Java:kaigejava)就和大家嘮嘮在運行時候Java虛擬機的數(shù)據(jù)區(qū)域。在下篇文章中,咱們在詳細嘮嘮堆區(qū)。
