天天看點

Java虛拟機--筆記

好記性不如爛筆頭,對Java虛拟機部分做個筆記。–閱讀cyc大佬的筆記

一、運作時資料區域/Java記憶體區域

JDK1.8:

與之前的最大差別是:中繼資料區取代了永久代。

中繼資料區不在虛拟機中,而是使用本地記憶體。

方法區是一個JVM規範,永久代與中繼資料區本質上都是方法區的實作。

JDK1.8之後,原來永久代的資料被分到堆和元空間中。元空間存儲類的元資訊,堆中存放靜态變量和常量池等。

Java虛拟機--筆記
Java虛拟機--筆記

面試題:程式計數器、虛拟機棧、本地方法棧為什麼是私有的?

1、程式計數器私有是為了線程切換後能恢複到正确的執行位置。
	2、虛拟機棧、本地方法棧私有是為了保證線程中的局部變量不被其它線程通路到。
           

1、程式計數器

是線程私有的。----主要是為了線程切換後能恢複到正确的執行位置。

作用:

1、位元組碼解釋器通過改變程式計數器來依次讀取指令,進而實作代碼的流程控制。如:順序執行、選擇、循環、異常處理。

2、在多線程的情況下,程式計數器用于記錄目前線程執行的位置,當線程被切換回來的時候知道該線程上次執行到何處。

記錄正在執行的虛拟機位元組碼指令的位址,如果正在執行的是本地方法則記錄的是 undefined 位址。

注意:程式計數器是唯一一個不會出現OOM的記憶體區域,與線程同生共死。

2、Java虛拟機棧

Java虛拟機棧是Java方法執行的記憶體模型,棧中存放棧幀,每個棧幀對應一個被調用的方法

從方法調用至執行完成的過程,對應一個棧幀在Java虛拟機棧中入棧和出棧的過程。

棧是線程私有的,就是說線程之間的棧是隔離的;當程式中某個線程開始執行一個方法時就會相應地建立一個棧幀并且入棧,位于棧頂,方法結束後,棧幀出棧。

棧幀用于存儲局部變量表、操作數棧、常量池引用等資訊。

Java虛拟機--筆記

棧幀:是用于支援虛拟機進行方法調用和方法執行的資料結構,是虛拟機運作時資料區中的虛拟機棧的棧元素。

每個棧幀中包括:

1、局部變量表:用來存儲方法中的局部變量(非靜态變量、函數形參)。

變量為基本資料類型時,直接存儲值;變量為引用類型時,存儲指向具體對象的引用。

2、操作數棧:Java虛拟機的解釋執行引擎–基于棧的執行引擎,棧即是操作數棧。

3、指向運作時常量池的引用:存儲程式執行時可能用到常量的引用。

4、方法傳回位址:存儲 方法執行完成後的傳回位址。

可以通過**-Xss參數**指定每個線程的Java虛拟機棧記憶體大小,JDK1.4中預設為256K,1.5之後預設為1M。

java -Xss2M HackTheJava
           

該區域可能抛出以下異常:

1、當線程請求的棧深度超過最大值,會抛出StackOverflowError

2、棧進行動态擴充時如果無法申請到足夠記憶體,會抛出OutOfMemoryError

3、本地方法棧

本地方法棧類似Java虛拟機棧,也是線程私有的,Java虛拟機棧為執行Java方法服務,本地方法棧為本地方法服務。

一般是用其它語言編寫的,被編譯為基于本機硬體和作業系統的程式,對這些方法需要特别處理。

4、堆

堆用來存儲對象本身和數組,JVM中隻有一個堆,是程序中最大的一塊記憶體,被所有線程共享。

所有的對象都在這裡配置設定記憶體,是垃圾收集的主要區域("GC"堆)。

現代的垃圾收集器基本采用分代收集算法,針對不同類型的對象采用不同的垃圾回收算法。

将堆分成:新生代(Young Generation)、老年代(Old Generation)

堆不需要連續記憶體,可以動态增加記憶體,增加失敗會抛出 OutOfMemoryError 異常。

可以通過 -Xms 和 -Xmx 這兩個虛拟機參數來指定一個程式的堆記憶體大小,第一個參數設定初始值,第二個參數設定最大值。

java -Xms1M -Xmx2M HackTheJava
           
一般情況下堆的分區:

老年代 : 三分之二的堆空間
年輕代 : 三分之一的堆空間
	eden區: 8/10 的年輕代空間
	survivor0 : 1/10 的年輕代空間
	survivor1 : 1/10 的年輕代空間
           

5、方法區

方法區是一塊所有線程共享的記憶體邏輯區域,JVM中隻有一個方法區,用來存儲一些線程可共享内容。

是線程安全的,多個線程同時通路方法區中同一個内容時,隻能有一個線程裝載該資料,其他線程隻能等待。

方法區可存儲的内容有:類的全路徑名、類的直接超類的全限定名、類的通路修飾符、類的類型(類或接口)、類的直接接口全限定名的有序清單、常量池(字段,方法資訊,靜态變量,類型引用(class))等。

用于存放已被加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等。

和堆一樣不需要連續的記憶體,可以動态擴充,動态擴充失敗一樣會抛出 OutOfMemoryError 異常。

對這塊區域垃圾回收的主要目标是對常量池的回收和對類的解除安裝,但是一般難以實作。

HotSpot 虛拟機把它當成永久代來進行垃圾回收。但很難确定永久代的大小,因為它受到很多因素影響,并且每次 Full GC 之後永久代的大小都會改變,是以經常會抛出 OutOfMemoryError 異常。

為了更容易管理方法區,從 JDK 1.8 開始,移除永久代,并把方法區移至元空間,它位于本地記憶體中,而不是虛拟機記憶體中。

方法區是一個JVM規範,永久代與元空間都是一種實作方式。

JDK1.8之後,原來永久代的資料被分到堆和元空間中。元空間存儲類的元資訊,堆中存放靜态變量和常量池等。

面試題:為什麼要将永久代替換為元空間?

1、永久代有一個JVM本身設定的固定大小上限,無法進行調整。

而元空間使用直接記憶體,受本機可用記憶體的限制。

2、元空間記憶體放類的中繼資料,存放數量不再由永久代的最大大小限制,可以加載更多的類。

3、JRockit中沒有永久代這個東西,JDK1.8合并HotSpot和JRockit也不需要這個。

6、運作時常量池

是方法區的一部分。

Class檔案中的常量池(編譯器生成的字面量和符号引用)會在類加載後被放入這個區域。

除了編譯期生成的常量,還允許動态生成,如String類的intern()。

7、直接記憶體

不是虛拟機運作時資料區的一部分,但是也被頻繁使用。

JDK1.4中新引入了NIO類,可以使用Native函數庫直接配置設定堆外記憶體,然後通過Java堆裡的DirectByteBuffer 對象作為這塊記憶體的引用進行操作。

避免了在堆記憶體和堆外記憶體來回拷貝資料。

二、垃圾收集

針對堆和方法區進行。

程式計數器、虛拟機棧、本地方法棧這三個區域屬于線程私有,隻存在于線程的生命周期内,線程結束即消失。

面試題:如何判斷一個常量是廢棄常量?

運⾏時常量池主要回收的是廢棄的常量。

假如在常量池中存在字元串 “abc”,如果目前沒有任何String對象引⽤該字元串常量的話,就說明常量 “abc” 就是廢棄常量,如果這時發⽣記憶體回收的話⽽且有必要的話, “abc” 就會被系統清理出常量池。

1、判斷一個對象是否可被回收

1、引用計數算法

為對象添加一個引用計數器,當對象增加一個引用時計數器加一,引用失效時計數器減一。

引用計數為0的對象可被回收。

若出現兩個對象循環引用的情況,此時引用計數器永不為0,無法回收。

循環引用的存在,Java虛拟機不使用引用計數器算法。

2、可達性分析算法

以GC Roots為起始點進行搜尋,可達的對象都是存活的,不可達的對象可被回收。

Java虛拟機--筆記

Java虛拟機使用可達性分析算法判斷對象是否可被回收。

GC Roots一般包含:

1、虛拟機棧中局部變量表中引用的對象

2、本地方法棧中JNI中引用的對象

3、方法區中類靜态屬性引用的對象

4、方法區中的常量引用的對象

3、方法區的回收

方法區主要存放永久代對象,主要是對常量池的回收和對類的解除安裝。

為避免記憶體溢出,在大量使用反射和動态代理的場景都需要虛拟機具備類的解除安裝功能。

面試題:如何判斷一個類是無用的類?

類的解除安裝條件很多,需滿足以下三個條件,滿足條件也不一定會被解除安裝:

1、該類所有執行個體都已經被回收,此時堆中不存在該類的任何執行個體。

2、加載該類的ClassLoader已經被回收。

3、該類對應的Class對象沒有在任何地方被引用===無法在任何地方通過反射通路該類方法。

4、finalize()

用于關閉外部資源。

但是try-finally等方式可以做得更好,并且finalize()方法運作代價大,不确定性大,無法保證各對象調用順序,最好不使用。

當一個對象可以被回收時,如果需要執行該對象的finalize方法,那麼可能在該方法中讓對象重新被引用,實作自救。自救隻能進行一次,如果回收的對象之前調用了該方法自救,後面回收時不會再調用該方法。

2、四大引用類型

判斷對象是否可被回收與引用有關。

1、強引用

被強引用關聯的對象不會被回收。

使用new一個新對象的方式建立強引用。

2、軟引用

被軟引用關聯的對象隻有在記憶體不夠的情況下才會被回收。

使用SoftReference類建立軟引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使對象隻被軟引用關聯
           

3、弱引用

被弱引用關聯的對象一定會被回收,隻能存活到下一次垃圾回收發生之前。

使用WeakReference類建立弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<>(obj);
obj = null;
           

4、虛引用

又稱幽靈引用或幻影引用。

一個對象是否有虛引用的存在,不會對其生存時間造成影響,也無法通過虛引用得到一個對象。

設定虛引用的唯一目的是能在這個對象被回收時收到一個系統通知。

使用PhantomReference來建立虛引用。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
           

3、垃圾收集算法

1、标記-清除

Java虛拟機--筆記

在标記階段,程式會檢查每個對象是否為活動對象,若是活動對象則程式會在對象頭部打上标記。

在清除階段,會進行對象回收并取消标志位。

另外,還會判斷回收後的分塊與前一個空閑分塊是否連續;若連續,會合并這兩個分塊。

回收對象就是把對象作為分塊,連接配接到被稱為 “空閑連結清單” 的單向連結清單,之後進行配置設定時隻需周遊這個空閑連結清單,就可找到分塊。

在配置設定時,程式會搜尋空閑連結清單尋找空間大于等于新對象大小size的塊block。

如果它找到的塊等于 size,會直接傳回這個分塊;如果找到的塊大于 size,會将塊分割成大小為 size 與 (block - size) 的兩部分,傳回大小為 size 的分塊,并把大小為 (block - size) 的塊傳回給空閑連結清單。

不足:

1、标記和清除過程效率都不高。

2、會産生大量不連續的記憶體碎片,導緻無法給大對象配置設定記憶體。

2、标記-整理

Java虛拟機--筆記

讓所有存活的對象都向一端移動,然後直接清理掉邊界以外的記憶體。

優點:不會産生記憶體碎片

缺點:需要移動大量對象,處理效率比較低

3、複制

Java虛拟機--筆記

将記憶體劃分為大小相等的兩塊,每次隻使用其中一塊,當這一塊記憶體用完了就将還存活的對象複制到另一塊上面,然後再把使用過的記憶體空間進行一次清理。

主要不足是:隻使用了記憶體的一半

現在的商業虛拟機都采用這種收集算法回收新生代,但是并不是劃分為大小相等的兩塊,而是一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。
在回收時,将 Eden 和 Survivor 中還存活着的對象全部複制到另一塊 Survivor 上,最後清理 Eden 和使用過的那一塊 Survivor。

HotSpot 虛拟機的 Eden 和 Survivor 大小比例預設為 8:1,保證了記憶體的使用率達到 90%。
如果每次回收有多于 10% 的對象存活,那麼一塊 Survivor 就不夠用了,此時需要依賴于老年代進行空間配置設定擔保,也就是借用老年代的空間存儲放不下的對象。

---HotSpot VM是Sun JDK和Open JDK 中所帶的虛拟機,也是目前使用範圍最廣的虛拟機。
           

4、分代收集

為了提升GC效率,現在的商業虛拟機采用分代收集算法,根據對象存活周期将記憶體劃分為幾塊,不同塊采用适當的收集算法

一般将堆分為新生代和老年代。

新生代使用:複制算法–每次收集都有大量對象死去

老年代使用:标記-清除算法 或者 标記-整理算法–存活對象多

為什麼新生代采用複制算法,老年代采用标整算法?

新生代使用複制算法---存活下來的對象是少數,需要複制的對象少
因為新生代對象的生存時間比較短,80%的都要回收的對象,采用标記-清除算法則記憶體碎片化比較嚴重,标記整理算法需要移動大量的對象,處理效率低
采用複制算法可以靈活高效,便于整理空間。

老年代采用标記整理---存活對象多,不能使用複制算法,占空間
标記整理算法主要是為了解決标記清除算法存在記憶體碎片的問題,又解決了複制算法兩個Survivor區的問題,
因為老年代的空間比較大,不可能采用複制算法,特别占用記憶體空間
           

4、垃圾收集器

Java虛拟機--筆記

以上是HotSpot虛拟機中的7個垃圾收集器,連線表示可以配合使用。

因為目前為止還沒有完美的收集器出現,更沒有萬能的收集器,隻是針對具體應用最合适的收集器,進行分代收集(新生代老年代)

單線程與多線程:單線程指垃圾收集器隻使用一個線程,多線程使用多個線程。

串行與并行:

串行指的是垃圾收集器與使用者程式交替執行,執行垃圾收集的時候需要停頓使用者程式;

并行指的是垃圾收集器和使用者程式同時執行。CMS和G1以并行的方式執行。

1、Serial收集器

Java虛拟機--筆記

Serial收集器以串行的方式執行。

是單線程的收集器,隻會使用一個線程進行垃圾收集工作,會暫停所有的使用者線程,隻有當垃圾回收完成時,才會重新喚醒主線程繼續執行。是以不适合伺服器環境

優點: 簡單高效,單個CPU環境下,沒有線程互動的開銷,擁有最高的單線程收集效率。

是Client場景下的預設新生代收集器,因為在該場景下記憶體一般來說不會很大。

收集一兩百兆垃圾的停頓時間可控制在一百多毫秒以内,隻要不是太頻繁,這點停頓時間可以接受。

2、Serial Old收集器

Java虛拟機--筆記

是Serial收集器的老年代版本,也是給Client場景下的虛拟機使用。

如果用在Server場景下,有兩大用途:

1、在 JDK 1.5 以及之前版本(Parallel Old 誕生以前)中與 Parallel Scavenge 收集器搭配使用。

2、作為 CMS 收集器的後備預案,在并發收集發生 Concurrent Mode Failure 時使用。

3、ParNew收集器

Java虛拟機--筆記

ParNew收集器是Serial收集器的多線程版本。

是Server場景下預設的新生代收集器,除性能原因外,主要是因為除了Serial收集器,隻有它能與CMS收集器配合使用。

4、Parallel Scavenge收集器

是多線程收集器

其他收集器目标是盡可能縮短垃圾收集時使用者線程的停頓時間,而Parallel Scavenge收集器的目标是達到一個可控制的吞吐量,是以被稱為**“吞吐量優先”收集器**。

吞吐量指CPU用于運作使用者程式的時間占總時間的比值。

停頓時間越短就越适合與使用者互動的程式,良好的響應速度能提升使用者體驗。

高吞吐量可以高效率利用CPU時間,盡快完成程式運算任務,适合在背景運算而不需要太多互動的任務。

縮短停頓時間是以犧牲吞吐量和新生代空間換取的:新生代空間變小,垃圾回收變頻繁,導緻吞吐量下降

可以通過一個開關參數打開 GC 自适應的調節政策(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 區的比例、晉升老年代對象年齡等細節參數了。虛拟機會根據目前系統的運作情況收集性能監控資訊,動态調整這些參數以提供最合适的停頓時間或者最大的吞吐量。

5、Parallel Old收集器

Java虛拟機--筆記

是Parallel Scavenge收集器的老年代版本。

在注重吞吐量及CPU資源敏感的場合,可以優先考慮Parallel Scavenge加Parallel Old收集器

6、CMS收集器

Java虛拟機--筆記

CMS(Concurrent Mark Sweep), 并發标記清除,Mark Sweep 指的是标記 - 清除算法。

四個流程:

1、初始标記:僅僅隻是标記一下GC Roots能直接關聯到的對象,速度快,需要停頓。

2、并發标記:進行GC Roots Tracing的過程,在整個回收過程中耗時最長,不需要停頓。

3、重新标記:為了修正并發标記期間因使用者程式繼續運作導緻标記産生變動的那部分對象的标記記錄,需要停頓。

4、并發清除:不需要停頓。

整個過程中耗時最長的并發标記和并發清除過程,收集器線程都可以與使用者線程一起工作,不需要停頓。

缺點:

1、吞吐量低:停頓時間低是以犧牲吞吐量為代價的,導緻CPU使用率不夠高。

2、無法處理浮動垃圾,可能出現Concurrent Mode Failure。

浮動垃圾指并發清除階段由于使用者線程繼續運作而産生的垃圾,這部分垃圾隻能到下一次GC時才能進行回收。

由于浮動垃圾的存在,是以需要預留出一部分記憶體,CMS不能像其它收集器那樣等老年代快滿的時候再回收。

如果預留的記憶體不夠存放浮動垃圾,就會出現Concurrent Mode Failure,這時虛拟機将臨時啟用Serial Old替代CMS

3、标記-清除算法導緻的空間碎片,往往出現老年代空間剩餘,但無法找到足夠大連續空間配置設定目前對象,不得不提前出發一次Full GC。

7、G1收集器

G1(Garbage-First),面向服務端應用的垃圾收集器,在多CPU和大記憶體的場景下有很好的性能。

—旨在未來替換掉CMS收集器

Java虛拟機--筆記

堆被分為新生代和老年代,其它收集器進行收集的範圍都是整個新生代或者老年代,而G1可直接對新生代和老年代一起回收。

G1收集器把堆劃分成多個大小相等的獨立區域(Region),新生代和老年代不再實體隔離。

Java虛拟機--筆記

引入Region的概念,将原來的一整塊記憶體空間劃分成多個小空間,使每個小空間可以單獨進行垃圾回收。

使可預測的停頓時間模型成為可能–

–通過記錄每個Region 垃圾回收時間 及 回收所獲得的空間 (通過過去回收的經驗獲得),并維護一個優先清單,每次根據允許的收集時間,優先回收價值最大的Region。

每個Region都有一個Remembered Set,用于記錄該Region對象的引用對象所在的Region。

通過Remembered Set,可達性分析的時候就可以避免全堆掃描。----有路徑壓縮的感覺

Java虛拟機--筆記

如果不計算維護Remembered Set的操作, G1收集器可以分為以下步驟:

1、初始标記

2、并發标記

**3、最終标記:**為了修正在并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分标記記錄,虛拟機将這段時間對象變化記錄線上程的Remembered Set Logs裡面,最終标記階段需要将Remembered Set Logs的資料合并到Remembered Set中。這階段需要停頓線程,但是可以并行執行。

**4、篩選回收:**首先對每個Region中的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃。

此階段也可做到與使用者程式一起并發執行,但因為隻回收一部分Region,時間是使用者可控制的,而且停頓使用者線程将大幅度提高收集效率。

G1收集器具備的特點:

**1、空間整合:**整體來看是基于标記-整理算法實作的收集器,從局部–兩個Region之間–上來看是基于複制算法實作的,這意味着運作期間不會産生記憶體空間碎片。

**2、可預測的停頓:**能讓使用者明确指定在一個長度為M毫秒的時間片段内,消耗在GC上的時間不能超過N毫秒。

G1的缺點:

region 大小和大對象很難保證一緻,這會導緻空間的浪費;特别大的對象是可能占用超過一個 region 的。并且,region 太小不合适,會令你在配置設定大對象時更難找到連續空間,這是一個長久存在的情況。

三、記憶體配置設定政策與回收政策

1、Minor GC和Full GC

Minor GC: 回收新生代,因為新生代對象存活時間很短,是以Minor GC會頻繁執行,執行的速度也一般比較快

Full GC:回收新生代和老年代,老年代對象存活時間長,是以Full GC很少執行,執行速度會比Minor GC慢很多

2、記憶體配置設定政策

1、對象優先在Eden配置設定

大多數情況下,對象在新生代Eden上配置設定,當Eden空間不夠時,發起Minor GC。

2、大對象直接進入老年代

大對象:需要連續記憶體空間的對象,如很長的字元串或數組。

經常出現大對象會提前觸發垃圾收集以擷取足夠的連續空間配置設定給大對象。

-XX:PretenureSizeThreshold,大于此值的對象直接在老年代配置設定,避免在 Eden 和 Survivor 之間的大量記憶體複制。

3、長期存活的對象進入老年代

為對象定義年齡計數器,對象在Eden出生并經過Minor GC依然存活,将其移動到Survivor中,年齡就增加一歲,增加到一定年齡則移動到老年代中。

-XX:MaxTenuringThreshold 用來定義年齡的門檻值。

4、動态對象年齡判定

虛拟機并不是永遠要求對象的年齡必須達到MaxTenuringThreshold才移入老年代,如果在Survivor中相同年齡所有對象大小的總和大于Survivor空間的一半,則年齡大于或等于該年齡的對象可以直接進入老年代

5、空間配置設定擔保

在發生Minor GC之前,虛拟機先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果條件成立的話,Minor GC可以确認是安全的。

如果不成立,虛拟機會檢視HandlePromotionFailure 的值是否允許擔保失敗,如果允許那麼就會繼續檢查老年代最大的連續空間是否大于曆次晉升到老年代對象的平均大小,如果大于,嘗試進行一次Minor GC;如果小于,或HandlePromotionFailure 的值不允許冒險,就需要進行一次Full GC。

3、Full GC的觸發條件

Minor GC觸發條件:當Eden空間滿時,将觸發一次Minor GC。

Full GC的觸發條件:

1、調用System.gc()

隻是建議虛拟機執行Full GC(),但是虛拟機不一定真正去執行。

2、老年代空間不足

大對象直接進入老年代、長期存活的對象進入老年代等會導緻老年代空間不足。

盡量不要建立過大的對象及數組。

還可以通過-Xmn虛拟機參數調大新生代的大小,讓對象盡量在新生代被回收,不進入老年代。

還可以通過 -XX:MaxTenuringThreshold 調大對象進入老年代的年齡,讓對象在新生代多存活一段時間。

3、空間配置設定擔保失敗

使用複制算法的Minor GC需要老年代的記憶體空間做擔保,擔保失敗會執行一次Full GC。

4、JDK1.7及以前的永久代空間不足

在 JDK 1.7 及以前,HotSpot 虛拟機中的方法區是用永久代實作的,永久代中存放的為一些 Class 的資訊、常量、靜态變量等資料。

當系統中要加載的類、反射的類和調用的方法較多時,永久代可能會被占滿,在未配置為采用 CMS GC 的情況下也會執行 Full GC。如果經過 Full GC 仍然回收不了,那麼虛拟機會抛出 java.lang.OutOfMemoryError。

為避免以上原因引起的 Full GC,可采用的方法為增大永久代空間或轉為使用 CMS GC。

5、Concurrent Mode Failure

執行CMS GC的過程中同時有對象要放入老年代,而此時老年代空間不足(可能是GC過程中浮動垃圾過多導緻暫時性的空間不足),便會報Concurrent Mode Failure錯誤,觸發Full GC。

面試題:Java對象的建立過程/new一個對象的過程?

Java虛拟機--筆記

1、類加載檢查:首先檢查這個指令的參數是否能在常量池中定位到這個類的符号引用,并且檢查這符号引用代表的類是否已被加載、解析、驗證、初始化,如果沒有先執行類加載過程(class.forname())。

2、配置設定記憶體:類加載檢查通過後,虛拟機會為新生對象配置設定記憶體。主要有指針碰撞、空閑清單兩種方式----由Java堆是否規整決定,Java堆是否規整由垃圾收集器采用的垃圾收集器是否帶有壓縮整理功能決定。

(1)指針碰撞:

适用:當虛拟機使用複制算法或标記整理算法實作的垃圾收集器時,記憶體區域都是規整的。

原理:用過的記憶體放在一邊,空閑的記憶體在另一邊,中間用一個指針作為分界點,當需要為新對象配置設定記憶體時隻需把指針向空閑的一邊移動一段與對象大小相等的距離。

垃圾收集器:Serial、ParNew

(2)空閑清單:

适用:當虛拟機使用标記清除算法實作的垃圾收集器時,記憶體都是碎片化的

原理:虛拟機維護一個清單,清單中記錄哪些記憶體塊可用,在配置設定記憶體的時候,找一塊足夠大的記憶體空間給對象執行個體,更新清單記錄。

垃圾收集器:CMS

記憶體配置設定的并發問題:使用以下兩種保證線程安全:

(1)CAS+失敗重試:每次不加鎖⽽是假設沒有沖突⽽去完成某項操作,如果因為沖突失敗就重試,直到成功為⽌。 CAS+失敗重試 保證更新操作的原子性。

(2)TLAB:為每個線程預先在Eden區配置設定一塊記憶體,JVM在給線程中對象配置設定記憶體時,首先在TLAB配置設定。當放不下時,再采用CAS配置設定。

3、初始化零值:記憶體配置設定完成後,虛拟機需要将配置設定到的記憶體空間初始化為零值(不包括對象頭)。

保證了對象的執行個體字段在 Java 代碼中可以不賦初始值就直接使⽤。

4、設定對象頭資訊:如所屬類,中繼資料資訊,哈希碼,gc分代年齡,等等。

5、執行init()方法:執行執行個體初始化代碼。–傳回建立出來對象的引用。

Java虛拟機--筆記

面試題:對象的兩種通路定位方式?

Java程式通過棧上的Reference資料來操作堆上的具體對象。

1、句柄:Java堆中将會劃分出⼀塊記憶體來作為句柄池, reference 中存儲的就是對象的句柄位址,⽽句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊;

優勢:reference中存儲穩定的句柄位址,對象被移動隻需要改變句柄中的執行個體資料指針,reference本身不需要修改。

Java虛拟機--筆記

2、直接指針:Java 堆對象的布局中就必須考慮如何放置通路類型資料的相關資訊,而reference 中存儲的直接就是對象的位址。

優勢:速度快,節省了一次指針定位的時間開銷。

Java虛拟機--筆記

四、類加載機制

類是在運作期間第一次使用時 動态加載的,而不是一次性加載所有類。因為一次性加載會占用很多記憶體。

1、類的生命周期

Java虛拟機--筆記

Loading --> Verification --> Preparation --> Resolution --> Initialiazation --> Using --> Unloading

2、類的加載過程

包含加載、驗證、準備、解析和初始化5個階段。

1、加載

加載過程完成:

1、通過類的完全限定名稱擷取定義該類的二進制位元組流。
2、将該位元組流表示的靜态存儲結構轉換為方法區的運作時存儲結構。
3、在記憶體中生成一個代表該類的Class對象,作為方法區中該類各種資料的通路入口。
           

二進制位元組流可以通過一下方式中擷取:

1、從ZIP包擷取,成為jar、ear、war格式的基礎。
2、從網絡中擷取,最典型的應用是Applet
3、運作時計算生成,例如動态代理技術,
	在java.lang.reflect.Proxy使用ProxyGenerateProxyClass的代理類的二進制位元組流。
4、由其他檔案生成,例如由JSP檔案生成對應的Class類。
           

2、驗證

確定Class檔案的位元組流中包含的資訊符合目前虛拟機的技術,并不會危害虛拟機自身安全。

3、準備

類變量:被static 關鍵字修飾的變量。

準備階段為類變量配置設定記憶體并設定初始值,使用方法區的記憶體。

執行個體變量不會在這階段配置設定記憶體,會在對象執行個體化的時候随對象一起被配置設定在堆中。

執行個體化不是類加載的一個過程,類加載發生在所有執行個體化操作之前,類加載隻進行一次,執行個體化可以多次

//初始值一般為 0 值,例如下面的類變量 value 被初始化為 0 而不是 123。
public static int value = 123;


//如果類變量是常量,那麼它将初始化為表達式所定義的值而不是 0。---final
//例如下面的常量 value 被初始化為 123 而不是 0。
public static final int value = 123;
           

4、解析

将常量池的符号引用替換為直接引用的過程。

解析過程在某些情況下可以在初始化階段之後再開始,是為了支援Java的動态綁定。

Java的動态綁定和靜态綁定-多态

當子類和父類存在同一個方法時,子類重寫父類的方法,程式在運作時調用方法,是調用父類方法還是子類方法?
---确定這種調用何種方法的操作稱為綁定,綁定分為靜态綁定和動态綁定。
	1、靜态綁定
		--靜态綁定是在程式執行前就已經被綁定了,程式編譯過程中就已經知道這個方法是哪個類中的方法。
		--Java中隻有private、static、final修飾的方法及構造方法是靜态綁定。
			--private方法不能被繼承,不存在調用其子類的對象,隻能調用對象自身,是以private方法和定義該方法的類綁在一起
			--static方法,類方法,屬于類檔案。不依賴對象而存在,調用的時候就知道是哪個類
			--final方法不能被重寫,調用的方法是一樣的
			總結:如果一個方法不可被繼承或者繼承後不能被重寫,采用的即是靜态綁定。
	2、動态綁定
		--編譯器在每次調用方法時都要進行搜尋,時間開銷大。是以虛拟機會預先為每個類建立一個方法表,其中列出了所有方法的簽名和實際調用的方法。
		--動态綁定的過程:
				1、虛拟機提取對象的實際類型的方法表
				2、虛拟機搜尋方法簽名(方法簽名包括:方法名、參數的數量和類型),此時虛拟機已經知道調用哪種方法
				3、虛拟機調用方法
           

5、初始化

初始化階段才真正開始執行類中定義的Java程式代碼。

**初始化階段是虛拟機執行類構造器<clinit>()方法的過程。**在準備階段,類變量已經賦過一次系統要求的初始值,初始化階段,通過程式制定的主觀計劃去初始化類變量和其他資源。

<clinit>()是由編譯器自動收集類中所有類變量的指派動作和靜态語句塊中的語句合并産生的,編譯器收集的順序由語句在源檔案中出現順序決定。

注意:

1、靜态語句塊隻能通路到定義在它之前的類變量,定義在它之後的類變量在語句塊中隻能指派不能通路。

public class Test {

    static {
        i = 0;                // 給變量指派可以正常編譯通過
        System.out.print(i);  // 這句編譯器會提示“非法向前引用”
    }
    static int i = 1;//i變量定義在靜态語句塊之後
}
           

2、虛拟機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。

是以在虛拟機中第一個被執行的<clinit>()方法的類肯定是java.lang.Object。

由于父類的<clinit>()方法先執行,也就意味着父類定義的靜态語句塊的執行要優先于子類。

static class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}

static class Sub extends Parent {
//執行完父類的靜态語句塊再執行這句
    public static int B = A;
}

public static void main(String[] args) {
     System.out.println(Sub.B);  // 2
}
           

3、接口中不可以使用靜态語句塊,但仍然有類變量初始化的指派操作,是以接口也會生成<clinit>()方法。

接口中的屬性都是static final類型的常量,在準備階段就已經初始化。

不同的是:

執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法。

隻有當父接口中定義的變量使用時,父接口才會初始化。

另外接口的實作類在初始化時也不會執行接口的<clinit>()方法。

4、虛拟機會保證一個類的<clinit>()方法在多線程環境下被正确的加鎖和同步,如果多個線程同時初始化一個類,則會有一個線程執行這個類的<clinit>()方法,其他線程都會阻塞等待,直到活動線程執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時的操作,就可能造成多個線程阻塞,實際中這種阻塞很隐蔽。

init() 和 clinit() 的差別:

1、兩個方法的執行時機不同
	init() 是對象構造器方法,程式在執行new一個對象調用該對象類的constructor方法時才會執行init()方法
	clinit()時類構造器方法,jvm在執行類的  加載-驗證-準備-解析-初始化  中的初始化階段,jvm會調用clinit()方法。
	
2、兩個方法的執行目的不同
	init()是instance執行個體構造器,對非靜态變量進行初始化
	clinit() 是class類構造器,對靜态變量、靜态代碼塊進行初始化
           

3、類初始化時機

1、主動引用

虛拟機規範中沒有強制限制何時進行加載,但是規範嚴格規定了有且隻有以下五種情況必須對類進行初始化(加載、驗證、準備都會随之發生):

1、遇到new、getstatic、putstatic、invokestatic時,若類沒有進行過初始化,必須先觸發其初始化。

–使用new關鍵字執行個體化對象時

–讀取或者設定一個類的靜态字段(被final修飾、已在編譯器把結果放入常量池的靜态字段除外)時

–調用一個類的靜态方法時

2、使用java.lang.reflect包的方法對類進行反射調用時,如果類沒有進行初始化,需要先觸發其初始化。

3、初始化一個類的時候,如果發現父類還沒有進行過初始化,則需要先觸發其父類的初始化

4、當虛拟機啟動時,使用者需要制定一個要指向的主類(包含main()方法的類),虛拟機會先初始化這個主類

5、JDK1.7中,如果一個 java.lang.invoke.MethodHandle 執行個體最後的解析結果為 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化;

2、被動引用

以上五種稱為對一個類進行主動引用。除這五種,所有引用類的方式都不會觸發初始化,稱為被動引用。

1、通過子類引用父類的靜态字段,不會導緻子類初始化。

2、通過數組定義來引用類,不會觸發此類的初始化。該過程會對數組類進行初始化,數組類是一個由虛拟機自動生成的、直接繼承自Object的子類,其中包含了數組的屬性和方法。

3、常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,不會觸發定義常量的類的初始化。

4、類與類加載器

兩個類相等=====需要類本身相等,并且使用同一個類加載器進行加載。

因為每一個類加載器都擁有一個獨立的類名稱空間。

類的相等:類的Class對象的equals()方法、isAssignableFrom() 方法、isInstance() 方法的傳回結果為 true,也包括使用 instanceof 關鍵字做對象所屬關系判定結果為 true。

5、類加載器分類

從Java虛拟機的角度,分為:

1、啟動類加載器(Bootstrap ClassLoader),使用C++實作,是虛拟機自身的一部分。

2、所有其他類的加載器,使用Java實作,獨立于虛拟機,繼承自抽象類java.lang.ClassLoader。

從Java開發人員的角度,分為:

1、啟動類加載器(Bootstrap ClassLoader)

此類加載器負責将存放在 <JRE_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,

并且是虛拟機識别的(僅按照檔案名識别,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會被加載)類庫加載到虛拟機記憶體中。

啟動類加載器無法被 Java 程式直接引用,使用者在編寫自定義類加載器時,如果需要把加載請求委派給啟動類加載器,直接使用 null 代替即可。

2、擴充類加載器(Extension ClassLoader)

這個類加載器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實作的。

它負責将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統變量所指定路徑中的所有類庫加載到記憶體中,開發者可以直接使用擴充類加載器。

3、應用程式類加載器(Application ClassLoader)

這個類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實作的。

由于這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的傳回值,是以一般稱為系統類加載器。

它負責加載使用者類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程式中沒有自定義過自己的類加載器,一般情況下這個就是程式中預設的類加載器。

rt.jar:-----(runtime,rt),是java程式在運作時必不可少的檔案。包含jdk的基礎類庫。也就是Java doc裡面的所有類的class檔案
tools.jar:-----主要包含一些工具的類庫。系統用來編譯一個類的時候用到的,javac的時候。
dt.jar:----是關于運作環境的類庫,主要是swing的包。

----rt.jar在%JAVA_HOME%\jre\lib,dt.jar和tools.jar在%JAVA_HOME%\lib下。
----path變量的含義就是在任何路徑下都可以識别java、javac指令。
----classpath變量的含義就是告訴JVM要使用或執行的class放在什麼路徑上,便于JVM加載class檔案
           

6、雙親委派模型

類加載器之間的層次關系:

Java虛拟機--筆記

類加載器之間的層次關系,稱為雙親委派模型(Parents Delegation Model)

該模型要求除了頂層的啟動類加載器外,其他的類加載器都要有自己的父類加載器。

這裡的父子關系一般通過組合關系實作,而不是繼承。

1、工作過程

一個類加載器首先将類加載請求轉發到父類加載器,隻有當父類加載器無法完成時才嘗試自己加載。

2、好處

使得Java類随着它的類加載器一起具有一種帶有優先級的層次關系,進而使得基礎類得到統一。

例如 java.lang.Object 存放在 rt.jar 中,如果編寫另外一個 java.lang.Object 并放到 ClassPath 中,程式可以編譯通過。由于雙親委派模型的存在,是以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優先級更高,這是因為 rt.jar 中的 Object 使用的是啟動類加載器,而 ClassPath 中的 Object 使用的是應用程式類加載器。rt.jar 中的 Object 優先級更高,那麼程式中所有的 Object 都是這個 Object。

3、實作

loadClass() 方法運作過程如下:先檢查類是否已經加載過,如果沒有則讓父類加載器去加載。當父類加載器加載失敗時抛出 ClassNotFoundException,此時嘗試自己去加載。

7、自定義類加載器實作

以下代碼中的 FileSystemClassLoader 是自定義類加載器,繼承自 java.lang.ClassLoader,用于加載檔案系統上的類。它首先根據類的全名在檔案系統上查找類的位元組代碼檔案(.class 檔案),然後讀取該檔案内容,最後通過 defineClass() 方法來把這些位元組代碼轉換成 java.lang.Class 類的執行個體。

java.lang.ClassLoader 的 loadClass() 實作了雙親委派模型的邏輯,自定義類加載器一般不去重寫它,但是需要重寫 findClass() 方法。

public class FileSystemClassLoader extends ClassLoader {

    private String rootDir;
    
    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}