Java虛拟機在執行Java程式的過程中會把它所管理的記憶體劃分為若幹不同的資料區域,這些區域都有各自的用途以及建立和銷毀的時間。
Java虛拟機所管理的記憶體将會包含下面幾個執行時資料區域,例如以下圖所看到的:

以下就每個區域進行闡述。
回到頂部
程式計數器,能夠看做是目前線程所運作的位元組碼的行号訓示器。在虛拟機的概念模型裡。位元組碼解釋器工作就是通過改變程式計數器的值來選擇下一條須要運作的位元組碼指令,分支、循環、跳轉、異常處理、線程恢複等基礎功能都要依賴這個計數器來完畢。
多線程中,為了讓線程切換後能恢複到正确的運作位置,每條線程都須要有一個獨立的程式計數器。各條線程之間互不影響、獨立存儲,是以這塊記憶體是線程私有的。
當線程正在運作的是一個Java方法。這個計數器記錄的是在正在運作的虛拟機位元組碼指令的位址;當運作的是Native方法,這個計數器值為空。
此記憶體區域是唯一一個沒有規定不論什麼OutOfMemoryError情況的區域。
Java虛拟機棧也是線程私有的,它的生命周期與線程同樣。
虛拟機棧描寫叙述的是Java方法運作的記憶體模型:每個方法在運作的同一時候都會建立一個棧幀用于存儲局部變量表、操作數棧、動态連結清單、方法出口資訊等。每個方法從調用直至運作完畢的過程,就相應着一個棧幀在虛拟機棧中入棧到出棧的過程。
局部變量表中存放了編譯器可知的各種基本資料類型(boolean、byte、char、short、int、float、long、double)、對象引用和returnAddress類型(指向了一條位元組碼指令的位址)。
假設擴充時無法申請到足夠的記憶體。就會抛出OutOfMemoryError異常。
本地方法棧與虛拟機的作用相似,不同之處在于虛拟機棧為虛拟機運作的Java方法服務,而本地方法棧則為虛拟機使用到的Native方法服務。有的虛拟機直接把本地方法棧和虛拟機棧合二為一。
會抛出stackOverflowError和OutOfMemoryError異常。
Java堆是全部線程共享的一塊記憶體區域。在虛拟機啟動時建立。此記憶體區域的唯一目的就是存放對象執行個體。
Java堆是垃圾收集器管理的主要區域。
因為如今收集器基本採用分代回收算法。是以Java堆還可細分為:新生代和老年代。從記憶體配置設定的角度來看,線程共享的Java堆中可能劃分出多個線程私有的配置設定緩沖區(TLAB)。
Java堆能夠處于實體上不連續的記憶體空間,僅僅要邏輯上連續的就可以。
在實作上,既能夠實作固定大小的,也能夠是擴充的。
假設堆中沒有記憶體完畢執行個體配置設定,而且堆也無法完畢擴充時。将會抛出OutOfMemoryError異常。
方法區是各個線程共享的記憶體區域。它用于存儲已被虛拟機載入的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。
相對而言,垃圾收集行為在這個區域比較少出現,但并不是資料進了方法區就永久的存在了。這個區域的記憶體回收目标主要是針對常量池的回收和對類型的解除安裝,
當方法區無法滿足記憶體配置設定須要時,将抛出OutOfMemoryError異常。
是方法區的一部分,它用于存放編譯期生成的各種字面量和符号引用。
直接記憶體不是虛拟機執行時資料區的一部分。在NIO類中引入一種基于通道與緩沖區的IO方式,它能夠使用Native函數庫直接配置設定堆外記憶體,然後通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊記憶體的引用進行操作。
直接記憶體的配置設定不會受到Java堆大小的限制,可是會受到本機記憶體大小的限制,全部也可能會抛OutOfMemoryError異常。
建立一個對象一般是須要newkeyword。當虛拟機遇到一條new指令時,首先檢查這個指令的參數是否在常量池中定位到一個類的符号引用,而且檢查這個符号引用代表的類是否已被載入、解析和初始化過。假設那麼運作對應的類載入過程。
類載入檢查通過後,虛拟機将為新生對象配置設定記憶體。為對象配置設定空間的任務等同于把一塊确定大小的記憶體從Java堆中劃分出來。配置設定的方式有兩種:一種叫指針碰撞,如果Java堆中記憶體是絕對規整的,用過的和空暇的記憶體各在一邊,中間放着一個指針作為分界點的訓示器。配置設定記憶體就是把那個指針向空暇空間的那邊挪動一段與對象大小相等的距離。
還有一種叫空暇清單:如果Java堆中的記憶體不是規整的。虛拟機就須要維護一個清單。記錄哪個記憶體塊是可用的,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的記錄。採用哪種配置設定方式是由Java堆是否規整決定的,而Java堆是否規整是由所採用的垃圾收集器是否帶有壓縮整理功能決定的。
另外一個須要考慮的問題就是對象建立時的線程安全問題,有兩種解決方式:一是對配置設定記憶體空間的動作進行同步處理。還有一種是吧記憶體配置設定的動作依照線程劃分在不同的空間之中進行。即每一個線程在Java堆中預先配置設定一小塊記憶體(TLAB)。哪個線程要配置設定記憶體就在哪個線程的TLAB上配置設定,僅僅有TLAB用完并配置設定新的TLAB時才須要同步鎖定。
記憶體配置設定完畢後,虛拟機須要将配置設定到的記憶體空間初始化為零值。
這一步操作保證了對象的執行個體字段在Java代碼中能夠不賦初始值就能夠直接使用。
接下來虛拟機要對對象進行必要的設定,比如這個對象是哪個類的執行個體、怎樣才幹找到類的中繼資料資訊等,這些資訊存放在對象的對象頭中。
上面的工作都完畢以後,從虛拟機的角度來看一個新的對象已經産生了。可是從Java程式的角度,還須要運作init方法。把對象依照程式猿的意願進行初始化,這樣一個真正可用的對象才算全然産生出來。
在HotSpot虛拟機中。對象在記憶體中存儲的布局可分為三個部分:對象頭、執行個體資料和對齊填充。
對象頭包含兩個部分:第一部分用于存儲對象自身的執行時資料,如哈希碼、GC分代年齡、線程所持有的鎖等。官方稱之為“Mark Word”。第二個部分為是類型指針。即對象指向它的類中繼資料的指針。虛拟機通過這個指針來确定這個對象是哪個類的執行個體。
執行個體資料是對象真正存儲的有效資訊。也是程式代碼中所定義的各種類型的字段内容。
對齊填充并非必定存在的,隻起着占位符的作用。、Hotpot VM要求對象起始位址必須是8位元組的整數倍,對象頭部分正好是8位元組的倍數。是以當執行個體資料部分沒有對齊時,須要通過對齊填充來對齊。
Java程式通過棧上的reference資料來操作堆上的詳細對象。基本的訪問方式有使用句柄和直接指針兩種:
句柄:Java堆将會劃出一塊記憶體來作為句柄池,引用中存儲的就是對象的句柄位址。而句柄中包括了對象執行個體資料與類型資料各自的詳細位址資訊。如圖所看到的:
直接指針:Java堆對象的布局要考慮怎樣放置訪問類型資料的相關資訊,引用中存儲的就是對象位址。
如圖所看到的:
兩個方式各有優點,使用句柄最大的優點是引用中存儲的是穩定的句柄位址,對象被移動時僅僅會改變句柄中執行個體的位址。引用不須要改動、使用直接指針訪問的優點是速度更快。它節省了一次指針定位的時間開銷。