Java程序在內存中運行詳解


Java語言是一門編譯型語言,需要將編寫的源代碼(.java文件)編譯之后(.class字節碼文件),通過 jvm 才能正常的執行,下面的內容記錄了一個程序從編寫到執行整個過程在內存中是怎么一個變的。

一、JVM的內存分布

先了解下 JVM 的內存分布,因為Java程序想要運行,就要依靠 JVM,可以把JVM理解成Java程序和操作系統之間的橋梁,JVM 實現了Java 的平台無關性,由此可見JVM的重要性。所以在學習 Java 內存分配原理的時候一定要牢記這一切都是在 JVM 中進行的,JVM 是內存分配原理的基礎與前提。

1.jvm內存分布圖

從圖片中看,一共分為了5大區域,分別是:方法區、堆、棧、本地方法區、程序計數器。

這里我們主要了解下 方法區堆、 棧、這三個區域。

2.方法區:

方法區是一塊所有線程共享的內存區域。
保存系統的類信息,比如,類的字段,方法,常量池等等。

方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區溢出,虛擬機同樣會拋出內存溢出的錯誤

jdk1.6和jdk1.7方法區可以理解為永久區。

jdk1.8已經將方法區取消,替代的是元數據區。

jdk1.8的元數據區可以使用參數-XX:MaxMetaspaceSzie設定大小,這是一塊堆外的直接內存,與永久區不同,如果不指定大小,默認情況下,虛擬機會耗盡可用系統內存。

3.堆:

用來存放動態產生的數據,比如new出來的對象。注意創建出來的對象只包含屬於各自的成員變量,並不包括成員方法。因為同一個類的對象擁有各自的成員變量,存儲在各自的堆中,但是他們共享該類的方法,並不是每創建一個對象就把成員方法復制一次。在堆中只會存儲成員方法的地址,在調用的時候,根據地址去方法區中執行對應的成員方法。

4. 棧:

棧生命周期與線程相同。啟動一個線程,程序調用函數,棧幀被壓入棧中,函數調用結束,相應的是棧幀的出棧。

棧幀由局部變量表,操作數棧,幀數據區組成。

局部變量表:存放的是函數的入參,以及局部變量。

操作數棧:存放調用過程中的計算結果的臨時存放區域。

幀數據區:存放的是異常處理表和函數的返回,訪問常量池的指針。

舉個例子,線程執行進入方法A,則會創建棧幀入棧,A方法調用了B方法,B棧幀入棧,B方法中調用C方法,C創建了棧幀壓入棧中,接下來是D入棧

反過來,D方法執行完,棧幀出棧,接着是C、B、A。

jvm內存模型詳解記錄

二、程序執行的過程

從上圖我們看到了一個程序在內存中執行的過程。

上圖的執行流程:

1.從 disk 中將 MainApp.class 加載到 jvm 的方法區中。

2.執行 main 方法,將該 main 方法中包含的變量和函數,壓到棧中。

3.開始執行 main 方法中的指令,創建一個 animal 對象, 將 new 出來的 animal 對象存儲到堆中,animal 引用指向堆中的 animal 對象,堆中的 animal 對象指向方法區中的 Animal 類。

4.繼續執行 main 方法中的指令,調用 animal 對象中的 printName() 方法,這時 animal 應用調用 animal 對象, animal 對象找到方法區的 Animal 類中的 printName() 字節碼信息,根據該描述信息,開始執行 printName方法。

三、只有一個對象時的內存圖

從左側我們看到有兩個類,按照Java程序的執行流程,會把這兩個類編譯成 .class 文件,即圖中最右邊的 Phone.class he Demo01PhoneOne.class。

首先程序開始執行是從 main() 方法開始,這個時候會把 main() 方法壓到棧中,main() 方法中的第一句代碼是先創建一個 Phone 對象,當我們 new 一個對象時,會把 new 出來的對象放到堆中,相對應的給這個對象分配一個地址值,在棧中會產生一個實例 one 會指向這個地址,可以看到堆中的對象包含了自身的成員變量和成員方法的引用。

接着繼續執行下面的代碼,直接打印對象的屬性值,由於對象屬性沒有進行賦值,所以輸出的都是對應數據類型的默認值。 繼續下面的操作,就是給對象的屬性進行賦值,由於 one 是指向了對象,所以直接可以進行操作,這時在堆中的屬性值就會被賦予對應的值了。再次打印的時候就會打印出對應的值。

再到后面,繼續調用了對象的成員方法,這個時候需要先在堆中找到這個成員方法的應用,然后找到方法區中將對應的代碼壓到棧中,繼續執行。調用方法會傳入對應的參數,也是放到棧中的,執行完這個方法之后,壓到棧中的這一部分代碼就會出棧,直到 main() 方法中所有的代碼執行完,棧中的內容也就全部消失,內存也就隨之釋放。

四、兩個對象使用同一個方法的內存圖

這里和上面不同的是創建了兩個對象,但是操作的內容還是和上面一樣的。唯一區別就是在調用成員方法時,調用的是同一個。

剛開始也說到了,同一個類創建多個對象時,他們是各自擁有自己的成員變量了,但是應用的成員方法卻是同一個。

從圖中我們就可以看出,給兩個對象進行賦值時,是會打印出不同的值的。調用方法時,使用的還是同一個方法。

五、兩個引用指向同一個對象的內存圖

當我理解了前面兩個圖后,看到這里應該也不會有什么難度了,這里我們只 new 了一個對象,但是卻有兩個實例,從圖中也可以看到堆里面只有一個對象。

看到圖最左邊,我們把 one 實例直接就賦值給了 two, 其實就是把 one 的地址值賦給了 two, 這時 two 也就和one 指向了同一個對象。這時去改變對象中的值,就會把 one 原來賦的值直接覆蓋掉。最終打印的就是 two 實例賦的值了。

六、使用對象類型作為方法參數的內存圖

使用對象類型作為方法的參數,在傳遞的過程中,實際上傳遞的是引用,即對象的地址值。當我們在另外一個方法中改變了這個對象的屬性時,對象原來的值就會被覆蓋。

七、對象類型作為方法返回值得內存圖

對象類型作為返回值也是一樣的道理,返回的實際是對象的地址值。

八、總結

  1. 分清什么是實例什么是對象。Class a= new Class(); 此時 a 叫實例,而不能說 a 是對象。實例在棧中,對象在堆中,操作實例實際上是通過實例的指針間接操作對象。多個實例可以指向同一個對象。

  2. 棧中的數據和堆中的數據銷毀並不是同步的。方法一旦結束,棧中的局部變量立即銷毀,但是堆中對象不一定銷毀。因為可能有其他變量也指向了這個對象,直到棧中沒有變量指向堆中的對象時,它才銷毀,而且還不是馬上銷毀,要等垃圾回收掃描時才可以被銷毀。

  3. 以上的棧、堆、代碼段、數據段等等都是相對於應用程序而言的。每一個應用程序都對應唯一的一個JVM實例,每一個JVM實例都有自己的內存區域,互不影響。並且這些內存區域是所有線程共享的。這里提到的棧和堆都是整體上的概念,這些堆棧還可以細分。

  4. 類的成員變量在不同對象中各不相同,都有自己的存儲空間(成員變量在堆中的對象中)。而類的方法卻是該類的所有對象共享的,只有一套,對象使用方法的時候方法才被壓入棧,方法不使用則不占用內存。

  5. 對象類型作為方法的參數或者方法的返回值時,傳遞的都是對象的地址值。再其他地方修改這個對象的屬性值時,原有的值就會被覆蓋掉。

參考文章:

https://blog.csdn.net/yangyuankp/article/details/7651251
http://www.yq1012.com/jichu/4540.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM