對JVM內存的理解

1.JVM內存大致分爲五個區域

1)棧

棧內存,線程私有,生命週期與線程相同。每個java方法在執行的時候都會創建一個棧幀,用來創建這個方法的操作數棧,局部變量表,方法正常完成和異常完成信息,動態連接等信息。每個方法的開始執行到結束過程都對應了它的棧幀在棧中的入棧和出棧的過程。一般而言,的棧指的就是棧幀裏存儲局部變量表的內存部分。

局部變量表:是一組變量值的存儲空間,它用於存儲方法,參數,以及方法內定義的局部變量。

局部變量表所需要的內存空間在代碼編譯期就已經分配完成,當進入一個方法時這個方法在棧幀中需要分配多大的局部變量表空間是完全確定的,在方法運行期間不會改變局部變量表大小。(*是否是局部變量未初始化時,不可使用的原因?)

2)堆

堆內存,線程共享,是JVM管理的內存最大的一塊內存區域,在虛擬機啓動時創建。因爲堆裏面存儲的對象是線程共享,所以多線程的時候也需要同步機制。值得一提的是,成員變量,不是靜態變量不獨立於類的實例而存在,可包含基本類型和引用類型成員變量,都是存放在堆的對象裏,和對象同時生成和銷燬。所以說基本數據類型存放於棧中是不準確的。

java虛擬機規範對這塊的描述是:所有對象實例及數組都要在堆上分配內存,但隨着JIT編譯器的發展和逃逸分析技術的成熟,這個說法也不是那麼絕對,但是大多數情況都是這樣的。

即時編譯器:可以把把Java的字節碼,包括需要被解釋的指令的程序)轉換成可以直接發送給處理器的指令的程序)

逃逸分析:通過逃逸分析來決定某些實例或者變量是否要在堆中進行分配,如果開啓了逃逸分析,即可將這些變量直接在棧上進行分配,而非堆上進行分配。這些變量的指針可以被全局所引用,或者被其它線程所引用。

堆是所有線程共享的,它的目的是存放對象實例。同時它也是GC所管理的主要區域,因此常被稱爲GC堆,又由於現在收集器常使用分代算法,Java堆中還可以細分爲新生代和老年代。

根據虛擬機規範,Java堆可以存在物理上不連續的內存空間,就像磁盤空間只要邏輯是連續的即可。它的內存大小可以設爲固定大小,也可以擴展。

當前主流的虛擬機如HotPot都能按擴展實現(通過設置 -Xmx和-Xms),如果堆中沒有內存內存完成實例分配,而且堆無法擴展將報OOM錯誤(OutOfMemoryError)

3)方法區

方法區,線程共享,爲了區分堆,又被稱爲非堆。用於存儲已被虛擬機加載的類信息,靜態變量,靜態塊,靜態方法,成員方法,常量,即時編譯器編譯後的代碼等數據等。在老版jdk,方法區也被稱爲永久代。不過自從JDK7之後,Hotspot虛擬機便將運行時常量池從永久代移除了。

jdk8真正開始廢棄永久代,而使用元空間(Metaspace)

4)程序計數器

程序計數器是一塊很小的內存空間,它是線程私有的,可以認作爲當前線程的行號指示器。那麼計數器記錄虛擬機字節碼指令的地址。如果爲native【底層方法】,那麼計數器爲空。

 這塊內存區域是虛擬機規範中唯一沒有OutOfMemoryError的區域。

5)本地方法棧

本地方法棧是與虛擬機棧發揮的作用十分相似,區別是虛擬機棧執行的是Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的native方法服務,可能底層調用的c或者c++,我們打開jdk安裝目錄可以看到也有很多用c編寫的文件,可能就是native方法所調用的c代碼。

6)運行時常量池

運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。Java 虛擬機對Class 文件的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個字節用於存儲哪種數據都必須符合規範上的要求,這樣纔會被虛擬機認可、裝載和執行。但對於運行時常量池,Java 虛擬機規範沒有做任何細節的要求,不同的提供商實現的虛擬機可以按照自己的需要來實現這個內存區域。不過,一般來說,除了保存Class 文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中①。運行時常量池相對於Class 文件常量池的另外一個重要特徵是具備動態性,Java 語言並不要求常量一定只能在編譯期產生,也就是並非預置入Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是String 類的intern() 方法。既然運行時常量池是方法區的一部分,自然會受到方法區內存的限制,當常量池無法再申請到內存時會拋出OutOfMemoryError 異常

2.代碼執行時,內存變化

下面是內存表示圖: 
            這裏寫圖片描述

Java 字符串常量存放在堆內存還是JAVA方法區?

JDK1.7 及之後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。JDK1.8開始,取消了Java方法區,取而代之的是位於直接內存的元空間(metaSpace)

預備知識:

1.一個Java文件,只要有main入口方法,我們就認爲這是一個Java程序,可以單獨編譯運行。 
2.無論是普通類型的變量還是引用類型的變量(俗稱實例),都可以作爲局部變量,他們都可以出現在棧中。只不過普通類型的變量在棧中直接保存它所對應的值,而引用類型的變量保存的是一個指向堆區的指針,通過這個指針,就可以找到這個實例在堆區對應的對象。因此,普通類型變量只在棧區佔用一塊內存,而引用類型變量要在棧區和堆區各佔一塊內存。 
3.在方法的參數傳遞中,基本數據類型,String類是按值傳遞,即拷貝了一個副本!引用數據類型是按引用傳遞,即把棧中的地址傳入!

示例: 
這裏寫圖片描述
1.JVM自動尋找main方法,執行第一句代碼,創建一個Test類的實例,在棧中分配一塊內存,存放一個指向堆區對象的指針110925。 
2.創建一個int型的變量date,由於是基本類型,直接在棧中存放date對應的值9。 
3.創建兩個BirthDate類的實例d1、d2,在棧中分別存放了對應的指針指向各自的對象。他們在實例化時調用了有參數的構造方法,因此對象中有自定義初始值

這裏寫圖片描述

調用test對象的change1方法,並且以date爲參數。JVM讀到這段代碼時,檢測到i是局部變量,因此會把i放在棧中,並且把date的值賦給i。 
這裏寫圖片描述
把1234賦給i。很簡單的一步。 
這裏寫圖片描述
change1方法執行完畢,立即釋放局部變量i所佔用的棧空間。 
這裏寫圖片描述
調用test對象的change2方法,以實例d1爲參數。JVM檢測到change2方法中的b參數爲局部變量,立即加入到棧中,由於是引用類型的變量,所以b中保存的是d1中的指針,此時b和d1指向同一個堆中的對象。在b和d1之間傳遞是指針。 
這裏寫圖片描述 
change2方法中又實例化了一個BirthDate對象,並且賦給b。在內部執行過程是:在堆區new了一個對象,並且把該對象的指針保存在棧中的b對應空間,此時實例b不再指向實例d1所指向的對象,但是實例d1所指向的對象並無變化,這樣無法對d1造成任何影響。 
這裏寫圖片描述
change2方法執行完畢,立即釋放局部引用變量b所佔的棧空間,注意只是釋放了棧空間,堆空間要等待自動回收。 
這裏寫圖片描述
調用test實例的change3方法,以實例d2爲參數。同理,JVM會在棧中爲局部引用變量b分配空間,並且把d2中的指針存放在b中,此時d2和b指向同一個對象。再調用實例b的setDay方法,其實就是調用d2指向的對象的setDay方法。 
這裏寫圖片描述
調用實例b的setDay方法會影響d2,因爲二者指向的是同一個對象。 
這裏寫圖片描述

change3方法執行完畢,立即釋放局部引用變量b。

以上就是Java程序運行時內存分配的大致情況。其實也沒什麼,掌握了思想就很簡單了。無非就是兩種類型的變量:基本類型和引用類型。二者作爲局部變量,都放在棧中,基本類型直接在棧中保存值,引用類型只保存一個指向堆區的指針,真正的對象在堆裏。作爲參數時基本類型就直接傳值,引用類型傳指針。

附上java程序的運行原理圖

參考鏈接:https://www.cnblogs.com/lipeineng/p/8358601.html

               https://blog.csdn.net/liupeng900605/article/details/7826573?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1

https://www.cnblogs.com/weibanggang/p/11119410.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章