[TOC] Java程式在記憶體中運行詳解 Java語言是一門編譯型語言,需要將編寫的源代碼(.java文件)編譯之後(.class位元組碼文件),通過 jvm 才能正常的執行,下麵的內容記錄了一個程式從編寫到執行整個過程在記憶體中是怎麼一個變的。 一、JVM的記憶體分佈 先瞭解下 JVM 的記憶體分佈,因為 ...
目錄
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。
二、程式執行的過程
從上圖我們看到了一個程式在記憶體中執行的過程。
上圖的執行流程:
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 實例賦的值了。
六、使用對象類型作為方法參數的記憶體圖
使用對象類型作為方法的參數,在傳遞的過程中,實際上傳遞的是引用,即對象的地址值。當我們在另外一個方法中改變了這個對象的屬性時,對象原來的值就會被覆蓋。
七、對象類型作為方法返回值得記憶體圖
對象類型作為返回值也是一樣的道理,返回的實際是對象的地址值。
八、總結
分清什麼是實例什麼是對象。Class a= new Class(); 此時 a 叫實例,而不能說 a 是對象。實例在棧中,對象在堆中,操作實例實際上是通過實例的指針間接操作對象。多個實例可以指向同一個對象。
棧中的數據和堆中的數據銷毀並不是同步的。方法一旦結束,棧中的局部變數立即銷毀,但是堆中對象不一定銷毀。因為可能有其他變數也指向了這個對象,直到棧中沒有變數指向堆中的對象時,它才銷毀,而且還不是馬上銷毀,要等垃圾回收掃描時才可以被銷毀。
以上的棧、堆、代碼段、數據段等等都是相對於應用程式而言的。每一個應用程式都對應唯一的一個JVM實例,每一個JVM實例都有自己的記憶體區域,互不影響。並且這些記憶體區域是所有線程共用的。這裡提到的棧和堆都是整體上的概念,這些堆棧還可以細分。
類的成員變數在不同對象中各不相同,都有自己的存儲空間(成員變數在堆中的對象中)。而類的方法卻是該類的所有對象共用的,只有一套,對象使用方法的時候方法才被壓入棧,方法不使用則不占用記憶體。
對象類型作為方法的參數或者方法的返回值時,傳遞的都是對象的地址值。再其他地方修改這個對象的屬性值時,原有的值就會被覆蓋掉。
參考文章:
https://blog.csdn.net/yangyuankp/article/details/7651251
http://www.yq1012.com/jichu/4540.html