深入理解JVM:內存區域

一、運行時數據區域

Java虛擬機在運行時java程序的時候,會把它所管理的內存劃分成若干個不同的數據區域。其中jdk1.8前後版本有差別。

jdk1.8之前:                                                                        jdk1.8之後:

內存可分爲:程序計數器、棧、堆、方法區和直接內存。

整個內存數據區域是屬於當前進程的,當前進程擁有所有的資源和數據。而直接內存是所有進程共享的。

其中棧和程序計數器是線程私有的,也就是每一個線程擁有自己獨立的區域。互相不干擾。

二、程序計數器

我們的java代碼在程序執行之前就被編譯成字節碼。而這個程序計數器不是我們計算機組成原理的程序計數器(存放的計算機指令地址),而我們的jvm的pc是字節碼解釋器的指示器。存放的是字節碼的地址。如果執行的是java方法,這裏存儲的就是正在執行的字節碼的地址,如果執行的是本地方法存儲的就是undefined。

字節碼解釋器工作時通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等功能都需要依賴這個計數器來完成。

在多線程的環境下,pc還能保證恢復到原來線程的位置。

注意:程序計數器是唯一一個不會出現 OutOfMemoryError 的內存區域,它的生命週期隨着線程的創建而創建,隨着線程的結束而死亡。

三、Java虛擬機棧

描述的是 Java 方法執行的內存模型,每次方法調用的數據都是通過棧傳遞的。

棧幀:操作數棧

          動態鏈接

          方法出口信息

          局部變量表:rerturnAdress類型(指向了一個字節碼指令的地址)

                               各種基本數據類型和引用類型

Java 棧可用類比數據結構中棧,Java 棧中保存的主要內容是棧幀,每一次函數調用都會有一個對應的棧幀被壓入 Java 棧,每一個函數調用結束後,都會有一個棧幀被彈出。在java方法中y有兩鍾返回方法:拋出異常和return語句。兩種方式都回將棧幀彈出。

四、Java堆內存

Java 堆是所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這裏分配內存。這段區域也是垃圾回收器經常光顧的地方。

Java 堆還可以細分爲:新生代和老年代、永久代(方法區):再細緻一點有:Eden 空間、From Survivor、To Survivor 空間(這三個都是新生代)

jdk1.8之前                                                                               之後

JVMå åå­ç»æ-JDK7JVMå åå­ç»æ-JDK8

大部分情況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收後,如果對象還存活,則會進入 s0 或者 s1,並且對象的年齡還會加 1(Eden 區->Survivor 區後對象的初始年齡變爲 1),當它的年齡增加到一定程度(默認爲 15 歲),就會被晉升到老年代中。

五、方法區

方法區與 Java 堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然 Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。在jdk1.8之後方法區(永生帶)移入直接內存(元空間)。

六、運行時常量池

JDK1.8 版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。

七、對象的創建過程

Javaå建对象çè¿ç¨

(1)首先jvm遇到一個new的指令時,去咱們的常量池去查詢是否有這個類的符號引用(全限定類名),如果沒有表示未加載解析過這個類,就需要加載解析,同時在常量池中添加符號引用,在方法區添加類的信息。

(2)然後分配內存,分配的方式有兩種(指針碰撞和空閒列表)

(3)內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。

(4)設置對象頭,這個對象頭哦存儲的是:這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息和否啓用偏向鎖等。

(5)執行init方法,把對象按照程序員的意願進行初始化(也就是構造函數之類的開始初始化)。

八、對象訪問定位

(1)句柄方式:如果使用句柄的話,那麼 Java 堆中將會劃分出一塊內存來作爲句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息;

对象ç访é®å®ä½-使ç¨å¥æ

(2)直接指針: 如果使用直接指針訪問,那麼 Java 堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而 reference 中存儲的直接就是對象的地址。

对象ç访é®å®ä½-ç´æ¥æé

這兩種對象訪問方式各有優勢。使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 本身不需要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。

九、內存溢出

StackOverFlowError: 若 Java 虛擬機棧的內存大小不允許動態擴展,那麼當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,就拋出 StackOverFlowError 錯誤。

OutOfMemoryError: 若 Java 虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出 OutOfMemoryError 錯誤。

參考:https://snailclimb.gitee.io/javaguide/#/docs/java/jvm/Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F?id=%E5%86%99%E5%9C%A8%E5%89%8D%E9%9D%A2-%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98

 

 

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