首頁  >  文章  >  Java  >  詳細介紹Java記憶體區域與記憶體溢出異常

詳細介紹Java記憶體區域與記憶體溢出異常

黄舟
黄舟原創
2017-03-17 10:18:411252瀏覽

這篇文章主要介紹了Java記憶體區域與記憶體溢出異常詳解的相關資料,需要的朋友可以參考下

#Java記憶體區域與記憶體溢出異常

#概述

對於C 和C++程式開發的開發人員來說,在記憶體管理領域,程式設計師對記憶體擁有絕對的使用權,但是也要主要到正確的使用和清理內存,這就要求程式設計師有較高的水平。

而對於Java 程式設計師來說,在虛擬機器的自動記憶體管理機制的幫助下,不再需要為每個new 操作去寫配對的delete/free 程式碼,而且不容易出現記憶體洩漏和記憶體溢位問題,看起來由虛擬機器管理記憶體一切都很美好。不過,也正是因為Java 程式設計師把記憶體控制的權力交給了Java 虛擬機,一旦出現記憶體洩漏和溢出方面的問題,如果不了解虛擬機是怎樣使用記憶體的,那排查錯誤將會成為一項異常艱難的工作。

Java執行階段資料區

我們一般在開發中認為JVM不過有堆疊和堆疊兩部分組成,但是實際的Java 虛擬機器在執行Java 程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區。這些區域都有各自的用途,以及創建和銷毀的時間,有的區域隨著虛擬機器進程的啟動而存在,有些區域則是依賴用戶執行緒的啟動和結束而建立和銷毀。如下圖:

程式計數器

#如果學習過電腦組成原理的應該很清楚,程式計數器就相當於身分證一樣,由於JVM也有自己的CPU,在執行多執行緒程式的時候,透過時間片輪轉的方式,根據程式計數器來調度執行緒的執行。

程式計數器( Program Counter Register)是一塊較小的記憶體空間,它的作用可以看做是目前執行緒所執行的字節碼的行號指示器。在虛擬機器的概念模型裡(僅是概念模型,各種虛擬機器可能會透過一些更有效率的方式去實現),字節碼解釋器工作時就是透過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、執行緒復原等基礎功能都需要依賴這個計數器來完成。

由於Java 虛擬機器的多執行緒是透過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核心處理器來說是一個核心)只會執行一條線程中的指令。因此,為了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存。

如果執行緒正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機器字節碼指令的位址;如果正在執行的是Natvie 方法,這個計數器值則是空( Undefined) 。此記憶體區域是唯一在Java 虛擬機器規格中沒有規定任何 OutOfMemoryError 情況的區域。

Java 虛擬機器堆疊

與程式計數器一樣, Java 虛擬機器堆疊( Java Virtual Machine Stacks)也是執行緒私有的,它的生命週期與線程相同。

虛擬機器堆疊描述的是Java 方法執行的記憶體模型:每個方法執行的時候都會同時建立一個堆疊幀( Stack Frame)用於儲存局部變數表、操作棧、動態連結、方法出口等資訊。每一個方法被呼叫直到執行完成的過程,就對應一個堆疊幀在虛擬機器棧中從入棧到出棧的過程。

經常有人把 Java 記憶體區分為堆疊記憶體( Heap)和堆疊記憶體( Stack),這種分法比較粗糙, Java 記憶體區域的劃分其實遠比這複雜。這種劃分方式的流行只能說明大多數程式設計師最關注的、與物件記憶體分配關係最密切的記憶體區域是這兩塊。其中所指的「堆」在後面會專門講述,而所指的「棧」就是現在講的虛擬機棧,或者說是虛擬機棧中的局部變數表部分。

局部變數表存放了編譯期可知的各種基本資料型別( boolean、 byte、 char、 short、 int、 float、long、 double)、物件參考( reference 類型,它不等同於物件本身,根據不同的虛擬機器實現,它可能是指向物件起始位址的引用指針,也可能指向一個代表物件的句柄或者其他與此物件相關的位置)和returnAddress 類型(指向了一條字節碼指令的位址)。

其中 64 位元長度的 long 和 double 類型的資料會佔用 2 個局部變數空間(Slot),其餘的資料型別只佔 1 個。局部變數表所需的記憶體空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變數空間是完全確定的,在方法運行期間不會改變局部變數表的大小。

在Java 虛擬機器規格中,對這個區域規定了兩種例外狀況:如果執行緒請求的堆疊深度大於虛擬機器所允許的深度,將會拋出StackOverflowError 例外;如果虛擬機器堆疊可以動態擴展(目前大部分的Java 虛擬機器都可動態擴展,只不過Java 虛擬機規格中也允許固定長度的虛擬機堆疊),當擴展時無法申請到足夠的記憶體時會拋出OutOfMemoryError 例外。

本機方法堆疊

本機方法堆疊( Native Method Stacks)與虛擬機器堆疊所扮演的角色是非常相似的,其差異不過是虛擬機器堆疊為虛擬機器執行Java 方法(也就是字節碼)服務,而本機方法堆疊則是為虛擬機器使用到的Native方法服務。虛擬機器規範中對本機方法堆疊中的方法所使用的語言、使用方式與資料結構並沒有強制規定,因此具體的虛擬機器可以自由實作它。甚至有的虛擬機器(譬如 Sun HotSpot 虛擬機器)直接就把本地方法堆疊和虛擬機器棧合而為一。與虛擬機器堆疊一樣,本地方法堆疊區域也會拋出StackOverflowError 和OutOfMemoryError 異常。

Java 堆疊

對大多數應用程式來說, Java 堆( Java Heap)是 Java 虛擬機器所管理的記憶體中最大的一塊。 Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時創建。此記憶體區域的唯一目的就是存放物件實例,幾乎所有的物件實例都在這裡分配記憶體。這一點在Java 虛擬機規範中的描述是:所有的物件實例以及數組都要在堆上分配,但是隨著JIT 編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的物件都分配在堆上也漸漸變得不是那麼「絕對」了。

ava 堆是垃圾收集器管理的主要區域,因此很多時候也被稱為「GC 堆( 」 Garbage Collected Heap,幸好國內沒翻譯成「垃圾堆」)。如果從記憶體回收的角度來看,由於現在收集器基本上都是採用的分代收集演算法,所以Java 堆中還可以細分為:新生代和老年代;再細緻一點的有Eden 空間、 From Survivor空間、 To Survivor 空間等。如果從記憶體分配的角度來看,執行緒共享的 Java 堆中可能會分割出多個執行緒私有的分配緩衝區( Thread Local Allocation Buffer, TLAB)。不過,無論如何劃分,都與存放內容無關,無論哪個區域,存儲的都仍然是對象實例,進一步劃分的目的是為了更好地回收內存,或者更快地分配內存。在本章中,我們僅僅針對記憶體區域的角色進行討論, Java 堆中的上述各個區域的分配和回收等細節將會是下一章的主題。

根據 Java 虛擬機器規範的規定, Java 堆可以處於物理上不連續的記憶體空間中,只要邏輯上是連續的即可,就像我們的磁碟空間一樣。在實現時,既可以實現成固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(透過-Xmx 和-Xms 控制)。如果在堆中沒有記憶體完成實例分配,且堆也無法再擴展時,將會拋出 OutOfMemoryError 異常。

方法區

方法區( Method Area)與Java 堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常數靜態變數、即時編譯器編譯後的程式碼等資料。雖然 Java 虛擬機器規範把方法區描述為堆的一個邏輯部分,但它卻有一個名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。

Java 虛擬機器規格對這個區域的限制非常寬鬆,除了和 Java 堆一樣不需要連續的記憶體和可以選擇固定大小或可擴充外,還可以選擇不實作垃圾收集。相對而言,垃圾收集行為在這個區域是比較少出現的,但並非資料進入了方法區就如永久代的名字一樣「永久」存在了。這個區域的記憶體回收目標主要是針對常量池的回收和對類型的卸載,一般來說這個區域的回收「成績」比較難以令人滿意,尤其是類型的卸載,條件相當苛刻,但是這部分區域的回收確實是有必要的。

根據 Java 虛擬機器規格的規定,當方法區無法滿足記憶體分配需求時,就會拋出OutOfMemoryError 例外。

執行時間常數池

執行時間常數池( Runtime Constant Pool)是方法區的一部份。 Class 檔案中除了有類別的版本、欄位、方法、介面等描述等資訊外,還有一項資訊是常數池( Constant Pool Table),用於存放編譯期產生的各種字面量和符號引用,這部分內容將在類別載入後存放到方法區的運行時常數池中。

Java 虛擬機器對Class 檔案的每一部分(自然也包括常數池)的格式都有嚴格的規定,每個位元組用於儲存哪種資料都必須符合規範上的要求,這樣才會被虛擬機器認可、裝載和執行。但對於執行時間常數池, Java 虛擬機器規格沒有做任何細節的要求,不同的提供者實現的虛擬機器可以按照自己的需求來實現這個記憶體區域。不過,一般來說,除了保存 Class 檔案中描述的符號引用外,還會將翻譯出來的直接引用也儲存在運行時常數池中。

執行階段常數池相對於Class 檔案常數池的另一個重要特徵是具備動態性, Java 語言並不要求常數一定只能在編譯期產生,也就是並非預置入Class 檔案中常數池的內容才能進入方法區運行時常數池,運行期間也可能將新的常數放入池中,這種特性被開發人員利用得比較多的便是String 類別的intern()方法。

既然執行時間常數池是方法區的一部分,自然會受到方法區記憶體的限制,當常數池無法再申請到記憶體時會拋出 OutOfMemoryError 例外。

直接記憶體

直接記憶體( Direct Memory)並不是虛擬機器運行時資料區的一部分,也不是Java 虛擬機規格中定義的記憶體區域,但是這部分記憶體也被頻繁地使用,也可能導致OutOfMemoryError 異常出現。

在JDK 1.4 中新加入了NIO ( New Input/Output)類,引入了一種基於通道( Channel)與緩衝區( Buffer)的I/O 方式,它可以使用Native 函數庫直接分配堆外內存,然後透過一個儲存在Java 堆裡面的DirectByteBuffer 物件作為這塊記憶體的參考進行操作。這樣能在一些場景中顯著提高效能,因為避免了在 Java 堆和 Native 堆中來回複製資料。

顯然,本機直接記憶體的分配不會受到Java 堆大小的限制,但是,既然是內存,則肯定還是會受到本機總內存(包括RAM 及SWAP 區或者分頁文件)的大小及處理器尋址空間的限制。伺服器管理員配置虛擬機參數時,一般會根據實際內存設定-Xmx 等參數信息,但經常會忽略掉直接內存,使得各個內存區域的總和大於物理內存限制(包括物理上的和操作系統級的限制),從而導致動態擴充時出現OutOfMemoryError 異常。

以上是詳細介紹Java記憶體區域與記憶體溢出異常的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn