記錄正在執行的虛擬機字節碼指令的地址(若是正在執行的是本地方法則爲空)。html
每一個 Java 方法在執行的同時會建立一個棧幀用於存儲局部變量表、操做數棧、常量池引用等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。java
能夠經過 -Xss 這個虛擬機參數來指定一個程序的 Java 虛擬機棧內存大小:android
java -Xss=512M HackTheJava
該區域可能拋出如下異常:git
本地方法不是用 Java 實現,對待這些方法須要特別處理。程序員
與 Java 虛擬機棧相似,它們之間的區別只不過是本地方法棧爲本地方法服務。github
全部對象實例都在這裏分配內存。算法
是垃圾收集的主要區域("GC 堆"),現代的垃圾收集器基本都是採用分代收集算法,該算法的思想是針對不一樣的對象採起不一樣的垃圾回收算法,所以虛擬機把 Java 堆分紅如下三塊:數據庫
當一個對象被建立時,它首先進入新生代,以後有可能被轉移到老年代中。新生代存放着大量的生命很短的對象,所以新生代在三個區域中垃圾回收的頻率最高。爲了更高效地進行垃圾回收,把新生代繼續劃分紅如下三個空間:數組
Java 堆不須要連續內存,而且能夠動態增長其內存,增長失敗會拋出 OutOfMemoryError 異常。緩存
能夠經過 -Xms 和 -Xmx 兩個虛擬機參數來指定一個程序的 Java 堆內存大小,第一個參數設置初始值,第二個參數設置最大值。
java -Xms=1M -Xmx=2M HackTheJava
用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
和 Java 堆同樣不須要連續的內存,而且能夠動態擴展,動態擴展失敗同樣會拋出 OutOfMemoryError 異常。
對這塊區域進行垃圾回收的主要目標是對常量池的回收和對類的卸載,可是通常比較難實現,HotSpot 虛擬機把它當成永久代來進行垃圾回收。
運行時常量池是方法區的一部分。
Class 文件中的常量池(編譯器生成的各類字面量和符號引用)會在類加載後被放入這個區域。
除了在編譯期生成的常量,還容許動態生成,例如 String 類的 intern()。這部分常量也會被放入運行時常量池。
在 JDK 1.4 中新加入了 NIO 類,它可使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆裏的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在 Java 堆和 Native 堆中來回複製數據。
程序計數器、虛擬機棧和本地方法棧這三個區域屬於線程私有的,只存在於線程的生命週期內,線程結束以後也會消失,所以不須要對這三個區域進行垃圾回收。垃圾回收主要是針對 Java 堆和方法區進行。
給對象添加一個引用計數器,當對象增長一個引用時計數器加 1,引用失效時計數器減 1。引用計數爲 0 的對象可被回收。
兩個對象出現循環引用的狀況下,此時引用計數器永遠不爲 0,致使沒法對它們進行回收。
objA.instance = objB; objB.instance = objA;
經過 GC Roots 做爲起始點進行搜索,可以到達到的對象都是都是可用的,不可達的對象可被回收。
GC Roots 通常包含如下內容:
不管是經過引用計算算法判斷對象的引用數量,仍是經過可達性分析算法判斷對象的引用鏈是否可達,斷定對象是否存活都與引用有關。
Java 對引用的概念進行了擴充,引入四種強度不一樣的引用類型。
(一)強引用
只要強引用存在,垃圾回收器永遠不會回收調掉被引用的對象。
使用 new 一個新對象的方式來建立強引用。
Object obj = new Object();
(二)軟引用
用來描述一些還有用可是並不是必需的對象。
在系統將要發生內存溢出異常以前,將會對這些對象列進回收範圍之中進行第二次回收。
軟引用主要用來實現相似緩存的功能,在內存足夠的狀況下直接經過軟引用取值,無需從繁忙的真實來源獲取數據,提高速度;當內存不足時,自動刪除這部分緩存數據,從真正的來源獲取這些數據。
使用 SoftReference 類來實現軟引用。
Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj);
(三)弱引用
只能生存到下一次垃圾收集發生以前,當垃圾收集器工做時,不管當前內存是否足夠,都會被回收。
使用 WeakReference 類來實現弱引用。
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj);
(四)虛引用
又稱爲幽靈引用或者幻影引用。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用取得一個對象實例。
爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。
使用 PhantomReference 來實現虛引用。
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj);
由於方法區主要存放永久代對象,而永久代對象的回收率比新生代差不少,所以在方法區上進行回收性價比不高。
主要是對常量池的回收和對類的卸載。
類的卸載條件不少,須要知足如下三個條件,而且知足了也不必定會被卸載:
能夠經過 -Xnoclassgc 參數來控制是否對類進行卸載。
在大量使用反射、動態代理、CGLib 等 ByteCode 框架、動態生成 JSP 以及 OSGo 這類頻繁自定義 ClassLoader 的場景都須要虛擬機具有類卸載功能,以保證不會出現內存溢出。
finalize() 相似 C++ 的析構函數,用來作關閉外部資源等工做。可是 try-finally 等方式能夠作的更好,而且該方法運行代價高昂,不肯定性大,沒法保證各個對象的調用順序,所以最好不要使用。
當一個對象可被回收時,若是須要執行該對象的 finalize() 方法,那麼就有可能經過在該方法中讓對象從新被引用,從而實現自救。
將須要回收的對象進行標記,而後清除。
不足:
將內存劃分爲大小相等的兩塊,每次只使用其中一塊,當這一塊內存用完了就將還存活的對象複製到另外一塊上面,而後再把使用過的內存空間進行一次清理。
主要不足是隻使用了內存的一半。
如今的商業虛擬機都採用這種收集算法來回收新生代,可是並非將內存劃分爲大小相等的兩塊,而是分爲一塊較大的 Eden 空間和兩塊較小的 Survior 空間,每次使用 Eden 空間和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活着的對象一次性複製到另外一塊 Survivor 空間上,最後清理 Eden 和 使用過的那一塊 Survivor。HotSpot 虛擬機的 Eden 和 Survivor 的大小比例默認爲 8:1,保證了內存的利用率達到 90 %。若是每次回收有多於 10% 的對象存活,那麼一塊 Survivor 空間就不夠用了,此時須要依賴於老年代進行分配擔保,也就是借用老年代的空間。
讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
如今的商業虛擬機採用分代收集算法,它根據對象存活週期將內存劃分爲幾塊,不一樣塊採用適當的收集算法。
通常將 Java 堆分爲新生代和老年代。
以上是 HotSpot 虛擬機中的 7 個垃圾收集器,連線表示垃圾收集器能夠配合使用。
它是單線程的收集器,不只意味着只會使用一個線程進行垃圾收集工做,更重要的是它在進行垃圾收集時,必須暫停全部其餘工做線程,每每形成過長的等待時間。
它的優勢是簡單高效,對於單個 CPU 環境來講,因爲沒有線程交互的開銷,所以擁有最高的單線程收集效率。
在 Client 應用場景中,分配給虛擬機管理的內存通常來講不會很大,該收集器收集幾十兆甚至一兩百兆的新生代停頓時間能夠控制在一百多毫秒之內,只要不是太頻繁,這點停頓是能夠接受的。
它是 Serial 收集器的多線程版本。
是 Server 模式下的虛擬機首選新生代收集器,除了性能緣由外,主要是由於除了 Serial 收集器,只有它能與 CMS 收集器配合工做。
默認開始的線程數量與 CPU 數量相同,可使用 -XX:ParallelGCThreads 參數來設置線程數。
是並行的多線程收集器。
其它收集器關注點是儘量縮短垃圾收集時用戶線程的停頓時間,而它的目標是達到一個可控制的吞吐量,它被稱爲「吞吐量優先」收集器。這裏的吞吐量指 CPU 用於運行用戶代碼的時間佔總時間的比值。
停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗。而高吞吐量則能夠高效率地利用 CPU 時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。
提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間 -XX:MaxGCPauseMillis 參數以及直接設置吞吐量大小的 -XX:GCTimeRatio 參數(值爲大於 0 且小於 100 的整數)。縮短停頓時間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,致使吞吐量降低。
還提供了一個參數 -XX:+UseAdaptiveSizePolicy,這是一個開關參數,打開參數後,就不須要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種方式稱爲 GC 自適應的調節策略(GC Ergonomics)。自適應調節策略也是它與 ParNew 收集器的一個重要區別。
Serial Old 是 Serial 收集器的老年代版本,也是給 Client 模式下的虛擬機使用。若是用在 Server 模式下,它有兩大用途:
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 資源敏感的場合,均可以優先考慮 Parallel Scavenge 加 Parallel Old 收集器。
CMS(Concurrent Mark Sweep),從 Mark Sweep 能夠知道它是基於標記 - 清除算法實現的。
特色:併發收集、低停頓。
分爲如下四個流程:
在整個過程當中耗時最長的併發標記和併發清除過程當中,收集器線程均可以與用戶線程一塊兒工做,不須要進行停頓。
具備如下缺點:
對 CPU 資源敏感。CMS 默認啓動的回收線程數是 (CPU 數量 + 3) / 4,當 CPU 不足 4 個時,CMS 對用戶程序的影響就可能變得很大,若是原本 CPU 負載就比較大,還要分出一半的運算能力去執行收集器線程,就可能致使用戶程序的執行速度突然下降了 50%,其實也讓人沒法接受。而且低停頓時間是以犧牲吞吐量爲代價的,致使 CPU 利用率變低。
沒法處理浮動垃圾。因爲併發清理階段用戶線程還在運行着,伴隨程序運行天然就還會有新的垃圾不斷產生。這一部分垃圾出如今標記過程以後,CMS 沒法在當次收集中處理掉它們,只好留到下一次 GC 時再清理掉,這一部分垃圾就被稱爲「浮動垃圾」。也是因爲在垃圾收集階段用戶線程還須要運行,那也就還須要預留有足夠的內存空間給用戶線程使用,所以它不能像其餘收集器那樣等到老年代幾乎徹底被填滿了再進行收集,須要預留一部分空間提供併發收集時的程序運做使用。可使用 -XX:CMSInitiatingOccupancyFraction 的值來改變觸發收集器工做的內存佔用百分比,JDK 1.5 默認設置下該值爲 68,也就是當老年代使用了 68% 的空間以後會觸發收集器工做。若是該值設置的過高,致使浮動垃圾沒法保存,那麼就會出現 Concurrent Mode Failure,此時虛擬機將啓動後備預案:臨時啓用 Serial Old 收集器來從新進行老年代的垃圾收集。
標記 - 清除算法致使的空間碎片,給大對象分配帶來很大麻煩,每每出現老年代空間剩餘,但沒法找到足夠大連續空間來分配當前對象,不得不提早觸發一次 Full GC。
G1(Garbage-First)收集器是當今收集器技術發展最前沿的成果之一,它是一款面向服務端應用的垃圾收集器,HotSpot 開發團隊賦予它的使命是(在比較長期的)將來能夠替換掉 JDK 1.5 中發佈的 CMS 收集器。
具有以下特色:
在 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 收集器的運做大體可劃分爲如下幾個步驟:
收集器 | 串行、並行 or 併發 | 新生代 / 老年代 | 算法 | 目標 | 適用場景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 複製算法 | 響應速度優先 | 單 CPU 環境下的 Client 模式 |
Serial Old | 串行 | 老年代 | 標記-整理 | 響應速度優先 | 單 CPU 環境下的 Client 模式、CMS 的後備預案 |
ParNew | 並行 | 新生代 | 複製算法 | 響應速度優先 | 多 CPU 環境時在 Server 模式下與 CMS 配合 |
Parallel Scavenge | 並行 | 新生代 | 複製算法 | 吞吐量優先 | 在後臺運算而不須要太多交互的任務 |
Parallel Old | 並行 | 老年代 | 標記-整理 | 吞吐量優先 | 在後臺運算而不須要太多交互的任務 |
CMS | 併發 | 老年代 | 標記-清除 | 響應速度優先 | 集中在互聯網站或 B/S 系統服務端上的 Java 應用 |
G1 | 併發 | both | 標記-整理 + 複製算法 | 響應速度優先 | 面向服務端應用,未來替換 CMS |
對象的內存分配,也就是在堆上分配。主要分配在新生代的 Eden 區上,少數狀況下也可能直接分配在老年代中。
大多數狀況下,對象在新生代 Eden 區分配,當 Eden 區空間不夠時,發起 Minor GC。
關於 Minor GC 和 Full GC:
大對象是指須要連續內存空間的對象,最典型的大對象是那種很長的字符串以及數組。常常出現大對象會提早觸發垃圾收集以獲取足夠的連續空間分配給大對象。
提供 -XX:PretenureSizeThreshold 參數,大於此值的對象直接在老年代分配,避免在 Eden 區和 Survivor 區之間的大量內存複製。
JVM 爲對象定義年齡計數器,通過 Minor GC 依然存活,而且能被 Survivor 區容納的,移被移到 Survivor 區,年齡就增長 1 歲,增長到必定年齡則移動到老年代中(默認 15 歲,經過 -XX:MaxTenuringThreshold 設置)。
JVM 並非永遠地要求對象的年齡必須達到 MaxTenuringThreshold 才能晉升老年代,若是在 Survivor 區中相同年齡全部對象大小的總和大於 Survivor 空間的一半,則年齡大於或等於該年齡的對象能夠直接進入老年代,無需等待 MaxTenuringThreshold 中要求的年齡。
在發生 Minor GC 以前,JVM 先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是條件成立的話,那麼 Minor GC 能夠確認是安全的;若是不成立的話 JVM 會查看 HandlePromotionFailure 設置值是否容許擔保失敗,若是容許那麼就會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次 Minor GC,儘管此次 Minor GC 是有風險的;若是小於,或者 HandlePromotionFailure 設置不容許冒險,那這時也要改成進行一次 Full GC。
對於 Minor GC,其觸發條件很是簡單,當 Eden 區空間滿時,就將觸發一次 Minor GC。而 Full GC 則相對複雜,有如下條件:
此方法的調用是建議 JVM 進行 Full GC,雖然只是建議而非必定,但不少狀況下它會觸發 Full GC,從而增長 Full GC 的頻率,也即增長了間歇性停頓的次數。所以強烈建議能不使用此方法就不要使用,讓虛擬機本身去管理它的內存。可經過 -XX:+ DisableExplicitGC 來禁止 RMI 調用 System.gc()。
老年代空間不足的常見場景爲前文所講的大對象直接進入老年代、長期存活的對象進入老年代等,當執行 Full GC 後空間仍然不足,則拋出 Java.lang.OutOfMemoryError。爲避免以上緣由引發的 Full GC,調優時應儘可能作到讓對象在 Minor GC 階段被回收、讓對象在新生代多存活一段時間以及不要建立過大的對象及數組。
使用複製算法的 Minor GC 須要老年代的內存空間做擔保,若是出現了 HandlePromotionFailure 擔保失敗,則會觸發 Full GC。
在 JDK 1.7 及之前,HotSpot 虛擬機中的方法區是用永久代實現的,永久代中存放的爲一些 Class 的信息、常量、靜態變量等數據,當系統中要加載的類、反射的類和調用的方法較多時,永久代可能會被佔滿,在未配置爲採用 CMS GC 的狀況下也會執行 Full GC。若是通過 Full GC 仍然回收不了,那麼 JVM 會拋出 java.lang.OutOfMemoryError,爲避免以上緣由引發的 Full GC,可採用的方法爲增大永久代空間或轉爲使用 CMS GC。
執行 CMS GC 的過程當中同時有對象要放入老年代,而此時老年代空間不足(有時候「空間不足」是 CMS GC 時當前的浮動垃圾過多致使暫時性的空間不足觸發 Full GC),便會報 Concurrent Mode Failure 錯誤,並觸發 Full GC。
類是在運行期間動態加載的。
包括如下 7 個階段:
其中解析過程在某些狀況下能夠在初始化階段以後再開始,這是爲了支持 Java 的動態綁定。
虛擬機規範中並無強制約束什麼時候進行加載,可是規範嚴格規定了有且只有下列五種狀況必須對類進行初始化(加載、驗證、準備都會隨着發生):
遇到 new、getstatic、putstatic、invokestatic 這四條字節碼指令時,若是類沒有進行過初始化,則必須先觸發其初始化。最多見的生成這 4 條指令的場景是:使用 new 關鍵字實例化對象的時候;讀取或設置一個類的靜態字段(被 final 修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候;以及調用一個類的靜態方法的時候。
使用 java.lang.reflect 包的方法對類進行反射調用的時候,若是類沒有進行初始化,則須要先觸發其初始化。
當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。
當虛擬機啓動時,用戶須要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類;
當使用 JDK.7 的動態語言支持時,若是一個 java.lang.invoke.MethodHandle 實例最後的解析結果爲 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化;
以上 5 種場景中的行爲稱爲對一個類進行主動引用。除此以外,全部引用類的方式都不會觸發初始化,稱爲被動引用。被動引用的常見例子包括:
System.out.println(SubClass.value); // value 字段在 SuperClass 中定義
SuperClass[] sca = new SuperClass[10];
System.out.println(ConstClass.HELLOWORLD);
包含了加載、驗證、準備、解析和初始化這 5 個階段。
加載是類加載的一個階段,注意不要混淆。
加載過程完成如下三件事:
其中二進制字節流能夠從如下方式中獲取:
確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
主要有如下 4 個階段:
(一)文件格式驗證
驗證字節流是否符合 Class 文件格式的規範,而且能被當前版本的虛擬機處理。
(二)元數據驗證
對字節碼描述的信息進行語義分析,以保證其描述的信息符合 Java 語言規範的要求。
(三)字節碼驗證
經過數據流和控制流分析,確保程序語義是合法、符合邏輯的。
(四)符號引用驗證
發生在虛擬機將符號引用轉換爲直接引用的時候,對類自身之外(常量池中的各類符號引用)的信息進行匹配性校驗。
類變量是被 static 修飾的變量,準備階段爲類變量分配內存並設置初始值,使用的是方法區的內存。
實例變量不會在這階段分配內存,它將會在對象實例化時隨着對象一塊兒分配在 Java 堆中。
初始值通常爲 0 值,例以下面的類變量 value 被初始化爲 0 而不是 123。
public static int value = 123;
若是類變量是常量,那麼會按照表達式來進行初始化,而不是賦值爲 0。
public static final int value = 123;
將常量池的符號引用替換爲直接引用的過程。
初始化階段才真正開始執行類中的定義的 Java 程序代碼。初始化階段即虛擬機執行類構造器 <clinit>() 方法的過程。
在準備階段,類變量已經賦過一次系統要求的初始值,而在初始化階段,根據程序員經過程序制定的主觀計劃去初始化類變量和其它資源。
<clinit>() 方法具備如下特色:
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>() 方法中有耗時的操做,就可能形成多個進程阻塞,在實際過程當中此種阻塞很隱蔽。
虛擬機設計團隊把類加載階段中的「經過一個類的全限定名來獲取描述此類的二進制字節流 ( 即字節碼 )」這個動做放到 Java 虛擬機外部去實現,以便讓應用程序本身決定如何去獲取所須要的類。實現這個動做的代碼模塊稱爲「類加載器」。
對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在 Java 虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類名稱空間。通俗而言:比較兩個類是否「相等」(這裏所指的「相等」,包括類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果,也包括使用 instanceof() 關鍵字作對象所屬關係斷定等狀況),只有在這兩個類是由同一個類加載器加載的前提下才有意義,不然,即便這兩個類來源於同一個 Class 文件,被同一個虛擬機加載,只要加載它們的類加載器不一樣,那這兩個類就一定不相等。
從 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)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。
應用程序都是由三種類加載器相互配合進行加載的,若是有必要,還能夠加入本身定義的類加載器。下圖展現的類加載器之間的層次關係,稱爲類加載器的雙親委派模型(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{ // 先檢查請求的類是否已經被加載過了 Class c = findLoadedClass(name); if(c == null) { try{ if(parent != null) { c = parent.loadClass(name, false); } else{ c = findBootstrapClassOrNull(name); } } catch(ClassNotFoundException e) { // 若是父類加載器拋出 ClassNotFoundException,說明父類加載器沒法完成加載請求 } if(c == null) { // 若是父類加載器沒法完成加載請求,再調用自身的 findClass() 來進行加載 c = findClass(name); } } if(resolve) { resolveClass(c); } return c; }
配置 | 描述 |
---|---|
-Xms | 初始化堆內存大小 |
-Xmx | 堆內存最大值 |
-Xmn | 新生代大小 |
-XX:PermSize | 初始化永久代大小 |
-XX:MaxPermSize | 永久代最大容量 |
配置 | 描述 |
---|---|
-XX:+UseSerialGC | 串行垃圾回收器 |
-XX:+UseParallelGC | 並行垃圾回收器 |
-XX:+UseConcMarkSweepGC | 併發標記掃描垃圾回收器 |
-XX:ParallelCMSThreads= | 併發標記掃描垃圾回收器 = 爲使用的線程數量 |
-XX:+UseG1GC | G1 垃圾回收器 |
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar