JVM內存區域

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中所有的參數傳遞都是依靠操作數棧進行的,例如如下代碼:

1
2
3
4
5
static int methed1(int a,int b){
    int c=0;
    c=a+b;
    return c;
  }

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

1
2
3
4
5
6
7
8
1.	0壓棧
2. 彈出int存放局部變量c
3. 局部變量a壓棧
4. 局部變量b壓棧
5. 彈出兩個變量求和,將結果壓棧
6. 彈出結果放到局部變量c
7. 局部變量c壓棧
8. return
常量池指針

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

棧中可能引起的異常

1. StackOverflowError
這個錯誤主要是由線程請求的棧深度大於了線程所允許的最大深度而引起的。那麼棧的深度又是個什麼鬼呢<br>
我們知道,一次方法調用就會創建一個幀,一個幀中又包含了我們上邊剛剛說起的那麼多東西,而它們的生命週期是隨着方法調用纔會銷燬的。這些東西的存在都是需要佔用內存的,而棧的內存肯定是有一個極限的。看一下下方的這個無限的遞歸方法:
1
2
3
4
5
	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

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

1
2
3
4
5
6
7
 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. 綜合複習

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

1
2
3
4
5
6
7
public class User{
  private String name;
  public User(String name){ 
    this.name = name; 
  }
  //省略getset方法
}
1
2
3
4
5
6
public class Test{
  public static void main(String[] args){ 
    User user1=new User("張三");
      User user2=new User("李四");
   }
}

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

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


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