前言
在使用c++進行編程時,我們通過new創建的每一個對象都需要有對應的delete操作去釋放對象所占用的內存,對內存的掌控度比較高,但是程序員需要知道對象什么時候不需要使用了,并需要手動釋放內存,如果忘記了delete釋放,很容易出現內存泄漏(申請內存后,沒有釋放,會一直占用著)和內存溢出(因為過多的內存泄漏導致無法申請足夠的內存,即out of memory)的問題。
相比之下,java虛擬機提供了自動內存管理機制,java程序員可以解放雙手,不再需要去寫delete等手動釋放內存的代碼,虛擬機會自動將內存中無用的對象占用的內存釋放。
了解jvm的必要
雖然有自動內存管理機制的存在,但是不代表寫的每個java程序都不存在內存泄漏和內存溢出問題,我們需要對虛擬機有足夠的了解,才能在發生內存泄露和內存溢出的時候有效地排查問題。
本文將對jvm虛擬機運行時內存進行一個基本的介紹,后續的文章也會講解jvm其他知識,大部分都是自己的讀書總結加上自己的理解。希望將自己的所學進行總結的同時能惠及他人,如果有什么地方講的不對,希望各位同學能夠指出。
內存劃分
java虛擬機將其管理的內存劃分為以下幾塊:
- 程序計數器 (PC Register)
- 虛擬機棧 (JVM Stack)
- 本地方法棧 (Native Method Stack)
- 堆 (Heap)
- 方法區 (Method Area)
各個區域都有其各自的特點和作用,以及不同的創建和銷毀的時間
各個區域的介紹
程序計數器
- 描述
- 程序計數器是一個較小的內存區域
- 作用
- 記錄著當前線程所執行的字節碼行號。
- 字節碼解釋器在工作的時候,通過改變這個計數器的值來選取下一條要執行的字節碼指令。
- 分支,循環,跳轉,異常處理,線程恢復等功能都需要使用到這個程序計數器。
- 特點
- 線程私有--每個線程都有一個獨立的程序計數器。
- 如果當前線程正在執行一個java方法,這程序計數器的值為虛擬機字節碼指令的地址,如果執行的是一個
Native
方法,這個計數器的值則為空。 - 程序計數器是唯一沒有規定OutOfMemoryError的內存區域。
- 創建時間
- 每個線程啟動的時候會創建一個較小的內存區域作為線程的程序計數器
- 銷毀時間
- 線程結束時會釋放該內存區域
擴展問題1:為什么需要程序計數器?
java虛擬機的多線程是通過線程輪轉,分配CPU時間片來執行java程序,當線程切換時,為了能夠回到原來的字節碼執行位置繼續程序的執行,所以每個線程會有一個程序計數器。
擴展問題2:Native方法是什么?
java程序執行的時候調用的方法,有些是用java語言實現的,有些是用其他語言編寫實現的,用其他語言實現的方法稱為Native方法或本地方法,native方法會使用native
關鍵字進行標注,如Object
類的getClass()
方法:
public class Object {
public final native Class<?> getClass();
...
}
由于native方法不是java實現的,也就沒有字節碼行號之說,此時程序計數器的值應當為空(undefined)。
虛擬機棧
- 描述
- 虛擬機棧是描述java方法執行過程的一個內存模型。
- 具體描述:每個方法在執行的時候都會創建一個棧幀,棧幀中存儲的是java方法的局部變量表、操作數棧、動態鏈接、方法出口等信息。java程序在執行的時候每調用一個java方法都會對應的創建棧幀并壓入虛擬機棧中,當方法執行完畢,又會將棧幀從虛擬機棧中彈出。虛擬機棧就是棧幀存放的一個棧結構的內存區域。
- 作用
- 描述java方法執行的過程,保存棧幀。
- 特點
- 線程私有
- 此區域可能會有兩種內存異常情況:
- 當棧的深度大于虛擬機所限制的最大深度,會拋出StackOverflowError異常。
- 如果虛擬機棧動態擴展無法申請到足夠的內存,就會拋出OutOfMemoryError異常。
- 創建時間
- 線程啟動的時候
- 銷毀時間
- 線程結束的時候
擴展1:局部變量表
局部變量表用于存放編譯期可知的各種基本數據類型、對象引用、returnAddress類型(一條字節碼指令的地址)。
對于基本數據類型,存放的是變量的名和值;
對于引用類型,存放的是指向對象在堆中的起始地址。
ps: 對于64位的long或double類型的局部變量會占用兩個局部變量表空間(Slot),其余的數據類型都是只占用一個局部變量表空間。
局部變量表所需要的空間在編譯期間已經計算好了,在一個方法執行時,需要為棧幀分配多少局部變量表空間是完全確定的。
本地方法棧
本地方法棧的特性和虛擬機棧幾乎一樣。
- 本地方法棧與虛擬機棧的區別
- 本地方法棧為本地方法服務
- 本地方法棧可能出現的異常
- 同虛擬機棧一樣可能拋出
StackOverflowError
和OutOfMemoryError
異常。
- 同虛擬機棧一樣可能拋出
堆
- 描述
- 堆內存的唯一目的是存放對象實例。
- 堆內存是垃圾收集的主要區域,因此也叫GC堆
- 作用
- 存放對象實例
- 特點
- 虛擬機所管理的內存中最大的一塊
- 幾乎所有的對象都在堆區分配內存,當然也有例外,JIT編譯器有可能會進行優化,直接在棧上分配,有關信息可以直接搜索“逃逸分析”了解,這不在本文的討論范圍內。
- 所有線程共享的一塊內存區域
- 堆內存在物理上不一定是連續的,保證邏輯連續即可
- 堆內存區域無法滿足分配對象實例所需內存,可能拋出OutOfMemoryError異常
- 堆內存設置固定大小也可以動態擴展,可在啟動參數上指定最小大小及擴容的上限。
- 創建時間
- 虛擬機啟動的時候就創建了堆內存
擴展1: 堆區細分
jvm為了垃圾回收的方便,將堆劃分為新生代
和老年代
,新創建的對象基本上都放在新生代中,而存活比較久的對象則會移到老年代中。新生代和老年代采用不同的垃圾收集算法,可以更高效地回收內存。采用復制算法的新生代還可以細分為Eden
、From Survivor
和To Survivor
。具體的詳情是怎樣的,為了不偏離這篇文章的主旨,這里先打個問號,后序的文章將會詳細介紹堆區的幾個劃分的用途。
堆區雖然是線程共享的,但是如果設定了啟動參數-XX:+UseTLAB
,則開啟了本地線程分配緩沖(Thread local Allocation Buffer, TLAB),會為每個線程單獨在堆中劃分出一個TLAB
,哪個線程需要分配內存,就先在該線程對應的TLAB中分配內存,當TLAB用完,才在堆區的Eden
中繼續申請一塊TLAB
。
方法區
方法區是用于存放虛擬機加載的類信息、常量、靜態變量、編譯后的代碼等數據。
方法區特點:
- 線程共享
- 方法區大小可固定也可以動態擴展。
- 與堆區一樣不需要連續的物理內存,但要求邏輯連續。
- 該區域的垃圾收集目標主要是針對運行時常量池的回收和對類進行卸載。
- 可能出現
OutOfMemoryError
異常。
擴展1:運行時常量池:
class文件中有個常量池,運行時常量池就是class文件中常量池經過類加載后存放的內存區域。
常量池主要存放兩類常量:字面量和符號引用。
字面量指字符串,聲明為final的常量值等;而符號引用是java編譯后生成的各種常量,其包括:
- 類和接口的全限定名
- 成員變量的名稱和描述符
- 方法的名稱和描述符
jdk1.8
之前,方法區是用永久代
實現的,
在jdk1.7
以下的版本,運行時常量池是方法區的一部分,而jdk1.7
及之后的版本,運行時常量池中的字符串常量池已經不在方法區,而是在java堆中開辟了一塊區域作為字符串常量池。
在jdk1.8開始,已經沒有永久代的概念,譬如符號引用(Symbols)轉移到了native 堆中的元空間;字面量也在 java heap;類的靜態變量(class statics)轉移到了java heap
擴展2:常量是否只能在編譯期產生?
否,運行期也可能將新的常量放入運行時常量池中,比如String
的intern
方法。在jdk1.7的表現如下:
// 如果運行時常量池中,存在"10"這個字符串常量
// 則將常量池中的字符串對象返回,
// 如果不存在,則直接在運行時常量池中創建“10"這個字符串,并將其返回。
String s = String.valueOf(10).intern();
直接內存
前面講的幾塊都屬于虛擬機管理的運行時數據區域,java程序中也有可能會用到不是虛擬機運行時內存區域的一部分。這塊內存我們通常稱為直接內存
直接內存不受java堆大小的限制,但是受本機物理內存的限制。
直接內存也可能導致出現OutOfMemoryError異常。
直接內存的例子:
jdk 1.4 加入的NIO類,引入了一種基于通道Channel
和緩沖區Buffer
的IO方式。直接通過Native方法在java堆外的直接內存
中分配內存, 通過存儲在java堆中的DirectByteBuffer對象作為這塊直接內存的引用。操作DirectByteBuffer即可操作直接內存,這樣做的好處是避免了要使用直接內存的時候需要先復制到java堆中。直接操作直接內存更加高效。
點贊是對我最大的鼓勵