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

zzzzMing發表於2018-08-20

一.綜述

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

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

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

二.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計算相關的變數被修改了,這也有可能導致記憶體洩漏,因為這時候這個對應基本已經不可達了。

 

相關文章