Java高階之路系列文章之第4講:初學者的記憶體分析模型
通過本篇文章的閱讀,筆者希望你能知道以下問題:
1.什麼是JVM?
2.JVM的記憶體區域有哪些?
3.學會畫簡單的記憶體分析圖分析程式。
0.JVM是Java進階的必經之路
我們都知道程式是需要記憶體的,Java程式的記憶體這塊主要是JVM,當程式猿達到某個階段的時候,深掘記憶體是必經之路,那麼JVM是什麼呢?
官方解釋:【JVM是JRE的一部分。它是一個虛構出來的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。JVM有自己完善的硬體架構,如處理器、堆疊、暫存器等,還具有相應的指令系統。Java語言最重要的特點就是跨平臺執行。使用JVM就是為了支援與作業系統無關,實現跨平臺。所以,JAVA虛擬機器JVM是屬於JRE的,而現在我們安裝JDK時也附帶安裝了JRE(當然也可以單獨安裝JRE)。】
筆者是一名Android程式猿,學JVM相關知識就是為了深入記憶體,知道記憶體優化相關知識,同時也能分析分析一個物件如何在JVM中從出生到死亡的過程,對於某些開源框架而言,在分析原始碼的時候,會提供大的幫助,不知道你面試的公司當中有沒有一個這樣的條件,就是”JVM記憶體調優”,這裡筆者也表達的不夠清晰,對於JVM的看法,每個人學了都有自己的理解,畢竟JVM的學習是有些難度的,接下來筆者就來一一介紹JVM裡記憶體的區域,不過筆者沒有打算在這裡細細的介紹,因為這還不是介紹的時機,以免太過於複雜對你而言如天書般,我們只需要抽象出一種簡單的記憶體模型去分析分析某些簡單demo,在熟悉這種分析模式之後,我們將會逐漸深入學習JVM的點點滴滴。
1.JVM的每個記憶體區域
- 堆:對於大多數應用來說,Java堆(Java Heap)是Java虛擬機器管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊資料區域,在虛擬機器啟動時建立,這一記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。但是隨著JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的物件都分配在堆上也逐漸變得不是那麼“絕對”。
堆中可細分為新生代和老年代,在細分可以分為Eden空間、Form Survivor空間、to Survivor空間。
Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”。
根據Java虛擬機器規範規定,Java堆可以處於物理上不連續的記憶體中,即只要邏輯上是連續的即可,就像我們的磁碟空間一樣。在實現時,可以固定大小也是可擴充套件的。主流的虛擬機器都是按照可擴充套件來實現的(通過-Xmx和-Xms來控制)。如果在堆中沒有記憶體可分配,並且堆也無法繼續擴充套件時,將會丟擲OutOfMemortError異常。
Java的普通物件存活在堆中,與棧不同,堆的空間不會隨著方法呼叫結束而清空。因此,在某個方法中建立的物件,可以在方法呼叫結束之後,繼續存在堆中。這帶來的一個問題是,如果我們不斷的建立新的物件,記憶體控制元件將會最終消耗殆盡。 - 方法區:方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已經被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯之後的程式碼等資料。雖然Java虛擬機器將其描述為堆的一個邏輯部分,但它卻有一個別名叫做Non-Heap(非堆)。目的是與Java堆區分開來。(以前很多人把方法區稱為永久代,現在JDK1.8中已經用元資料區域取代了永久代)。
- JVM棧是執行緒私有的,每個執行緒建立的同時都會建立JVM棧,JVM棧中存放的為當前執行緒中區域性基本型別的變數(java中定義的八種基本型別:boolean、char、byte、short、int、long、float、double)、部分的返回結果以及Stack Frame,非基本型別的物件在JVM棧上僅存放一個指向堆上的地址。
- 本地方法棧(Native Method Stack)與虛擬機器棧所發揮的作用是非常相似的。他們之間的區別就是Java虛擬機器棧是位虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧為位虛擬機器使用到的Native方法服務。
其實虛擬機器規範中對本地方發棧中方法所使用的語言、使用方式以及資料結構都沒有強制規定,因此具體的虛擬機器可以自由地實現它。甚至在有的虛擬機器(如Sun HotSpot虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一。與虛擬機器棧一樣,本地方法棧區域也會丟擲StackOverflowError和OutOfMemory異常。 - 執行時常量池(Runtime Constant Pool)是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊就是常量池。用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入到方法區的執行時常量池中存放。並非預置入Class檔案中常量池的內容才進入方法執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法。
以上是筆者從大神部落格上copy過來的,不需要搞得很懂,畢竟對於剛剛接觸JVM的程式猿而言,初始表象即可。我們接下來看看如何通過簡單的抽象記憶體模型去分析分析程式某些結果的原因。
2.一個簡單的例子教你學會使用簡單的記憶體模型分析程式
因為JVM記憶體的每一個區域都有其自己的自責,如果我們在分析程式時,把JVM的每一個記憶體區域都分析到,這樣未免太過於複雜了,所以筆者這裡給出一種簡單的初學者的記憶體分析模式,對於理解和分析一般的程式碼沒有任何難度的,一支筆,一個堆,一個棧,一個常量池,你就可以分析任何你想分析的程式,請看以下程式碼,並說出為什麼輸出結果是這樣的呢?
int a = 3;
int b = a;
a = 4;
System.out.println("b = "+ b);//執行結果b = 3
我們來畫一畫這段程式碼的記憶體分析圖,在分析之前我們先要學會話記憶體模型圖,對於初學者而言,我們常常接觸有方法棧,堆,常量池即可,我們來分析分析以上的程式碼吧。首先我們看第一句程式碼“int a = 3;”,由於這句程式碼是發生於main方法中,因此我們先來畫一個mian方法棧,然後再畫一個堆,最後再畫常量池。畫好的記憶體模型圖如下:
接下來我們就來畫一畫沒執行一句程式碼後記憶體模型圖的變化如何:
執行第一句:”int a = 3”
你可能會問我,為什麼這個”3”是屬於main方法棧的啊,關於這些常量儲存在哪裡,有這一個總結:
在方法中宣告的變數可以是基本型別的變數,也可以是引用型別的變數。
(1)當宣告是基本型別的變數的時,其變數名及值(變數名及值是兩個概念)是放在JAVA虛擬機器棧中
(2)當宣告的是引用變數時,所宣告的變數(該變數實際上是在方法中儲存的是記憶體地址值)是放在JAVA虛擬機器的棧中,該變數所指向的物件是放在堆類存中的。
二:在類中宣告的變數是成員變數,也叫全域性變數,放在堆中的(因為全域性變數不會隨著某個方法執行結束而銷燬)。
同樣在類中宣告的變數即可是基本型別的變數 也可是引用型別的變數
(1)當宣告的是基本型別的變數其變數名及其值放在堆記憶體中的
(2)引用型別時,其宣告的變數仍然會儲存一個記憶體地址值,該記憶體地址值指向所引用的物件。引用變數名和對應的物件仍然儲存在相應的堆中
我們在畫變數的時候,一定要遵循上面的總結。
執行第二句:”int b = a;”
“b = a”,實際上是把a指向的”3”的地址賦值給了int型別變數b,因此此時b的值也同樣是3,接下來我們再來看看第三句程式碼的執行:
執行第三句:”a = 4”
你可以從圖中看到,儘管a變數指向的地址發生了變化,但是b是沒有任何變化的,因此最後的結果就是輸出”b = 3”,對於為什麼常量3的地址為什麼是”0x11111111”,其實不必太在意這個地址,因為那是筆者隨便定的,因為當程式執行到右邊的3時,JVM就會在main方法棧儲存執行時變數的地方開闢一個空間,當然這是一種隨機的行為,並把開闢後的地址交給變數a的指向地址區域,也就是圖中a的右邊區域,我就不細緻分析了,希望讀者自己好好研究,這只是一個簡單的int資料的例子,在往後學習類與物件之後,那就會變得複雜很多,把基礎打牢固吧!