Java JVM一

1. 內(nèi)存模型以及分區(qū),需要詳細(xì)到每個(gè)區(qū)放什么

  • 程序計(jì)數(shù)器(Program Counter Register)

是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存。
如果正在執(zhí)行的是 Native 方法,這個(gè)計(jì)數(shù)器值則為空。

  • Java 虛擬機(jī)棧(Java Virtual Machine Stacks)

JVM Stack 也是線程私有的,它的生命周期與線程相同。
虛擬機(jī)棧描述的是 Java 方法執(zhí)行的內(nèi)存模型。
每個(gè)方法在執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。
每一個(gè)方法從調(diào)用直至執(zhí)行完成的過程,就對(duì)應(yīng)一個(gè)棧幀在虛擬機(jī)棧中的入棧和出棧。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型、對(duì)象引用和 returnAddress 類型。
其中 64 位長(zhǎng)度的 long 和 double 類型的數(shù)據(jù)會(huì)占用 2 個(gè)局部變量空間(slot),取余的數(shù)據(jù)類型只占用 1 個(gè)。
局部變量表所需要的內(nèi)存空間在編譯期完成分配,在方法的運(yùn)行期不會(huì)改變局部變量表的大小。
StackOverflowError 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度。
OutOfMemoryError 虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展,如果擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存。

  • 本地方法棧(Native Method Stack)

本地方法棧與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法服務(wù),而本地方法棧則為虛擬機(jī)使用到的 Native 方法服務(wù)。

  • Java 堆(Java Heap)

Java 堆是 JVM 所管理的內(nèi)存中最大的一塊。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例。
所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配,但隨著 JIT(即時(shí)編譯器)編譯器的發(fā)展與逃逸分析技術(shù)逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化發(fā)生,所有的對(duì)象都分配在堆上也漸漸變得不是那么絕對(duì)了。
Java 堆是垃圾回收器管理的主要區(qū)域,因此很多時(shí)候也被稱做 GC堆(Garbage Collected Heap)。
Java 堆可以是規(guī)定大小的,也可以是可擴(kuò)展的。當(dāng)前主流的虛擬機(jī)都是按照可擴(kuò)展來實(shí)現(xiàn)的(通過 -Xmx 和 -Xms 控制)。
OutOfMemoryError 堆中沒有內(nèi)存完成實(shí)例分配。

  • 方法區(qū)(Method Area)

方法區(qū)與 Java 堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、及時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
可以選擇固定大小和可擴(kuò)展,可以選擇不實(shí)現(xiàn)垃圾收集。
OutOfMemoryError 方法區(qū)無法滿足內(nèi)存分配需求。

  • 運(yùn)行時(shí)常量池(Runtime Constant Pool)

運(yùn)行時(shí)常量池是方法區(qū)的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池,用于存放編譯器生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
運(yùn)行時(shí)常量池相對(duì)于 Class 文件常量池的另一個(gè)重要特征是具備動(dòng)態(tài)性,運(yùn)行期也可以將常量放入池中,如 String 類 的 intern 方法。
OutOfMemoryError 常量池?zé)o法滿足內(nèi)存分配需求。

  • 直接內(nèi)存(Direct Memory)

直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分。但是同樣可以導(dǎo)致 OutOfMemoryError 異常出現(xiàn)。
JDK1.4 中新加入了 NIO(New Input/Output)類,引入了一種基于通道和緩沖區(qū)的 IO 方式,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在 Java 堆中的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽?Java 堆和 Native 堆中來回復(fù)制數(shù)據(jù)。

2. 堆里面的分區(qū):Eden,survival from to,老年代,各自的特點(diǎn)

  • 分代回收算法

一般是把 Java 堆分為新生代和老年代,這樣可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/p>

  • Eden區(qū)

Eden區(qū)位于Java堆的年輕代,是新對(duì)象分配內(nèi)存的地方,由于堆是所有線程共享的,因此在堆上分配內(nèi)存需要加鎖。而Sun JDK為提升效率,會(huì)為每個(gè)新建的線程在Eden上分配一塊獨(dú)立的空間由該線程獨(dú)享,這塊空間稱為TLAB(Thread Local Allocation Buffer)。在TLAB上分配內(nèi)存不需要加鎖,因此JVM在給線程中的對(duì)象分配內(nèi)存時(shí)會(huì)盡量在TLAB上分配。如果對(duì)象過大或TLAB用完,則仍然在堆上進(jìn)行分配。如果Eden區(qū)內(nèi)存也用完了,則會(huì)進(jìn)行一次Minor GC(young GC)。

  • Survival from to

Survival區(qū)與Eden區(qū)相同都在Java堆的年輕代。Survival區(qū)有兩塊,一塊稱為from區(qū),另一塊為to區(qū),這兩個(gè)區(qū)是相對(duì)的,在發(fā)生一次Minor GC后,from區(qū)就會(huì)和to區(qū)互換。在發(fā)生Minor GC時(shí),Eden區(qū)和Survival from區(qū)會(huì)把一些仍然存活的對(duì)象復(fù)制進(jìn)Survival to區(qū),并清除內(nèi)存。Survival to區(qū)會(huì)把一些存活得足夠舊的對(duì)象移至年老代。

  • 年老代

年老代里存放的都是存活時(shí)間較久的,大小較大的對(duì)象,因此年老代使用標(biāo)記整理算法。當(dāng)年老代容量滿的時(shí)候,會(huì)觸發(fā)一次Major GC(full GC),回收年老代和年輕代中不再被使用的對(duì)象資源。

3. 對(duì)象的創(chuàng)建方法,對(duì)象的內(nèi)存布局,對(duì)象的訪問定位

3.1 對(duì)象的創(chuàng)建方法

對(duì)象所需內(nèi)存的大小在類加載完成后便完全確定(對(duì)象內(nèi)存布局),為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來。
根據(jù)Java堆中是否規(guī)整有兩種內(nèi)存的分配方式:(Java堆是否規(guī)整由所采用的垃圾收集器是否帶有壓縮整理功能決定)

  • 指針碰撞(Bump the pointer) :

Java堆中的內(nèi)存是規(guī)整的,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,分配內(nèi)存也就是把指針向空閑空間那邊移動(dòng)一段與內(nèi)存大小相等的距離。例如:Serial、ParNew等收集器。

  • 空閑列表(Free List) :

Java堆中的內(nèi)存不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),就沒有辦法簡(jiǎn)單的進(jìn)行指針碰撞了。虛擬機(jī)必須維護(hù)一張列表,記錄哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄。例如:CMS這種基于Mark-Sweep算法的收集器。

3.1.1 并發(fā)處理

對(duì)象創(chuàng)建在虛擬機(jī)中時(shí)非常頻繁的行為,即使是僅僅修改一個(gè)指針指向的位置,在并發(fā)情況下也并不是線程安全的,可能出現(xiàn)正在給對(duì)象A分配內(nèi)存,指針還沒來得及修改,對(duì)象B又同時(shí)使用了原來的指針來分配內(nèi)存的情況。

  • 同步

虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性。

  • 本地線程分配緩沖(Thread Local Allocation Buffer, TLAB)

把內(nèi)存分配的動(dòng)作按照線程劃分為在不同的空間之中進(jìn)行,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存(TLAB)。哪個(gè)線程要分配內(nèi)存,就在哪個(gè)線程的TLAB上分配。只有TLAB用完并分配新的TLAB時(shí),才需要同步鎖定。

3.2 對(duì)象的內(nèi)存布局

在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)。

3.2.1 對(duì)象頭

HotSpot虛擬機(jī)的對(duì)象頭包括兩部分信息:運(yùn)行時(shí)數(shù)據(jù)和類型指針。

  • 運(yùn)行時(shí)數(shù)據(jù)

用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等。

  • 類型指針

即對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
如果對(duì)象是一個(gè)Java數(shù)組,那在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過普通Java對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小,但是從數(shù)組的元數(shù)據(jù)中無法確定數(shù)組的大小。
(并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類型指針,換句話說,查找對(duì)象的元數(shù)據(jù)并不一定要經(jīng)過對(duì)象本身,可參考對(duì)象的訪問定位)

3.2.2 實(shí)例數(shù)據(jù)

實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也是在程序代碼中所定義的各種類型的字段內(nèi)容。無論是從父類中繼承下來的,還是在子類中定義的,都需要記錄下來。
HotSpot虛擬機(jī)默認(rèn)的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oop,從分配策略中可以看出,相同寬度的字段總是分配到一起。

3.2.3 對(duì)齊填充

HotSpot虛擬機(jī)要求對(duì)象的起始地址必須是8字節(jié)的整數(shù)倍,也就是對(duì)象的大小必須是8字節(jié)的整數(shù)倍。而對(duì)象頭部分正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒有對(duì)齊的時(shí)候,就需要通過對(duì)齊填充來補(bǔ)全。

3.3 對(duì)象的訪問定位

Java程序需要通過棧上的引用數(shù)據(jù)來操作堆上的具體對(duì)象。對(duì)象的訪問方式取決于虛擬機(jī)實(shí)現(xiàn),目前主流的訪問方式有使用句柄和直接指針兩種。
句柄,可以理解為指向指針的指針,維護(hù)指向?qū)ο蟮闹羔樧兓鴮?duì)象的句柄本身不發(fā)生變化;
指針,指向?qū)ο?,代表?duì)象的內(nèi)存地址。

3.3.1 句柄

Java堆中劃分出一塊內(nèi)存來作為句柄池,引用中存儲(chǔ)對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息。

優(yōu)勢(shì):引用中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而引用本身不需要修改。

3.3.2 直接指針

如果使用直接指針訪問,那么Java堆對(duì)象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,而引用中存儲(chǔ)的直接就是對(duì)象地址。

優(yōu)勢(shì):速度更快,節(jié)省了一次指針定位的時(shí)間開銷。由于對(duì)象的訪問在Java中非常頻繁,因此這類開銷積少成多后也是非??捎^的執(zhí)行成本。(例如HotSpot)

4. GC的兩種判定方法:引用計(jì)數(shù)與可達(dá)性分析算法(對(duì)象已死的判斷方式)

4.1 引用計(jì)數(shù)

給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加1;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1;任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能再被使用的。
Java 虛擬機(jī)里面沒有選用引用計(jì)數(shù)算法來管理內(nèi)存,其中最主要的原因是它很難解決對(duì)象之間相互循環(huán)引用的問題。

4.2 可達(dá)性分析算法(引用鏈)

通過一系列的稱為 GC Roots 的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到 GC Roots 沒有任何引用鏈相連(用圖論的化來說,就是從 GC Roots 到這個(gè)對(duì)象不可達(dá))時(shí),則證明此對(duì)象是不可用的。
在 Java 語言中,可作為 GC Roots 的對(duì)象包括下面幾種:
1. 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
2. 方法區(qū)靜態(tài)屬性引用的對(duì)象
3. 方法區(qū)中常量引用的對(duì)象
4. 本地方法棧中引用的對(duì)象
四類引用:強(qiáng)引用、軟引用、弱引用、虛引用(唯一的目的是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知)

5. GC的三種收集方法:標(biāo)記清除、復(fù)制算法、標(biāo)記整理、分代收集算法的原理與特點(diǎn)

5.1 標(biāo)記清除

如同它的名字一樣,算法分為標(biāo)記和清楚兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有標(biāo)記的對(duì)象。

不足:
一個(gè)是效率問題,標(biāo)記和清除兩個(gè)過程的效率都不高;
另一個(gè)是空間問題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行過程中需要分配較大對(duì)象時(shí),無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。

5.2 復(fù)制算法

為了解決小路問題,復(fù)制算法出現(xiàn)了,它將可用的內(nèi)存按容量分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
這種算法的代價(jià)是將內(nèi)存縮小為原來的一半。

5.3 標(biāo)記整理

復(fù)制收集算法在對(duì)象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作,效率將會(huì)變低。老年代一般不能直接選用這種算法。
根據(jù)老年代的特點(diǎn),提出一種標(biāo)記整理算法,標(biāo)記過程仍然和標(biāo)記清除算法一樣,但是后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。

5.4 分代收集算法

當(dāng)前商業(yè)虛擬機(jī)的垃圾回收都采用分代收集算法,根據(jù)對(duì)象存活周期不同將內(nèi)存劃分為幾塊。一般是把 Java 堆分為新生代和老年代,這樣就可根據(jù)各個(gè)年代的特點(diǎn)采用最適合的收集算法。
在新生代中,每次垃圾收集都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法。
在老年代中,因?yàn)閷?duì)象存活率高,沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用標(biāo)記清除或者標(biāo)記整理算法來進(jìn)行回收。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容