Java内存区域分布
本文是《深入理解Java虛擬機》中第二章的讀書總結。
看了幾個星期《深入理解Java虛擬機》,覺得有必要將看到的東西記錄下來,以便日后看看。
Java是平臺無關的語言,也就是說Java代碼可以運行在不同的機器上,要實現這種“一次編碼,處處運行”的目標,Java使用虛擬機來消除平臺多樣性。Java相比于C和C++不同的一個地方在于,Java程序員不需要手動回收內存,而C/C++需要使用delete/free代碼來手動釋放不使用的內存。那么,這里首先介紹一下Java虛擬機的內存區域是如何組織的。
Java虛擬機在執行Java程序的過程中會把管理的內存劃分為若干個不同的數據區域。這些區域都有各自的用途,以及創建和銷毀的時間。Java虛擬機中內存的總體分布如圖所示:
一共包括方法區、虛擬機棧、本地方法棧、堆、程序計數器等部分。其中,方法區和堆是所有線程共享的內存區域,而虛擬機棧、本地方法棧和程序計數器是線程私有的內存區域,線程間不共享這部分的數據。
1、程序計數器
程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看做是當前線程所執行的字節碼的行號指示器。Java虛擬機的程序計數器和CPU的程序計數器類似,CPU中執行的是機器碼,使用程序計數器來指示下一條指令的地址;類似的,在虛擬機的概念模型里,字節碼解釋器工作時就是通過改變這個計數器的值來選定下一條要執行的字節碼指令的行號,因為Java代碼編譯后的Class文件中的字節碼指令是一行一行的,這部分的內容會在后面介紹。Java程序的分支、循環、跳轉、異常處理和線程恢復等基礎功能都需要程序計數器來完成。
由于程序計數器和字節碼指令的執行有關,而每個線程都有各自要執行的字節碼指令。Java虛擬機的多線程是通過為每個線程輪流分配處理器的使用時間來實現的,同一時刻一個處理器只會執行一條線程中的指令。因此,為了處理器切換線程后能夠從正確的地方繼續執行,每個線程都應該有自己的程序計數器,這樣多線程執行的時候才不會相互影響。即,程序計數器是線程私有的內存。
如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Native方法,這個計數器值則為空。此區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。
2、Java虛擬機棧
和程序計數器一樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,生命周期與線程相同。在Java虛擬機中,每個方法的執行都會創建一個棧幀(Stack Frame),棧幀用來存儲局部變量表、操作數棧、動態鏈接、方法出入口等信息。每一個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機中入棧到出棧的信息。
Java每一個線程都會涉及到很多的方法調用,因此每個線程都有自己的虛擬機棧,即線程私有。
棧幀中的局部變量表存放了編譯期間可知的各種基本數據類型(boolean、byte、char、short、int、float、long和double)、對象引用(Reference類型,不等同于對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。
局部變量表中存放數據的基本單元是局部變量空間(Slot),除了64為長度的long和double使用兩個slot外,其他所有類型的數據都使用一個slot。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法所需要的空間大小是完全確定的,在方法運行期間不會改變局部變量表的大小。
Java虛擬機規范對Java虛擬機棧規定了兩個異常情況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機??梢詣討B擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。
3、本地方法棧
本地方法棧(Native Method Stack)和虛擬機棧的作用非常相似,區別就是虛擬機棧為虛擬機執行Java代碼服務,而本地方法棧為虛擬機使用到的Native方法服務。和虛擬機棧一樣,本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。同時,本地方法棧也是線程私有的。
4、Java堆
對于大多數應用來說,Java堆(Java Heap)是Java虛擬機鎖管理的內存中最大的一塊。Java堆是被所有線程共享的一塊區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。隨著JIT編譯器的發展和逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化。
Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”。從內存回收的角度看,由于現在的垃圾收集器基本采用分代收集算法,所以Java堆還可以分為新生代和老年代;再細致的有Eden空間、From Survivor空間和To Survivor空間等。對于垃圾回收的內容會在后面介紹。從內存分配的角度看,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區(Thread Local Allocation Buffer,TLAB)。不過無論怎么劃分,都與存放內容無關,無論哪個區域,存儲的仍都是對象實例。
Java堆可以處理物理上不連續的內存空間中,只要邏輯上是連續的即可。在實現上,堆既可以是大小固定的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(使用-Xmx和-Xms控制)。如果在堆中沒有內存完成實例分配,并且堆也無法擴展時,將會拋出OutOfMemoryError異常。
5、方法區
方法區(Method Area)也是線程共享的內存區域,用于存儲已被虛擬機加載的類信息、常量、靜態變量和即時編譯器編譯后的代碼等數據。
Java虛擬機規范對方法區的限制非常寬松,除了和Java堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾收集。相對而言,垃圾收集行為在這個區域是較少出現的,但并非數據進入了方法區就永久存在了。這個區域的內存回收目標主要針對常量池的回收和對類型的卸載。一般來說,由于類型的卸載的條件非??量?#xff0c;所以這部分的垃圾回收成績不令人滿意。當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。
6、運行時常量池
運行時常量池(Running Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯期間生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。Java虛擬機規范沒有做任何關于此部分的細節要求,一般來說,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。
運行時常量池相對于Class文件常量池的另一個重要特征是具備動態性,Java語言并不要求常量一定只有編譯期間才能產生,也就是并非欲置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量池放入池中。
既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法申請到內存時會拋出OutOfMemoryError異常。
7、直接內存
直接內存(Direct Memory)并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域。但是這部分內存也被頻繁的使用,而且也可能導致OutOfMemoryError異常出現,所以也放在這里介紹。
在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作。這樣能在一些場合中顯著提高性能,因為避免了在Java堆和Native堆中來回復制數據。
由于直接使用本機內存,所以當內存不夠時也會拋出OutOfMemoryError異常。
總結
以上是生活随笔為你收集整理的Java内存区域分布的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 战地2042游戏闪退怎么办
- 下一篇: HotSpot虚拟机在Java堆中对对象