深入理解java虛擬機學習總結(超讚!!!)

我是技術搬運工,好東西當然要和大家分享啦.原文地址

內存模型

注:白色區域爲線程私有的,藍色區域爲線程共享的。

1. 程序計數器

記錄正在執行的虛擬機字節碼指令的地址(如果正在執行的是 Native 方法則爲空)。

2. Java 虛擬機棧

每個 Java 方法在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。

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

  1. 當線程請求的棧深度超過最大值,會拋出 StackOverflowError 異常;
  2. 棧進行動態擴展時如果無法申請導足夠內存,會拋出 OutOfMemoryError 異常。

3. 本地方法棧

與 Java 虛擬機棧類似,它們之間的區別只不過是本地方法棧爲本地方法服務。

4. Java 堆

所有對象實例都在這裏分配內存。

這塊區域是垃圾收集器管理的主要區域("GC 堆 ")。現在收集器基本都是採用分代收集算法,Java 堆還可以分成:新生代和老年代(新生代還可以分成 Eden 空間、From Survivor 空間、To Survivor 空間等)。

不需要連續內存,可以通過 -Xmx 和 -Xms 來控制動態擴展內存大小,如果動態擴展失敗會拋出 OutOfMemoryError 異常。

5. 方法區

用於存放已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

和 Java 堆一樣不需要連續的內存,並且可以動態擴展,動態擴展失敗一樣會拋出 OutOfMemoryError 異常。

對這塊區域進行垃圾回收的主要目標是對常量池的回收和對類的卸載,但是一般比較難實現,HotSpot 虛擬機把它當成永久代來進行垃圾回收。

6. 運行時常量池

運行時常量池是方法區的一部分。

類加載後,Class 文件中的常量池(用於存放編譯期生成的各種字面量和符號引用)就會被放到這個區域。

在運行期間也可以用過 String 類的 intern() 方法將新的常量放入該區域。

7. 直接內存

在 JDK 1.4 中新加入了 NIO 類,引入了一種基於通道(Channel)與緩衝區(Buffer)的 I/O 方式,它可以使用 Native 函數庫直接分配堆外內存,然後通過一個存儲在 Java 堆裏的 DirectByteBuffer 對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在 Java 堆和 Native 堆中來回複製數據。

垃圾收集

程序計數器、虛擬機棧和本地方法棧這三個區域屬於線程私有的,只存在於線程的生命週期內,線程結束之後也會消失,因此不需要對這三個區域進行垃圾回收。

垃圾回收主要是針對 Java 堆和方法區進行。

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

1.1 引用計數

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

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

兩個對象會出現循環引用問題,此時引用計數器永遠不爲 0,導致 GC 收集器無法回收。

objA.instance = objB;
objB.instance = objA;

1.2 可達性

通過 GC Roots 作爲起始點進行搜索,能夠到達到的對象都是都是可用的,不可達的對象可被回收。

GC Roots 一般包含以下內容:

  1. 虛擬機棧中引用的對象
  2. 方法區中類靜態屬性引用的對象
  3. 方法區中的常量引用的對象
  4. 本地方法棧中引用的對象

1.3 引用類型

無論是通過引用計算算法判斷對象的引用數量,還是通過可達性分析算法判斷對象的引用鏈是否可達,判定獨享是否存活都與“引用”有關。

1.3.1 強引用

只要強引用存在,垃圾回收器永遠不會回收調掉被引用的對象。

Object obj = new Object();

1.3.2 軟引用

非必須引用,內存溢出之前進行回收。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();

sf 是對 obj 的一個軟引用,通過 sf.get() 方法可以取到這個對象,當然,當這個對象被標記爲需要回收的對象時,則返回 null;

軟引用主要用戶實現類似緩存的功能,在內存足夠的情況下直接通過軟引用取值,無需從繁忙的真實來源查詢數據,提升速度;當內存不足時,自動刪除這部分緩存數據,從真正的來源查詢這些數據。

1.3.3 弱引用

只能生存到下一次垃圾收集發生之前,當垃圾收集器工作時,無論當前內存是否足夠,都會被回收。

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

1.3.4 虛引用

又稱爲幽靈引用或者幻影引用,一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();
pf.isEnQueued();

1.3 方法區的回收

在方法區主要是對常量池的回收和對類的卸載。

常量池的回收和堆中對象回收類似。

類的卸載條件很多,需要滿足以下三個條件,並且滿足了也不一定會被卸載:

  1. 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  2. 加載該類的 ClassLoader 已經被回收。
  3. 該類對應的 java.lang.Class 對象沒有在任何地方被引用,也就無法在任何地方通過反射訪問該類方法。

可以通過 -Xnoclassgc 參數來控制是否對類進行卸載。

在大量使用反射、動態代理、CGLib 等 ByteCode 框架、動態生成 JSP 以及 OSGo 這類頻繁自定義 ClassLoader 的場景都需要虛擬機具備類卸載功能,以保證不會出現內存溢出。

1.4 finalize()

當一個對象可被回收時,如果該對象有必要執行 finalize() 方法,那麼就有可能可能通過在該方法中讓對象重新被引用,從而實現自救。

finalize() 類似 C++ 的虛構函數,用來做關閉外部資源等工作。但是 try-finally 等方式可以做的更好,並且該方法運行代價高昂,不確定性大,無法保證各個對象的調用順序,因此最好不要使用。

2. 垃圾收集算法

2.1 標記 - 清除算法

將需要回收的對象進行標記,然後清除。

不足:

  1. 標記和清除過程效率都不高
  2. 會產生大量碎片

之後的算法都是基於該算法進行改進。

2.2 複製算法

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

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

現在的商業虛擬機都採用這種收集算法來回收新生代,但是並不是將內存劃分爲大小相等的兩塊,而是分爲一塊較大的 Eden 空間和兩塊較小的 Survior 空間,每次使用 Eden 空間和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活着的對象一次性複製到另一塊 Survivor 空間上,最後清理 Eden 和 Survivor。HotSpot 虛擬機的 Eden 和 Survivor 的大小比例默認爲 8:1,保證了內存的利用率達到 90 %。如果每次回收有多於 10% 的對象存活,那麼一塊 Survivor 空間就不夠用了,需要依賴於老年代進行分配擔保,也就是借用老年代的空間。

2.3 標記 - 整理算法

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

2.4 分代收集算法

現在的商業虛擬機採用分代收集算法,它使用了前面介紹的幾種收集算法,根據對象存活週期將內存劃分爲幾塊,不同塊採用適當的收集算法。

一般將 Java 堆分爲新生代和老年代。

  1. 新生代使用:複製算法
  2. 老年代使用:標記 - 清理 或者 標記 - 整理 算法。

3. 垃圾收集器

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

3.1 Serial 收集器

它是單線程的收集器,不僅意味着只會使用一個線程進行垃圾收集工作,更重要的是它在進行垃圾收集時,必須暫停所有其他工作線程,往往造成過長的等待時間。

它的優點是簡單高效,對於單個 CPU 環境來說,由於沒有線程交互的開銷,因此擁有最高的單線程收集效率。

在 Client 應用場景中,分配給虛擬機管理的內存一般來說不會很大,該收集器收集幾十兆甚至一兩百兆的新生代停頓時間可以控制在一百多毫秒以內,只要不是太頻繁,這點停頓是可以接受的。

3.2 ParNew 收集器

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

是 Server 模式下的虛擬機首選新生代收集器,除了性能原因外,主要是因爲除了 Serial 收集器,只有它能與 CMS 收集器配合工作。

默認開始的線程數量與 CPU 數量相同,可以使用 -XX:ParallelGCThreads 參數來設置線程數。

3.3 Parallel Scavenge 收集器

是並行的多線程收集器。

其它收集器關注點是儘可能縮短垃圾收集時用戶線程的停頓時間,而它的目標是達到一個可控制的吞吐量,它被稱爲“吞吐量優先”收集器。這裏的吞吐量指 CPU 用於運行用戶代碼的時間佔總時間的比值。

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

提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間 -XX:MaxGCPauseMillis 參數以及直接設置吞吐量大小的 -XX:GCTimeRatio 參數(值爲大於 0 且小於 100 的整數)。縮短停頓時間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,導致吞吐量下降。

還提供了一個參數 -XX:+UseAdaptiveSizePolicy,這是一個開關參數,打開參數後,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種方式稱爲 GC 自適應的調節策略(GC Ergonomics)。自適應調節策略也是它與 ParNew 收集器的一個重要區別。

3.4 Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,也是給 Client 模式下的虛擬機使用。如果用在 Server 模式下,它有兩大用途:

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

3.5 Parallel Old 收集器

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

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

3.6 CMS 收集器

CMS(Concurrent Mark Sweep),從 Mark Sweep 可以知道它是基於 標記 - 清除 算法實現的。

特點:併發收集、低停頓。

分爲以下四個流程:

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

在整個過程中耗時最長的併發標記和併發清除過程中,收集器線程都可以與用戶線程一起工作,不需要進行停頓。

具有以下缺點:

  1. 對 CPU 資源敏感。CMS 默認啓動的回收線程數是 (CPU 數量 + 3) / 4,當 CPU 不足 4 個時,CMS 對用戶程序的影響就可能變得很大,如果本來 CPU 負載就比較大,還要分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然降低了 50%,其實也讓人無法接受。並且低停頓時間是以犧牲吞吐量爲代價的,導致 CPU 利用率變低。

  2. 無法處理浮動垃圾。由於併發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生。這一部分垃圾出現在標記過程之後,CMS 無法在當次收集中處理掉它們,只好留到下一次 GC 時再清理掉,這一部分垃圾就被稱爲“浮動垃圾”。也是由於在垃圾收集階段用戶線程還需要運行,那也就還需要預留有足夠的內存空間給用戶線程使用,因此它不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程序運作使用。可以使用 -XX:CMSInitiatingOccupancyFraction 的值來改變觸發收集器工作的內存佔用百分比,JDK 1.5 默認設置下該值爲 68,也就是當老年代使用了 68% 的空間之後會觸發收集器工作。如果該值設置的太高,導致浮動垃圾無法保存,那麼就會出現 Concurrent Mode Failure,此時虛擬機將啓動後備預案:臨時啓用 Serial Old 收集器來重新進行老年代的垃圾收集。

  3. 標記 - 清除算法導致的空間碎片,給大對象分配帶來很大麻煩,往往出現老年代空間剩餘,但無法找到足夠大連續空間來分配當前對象,不得不提前出發一次 Full GC。

3.7 G1 收集器

G1(Garbage-First)收集器是當今收集器技術發展最前沿的成果之一,它是一款面向服務端應用的垃圾收集器,HotSpot 開發團隊賦予它的使命是(在比較長期的)未來可以替換掉 JDK 1.5 中發佈的 CMS 收集器。

具備如下特點:

  • 並行與併發:能充分利用多 CPU 環境下的硬件優勢,使用多個 CPU 來縮短停頓時間;
  • 分代收集:分代概念依然得以保留,雖然它不需要其它收集器配合就能獨立管理整個 GC 堆,但它能夠採用不同方式去處理新創建的對象和已存活一段時間、熬過多次 GC 的舊對象來獲取更好的收集效果。
  • 空間整合:整體來看是基於“標記 - 整理”算法實現的收集器,從局部(兩個 Region 之間)上來看是基於“複製”算法實現的,這意味着運行期間不會產生內存空間碎片。
  • 可預測的停頓:這是它相對 CMS 的一大優勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了降低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲 M 毫秒的時間片段內,消耗在 GC 上的時間不得超過 N 毫秒,這幾乎已經是實時 Java(RTSJ)的垃圾收集器的特徵了。

在 G1 之前的其他收集器進行收集的範圍都是整個新生代或者老生代,而 G1 不再是這樣,Java 堆的內存佈局與其他收集器有很大區別,將整個 Java 堆劃分爲多個大小相等的獨立區域(Region)。雖然還保留新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,而都是一部分 Region(不需要連續)的集合。

之所以能建立可預測的停頓時間模型,是因爲它可以有計劃地避免在整個 Java 堆中進行全區域的垃圾收集。它跟蹤各個 Region 裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的 Region(這也就是 Garbage-First 名稱的來由)。這種使用 Region 劃分內存空間以及有優先級的區域回收方式,保證了它在有限的時間內可以獲取儘可能高的收集效率。

Region 不可能是孤立的,一個對象分配在某個 Region 中,可以與整個 Java 堆任意的對象發生引用關係。在做可達性分析確定對象是否存活的時候,需要掃描整個 Java 堆才能保證準確性,這顯然是對 GC 效率的極大傷害。爲了避免全堆掃描的發生,每個 Region 都維護了一個與之對應的 Remembered Set。虛擬機發現程序在對 Reference 類型的數據進行寫操作時,會產生一個 Write Barrier 暫時中斷寫操作,檢查 Reference 引用的對象是否處於不同的 Region 之中,如果是,便通過 CardTable 把相關引用信息記錄到被引用對象所屬的 Region 的 Remembered Set 之中。當進行內存回收時,在 GC 根節點的枚舉範圍中加入 Remembered Set 即可保證不對全堆掃描也不會有遺漏。

如果不計算維護 Remembered Set 的操作,G1 收集器的運作大致可劃分爲以下幾個步驟:

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

3.8 七種垃圾收集器的比較

收集器串行、並行 or 併發新生代 / 老年代算法目標適用場景
Serial串行新生代複製算法響應速度優先單 CPU 環境下的 Client 模式
Serial Old串行老年代標記 - 整理響應速度優先單 CPU 環境下的 Client 模式、CMS 的後備預案
ParNew並行新生代複製算法響應速度優先多 CPU 環境時在 Server 模式下與 CMS 配合
Parallel Scavenge並行新生代複製算法吞吐量優先在後臺運算而不需要太多交互的任務
Parallel Old並行老年代標記 - 整理吞吐量優先在後臺運算而不需要太多交互的任務
CMS併發老年代標記 - 清除響應速度優先集中在互聯網站或 B/S 系統服務端上的 Java 應用
G1併發both標記 - 整理 + 複製算法響應速度優先面向服務端應用,將來替換 CMS

4. 內存分配與回收策略

4.1 優先在 Eden 分配

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

4.2 大對象直接進入老年代

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

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

JVM 爲對象定義年齡計數器,經過 Minor GC 依然存活且被 Survivor 區容納的,移動到 Survivor 區,年齡加 1,每經歷一次 Minor GC 不被清理則年齡加 1,增加到一定年齡則移動到老年區(默認 15 歲,通過 -XX:MaxTenuringThreshold 設置);

4.4 動態對象年齡判定

若 Survivor 區中同年齡所有對象大小總和大於 Survivor 空間一半,則年齡大於等於該年齡的對象可以直接進入老年代;

4.5 空間分配擔保

在發生 Minor GC 之前,JVM 先檢查老年代最大可用連續空間是否大於新生代所有對象總空間,成立的話 Minor GC 確認是安全的;否則繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代對象的平均大小,大於的話進行 Minor GC,小於的話進行 Full GC。

4.6 Full GC 的觸發條件

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

4.6.1 調用 System.gc()

此方法的調用是建議 JVM 進行 Full GC,雖然只是建議而非一定,但很多情況下它會觸發 Full GC,從而增加 Full GC 的頻率,也即增加了間歇性停頓的次數。因此強烈建議能不使用此方法就不要使用,讓虛擬機自己去管理它的內存,可通過 -XX:+ DisableExplicitGC 來禁止 RMI 調用 System.gc()。

4.6.2 老年代空間不足

老年代空間不足的常見場景爲前文所講的大對象直接進入老年代、長期存活的對象進入老年代等,當執行 Full GC 後空間仍然不足,則拋出如下錯誤: Java.lang.OutOfMemoryError: Java heap space 爲避免以上兩種狀況引起的 Full GC,調優時應儘量做到讓對象在 Minor GC 階段被回收、讓對象在新生代多存活一段時間及不要創建過大的對象及數組。

4.6.3 空間分配擔保失敗

使用複製算法的 Minor GC 需要老年代的內存空間作擔保,如果出現了 HandlePromotionFailure 擔保失敗,則會觸發 Full GC。

4.6.4 JDK 1.7 及以前的永久代空間不足

在 JDK 1.7 及以前,HotSpot 虛擬機中的方法區是用永久代實現的,永久代中存放的爲一些 class 的信息、常量、靜態變量等數據,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation 可能會被佔滿,在未配置爲採用 CMS GC 的情況下也會執行 Full GC。如果經過 Full GC 仍然回收不了,那麼 JVM 會拋出如下錯誤信息:java.lang.OutOfMemoryError: PermGen space 爲避免 PermGen 佔滿造成 Full GC 現象,可採用的方法爲增大 PermGen 空間或轉爲使用 CMS GC。

在 JDK 1.8 中用元空間替換了永久代作爲方法區的實現,元空間是本地內存,因此減少了一種 Full GC 觸發的可能性。

4.6.5 Concurrent Mode Failure

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

類加載機制

類是在運行期間動態加載的。

1 類的生命週期

包括以下 7 個階段:

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

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

2. 類初始化時機

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

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

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

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

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

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

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

1. 通過子類引用父類的靜態字段,不會導致子類初始化。

System.out.println(SubClass.value); // value 字段在 SuperClass 中定義

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

SuperClass[] sca = new SuperClass[10];

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

System.out.println(ConstClass.HELLOWORLD);

3. 類加載過程

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

3.1 加載

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

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

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流。
  2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時存儲結構。
  3. 在內存中生成一個代表這個類的 Class 對象,作爲方法區這個類的各種數據的訪問入口。

其中二進制字節流可以從以下方式中獲取:

  • 從 ZIP 包讀取,這很常見,最終成爲日後 JAR、EAR、WAR 格式的基礎。
  • 從網絡中獲取,這種場景最典型的應用是 Applet。
  • 運行時計算生成,這種場景使用得最多得就是動態代理技術,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理類的二進制字節流。
  • 由其他文件生成,典型場景是 JSP 應用,即由 JSP 文件生成對應的 Class 類。
  • 從數據庫讀取,這種場景相對少見,例如有些中間件服務器(如 SAP Netweaver)可以選擇把程序安裝到數據庫中來完成程序代碼在集羣間的分發。...

3.2 驗證

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

主要有以下 4 個階段:

  1. 文件格式驗證
  2. 元數據驗證(對字節碼描述的信息進行語義分析)
  3. 字節碼驗證(通過數據流和控制流分析,確保程序語義是合法、符合邏輯的,將對類的方法體進行校驗分析)
  4. 符號引用驗證

3.3 準備

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

實例變量不會在這階段分配內存,它將會在對象實例化時隨着對象一起分配在 Java 堆中。

初始值一般爲 0 值,例如下面的類變量 value 被初始化爲 0 而不是 123。

public static int value = 123;

如果類變量是常量,那麼會按照表達式來進行初始化,而不是賦值爲 0。

public static final int value = 123;

3.4 解析

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

3.5 初始化

初始化階段即虛擬機執行類構造器 <clinit>() 方法的過程。

在準備階段,類變量已經賦過一次系統要求的初始值,而在初始化階段,根據程序員通過程序制定的主觀計劃去初始化類變量和其它資源。

<clinit>() 方法具有以下特點:

  • 是由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊(static{} 塊)中的語句合併產生的,編譯器收集的順序由語句在源文件中出現的順序決定。特別注意的是,靜態語句塊只能訪問到定義在它之前的類變量,定義在它之後的類變量只能賦值,不能訪問。例如以下代碼:
public class Test {
    static {
        i = 0;                // 給變量賦值可以正常編譯通過
        System.out.print(i);  // 這句編譯器會提示“非法向前引用”
    }
    static int i = 1;
}
  • 與類的構造函數(或者說實例構造器 <init>())不同,不需要顯式的調用父類的構造器。虛擬機會自動保證在子類的 <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);  // 輸出結果是父類中的靜態變量值 A,也就是 2
}
  • <clinit>() 方法對於類或接口不是必須的,如果一個類中不包含靜態語句塊,也沒有對類變量的賦值操作,編譯器可以不爲該類生成 <clinit>() 方法。

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

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

4. 類加載器

虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流 ( 即字節碼 )”這個動作放到 Java 虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個動作的代碼模塊稱爲“類加載器”。

4.1 類與類加載器

對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在 Java 虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。通俗而言:比較兩個類是否“相等”(這裏所指的“相等”,包括類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果,也包括使用 instanceof() 關鍵字對做對象所屬關係判定等情況),只有在這兩個類時由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個 Class 文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

4.2 類加載器分類

從 Java 虛擬機的角度來講,只存在以下兩種不同的類加載器:

一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器用 C++ 實現,是虛擬機自身的一部分;另一種就是所有其他類的加載器,這些類由 Java 實現,獨立於虛擬機外部,並且全都繼承自抽象類 java.lang.ClassLoader。

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

  • 啓動類加載器(Bootstrap ClassLoader) 此類加載器負責將存放在 <JAVA_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)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

4.3 雙親委派模型

應用程序都是由三種類加載器相互配合進行加載的,如果有必要,還可以加入自己定義的類加載器。下圖展示的類加載器之間的層次關係,稱爲類加載器的雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啓動類加載器外,其餘的類加載器都應有自己的父類加載器,這裏類加載器之間的父子關係一般通過組合(Composition)關係來實現,而不是通過繼承(Inheritance)的關係實現。

工作過程

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載,而是把這個請求委派給父類加載器,每一個層次的加載器都是如此,依次遞歸,因此所有的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成此加載請求(它搜索範圍中沒有找到所需類)時,子加載器纔會嘗試自己加載。

好處

使用雙親委派模型來組織類加載器之間的關係,使得 Java 類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如類 java.lang.Object,它存放再 rt.jar 中,無論哪個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,因此 Object 類在程序的各種類加載器環境中都是同一個類。相反,如果沒有雙親委派模型,由各個類加載器自行加載的話,如果用戶編寫了一個稱爲`java.lang.Object 的類,並放在程序的 ClassPath 中,那系統中將會出現多個不同的 Object 類,程序將變得一片混亂。如果開發者嘗試編寫一個與 rt.jar 類庫中已有類重名的 Java 類,將會發現可以正常編譯,但是永遠無法被加載運行。

實現

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
    //check the class has been loaded or not
    Class c = findLoadedClass(name);
    if(c == null) {
        try{
            if(parent != null) {
                c = parent.loadClass(name, false);
            } else{
                c = findBootstrapClassOrNull(name);
            }
        } catch(ClassNotFoundException e) {
            //if throws the exception , the father can not complete the load
        }
        if(c == null) {
            c = findClass(name);
        }
    }
    if(resolve) {
        resolveClass(c);
    }
    return c;
}

JVM 參數

GC 優化配置

配置描述
-Xms初始化堆內存大小
-Xmx堆內存最大值
-Xmn新生代大小
-XX:PermSize初始化永久代大小
-XX:MaxPermSize永久代最大容量

GC 類型設置

配置描述
-XX:+UseSerialGC串行垃圾回收器
-XX:+UseParallelGC並行垃圾回收器
-XX:+UseConcMarkSweepGC併發標記掃描垃圾回收器
-XX:ParallelCMSThreads=併發標記掃描垃圾回收器 = 爲使用的線程數量
-XX:+UseG1GCG1 垃圾回收器
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章