JVM記憶體區域

西北野狼發表於2017-02-12

Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為多個區域,這些區域各有自己的用途以及獨特的建立和銷燬時間,今天就帶著大家來揭開這些不同的資料區域的面紗

先來一張最經典的圖:
1

今天我們來學習一下圖片上方的程式計數器、方法區、棧、堆幾個部分。

1.程式計數器

程式計數器是隨著一條執行緒的啟動而建立的,每一個執行緒獨有一個程式計數器,多個執行緒之間互不影響。(可以理解為Java中的ThreadLocal)

程式計數器為什麼要這樣設計呢?

想要知道程式計數器為何如此設計我們先要知道它儲存的是什麼?

  1. 如果當前執行緒正在執行的是一個java方法,那麼這個執行緒的程式計數器記錄的是正在執行的虛擬機器位元組碼指令的地址,如果正在執行的是native方法,這個計數器則為undefined。
  2. 我們知道多執行緒其實就是通過執行緒輪流切換並分配處理器執行時間的方式實現的,在任何一個確定的時刻,一個處理器都只會執行一條執行緒中的指令。當切換到另外一條執行緒時,若是當前執行緒沒有程式計數器來記錄此刻的執行位置,下次處理機再執行這條執行緒時就不知道該從哪開始了。

2. 棧

本地方法棧和虛擬機器棧可以統稱為棧,由於本地方法棧是jvm呼叫作業系統native方法所使用的棧且它們的作用是非常相似的,所以這裡我們重點看一下虛擬機器棧。

虛擬機器棧

虛擬機器棧與程式計數器一樣,也是執行緒私有的,每個執行緒都會有一個自己的虛擬機器棧。它描述的java方法執行的記憶體模型

而每一個虛擬機器棧呢又是有由多個幀組成的,當一個方法被呼叫時就會產生一個幀,幀的生命週期跟隨著這個方法的執行週期。


每一個幀裡面又包括了被呼叫的這個方法的區域性變數表、運算元棧、常量池指標、動態連結、方法出口等資訊。

區域性變數表

區域性變數表包含了編譯器可知的基本資料型別和物件引用。

在下方的靜態方法中區域性變數表就存放了a和b

1複製程式碼
static void methed1(String a,int b)複製程式碼

而非靜態方法中就會多了一個當前物件this,此區域性變數表存放的就是this、a、b

1複製程式碼
void methed1(String a,int b)複製程式碼
運算元棧

Java中所有的引數傳遞都是依靠運算元棧進行的,例如如下程式碼:

12345複製程式碼
static int methed1(int a,int b){     int c=0;     c=a+b;     return c;   }複製程式碼

其實這短短的三行程式碼執行的過程是這樣的:

12345678複製程式碼
1.	0壓棧2.	彈出int存放區域性變數c3.	區域性變數a壓棧4.	區域性變數b壓棧5.	彈出兩個變數求和,將結果壓棧6.	彈出結果放到區域性變數c7.	區域性變數c壓棧8.	return複製程式碼
常量池指標

顧名思義,指向常量池的指標。

棧中可能引起的異常

1. StackOverflowError
這個錯誤主要是由執行緒請求的棧深度大於了執行緒所允許的最大深度而引起的。那麼棧的深度又是個什麼鬼呢<br>
我們知道,一次方法呼叫就會建立一個幀,一個幀中又包含了我們上邊剛剛說起的那麼多東西,而它們的生命週期是隨著方法呼叫才會銷燬的。這些東西的存在都是需要佔用記憶體的,而棧的記憶體肯定是有一個極限的。看一下下方的這個無限的遞迴方法:

12345	int c=0;	int methed1(String a,int b){      	++c;	return methed1(a,b);}


方法每執行一次,就會建立一個幀,一個幀裡面又包含了區域性變數表運算元棧常量池指標等。就這樣隨著方法的執行虛擬機器棧佔用的記憶體越來越多就會引起StackOverflowError。
複製程式碼

如何解決?
使用-Xss10m引數調整棧的大小,可以使用不同的引數來驗證一下當丟擲異常時c的值,c的值越大代表棧的深度越深。
複製程式碼
2. OutOfMemoryError: unable to create new native thread

由上方的學習我們知道,每一個執行緒都有一個自己獨有的虛擬機器棧,然後這些虛擬機器棧中又包含了辣麼多東西。當建立的執行緒多到棧的記憶體不足以支撐時就會引起此異常。

1234567複製程式碼
 while(true){    new Thread(()->{            try {                Thread.sleep(60*60*1000);            } catch(InterruptedException e) { }            }).start();}複製程式碼
如何解決?
同1,使用-Xss10m引數調整棧的大小。
複製程式碼

3. 堆

在我們的程式中,跟我們打交道最多的就是堆裡的物件了。基本上所有(不包括常量池中存在的)通過new操作建立的物件都會儲存在堆中。所以與棧的執行緒私有不同,堆是所有執行緒共享的(畢竟不共享難道每個執行緒呼叫時都new一次物件豈不是瘋了),所以它也是虛擬裡最大的一塊。


如果根據垃圾收集演算法來分的話,堆還可以再細分下去。首先呢,堆可以分為新生代和老年代,而新生代又分為eden區和s0、s1(s0、s1又叫from、to)三個區,如下圖所示:。
3


當一個普通的物件剛new出來的時候它是存在於eden區的,然後呢在進行垃圾回收時回進入s0和s1區,如果幾輪垃圾回收後都沒有被回收的話就會進入變成一個老年物件進入老年代。當然,有的物件也比較特殊,比如說一些大物件或者伴隨整個程式生命週期的物件在剛出生的時候就會進入老年代避免一些不必要的垃圾回收,關於詳細內容可參考我的另一篇部落格:JVM垃圾收集演算法

堆中可能引起的異常

1. java.lang.OutOfMemoryError: Java heap space

這個異常就是由於堆中存在大量的物件,這些物件無法通過垃圾回收進行收集從而導致的堆記憶體溢位。

如何解決?

可以適當根據機器的效能使用-Xms -Xmx引數調整棧的大小,不過如果想要治本的話還是要選擇優化程式碼和演算法。

直接記憶體

直接記憶體並不屬於執行時資料區的一部分,當然也不屬於堆。之所以放到這裡是因為直接記憶體雖然不屬於執行時資料區,但是它也是需要佔用記憶體的,如果我們在分配記憶體時把本機的總記憶體都分配給執行時資料區的各個部分而忽略了直接記憶體的話同樣也是會引起OutOfMemoryError的。

4. 方法區

方法區同樣是各個執行緒共享的記憶體區域,它主要儲存已經被虛擬機器載入的類資訊

1. 類資訊
  1. 類的全限定名
  2. 父類的全限定名
  3. 直接實現介面的全限定名
  4. 型別標誌
  5. 類的訪問描述符(public、private、default、abstract、final、static)
2、常量池

存放該類所用到的常量的有序集合

3、欄位資訊
  1. 欄位修飾符(public、protect、private、default)
  2. 欄位的型別
  3. 欄位名稱
4、類的所有方法資訊
  1. 方法修飾符
  2. 方法返回型別
  3. 方法名
  4. 方法引數個數、型別、順序等
  5. 方法位元組碼
  6. 運算元棧和該方法在棧幀中的區域性變數區大小
  7. 異常表
5、類靜態變數
6、指向類載入器的引用
7、指向Class例項的引用(可以通過Class.forName獲取的引用)
8、方法表(非抽象類、非介面的類才會有)

一個儲存類中所有的方法的陣列,陣列中每個每個元素是對每個方法的直接引用

9、執行時常量池

Integer,Long等基本型別的包裝類 -127到128之間的快取資料

方法區可能引起的異常

1. java.lang.OutOfMemoryError: PermGen space

因為方法區主要是負責存放類的相關資訊,而且因為gc的次數也不像堆來的頻繁,所以當class越來越多的時候就會引起此異常。

如何解決?
使用-XX:PermSize引數調整方法區的大小。
複製程式碼

5. 綜合複習

看了堆、棧、方法區的介紹以後你理解他們之間的關係麼?

1234567複製程式碼
public class User{   private String name;   public User(String name){      this.name = name;    }   //省略getset方法}複製程式碼
123456複製程式碼
public class Test{   public static void main(String[] args){     	User user1=new User("張三");        User user2=new User("李四");    }}複製程式碼

不知道看完上方兩端程式碼,你所理解的關係和我畫的圖是否一致呢?
1

本文出自zhixiang.org.cn,轉載請保留。


相關文章