天天看點

Java的記憶體 -JVM 記憶體管理

一.綜述

如果你學過C或者C++,那麼你應該感受過它們對記憶體那種強大的掌控力。但是強大的能力往往需要更強大的控制力才能保證能力不被濫用,如果濫用C/C++的記憶體管理那麼很容易出現指針滿天飛的情況,不出問題還好,一出問題debug起來簡直讓人頭疼得不要不要的。借用一句話,“指針一時爽,重構火葬場”。

而對java程式員來說,則沒有這樣的煩惱,因為java直接将記憶體管理交由jvm來管理,這樣程式員在編寫程式的時候就不用擔心記憶體的使用情況而可以專注内容的實作。但這其實也造成了一點隐患,如果你不了解jvm記憶體管理的機制,很可能會因一些錯誤的代碼寫法而導緻記憶體洩漏或記憶體溢出。

注:所述内容取自Jdk1.6。

二.jvm記憶體結構
Java的記憶體 -JVM 記憶體管理

三.每部分存儲了哪些資料

1.程式計數器

程式計數器是一塊較小的記憶體空間,可以看作目前線程所執行位元組碼的行号訓示器,即指向正在執行的位元組碼。在概念模型中,位元組碼解釋器的工作就是通過改變這個程式計數器的值來選取下一條位元組碼的指令。

值得一提的是,因為java的多線程是通過線程輪流切換并配置設定處理器執行時間來實作的(即一個小的時間段内仍然隻有一個線程處于運作狀态),每個線程的執行指令都不一樣,為了使線程切換後能正确執行到該線程的下一指令,每個線程都需要一個獨立的程式計數器,是以程式計數器使線程私有的。

2.虛拟機棧

虛拟機堆和虛拟機棧可以說是jvm記憶體中最值得我們關注的兩塊記憶體區域。虛拟機棧是記憶體私有的,每個方法在執行的同時會建立一個棧幀。用于存儲局部變量表,操作數棧,動态連結等資訊。每一個方法調用到執行完成的過程,其實就是對應一個棧幀在虛拟機棧中入棧到出棧的過程。

在這個區域可能出現的異常情況有兩種,分别是StackOverflowError和OutOfMemoryError。當棧動态拓展過深,比如無限遞歸時會出現StackOverflowError,而當無法申請到足夠記憶體時,則發生OutOfMemoryError。

3.堆

對大部分應用來說,堆是jvm管理的記憶體中最大的一塊。與虛拟機棧不同,堆是被所有線程共享的。它的作用是存放對象的執行個體,幾乎所有的對象執行個體都在這裡配置設定記憶體。

堆同時也是垃圾收集器管理的主要區域,從記憶體回收的角度看,java堆可以分為“新生代”和“老年帶”。

java堆可以處于實體上不連續的記憶體空間中,隻需要其是邏輯上連續的即可,如我們的磁盤空間。當在堆中無法申請到足夠的記憶體空間時,會抛出OutOfMemoryError。

在JDK1.6之前,字元串常量池一直放在方法區中,但是到了jdk1.7的時候,常量池便被移出方法區,而轉到Java堆中區了。

在HotSpot虛拟機裡實作的字元串常量池功能的是一個StringTable類,它是一個Hash表。這個哈希表在每個HotSpot虛拟機的執行個體隻有一份,被所有的類共享。字元串常量由一個一個字元組成,并且相同字元串隻保留一份。

HotSpot虛拟機的說明如下:

Area: HotSpot 

Synopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences. 

RFE: 6962931 

大意便是說JDK1.7中的字元串不會再配置設定到Java的永久代中,而是配置設定到Java堆中。這意味着更多的資料将存于堆中而更少的資料存于方法區,這導緻堆大小需要調整以做适配。由于此更改,大多數應用程式隻會看到堆使用中的相對較小的差異,較大型的應用可能會發現其顯著差異

4.方法區

與java堆一樣,方法區也是所有線程共享的。它主要的功能時存儲虛拟機加載的類資訊,常量,靜态變量,編譯後的代碼資料等。可以明顯發現,方法區存放的這些資料都是比較難以被回收的,是以這個區的垃圾回收行為較少發生。

若在方法區中無法申請到足夠的記憶體時,将會抛出OutOfMemoryError。

另外方法區中有一個運作時常量池。注意這裡不是字元串常量池,它存儲的是類編譯時期生成的各種字面量和符号引用,并且每個類都有一個。

這裡介紹一下什麼是字面量和符号引用:

  • 字面量包括:1.文本字元串 2.八種基本類型的值 3.被聲明為final的常量等;
  • 符号引用包括:1.類和方法的全限定名 2.字段的名稱和描述符 3.方法的名稱和描述符。

四.記憶體溢出和記憶體洩漏

記憶體溢出很好了解,就是發生OutOfMemoryError,比如當Java堆中建立了太多執行個體,耗完記憶體後就會發生記憶體溢出。比如如下執行個體代碼:

public class HeapOOM{

     static class OOMObject{}

     public static void main(String[] args){
         List<OOMObject> list = new ArrayList<OOMObject>();
         while(true){
             list.add(new OOMObject());
         }
     }
 }           

由于無限循環不斷建立對象,最終會導緻記憶體溢出。

那麼記憶體洩漏呢?

記憶體洩漏的原因主要是一個對象已經不再需要使用,但被另一個長對象持有時,就有可能發生記憶體洩漏。比如在方法内一個對象被全局的HashMap持有,方法執行結束沒有釋放就會導緻記憶體洩漏。

再有就是當一個對象被存儲進HashSet後,其hashcode計算相關的變量被修改了,這也有可能導緻記憶體洩漏,因為這時候這個對應基本已經不可達了。