備戰- Java虛擬機

備戰- Java虛擬機html

 

    試問嶺南應很差,卻道,此心安處是吾鄉。java

 

簡介:備戰- Java虛擬機程序員

1、運行時數據區域

程序計算器、Java 虛擬機棧、本地方法棧、堆、方法區

在Java 運行環境參考連接:http://www.javashuo.com/article/p-mxyxzoee-dp.html算法

直接內存

在 JDK 1.4 中新引入了 NIO 類,它可使用 Native 函數庫直接分配堆外內存,而後經過 Java 堆裏的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在堆內存和堆外內存來回拷貝數據。數組

2、垃圾收集

垃圾收集主要是針對堆和方法區進行。程序計數器、虛擬機棧和本地方法棧這三個區域屬於線程私有的,只存在於線程的生命週期內,線程結束以後就會消失,所以不須要對這三個區域進行垃圾回收。緩存

引用計數算法

爲對象添加一個引用計數器,當對象增長一個引用時計數器加 1,引用失效時計數器減 1。引用計數爲 0 的對象可被回收。安全

在兩個對象出現循環引用的狀況下,此時引用計數器永遠不爲 0,致使沒法對它們進行回收。正是由於循環引用的存在,所以 Java 虛擬機不使用引用計數算法。網絡

 1 public class Test {  2 
 3     public Object instance = null;  4 
 5     public static void main(String[] args) {  6         Test a = new Test();  7         Test b = new Test();  8         a.instance = b;  9         b.instance = a; 10         a = null; 11         b = null; 12  doSomething(); 13  } 14 }
View Code

在上述代碼中,a 與 b 引用的對象實例互相持有了對象的引用,所以當咱們把對 a 對象與 b 對象的引用去除以後,因爲兩個對象還存在互相之間的引用,致使兩個 Test 對象沒法被回收。多線程

可達性分析算法

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

Java 虛擬機使用該算法來判斷對象是否可被回收,GC Roots 通常包含如下內容:

  • 虛擬機棧中局部變量表中引用的對象
  • 本地方法棧中 JNI 中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中的常量

方法區的回收

由於方法區主要存放永久代對象,而永久代對象的回收率比新生代低不少,因此在方法區上進行回收性價比不高。

主要是對常量池的回收和對類的卸載。

爲了不內存溢出,在大量使用反射和動態代理的場景都須要虛擬機具有類卸載功能。

類的卸載條件不少,須要知足如下三個條件,而且知足了條件也不必定會被卸載:

  • 該類全部的實例都已經被回收,此時堆中不存在該類的任何實例。
  • 加載該類的 ClassLoader 已經被回收。
  • 該類對應的 Class 對象沒有在任何地方被引用,也就沒法在任何地方經過反射訪問該類方法。

finalize()

相似 C++ 的析構函數,用於關閉外部資源。可是 try-finally 等方式能夠作得更好,而且該方法運行代價很高,不肯定性大,沒法保證各個對象的調用順序,所以最好不要使用。

當一個對象可被回收時,若是須要執行該對象的 finalize() 方法,那麼就有可能在該方法中讓對象從新被引用,從而實現自救。自救只能進行一次,若是回收的對象以前調用了 finalize() 方法自救,後面回收時不會再調用該方法。

引用類型

不管是經過引用計數算法判斷對象的引用數量,仍是經過可達性分析算法判斷對象是否可達,斷定對象是否可被回收都與引用有關。

Java 提供了四種強度不一樣的引用類型。

1. 強引用

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

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

Object obj = new Object();

2. 軟引用

被軟引用關聯的對象只有在內存不夠的狀況下才會被回收。

使用 SoftReference 類來建立軟引用。

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

3. 弱引用

被弱引用關聯的對象必定會被回收,也就是說它只能存活到下一次垃圾回收發生以前。

使用 WeakReference 類來建立弱引用。

1 Object obj = new Object(); 2 WeakReference<Object> wf = new WeakReference<Object>(obj); 3 obj = null;  // 使obj 對象只被弱引用關聯

4. 虛引用

又稱爲幽靈引用或者幻影引用,一個對象是否有虛引用的存在,不會對其生存時間形成影響,也沒法經過虛引用獲得一個對象。

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

使用 PhantomReference 來建立虛引用。

1 Object obj = new Object(); 2 PhantomReference<Object> pf = new PhantomReference<Object>(obj, null); 3 obj = null;  // 使obj 只能被虛引用關聯

垃圾收集算法

1. 標記 - 清除

標記清除算法(Mark-Sweep)是最基礎的一種垃圾回收算法,它分爲2部分,先把內存區域中的這些對象進行標記,哪些屬於可回收標記出來,而後把這些垃圾拎出來清理掉。就像上圖同樣,清理掉的垃圾就變成未使用的內存區域,等待被再次使用。

這邏輯再清晰不過了,而且也很好操做,但它存在一個很大的問題,那就是內存碎片。

上圖中等方塊的假設是 2M,小一些的是 1M,大一些的是 4M。等咱們回收完,內存就會切成了不少段。咱們知道開闢內存空間時,須要的是連續的內存區域,這時候咱們須要一個 2M的內存區域,其中有2個 1M 是無法用的。這樣就致使,其實咱們自己還有這麼多的內存的,但卻用不了。

不足:

  • 標記和清除過程效率都不高;
  • 會產生大量不連續的內存碎片,致使沒法給大對象分配內存。

2. 標記 - 整理

標記整理算法(Mark-Compact)標記過程仍然與標記 --- 清除算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,再清理掉端邊界之外的內存區域。

標記整理算法一方面在標記-清除算法上作了升級,解決了內存碎片的問題,也規避了複製算法只能利用一半內存區域的弊端。看起來很美好,但從上圖能夠看到,它對內存變更更頻繁,須要整理全部存活對象的引用地址,在效率上比複製算法要差不少。

優勢:

  • 不會產生內存碎片

不足:

  • 須要移動大量對象,處理效率比較低。

3. 複製

將內存劃分爲大小相等的兩塊,每次只使用其中一塊,當這一塊內存用完了就將還存活的對象複製到另外一塊上面,而後再把使用過的內存空間進行一次清理。

主要不足是隻使用了內存的一半。

如今的商業虛擬機都採用這種收集算法回收新生代,可是並非劃分爲大小相等的兩塊,而是一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活着的對象所有複製到另外一塊 Survivor 上,最後清理 Eden 和使用過的那一塊 Survivor。

HotSpot 虛擬機的 Eden 和 Survivor 大小比例默認爲 8:1,保證了內存的利用率達到 90%。若是每次回收有多於 10% 的對象存活,那麼一塊 Survivor 就不夠用了,此時須要依賴於老年代進行空間分配擔保,也就是借用老年代的空間存儲放不下的對象。

4. 分代收集

分代收集算法(Generational Collection)嚴格來講並非一種思想或理論,而是融合上述3種基礎的算法思想,而產生的針對不一樣狀況所採用不一樣算法的一套組合拳。對象存活週期的不一樣將內存劃分爲幾塊。通常是把 Java 堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記-清理或者標記——整理算法來進行回收。

通常將堆分爲新生代和老年代。

  • 新生代使用:複製算法
  • 老年代使用:標記 - 清除 或者 標記 - 整理 算法

5. GC的分類  

JVM 在進行GC 時,可能針對三個區域進行垃圾回收分別是新生代、老年代、方法區,大部分時候回收的都是新生代。GC類型主要有如下四種類型。

  • 新生代收集(Minor GC/Young GC):只針對新生代的垃圾收集。具體點的是Eden 區滿時觸發GC。 Survivor滿不會觸發Minor GC 。
  • 老年代收集(Major GC/Old GC):只針對老年代的垃圾收集。 目前,只有CMS 收集器會有單獨收集老年代的行爲。
  • 混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。 目前只有G1收集器會有這種行爲。
  • 整堆收集(Full GC):收集整個Java 堆和方法區的垃圾收集。

Java 堆(Java Heap)是JVM所管理的內存中最大的一塊,堆又是垃圾收集器管理的主要區域。

Java 堆主要分爲2個區域-年輕代與老年代,其中年輕代又分 Eden 區和 Survivor 區,其中 Survivor 區又分 From 和 To 2個區。爲何要分這麼多個區呢?

★ Eden 區

IBM 公司的專業研究代表,有將近98%的對象是朝生夕死,因此針對這一現狀,大多數狀況下,對象會在新生代 Eden 區中進行分配,當 Eden 區沒有足夠空間進行分配時,虛擬機會發起一次 Minor GC,Minor GC 相比 Major GC 更頻繁,回收速度也更快。
經過 Minor GC 以後,Eden 會被清空,Eden 區中絕大部分對象會被回收,而那些無需回收的存活對象,將會進到 Survivor 的 From 區(若 From 區不夠,則直接進入 Old 區)。

★ Survivor 區

Survivor 區至關因而 Eden 區和 Old 區的一個緩衝,相似於咱們交通燈中的黃燈。Survivor 又分爲2個區,一個是 From 區,一個是 To 區。每次執行 Minor GC,會將 Eden 區和 From 存活的對象放到 Survivor 的 To 區(若是 To 區不夠,則直接進入 Old 區)。

★ 爲何要分區?

若是沒有 Survivor 區,Eden 區每進行一次 Minor GC,存活的對象就會被送到老年代,老年代很快就會被填滿。而有不少對象雖然一次 Minor GC 沒有消滅,但其實也並不會存活多久,或許第二次,第三次就須要被清除。這時候移入老年區,很明顯不是一個明智的決定。

因此,Survivor 的存在乎義就是減小被送到老年代的對象,進而減小 Major GC 的發生。Survivor 的預篩選保證,只有經歷16次 Minor GC 還能在新生代中存活的對象,纔會被送到老年代。

★ 爲何要分兩個Survivor ?

設置兩個 Survivor 區最大的好處就是解決內存碎片化。

咱們先假設一下,Survivor 若是隻有一個區域會怎樣。Minor GC 執行後,Eden 區被清空了,存活的對象放到了 Survivor 區,而以前 Survivor 區中的對象,可能也有一些是須要被清除的。問題來了,這時候咱們怎麼清除它們?在這種場景下,咱們只能標記清除,而咱們知道標記清除最大的問題就是內存碎片,在新生代這種常常會消亡的區域,採用標記清除必然會讓內存產生嚴重的碎片化。由於 Survivor 有2個區域,因此每次 Minor GC,會將以前 Eden 區和 From 區中的存活對象複製到 To 區域。第二次 Minor GC 時,From 與 To 職責兌換,這時候會將 Eden 區和 To 區中的存活對象再複製到 From 區域,以此反覆。

這種機制最大的好處就是,整個過程當中,永遠有一個 Survivor space 是空的,另外一個非空的 Survivor space 是無碎片的。那麼,Survivor 爲何不分更多塊呢?比方說分紅三個、四個、五個?顯然,若是 Survivor 區再細分下去,每一塊的空間就會比較小,容易致使 Survivor 區滿,兩塊 Survivor 區多是通過權衡以後的最佳方案。

★ Old 區

老年代佔據着2/3的堆內存空間,只有在 Major GC 的時候纔會進行清理,每次 GC 都會觸發「Stop-The-World」。內存越大,STW 的時間也越長,因此內存也不只僅是越大就越好。因爲複製算法在對象存活率較高的老年代會進行不少次的複製操做,效率很低,因此老年代這裏採用的是標記——整理算法。

★ 兩張圖瞭解垃圾回收全流程

   

垃圾收集器

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

  • 單線程與多線程:單線程指的是垃圾收集器只使用一個線程,而多線程使用多個線程;
  • 串行與並行:串行指的是垃圾收集器與用戶程序交替執行,這意味着在執行垃圾收集的時候須要停頓用戶程序;並行指的是垃圾收集器和用戶程序同時執行。除了 CMS 和 G1 以外,其它垃圾收集器都是以串行的方式執行。

1. Serial 收集器

Serial 翻譯爲串行,也就是說它以串行的方式執行。

它是單線程的收集器,只會使用一個線程進行垃圾收集工做。

它的優勢是簡單高效,在單個 CPU 環境下,因爲沒有線程交互的開銷,所以擁有最高的單線程收集效率。

它是 Client 場景下的默認新生代收集器,由於在該場景下內存通常來講不會很大。它收集一兩百兆垃圾的停頓時間能夠控制在一百多毫秒之內,只要不是太頻繁,這點停頓時間是能夠接受的。

2. ParNew 收集器

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

它是 Server 場景下默認的新生代收集器,除了性能緣由外,主要是由於除了 Serial 收集器,只有它能與 CMS 收集器配合使用。

3. Parallel Scavenge 收集器

與 ParNew 同樣是多線程收集器。

其它收集器目標是儘量縮短垃圾收集時用戶線程的停頓時間,而它的目標是達到一個可控制的吞吐量,所以它被稱爲「吞吐量優先」收集器。這裏的吞吐量指 CPU 用於運行用戶程序的時間佔總時間的比值。

停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗。而高吞吐量則能夠高效率地利用 CPU 時間,儘快完成程序的運算任務,適合在後臺運算而不須要太多交互的任務。

縮短停頓時間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,致使吞吐量降低。

能夠經過一個開關參數打開 GC 自適應的調節策略(GC Ergonomics),就不須要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 區的比例、晉升老年代對象年齡等細節參數了。虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。

4. Serial Old 收集器

是 Serial 收集器的老年代版本,也是給 Client 場景下的虛擬機使用。若是用在 Server 場景下,它有兩大用途:

  • 在 JDK 1.5 以及以前版本(Parallel Old 誕生之前)中與 Parallel Scavenge 收集器搭配使用。
  • 做爲 CMS 收集器的後備預案,在併發收集發生 Concurrent Mode Failure 時使用。

5. Parallel Old 收集器

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

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

6. CMS 收集器

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

分爲如下四個流程:

  • 初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快,須要停頓。
  • 併發標記:進行 GC Roots Tracing 的過程,它在整個回收過程當中耗時最長,不須要停頓。
  • 從新標記:爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,須要停頓。
  • 併發清除:不須要停頓。

在整個過程當中耗時最長的併發標記和併發清除過程當中,收集器線程均可以與用戶線程一塊兒工做,不須要進行停頓。

具備如下缺點:

  • 吞吐量低:低停頓時間是以犧牲吞吐量爲代價的,致使 CPU 利用率不夠高。
  • 沒法處理浮動垃圾,可能出現 Concurrent Mode Failure。浮動垃圾是指併發清除階段因爲用戶線程繼續運行而產生的垃圾,這部分垃圾只能到下一次 GC 時才能進行回收。因爲浮動垃圾的存在,所以須要預留出一部份內存,意味着 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。若是預留的內存不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛擬機將臨時啓用 Serial Old 來替代 CMS。
  • 標記 - 清除算法致使的空間碎片,每每出現老年代空間剩餘,但沒法找到足夠大連續空間來分配當前對象,不得不提早觸發一次 Full GC。

7. G1 收集器

G1(Garbage-First),它是一款面向服務端應用的垃圾收集器,在多 CPU 和大內存的場景下有很好的性能。HotSpot 開發團隊賦予它的使命是將來能夠替換掉 CMS 收集器。

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

上圖中綠色的永久代在如今的Hotspot 中已被移除。

G1 把堆劃分紅多個大小相等的獨立區域(Region),新生代和老年代再也不物理隔離。

經過引入 Region 的概念,從而將原來的一整塊內存空間劃分紅多個的小空間,使得每一個小空間能夠單獨進行垃圾回收。這種劃分方法帶來了很大的靈活性,使得可預測的停頓時間模型成爲可能。經過記錄每一個 Region 垃圾回收時間以及回收所得到的空間(這兩個值是經過過去回收的經驗得到),並維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的 Region。

每一個 Region 都有一個 Remembered Set,用來記錄該 Region 對象的引用對象所在的 Region。經過使用 Remembered Set,在作可達性分析的時候就能夠避免全堆掃描。

若是不計算維護 Remembered Set 的操做,G1 收集器的運做大體可劃分爲如下幾個步驟:

  • 初始標記
  • 併發標記
  • 最終標記:爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 裏面,最終標記階段須要把 Remembered Set Logs 的數據合併到 Remembered Set 中。這階段須要停頓線程,可是可並行執行。
  • 篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據用戶所指望的 GC 停頓時間來制定回收計劃。此階段其實也能夠作到與用戶程序一塊兒併發執行,可是由於只回收一部分 Region,時間是用戶可控制的,並且停頓用戶線程將大幅度提升收集效率。

具有以下特色:

  • 空間整合:總體來看是基於「標記 - 整理」算法實現的收集器,從局部(兩個 Region 之間)上來看是基於「複製」算法實現的,這意味着運行期間不會產生內存空間碎片。
  • 可預測的停頓:能讓使用者明確指定在一個長度爲 M 毫秒的時間片斷內,消耗在 GC 上的時間不得超過 N 毫秒。

3、內存分配與回收策略

Minor GC 和 Full GC

  • Minor GC:回收新生代,由於新生代對象存活時間很短,所以 Minor GC 會頻繁執行,執行的速度通常也會比較快。

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

內存分配策略

1. 對象優先在 Eden 分配

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

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

大對象是指須要連續內存空間的對象,最典型的大對象是那種很長的字符串以及數組。

常常出現大對象會提早觸發垃圾收集以獲取足夠的連續空間分配給大對象。

-XX:PretenureSizeThreshold,大於此值的對象直接在老年代分配,避免在 Eden 和 Survivor 之間的大量內存複製。

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

爲對象定義年齡計數器,對象在 Eden 出生並通過 Minor GC 依然存活,將移動到 Survivor 中,年齡就增長 1 歲,增長到必定年齡則移動到老年代中。

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

4. 動態對象年齡斷定

虛擬機並非永遠要求對象的年齡必須達到 MaxTenuringThreshold 才能晉升老年代,若是在 Survivor 中相同年齡全部對象大小的總和大於 Survivor 空間的一半,則年齡大於或等於該年齡的對象能夠直接進入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。

5. 空間分配擔保

在發生 Minor GC 以前,虛擬機先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是條件成立的話,那麼 Minor GC 能夠確認是安全的。

若是不成立的話虛擬機會查看 HandlePromotionFailure 的值是否容許擔保失敗,若是容許那麼就會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次 Minor GC;若是小於,或者 HandlePromotionFailure 的值不容許冒險,那麼就要進行一次 Full GC。

Full GC 的觸發條件

對於 Minor GC,其觸發條件很是簡單,當 Eden 空間滿時,就將觸發一次 Minor GC。而 Full GC 則相對複雜,有如下條件:

1. 調用 System.gc()

只是建議虛擬機執行 Full GC,可是虛擬機不必定真正去執行。不建議使用這種方式,而是讓虛擬機管理內存。

2. 老年代空間不足

老年代空間不足的常見場景爲前文所講的大對象直接進入老年代、長期存活的對象進入老年代等。

爲了不以上緣由引發的 Full GC,應當儘可能不要建立過大的對象以及數組。除此以外,能夠經過 -Xmn 虛擬機參數調大新生代的大小,讓對象儘可能在新生代被回收掉,不進入老年代。還能夠經過 -XX:MaxTenuringThreshold 調大對象進入老年代的年齡,讓對象在新生代多存活一段時間。

3. 空間分配擔保失敗

使用複製算法的 Minor GC 須要老年代的內存空間做擔保,若是擔保失敗會執行一次 Full GC。具體內容請參考上面的第 5 小節的空間分配擔保。

4. JDK 1.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。

4、類加載機制

類是在運行期間第一次使用時動態加載的,而不是一次性加載全部類。由於若是一次性加載,那麼會佔用不少的內存。

類的生命週期

包括如下 7 個階段:

  • 加載(Loading)
  • 驗證(Verification)
  • 準備(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸載(Unloading)

1. 加載

加載是類加載的一個階段,注意不要混淆。

加載過程完成如下三件事:

  • 經過類的徹底限定名稱獲取定義該類的二進制字節流。
  • 將該字節流表示的靜態存儲結構轉換爲方法區的運行時存儲結構。
  • 在內存中生成一個表明該類的 Class 對象,做爲方法區中該類各類數據的訪問入口。

其中二進制字節流能夠從如下方式中獲取:

  • 從 ZIP 包讀取,成爲 JAR、EAR、WAR 格式的基礎。
  • 從網絡中獲取,最典型的應用是 Applet。
  • 運行時計算生成,例如動態代理技術,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理類的二進制字節流。
  • 由其餘文件生成,例如由 JSP 文件生成對應的 Class 類。

2. 驗證

確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。

3. 準備

類變量是被 static 修飾的變量,準備階段爲類變量分配內存並設置初始值,使用的是方法區的內存。

實例變量不會在這階段分配內存,它會在對象實例化時隨着對象一塊兒被分配在堆中。應該注意到,實例化不是類加載的一個過程,類加載發生在全部實例化操做以前,而且類加載只進行一次,實例化能夠進行屢次。

初始值通常爲 0 值,例以下面的類變量 value 被初始化爲 0 而不是 123。

public static int value = 123;   // 變量

若是類變量是常量,那麼它將初始化爲表達式所定義的值而不是 0。例以下面的常量 value 被初始化爲 123 而不是 0。

public static final int value = 123;  // 常量

4. 解析

將常量池的符號引用替換爲直接引用的過程。

其中解析過程在某些狀況下能夠在初始化階段以後再開始,這是爲了支持 Java 的動態綁定。

5. 初始化 

初始化階段才真正開始執行類中定義的 Java 程序代碼。初始化階段是虛擬機執行類構造器 <clinit\>() 方法的過程。在準備階段,類變量已經賦過一次系統要求的初始值,而在初始化階段,根據程序員經過程序制定的主觀計劃去初始化類變量和其它資源。

<clinit>() 是由編譯器自動收集類中全部類變量的賦值動做和靜態語句塊中的語句合併產生的,編譯器收集的順序由語句在源文件中出現的順序決定。特別注意的是,靜態語句塊只能訪問到定義在它以前的類變量,定義在它以後的類變量只能賦值,不能訪問。例如如下代碼:

1 public class Test { 2     static { 3         i = 0;                // 給變量賦值能夠正常編譯經過
4         System.out.print(i);  // 這句編譯器會提示「非法向前引用」
5  } 6     static int i = 1; 7 }

因爲父類的 <clinit>() 方法先執行,也就意味着父類中定義的靜態語句塊的執行要優先於子類。例如如下代碼:

 1 static class Parent {  2     public static int A = 1;  3     static {  4         A = 2;  5  }  6 }  7 
 8 static class Sub extends Parent {  9     public static int B = A; 10 } 11 
12 public static void main(String[] args) { 13      System.out.println(Sub.B);  // 2
14 }
View Code

接口中不可使用靜態語句塊,但仍然有類變量初始化的賦值操做,所以接口與類同樣都會生成 <clinit>() 方法。但接口與類不一樣的是,執行接口的 <clinit>() 方法不須要先執行父接口的 <clinit>() 方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的 <clinit>() 方法。

虛擬機會保證一個類的 <clinit>() 方法在多線程環境下被正確的加鎖和同步,若是多個線程同時初始化一個類,只會有一個線程執行這個類的 <clinit>() 方法,其它線程都會阻塞等待,直到活動線程執行 <clinit>() 方法完畢。若是在一個類的 <clinit>() 方法中有耗時的操做,就可能形成多個線程阻塞,在實際過程當中此種阻塞很隱蔽。

類初始化時機

1. 主動引用

虛擬機規範中並無強制約束什麼時候進行加載,可是規範嚴格規定了有且只有下列五種狀況必須對類進行初始化(加載、驗證、準備都會隨之發生):

  • 遇到 new、getstatic、putstatic、invokestatic 這四條字節碼指令時,若是類沒有進行過初始化,則必須先觸發其初始化。最多見的生成這 4 條指令的場景是:使用 new 關鍵字實例化對象的時候;讀取或設置一個類的靜態字段(被 final 修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候;以及調用一個類的靜態方法的時候。

  • 使用 java.lang.reflect 包的方法對類進行反射調用的時候,若是類沒有進行初始化,則須要先觸發其初始化。

  • 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。

  • 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類;

  • 當使用 JDK 1.7 的動態語言支持時,若是一個 java.lang.invoke.MethodHandle 實例最後的解析結果爲 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化;

2. 被動引用

以上 5 種場景中的行爲稱爲對一個類進行主動引用。除此以外,全部引用類的方式都不會觸發初始化,稱爲被動引用。被動引用的常見例子包括:

  • 經過子類引用父類的靜態字段,不會致使子類初始化。
System.out.println(SubClass.value);  // value 字段在 SuperClass 中定義
  • 經過數組定義來引用類,不會觸發此類的初始化。該過程會對數組類進行初始化,數組類是一個由虛擬機自動生成的、直接繼承自 Object 的子類,其中包含了數組的屬性和方法。
SuperClass[] sca = new SuperClass[10];
  • 常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化。
System.out.println(ConstClass.HELLOWORLD);

類與類加載器

兩個類相等,須要類自己相等,而且使用同一個類加載器進行加載。這是由於每個類加載器都擁有一個獨立的類名稱空間。

這裏的相等,包括類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果爲 true,也包括使用 instanceof 關鍵字作對象所屬關係斷定結果爲 true。

類加載機制參考連接:https://www.cnblogs.com/taojietaoge/p/10269844.html

類加載器分類

從 Java 虛擬機的角度來說,只存在如下兩種不一樣的類加載器:

  • 啓動類加載器(Bootstrap ClassLoader),使用 C++ 實現,是虛擬機自身的一部分;

  • 全部其它類的加載器,使用 Java 實現,獨立於虛擬機,繼承自抽象類 java.lang.ClassLoader。

從 Java 開發人員的角度看,類加載器能夠劃分得更細緻一些:

  • 啓動類加載器(Bootstrap ClassLoader)此類加載器負責將存放在 <JRE_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,而且是虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即便放在 lib 目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器沒法被 Java 程序直接引用,用戶在編寫自定義類加載器時,若是須要把加載請求委派給啓動類加載器,直接使用 null 代替便可。

  • 擴展類加載器(Extension ClassLoader)這個類加載器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統變量所指定路徑中的全部類庫加載到內存中,開發者能夠直接使用擴展類加載器。

  • 應用程序類加載器(Application ClassLoader)這個類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。因爲這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以通常稱爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。

雙親委派模型

應用程序是由三種類加載器互相配合從而實現類加載,除此以外還能夠加入本身定義的類加載器。

雙親委託參考連接:https://www.cnblogs.com/taojietaoge/p/10269844.html

下圖展現了類加載器之間的層次關係,稱爲雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啓動類加載器外,其它的類加載器都要有本身的父類加載器。這裏的父子關係通常經過組合關係(Composition)來實現,而不是繼承關係(Inheritance)。

1. 工做過程

一個類加載器首先將類加載請求轉發到父類加載器,只有當父類加載器沒法完成時才嘗試本身加載。

從上圖可用看出ClassLoader的加載序列,委託是從下往上,查找過程則是從上向下的,如下有幾個注意事項:

  1. 一個AppClassLoader 查找資源時,首先會查看緩存是否有,如有則從緩存中獲取,不然委託給父加載器。
  2. 重複第一步的遞歸操做,查詢類是否已被加載。
  3. 若是ExtClassLoader 也沒有加載過,則由Bootstrap ClassLoader 加載,它首先也會查找緩存,若是沒有找到的話,就去找本身的規定的路徑下,也就是sun.mic.boot.class 下面的路徑,找到就返回,找不到就讓子加載器本身去找。
  4. Bootstrap ClassLoader 若是沒有查找成功,則ExtClassLoader 本身在java.ext.dirs 路徑中去查找,查找成功就返回,查找不成功則再向下讓子加載器找。
  5. 如果ExtClassLoader 查找不成功,則由AppClassLoader 在java.class.path 路徑下本身查找查找,找到就返回,若是沒有找到就讓子類找,若是沒有子類則會拋出各類異常。

2. 好處

使得 Java 類隨着它的類加載器一塊兒具備一種帶有優先級的層次關係,從而使得基礎類獲得統一。

  • 防止重複加載同一個.class。經過委託去向上面問一問,加載過了,就不用再加載一遍。保證數據安全。
  • 保證核心.class不能被篡改。經過委託方式,不會去篡改核心.clas,即便篡改也不會去加載,即便加載也不會是同一個.class對象了。不一樣的加載器加載同一個.class也不是同一個Class對象。這樣保證了Class執行安全。

3. 實現

如下是抽象類 java.lang.ClassLoader 的代碼片斷,其中的 loadClass() 方法運行過程以下:先檢查類是否已經加載過,若是沒有則讓父類加載器去加載。當父類加載器加載失敗時拋出 ClassNotFoundException,此時嘗試本身去加載。

 1 public abstract class ClassLoader {  2     // The parent class loader for delegation
 3     private final ClassLoader parent;  4 
 5     public Class<?> loadClass(String name) throws ClassNotFoundException {  6         return loadClass(name, false);  7  }  8 
 9     protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 10         synchronized (getClassLoadingLock(name)) { 11             // First, check if the class has already been loaded
12             Class<?> c = findLoadedClass(name); 13             if (c == null) { 14                 try { 15                     if (parent != null) { 16                         c = parent.loadClass(name, false); 17                     } else { 18                         c = findBootstrapClassOrNull(name); 19  } 20                 } catch (ClassNotFoundException e) { 21                     // ClassNotFoundException thrown if class not found 22                     // from the non-null parent class loader
23  } 24 
25                 if (c == null) { 26                     // If still not found, then invoke findClass in order 27                     // to find the class.
28                     c = findClass(name); 29  } 30  } 31             if (resolve) { 32  resolveClass(c); 33  } 34             return c; 35  } 36  } 37 
38     protected Class<?> findClass(String name) throws ClassNotFoundException { 39         throw new ClassNotFoundException(name); 40  } 41 }
View Code

4. 自定義類加載器實現

在ClassLoader中有四個很重要實用的方法loadClass()、findLoadedClass()、findClass()、defineClass(),能夠用來建立屬於本身的類的加載方式;好比咱們須要動態加載一些東西,或者從C盤某個特定的文件夾加載一個class 文件,又或者從網絡上下載class 主內容而後再進行加載等。分三步搞定:

一、編寫一個類繼承ClassLoader 抽象類;

二、重寫findClass() 方法;

三、在findClass() 方法中調用defineClass() 方法便可實現自定義ClassLoader。

需求:

自定義一個classloader 其默認加載路徑爲"/TJT/Code"下的jar 包和資源。

實現:

首先建立一個Test.java,而後javac 編譯並把生成的Test.class 文件放到"/TJT/Code" 路徑下。

而後再編寫一個DiskClassLoader 繼承ClassLoader。

 1 package www.baidu;  2 import java.io.ByteArrayOutputStream;  3 import java.io.File;  4 import java.io.FileInputStream;  5 import java.io.IOException;  6 
 7 public class DiskClassLoader extends ClassLoader{  8 //自定義classLoader能將class二進制內容轉換成Class對象
 9     private String myPath; 10 
11     public DiskClassLoader(String path) { 12         myPath = path; 13  } 14 
15     //findClass()方法中定義了查找class的方法
16  @Override 17     protected Class<?> findClass(String name) throws ClassNotFoundException{ 18         String fileName = getFileName(name); 19         File file = new File(myPath,fileName); 20         try { 21             FileInputStream is = new FileInputStream(file); 22             ByteArrayOutputStream bos = new ByteArrayOutputStream(); 23             int len = 0; 24             try { 25                 while((len = is.read()) != -1) { 26  bos.write(len); 27  } 28             } catch (IOException e) { 29  e.printStackTrace(); 30  } 31             byte[] data = bos.toByteArray(); 32  is.close(); 33  bos.close(); 34             //數據經過defineClass()生成了Class對象
35             return defineClass(name, data,0,data.length ); 36         } catch (Exception e) { 37  e.printStackTrace(); 38  } 39         return super.findClass(name); 40  } 41 
42     private String getFileName(String name) { 43         int lastIndexOf = name.lastIndexOf('.'); 44         if (lastIndexOf == -1) { 45             return name + ".class"; 46         }else { 47             return name.substring(lastIndexOf + 1) + ".class"; 48  } 49  } 50 }
View Code
最後經過FindClassLoader 的測試類,調用在Test.class 裏面的一個find() 方法。
 1 package www.baidu;  2 import java.lang.reflect.Method;  3 
 4 public class FindClassLoader {  5     public static void main(String[] args) throws ClassNotFoundException {  6         //建立自定義classloader對象
 7         DiskClassLoader diskL = new DiskClassLoader("/TJT/Code");  8         System.out.println("classloader is: "+diskL);  9         try { 10              //加載class文件
11             Class clazz = diskL.loadClass("www.baidu.Test"); 12             if (clazz != null) { 13                 Object object = clazz.newInstance(); 14                 Method declaredMethod = clazz.getDeclaredMethod("find", null); 15                 //經過反射調用Test類的find()方法
16                 declaredMethod.invoke(object, null); 17  } 18         } catch (Exception e) { 19  e.printStackTrace(); 20  } 21  } 22 }
View Code

驗證找到指定路徑下的自定義classloader。

 

 

 

 

 

 

  

 

試問嶺南應很差

卻道

此心安處是吾鄉

相關文章
相關標籤/搜索