1. 程式人生 > >Java虛擬機器記憶體結構介紹

Java虛擬機器記憶體結構介紹

文章目錄

一 前言

  最近專案開始了各種頻繁的測試,各組工作也日漸緊張,聯機交易及日終批量在高壓力測試下,出現了越來越多的記憶體溢位問題。

  其實這裡說記憶體溢位並不嚴謹,因為我們沒有辦法很直觀的判斷出究竟是溢位還是洩漏。今天又有同事問我:為什麼會出現OOM?這個問題說起來簡單也不簡單,如果要就某一個場景來說,需要很客觀的分析才能下定論;但如果從理論上來闡述,又不是一件很困的事情,所以今天我就OOM問題對JVM記憶體結構做一個很簡單的介紹,希望能對一些朋友有所幫助。

  關於JVM記憶體結構更多的介紹,可以查閱官方釋出的JVM規範,當然如果對這一塊知識沒那麼迫切的需求,在網路上或者其他書籍中獲取一些線索也是不錯的。

二 JVM記憶體結構

  關於OOM的定義,在JVM規範中有很明確的說明,它的發生位置又和具體的記憶體位置緊密相關,所以在瞭解OOM的時候,首要的前提是摸清楚JVM的記憶體結構。

  簡而言之,OOM發生的條件很明確——JVM不足以申請到足夠的記憶體。那麼哪些記憶體結構會頻繁的申請記憶體,申請到的記憶體又被用來做了什麼事情,是否會發生OOM,這些內容我會按每一塊的記憶體結構進行簡單的介紹。

  先行說明下JVM的結構,按JVM規範,其記憶體結構大致分為以下幾部分:

  1. 程式計數器
  2. 虛擬機器棧
  3. 本地方法棧
  4. GC堆
  5. 方法區
  6. 執行時常量區

  後文的對於每一塊記憶體區域的介紹,我會從以下幾點進行描述:

  1. 記憶體區域的用途
  2. 記憶體資料訪問的限制
  3. 是否會發生OOM,何時發生

三 程式計數器

  Program Counter Register,它佔用了JVM非常小的一塊記憶體,小到可以忽略不計。然而其作用異常的關鍵,JVM是無法執行Java原始檔的,它只認Class檔案(並非只有Java原始檔才能編譯出來Class檔案,也就是說Java虛擬機器並非只能執行Java語言編寫的程式,可別把兩者綁死),然而Class也就是位元組碼檔案的執行是需要位元組碼直譯器來處理的,位元組碼直譯器在工作的時候就是通過改變程式計數器的值來選取下一條要執行的位元組碼指令的。

  我們常常編寫的程式流程,包括分支(if,else)、跳轉(break等)、迴圈(for,while等)、異常處理(try,catch等)以及執行緒恢復這些功能的實現全部依賴於程式計數器。你可以把它當作樂隊指揮,何時吹拉彈唱它說了算。

  Java是支援多執行緒的,那麼JVM在處理執行緒切換的時候,如何能夠保證每條執行緒的處理位置是正確的呢?答案也在程式計數器中,它儲存了每條執行緒的已執行到的位置,換言之每個執行緒都有一個獨立的程式計數器,各執行緒的程式計數器間不會互相影響,資料獨立儲存,我們將這種記憶體訪問方式稱之為“執行緒私有”。

  最後,因為程式計數器佔用的記憶體實在是太小了,所以JVM規範中沒有給它定義任何OOM,這是JVM中唯一一個沒有OOM的記憶體區域。

四 虛擬機器棧

  Java Virtual Machine Stacks,學習Java的時候,我們常常提到堆疊,其中棧指的就是Java虛擬機器棧,它用來描述Java方法在執行時候的記憶體模型,每一個方法在被呼叫的時候都會在棧中開闢一塊新的記憶體區域,這部分割槽域我們稱之為棧幀(Stack Frame),棧幀中儲存著方法區域性變數、運算元、動態連結以及返回值等資料,每一個方法從呼叫到結束就是一個棧幀進入Java虛擬機器棧到出棧的過程。

  棧中的資料是不能被多執行緒共享的,也就是說每個Java物件的方法其方法內宣告的變數、運算邏輯等生命週期僅在方法內部,在併發程式設計知識概念中,我們稱之為執行緒封閉。所以說虛擬機器棧的資料訪問是執行緒私有的。

  因為每一個棧幀的大小是不一樣的,這裡不是很嚴謹,應該說大部分情況下是不一樣的,Java方法的邏輯深度決定了棧幀的大小,這裡說的大小指的是其申請到的棧記憶體,因為壓入棧中的資料越多,其佔用的記憶體也越大。換句話說,棧的深度和記憶體消耗是有著直接聯絡的,所以JVM對虛擬機器棧規定了兩種記憶體相關的異常:

  1. StackOverflowError
  2. OutOfMemoryError

  注意,棧溢位僅線上程請求的棧深度大於JVM所允許的深度值時才會發生,而OOM發生在棧記憶體不足時發生,上面我提到過棧的深度和記憶體佔用是有直接關係的,所以本質上來說它們是對記憶體不足的不同描述而已。JVM優化時可以對執行緒分配的記憶體大小進行控制。這部分以後有機會再說。

五 本地方法棧

  Native Method Stack,在Java語法體系中,除JDK提供的由Java編寫的API之外還有一部分本地方法集,它用於與作業系統進行互動。JVM記憶體結構中的兩大棧,VirtualStack和NativeStack,除了用途不一樣,其他的說明差不多,也會出現棧溢位和OOM。

六 GC堆

  Java Heap,常規堆疊說法中的另一大主要記憶體區域——Java堆,我管它叫GC堆(垃圾堆),不僅因為這個名字好玩,也因為這樣描述十分形象,Java Heap是垃圾回收的主要戰場。

  GC堆可以說是JVM中佔用記憶體最大的一塊,其中存放了所有物件例項以及陣列,哪怕方法中宣告的引用物件,其棧中也只是存放了其物件的引用地址,物件的記憶體分配依然在堆上,這部分記憶體區域較為複雜,做JVM調優的時候尤為重要。

  因為所有的物件及陣列的記憶體分配均在堆上,所以這部分的資料訪問是執行緒共享的。

  堆的大小受JVM引數控制,當然也跟具體的作業系統有關,比如說32位的windows,即使我給堆分配了最大10G的記憶體,系統對程序的限制也只有2G,又有什麼用呢?隨著堆中的資料擴充套件,其記憶體消耗越來越大,當無法再申請到足夠記憶體的時候,就會丟擲OOM。

七 方法區

  Method Area,上文中提到的很多和記憶體相關的介紹都是針對物件級別的,而方法區不一樣,它存放JVM載入過的類、常量、靜態變數或者即時編譯器編譯後的程式碼資料。

  想想看方法區中存放的資料就知道這部分資料一定是執行緒共享的。

  這部分資料隨著類載入的越來越多,記憶體消耗也會變得越來越大,一些開源框架尤其喜歡動態建立型別(各種動態代理),對方法區的記憶體壓力變得更大,當記憶體分配不足時就會丟擲OOM。

八 執行時常量池

  Runtime Constant Pool,本質上它屬於方法區,單拎出來是因為它存放的資料更為細緻,方法區中除了存放有類的相關資訊,如成員、方法或者介面等,還有各種字面量和符號引用,這部分資料就存放在執行時常量池,這部分資訊可以參考String.intern()方法。

  既然屬於方法區那麼必然是執行緒共享的。

  既然屬於方法區那麼必然也會出現OOM。

九 直接記憶體

  Direct Memory,其實這部分並未在JVM規範中定義的,但是它的確存在,不知道你們有沒有編寫過NIO,至少我現在參與的專案,其通訊資料傳輸是NIO實現的,NIO使用的就是直接記憶體——更為直觀的說,就是JVM外的本地記憶體。

  既然是記憶體,那麼必然受系統記憶體的限制,除了JVM分配後的記憶體,以及其他程序使用的記憶體,剩下才能給直接記憶體使用,剩的少要的多,就會出現OOM。

十 總結

  綜上,JVM的記憶體結構大致就是這樣,唯一一個沒有OOM的就是程式計數器,其他記憶體區域都會有OOM的可能,那麼如何避免OOM就涉及到了更為複雜層面,不單單要求Java開發人員所編寫的程式是健壯的,它還對系統配置有所要求。

  對於分析OOM問題,已知的手段已經非常多了,這涉及到更為細緻的記憶體分析,包括GC頻次,GC位置等等,後面如果有時間我會對GC以及記憶體優化方面做一些筆記。

  我不是很建議去讀官方的JVM規範,首先它對於每一個Java程式設計師來說有些過於遙遠,而且長篇累牘,不如平時多積累些相關方面的知識,從書本也好,從網路上也好,當然最直觀的比如說我自己,身處一個龐大的專案中,你不得不去面對,這才是提升最快的。