面经——JVM
面經——JVM
目錄
注:題目從牛客 Java部門面經整理而來,題目的解釋很多轉載自 JavaGuide 大佬的文章和參考其他一些博客,因為總結的太好了,就照搬了。
2020秋招面經大匯總!(崗位劃分)
Java 學習/面試指南
1. JVM運行時內存劃分?PC+虛擬機棧+本地方法棧+堆+方法區+JDK1.7與1.8區別
2. 創建一個對象的步驟
3. 介紹下 Java 內存區域(運行時數據區)
4. Java 對象的創建過程(五步,建議能默寫出來并且要知道每一步虛擬機做了什么)
5. 對象的訪問定位的兩種方式(句柄和直接指針兩種方式)
一 概述
對于 Java 程序員來說,在虛擬機自動內存管理機制下,不再需要像 C/C++程序開發程序員這樣為每一個 new 操作去寫對應的 delete/free 操作,不容易出現內存泄漏和內存溢出問題。正是因為 Java 程序員把內存控制權利交給 Java 虛擬機,一旦出現內存泄漏和溢出方面的問題,如果不了解虛擬機是怎樣使用內存的,那么排查錯誤將會是一個非常艱巨的任務。
二 運行時數據區域
Java 虛擬機在執行 Java 程序的過程中會把它管理的內存劃分成若干個不同的數據區域。JDK. 1.8 和之前的版本略有不同,下面會介紹到。
JDK 1.8 之前:
JDK 1.8 :
線程私有的:
- 程序計數器
- 虛擬機棧
- 本地方法棧
線程共享的:
- 堆
- 方法區
- 直接內存 (非運行時數據區的一部分)
2.1 程序計數器
程序計數器是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等功能都需要依賴這個計數器來完成。
另外,為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各線程之間計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存。
從上面的介紹中我們知道程序計數器主要有兩個作用:
注意:程序計數器是唯一一個不會出現 OutOfMemoryError 的內存區域,它的生命周期隨著線程的創建而創建,隨著線程的結束而死亡。
2.2 Java 虛擬機棧
與程序計數器一樣,Java 虛擬機棧也是線程私有的,它的生命周期和線程相同,描述的是 Java 方法執行的內存模型,每次方法調用的數據都是通過棧傳遞的。
Java 內存可以粗糙的區分為堆內存(Heap)和棧內存 (Stack),其中棧就是現在說的虛擬機棧,或者說是虛擬機棧中局部變量表部分。 (實際上,Java 虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有:局部變量表、操作數棧、動態鏈接、方法出口信息。)
局部變量表主要存放了編譯器可知的各種數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它不同于對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)。
Java 虛擬機棧會出現兩種錯誤:StackOverFlowError 和 OutOfMemoryError。
- StackOverFlowError: 若 Java 虛擬機棧的內存大小不允許動態擴展,那么當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,就拋出 StackOverFlowError 錯誤。
- OutOfMemoryError: 若 Java 虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出 OutOfMemoryError 錯誤。
Java 虛擬機棧也是線程私有的,每個線程都有各自的 Java 虛擬機棧,而且隨著線程的創建而創建,隨著線程的死亡而死亡。
擴展:那么方法/函數如何調用?
Java 棧可用類比數據結構中棧,Java 棧中保存的主要內容是棧幀,每一次函數調用都會有一個對應的棧幀被壓入 Java 棧,每一個函數調用結束后,都會有一個棧幀被彈出。
Java 方法有兩種返回方式:
不管哪種返回方式都會導致棧幀被彈出。
2.3 本地方法棧
和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧為虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。
本地方法被執行的時候,在本地方法棧也會創建一個棧幀,用于存放該本地方法的局部變量表、操作數棧、動態鏈接、出口信息。
方法執行完畢后相應的棧幀也會出棧并釋放內存空間,也會出現 StackOverFlowError 和 OutOfMemoryError 兩種錯誤。
2.4 堆
Java 虛擬機所管理的內存中最大的一塊,Java 堆是所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這里分配內存。
Java 堆是垃圾收集器管理的主要區域,因此也被稱作GC 堆(Garbage Collected Heap)。從垃圾回收的角度,由于現在收集器基本都采用分代垃圾收集算法,所以 Java 堆還可以細分為:新生代和老年代。再細致一點有:Eden 空間、From Survivor、To Survivor 空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。
在 JDK 7 版本及JDK 7 版本之前,堆內存被通常被分為下面三部分:
JDK 8 版本之后方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。
上圖所示的 Eden 區、兩個 Survivor 區都屬于新生代(為了區分,這兩個 Survivor 區域按照順序被命名為 from 和 to),中間一層屬于老年代。
大部分情況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收后,如果對象還存活,則會進入 s0 或者 s1,并且對象的年齡還會加 1(Eden 區->Survivor 區后對象的初始年齡變為 1),當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。
修正(issue552):“Hotspot遍歷所有對象時,按照年齡從小到大對其所占用的大小進行累積,當累積的某個年齡大小超過了survivor區的一半時,取這個年齡和MaxTenuringThreshold中更小的一個值,作為新的晉升年齡閾值”。
動態年齡計算的代碼如下
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {//survivor_capacity是survivor空間的大小size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);size_t total = 0;uint age = 1;while (age < table_size) {total += sizes[age];//sizes數組是每個年齡段對象大小if (total > desired_survivor_size) break;age++;}uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;... }堆這里最容易出現的就是 OutOfMemoryError 錯誤,并且出現這種錯誤之后的表現形式還會有幾種,比如:
- OutOfMemoryError: GC Overhead Limit Exceeded : 當JVM花太多時間執行垃圾回收并且只能回收很少的堆空間時,就會發生此錯誤。
- java.lang.OutOfMemoryError: Java heap space :假如在創建新的對象時, 堆內存中的空間不足以存放新創建的對象, 就會引發 java.lang.OutOfMemoryError: Java heap space 錯誤。(和本機物理內存無關,和你配置的對內存大小有關!)
…
2.5 方法區
方法區與 Java 堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。雖然 Java 虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。
方法區也被稱為永久代。很多人都會分不清方法區和永久代的關系,為此我也查閱了文獻。
2.5.1 方法區和永久代的關系
《Java 虛擬機規范》只是規定了有方法區這么個概念和它的作用,并沒有規定如何去實現它。那么,在不同的 JVM 上方法區的實現肯定是不同的了。 方法區和永久代的關系很像 Java 中接口和類的關系,類實現了接口,而永久代就是 HotSpot 虛擬機對虛擬機規范中方法區的一種實現方式。 也就是說,永久代是 HotSpot 的概念,方法區是 Java 虛擬機規范中的定義,是一種規范,而永久代是一種實現,一個是標準一個是實現,其他的虛擬機實現并沒有永久代這一說法。
2.5.2 常用參數
JDK 1.8 之前永久代還沒被徹底移除的時候通常通過下面這些參數來調節方法區大小
-XX:PermSize=N //方法區 (永久代) 初始大小
-XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGen
相對而言,垃圾收集行為在這個區域是比較少出現的,但并非數據進入方法區后就“永久存在”了。
JDK 1.8 的時候,方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。
下面是一些常用參數:
-XX:MetaspaceSize=N //設置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //設置 Metaspace 的最大大小
與永久代很大的不同就是,如果不指定大小的話,隨著更多類的創建,虛擬機會耗盡所有可用的系統內存。
2.5.3 為什么要將永久代 (PermGen) 替換為元空間 (MetaSpace) 呢?
整個永久代有一個 JVM 本身設置固定大小上限,無法進行調整,而元空間使用的是直接內存,受本機可用內存的限制,雖然元空間仍舊可能溢出,但是比原來出現的幾率會更小。
當你元空間溢出時會得到如下錯誤: java.lang.OutOfMemoryError: MetaSpace
你可以使用 -XX:MaxMetaspaceSize 標志設置最大元空間大小,默認值為 unlimited,這意味著它只受系統內存的限制。-XX:MetaspaceSize 調整標志定義元空間的初始大小,如果未指定此標志,則 Metaspace 將根據運行時的應用程序需求動態地重新調整大小。
元空間里面存放的是類的元數據,這樣加載多少類的元數據就不由 MaxPermSize 控制了, 而由系統的實際可用空間來控制,這樣能加載的類就更多了。
在 JDK8,合并 HotSpot 和 JRockit 的代碼時, JRockit 從來沒有一個叫永久代的東西, 合并之后就沒有必要額外的設置這么一個永久代的地方了。
2.6 運行時常量池
運行時常量池是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池信息(用于存放編譯期生成的各種字面量和符號引用)
既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出 OutOfMemoryError 錯誤。
JDK1.7 及之后版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開辟了一塊區域存放運行時常量池。
2.7 直接內存
直接內存并不是虛擬機運行時數據區的一部分,也不是虛擬機規范中定義的內存區域,但是這部分內存也被頻繁地使用。而且也可能導致 OutOfMemoryError 錯誤出現。
JDK1.4 中新加入的 NIO(New Input/Output) 類,引入了一種基于通道(Channel) 與緩存區(Buffer) 的 I/O 方式,它可以直接使用 Native 函數庫直接分配堆外內存,然后通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內存的引用進行操作。這樣就能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆之間來回復制數據。
本機直接內存的分配不會受到 Java 堆的限制,但是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。
三 HotSpot 虛擬機對象探秘
通過上面的介紹我們大概知道了虛擬機的內存情況,下面我們來詳細的了解一下 HotSpot 虛擬機在 Java 堆中對象分配、布局和訪問的全過程。
3.1 對象的創建
下圖便是 Java 對象的創建過程,我建議最好是能默寫出來,并且要掌握每一步在做什么。
Step1:類加載檢查
虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,并且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
Step2:分配內存
在類加載檢查通過后,接下來虛擬機將為新生對象分配內存。對象所需的內存大小在類加載完成后便可確定,為對象分配空間的任務等同于把一塊確定大小的內存從 Java 堆中劃分出來。分配方式有 “指針碰撞” 和 “空閑列表” 兩種,選擇哪種分配方式由 Java 堆是否規整決定,而 Java 堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
內存分配的兩種方式:(補充內容,需要掌握)
選擇以上兩種方式中的哪一種,取決于 Java 堆內存是否規整。而 Java 堆內存是否規整,取決于 GC 收集器的算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,復制算法內存也是規整的
內存分配并發問題(補充內容,需要掌握)
在創建對象的時候有一個很重要的問題,就是線程安全,因為在實際開發過程中,創建對象是很頻繁的事情,作為虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機采用兩種方式來保證線程安全:
- CAS+失敗重試: CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性。
- TLAB: 為每一個線程預先在 Eden 區分配一塊兒內存,JVM 在給線程中的對象分配內存時,首先在 TLAB 分配,當對象大于 TLAB 中的剩余內存或 TLAB 的內存已用盡時,再采用上述的 CAS 進行內存分配
Step3:初始化零值
內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
Step4:設置對象頭
初始化零值完成之后,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。
Step5:執行 init 方法
在上面工作都完成之后,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建才剛開始, 方法還沒有執行,所有的字段都還為零。所以一般來說,執行 new 指令之后會接著執行 方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產生出來。
3.2 對象的內存布局
在 Hotspot 虛擬機中,對象在內存中的布局可以分為 3 塊區域:對象頭、實例數據和對齊填充。
Hotspot 虛擬機的對象頭包括兩部分信息,第一部分用于存儲對象自身的自身運行時數據(哈希碼、GC 分代年齡、鎖狀態標志等等),另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
實例數據部分是對象真正存儲的有效信息,也是在程序中所定義的各種類型的字段內容。
對齊填充部分不是必然存在的,也沒有什么特別的含義,僅僅起占位作用。 因為 Hotspot 虛擬機的自動內存管理系統要求對象起始地址必須是 8 字節的整數倍,換句話說就是對象的大小必須是 8 字節的整數倍。而對象頭部分正好是 8 字節的倍數(1 倍或 2 倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。
3.3 對象的訪問定位
建立對象就是為了使用對象,我們的 Java 程序通過棧上的 reference 數據來操作堆上的具體對象。對象的訪問方式由虛擬機實現而定,目前主流的訪問方式有①使用句柄和②直接指針兩種:
1. 句柄: 如果使用句柄的話,那么 Java 堆中將會劃分出一塊內存來作為句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息;
2. 直接指針: 如果使用直接指針訪問,那么 Java 堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,而 reference 中存儲的直接就是對象的地址。
這兩種對象訪問方式各有優勢。使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 本身不需要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。
四 重點補充內容
4.1 String 類和常量池
String 對象的兩種創建方式:
String str1 = "abcd"; //先檢查字符串常量池中有沒有"abcd",如果字符串常量池中沒有,則創建一個,然后 str1 指向字符串常量池中的對象,如果有,則直接將 str1 指向"abcd""; String str2 = new String("abcd");//堆中創建一個新的對象 String str3 = new String("abcd");//堆中創建一個新的對象 System.out.println(str1==str2);//false System.out.println(str2==str3);//false這兩種不同的創建方法是有差別的。
- 第一種方式是在常量池中拿對象;
- 第二種方式是直接在堆內存空間創建一個新的對象。
記住一點:只要使用 new 方法,便需要創建新的對象。
再給大家一個圖應該更容易理解:
String 類型的常量池比較特殊。它的主要使用方法有兩種:
- 直接使用雙引號聲明出來的 String 對象會直接存儲在常量池中。
- 如果不是用雙引號聲明的 String 對象,可以使用 String 提供的 intern 方法。String.intern() 是一個 Native 方法,它的作用是:如果運行時常量池中已經包含一個等于此 String 對象內容的字符串,則返回常量池中該字符串的引用;如果沒有,JDK1.7之前(不包含1.7)的處理方式是在常量池中創建與此 String 內容相同的字符串,并返回常量池中創建的字符串的引用,JDK1.7以及之后的處理方式是在常量池中記錄此字符串的引用,并返回該引用。
字符串拼接:
String str1 = "str";String str2 = "ing";String str3 = "str" + "ing";//常量池中的對象String str4 = str1 + str2; //在堆上創建的新的對象 String str5 = "string";//常量池中的對象System.out.println(str3 == str4);//falseSystem.out.println(str3 == str5);//trueSystem.out.println(str4 == str5);//false
盡量避免多個字符串拼接,因為這樣會重新創建對象。如果需要改變字符串的話,可以使用 StringBuilder 或者 StringBuffer。
4.2 String s1 = new String(“abc”);這句話創建了幾個字符串對象?
將創建 1 或 2 個字符串。如果池中已存在字符串常量“abc”,則只會在堆空間創建一個字符串常量“abc”。如果池中沒有字符串常量“abc”,那么它將首先在池中創建,然后在堆空間中創建,因此將創建總共 2 個字符串對象。
驗證:
String s1 = new String("abc");// 堆內存的地址值String s2 = "abc";System.out.println(s1 == s2);// 輸出 false,因為一個是堆內存,一個是常量池的內存,故兩者是不同的。System.out.println(s1.equals(s2));// 輸出 true結果:
false true4.3 8 種基本類型的包裝類和常量池
- Java 基本類型的包裝類的大部分都實現了常量池技術,即 Byte,Short,Integer,Long,Character,Boolean;這 5 種包裝類默認創建了數值[-128,127] 的相應類型的緩存數據,但是超出此范圍仍然會去創建新的對象。 為啥把緩存設置為[-128,127]區間?(參見issue/461)性能和資源之間的權衡。
- 兩種浮點數類型的包裝類 Float,Double 并沒有實現常量池技術。
Integer 緩存源代碼:
/** *此方法將始終緩存-128 到 127(包括端點)范圍內的值,并可以緩存此范圍之外的其他值。 */public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)return IntegerCache.cache[i + (-IntegerCache.low)];return new Integer(i);}應用場景:
- Integer i1=40;Java 在編譯的時候會直接將代碼封裝成 Integer i1=Integer.valueOf(40);,從而使用常量池中的對象。
- Integer i1 = new Integer(40);這種情況下會創建新的對象。
Integer 比較更豐富的一個例子:
Integer i1 = 40;Integer i2 = 40;Integer i3 = 0;Integer i4 = new Integer(40);Integer i5 = new Integer(40);Integer i6 = new Integer(0);System.out.println("i1=i2 " + (i1 == i2));System.out.println("i1=i2+i3 " + (i1 == i2 + i3));System.out.println("i1=i4 " + (i1 == i4));System.out.println("i4=i5 " + (i4 == i5));System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); System.out.println("40=i5+i6 " + (40 == i5 + i6));結果:
i1=i2 true i1=i2+i3 true i1=i4 false i4=i5 false i4=i5+i6 true 40=i5+i6 true解釋:
語句 i4 == i5 + i6,因為+這個操作符不適用于 Integer 對象,首先 i5 和 i6 進行自動拆箱操作,進行數值相加,即 i4 == 40。然后 Integer 對象無法與數值進行直接比較,所以 i4 自動拆箱轉為 int 值 40,最終這條語句轉為 40 == 40 進行數值比較。
6. 如何判斷對象是否死亡(兩種方法)。
7. 簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)。
8. 如何判斷一個常量是廢棄常量
9. 如何判斷一個類是無用的類
10. 垃圾收集有哪些算法,各自的特點?
11. HotSpot 為什么要分為新生代和老年代?
12. 常見的垃圾回收器有哪些?
13. 介紹一下 CMS,G1 收集器。
14. Minor Gc 和 Full GC 有什么不同呢?
本文導火索
當需要排查各種內存溢出問題、當垃圾收集成為系統達到更高并發的瓶頸時,我們就需要對這些“自動化”的技術實施必要的監控和調節。
1 揭開 JVM 內存分配與回收的神秘面紗
Java 的自動內存管理主要是針對對象內存的回收和對象內存的分配。同時,Java 自動內存管理最核心的功能是 堆內存中對象的分配與回收。
Java 堆是垃圾收集器管理的主要區域,因此也被稱作GC 堆(Garbage Collected Heap).從垃圾回收的角度,由于現在收集器基本都采用分代垃圾收集算法,所以 Java 堆還可以細分為:新生代和老年代:再細致一點有:Eden 空間、From Survivor、To Survivor 空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。
堆空間的基本結構:
上圖所示的 eden 區、s0(“From”) 區、s1(“To”) 區都屬于新生代,tentired 區屬于老年代。大部分情況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收后,如果對象還存活,則會進入 s1(“To”),并且對象的年齡還會加 1(Eden 區->Survivor 區后對象的初始年齡變為 1),當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。經過這次GC后,Eden區和"From"區已經被清空。這個時候,“From"和"To"會交換他們的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To”。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重復這樣的過程,直到“To”區被填滿,"To"區被填滿之后,會將所有對象移動到老年代中。
1.1 對象優先在 eden 區分配
目前主流的垃圾收集器都會采用分代回收算法,因此需要將堆內存分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。
大多數情況下,對象在新生代中 eden 區分配。當 eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC.下面我們來進行實際測試以下。
在測試之前我們先來看看 Minor GC 和 Full GC 有什么不同呢?
- 新生代 GC(Minor GC):指發生新生代的的垃圾收集動作,Minor GC 非常頻繁,回收速度一般也比較快。
- 老年代 GC(Major GC/Full GC):指發生在老年代的 GC,出現了 Major GC 經常會伴隨至少一次的 Minor GC(并非絕對),Major GC 的速度一般會比 Minor GC 的慢 10 倍以上。
測試:
public class GCTest {public static void main(String[] args) {byte[] allocation1, allocation2;allocation1 = new byte[30900*1024];//allocation2 = new byte[900*1024];} }通過以下方式運行:
添加的參數:-XX:+PrintGCDetails
運行結果 (紅色字體描述有誤,應該是對應于 JDK1.7 的永久代):
從上圖我們可以看出 eden 區內存幾乎已經被分配完全(即使程序什么也不做,新生代也會使用 2000 多 k 內存)。假如我們再為 allocation2 分配內存會出現什么情況呢?
簡單解釋一下為什么會出現這種情況: 因為給 allocation2 分配內存的時候 eden 區內存幾乎已經被分配完了,我們剛剛講了當 Eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC.GC 期間虛擬機又發現 allocation1 無法存入 Survivor 空間,所以只好通過 分配擔保機制 把新生代的對象提前轉移到老年代中去,老年代上的空間足夠存放 allocation1,所以不會出現 Full GC。執行 Minor GC 后,后面分配的對象如果能夠存在 eden 區的話,還是會在 eden 區分配內存。可以執行如下代碼驗證:
1.2 大對象直接進入老年代
大對象就是需要大量連續內存空間的對象(比如:字符串、數組)。大對象對虛擬機的內存分配來說就是一個壞消息,經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。
為什么要這樣呢?
為了避免為大對象分配內存時由于分配擔保機制帶來的復制而降低效率。
虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大于這個設置值的對象直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的內存復制(新生代采用復制算法收集內存)。
注意 PretenureSizeThreshold參數只對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認識這個參數,Parallel Scavenge收集器一般并不需要設置。如果遇到必須使用此參數的場合,可以考慮ParNew加CMS的收集器組合。
1.3 長期存活的對象將進入老年代
既然虛擬機采用了分代收集的思想來管理內存,那么內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代中。為了做到這一點,虛擬機給每個對象一個對象年齡(Age)計數器。
如果對象在 Eden 出生并經過第一次 Minor GC 后仍然能夠存活,并且能被 Survivor 容納的話,將被移動到 Survivor 空間中,并將對象年齡設為 1.對象在 Survivor 中每熬過一次 MinorGC,年齡就增加 1 歲,當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。
1.4 動態對象年齡判定
為了更好的適應不同程序的內存情況,虛擬機不是永遠要求對象年齡必須達到了某個值才能進入老年代,如果 Survivor 空間中相同年齡所有對象大小的總和大于 Survivor 空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無需達到要求的年齡。
2 對象已經死亡?
堆中幾乎放著所有的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象)。
2.1 引用計數法
給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加 1;當引用失效,計數器就減 1;任何時候計數器為 0 的對象就是不可能再被使用的。
這個方法實現簡單,效率高,但是目前主流的虛擬機中并沒有選擇這個算法來管理內存,其最主要的原因是它很難解決對象之間相互循環引用的問題。 所謂對象之間的相互引用問題,如下面代碼所示:除了對象 objA 和 objB 相互引用著對方之外,這兩個對象之間再無任何引用。但是他們因為互相引用對方,導致它們的引用計數器都不為 0,于是引用計數算法無法通知 GC 回收器回收他們。
public class ReferenceCountingGc {Object instance = null;public static void main(String[] args) {ReferenceCountingGc objA = new ReferenceCountingGc();ReferenceCountingGc objB = new ReferenceCountingGc();objA.instance = objB;objB.instance = objA;objA = null;objB = null;} }2.2 可達性分析算法
這個算法的基本思想就是通過一系列的稱為 “GC Roots” 的對象作為起點,從這些節點開始向下搜索,節點所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的。
2.3 再談引用
無論是通過引用計數法判斷對象引用數量,還是通過可達性分析法判斷對象的引用鏈是否可達,判定對象的存活都與“引用”有關。
JDK1.2 之前,Java 中引用的定義很傳統:如果 reference 類型的數據存儲的數值代表的是另一塊內存的起始地址,就稱這塊內存代表一個引用。
JDK1.2 以后,Java 對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)
1.強引用(StrongReference)
以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當內存空間不足,Java 虛擬機寧愿拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。
2.軟引用(SoftReference)
如果一個對象只具有軟引用,那就類似于可有可無的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,JAVA 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。
3.弱引用(WeakReference)
如果一個對象只具有弱引用,那就類似于可有可無的生活用品。弱引用與軟引用的區別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由于垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java 虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。
4.虛引用(PhantomReference)
"虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。
虛引用主要用來跟蹤對象被垃圾回收的活動。
虛引用與軟引用和弱引用的一個區別在于: 虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。
特別注意,在程序設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因為軟引用可以加速 JVM 對垃圾內存的回收速度,可以維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生。
2.4 不可達的對象并非“非死不可”
即使在可達性分析法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑階段”,要真正宣告一個對象死亡,至少要經歷兩次標記過程;可達性分析法中不可達的對象被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。當對象沒有覆蓋 finalize 方法,或 finalize 方法已經被虛擬機調用過時,虛擬機將這兩種情況視為沒有必要執行。
被判定為需要執行的對象將會被放在一個低優先級隊列中進行第二次標記,除非這個對象與引用鏈上的任何一個對象建立關聯,否則就會被真的回收。
2.5 如何判斷一個常量是廢棄常量
運行時常量池主要回收的是廢棄的常量。那么,我們如何判斷一個常量是廢棄常量呢?
假如在常量池中存在字符串 “abc”,如果當前沒有任何 String 對象引用該字符串常量的話,就說明常量 “abc” 就是廢棄常量,如果這時發生內存回收的話而且有必要的話,“abc” 就會被系統清理出常量池。
注意:JDK1.7 及之后版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開辟了一塊區域存放運行時常量池。
2.6 如何判斷一個類是無用的類
方法區主要回收的是無用的類,那么如何判斷一個類是無用的類的呢?
判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面 3 個條件才能算是 “無用的類” :
虛擬機可以對滿足上述 3 個條件的無用類進行回收,這里說的僅僅是“可以”,而并不是和對象一樣不使用了就會必然被回收。
3 垃圾收集算法
3.1 標記-清除算法
當堆中的有效內存空間(available memory)被耗盡的時候,就會停止整個程序(也被稱為stop the world),然后進行兩項工作,第一項則是標記,第二項則是清除。
它是最基礎的收集算法,后續的算法都是對其不足進行改進得到。這種垃圾收集算法會帶來兩個明顯的問題:
3.2 復制算法
為了解決效率問題,“復制”收集算法出現了。它可以將內存分為大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完后,就將還存活的對象復制到另一塊去,然后再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。復制算法不會產生空間碎片。
復制算法的缺點:
所以從以上的描述中不難看出,復制算法要想使用,最起碼對象的存活率要非常低才行,而且最重要的是,我們必須克服50%內存的浪費。
3.3 標記-整理算法
根據老年代的特點提出的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一端移動,然后直接清理掉端邊界以外的內存。
標記整理算法的缺點:
3.4 分代收集算法
當前虛擬機的垃圾收集都采用分代收集算法,這種算法沒有什么新的思想,只是根據對象存活周期的不同將內存分為幾塊。一般將 java 堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。
比如在新生代中,每次收集都會有大量對象死去,所以可以選擇復制算法,只需要付出少量對象的復制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇“標記-清除”或“標記-整理”算法進行垃圾收集。
延伸面試問題: HotSpot 為什么要分為新生代和老年代?
根據上面的對分代收集算法的介紹回答。
4 垃圾收集器
如果說收集算法是內存回收的方法論,那么垃圾收集器就是內存回收的具體實現。
雖然我們對各個收集器進行比較,但并非要挑選出一個最好的收集器。因為直到現在為止還沒有最好的垃圾收集器出現,更加沒有萬能的垃圾收集器,我們能做的就是根據具體應用場景選擇適合自己的垃圾收集器。試想一下:如果有一種任何場景下都適用的完美收集器存在,那么我們的 HotSpot 虛擬機就不會實現那么多不同的垃圾收集器了。
4.1 Serial 收集器
串行收集器是最古老,最穩定以及效率高的收集器,只使用一個線程去回收但其在進行垃圾收集過程中可能會產生較長的停頓(Stop-The-world狀態)。雖然在收集垃圾過程中需要暫停所有其他的工作線程,但是它簡單高效,對于限定單個CPU環境來說,沒有線程交互的開銷可以獲得最高的單線程垃圾收集效率,因此Serial垃圾收集器依然是Java虛擬機運行在Client模式下默認的新生代垃圾收集器。
對應JVM參數是:-XX:+UseSerialGC
開啟后會使用:Serial(young區用)+SerialOld(Old區用)的收集器組合
表示:新生代、老年代都會使用串行回收收集器,新生代使用復制算法,老年代使用標記-整理算法
4.2 ParNew 收集器
ParNew(并行)收集器:使用多線程進行垃圾回收,在垃圾收集時,會Stop-the-World暫停其他所有的工作線程直到它收集結束。
ParNew收集器其實就是Seria收集器新生代的并行多線程版本,最常見的應用場景是配合老年代的CMS GC工作,其余的行為和Serial收集器完全一樣,ParNew垃圾收集器在垃圾收集過程中同樣也要暫停所有其他的工作線程。它是很多Java虛擬機運行在Server模式下新生代的默認垃圾收集器。
常用對應JVM參數:-XX:+UseParNewGC,啟用ParNew收集器,只影響新生代的收集,不影響老年代。開啟參數后,會使用:ParNew(Young區用)+SerialOld的收集器組合,新生代使用復制算法,老年代采用標記-整理算法
但是,ParNew+Tenured這樣的搭配,java8已經不再推薦。
-XX:ParallelGCThreads 限制線程數量,默認開啟和CPU數目相同的線程數。
并行和并發概念補充:
- 并行(Parallel) :指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態。
- 并發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是并行,可能會交替執行),用戶程序在繼續運行,而垃圾收集器運行在另一個 CPU 上。
4.3 Parallel Scavenge 收集器
Parallel Scavenge收集器類似ParNew也是一個新生代垃圾收集器,使用復制算法,也是一個并行的多線程的垃圾收集器,俗稱吞吐量優先收集器。總結就是串行收集器在新生代和老年代的并行化。
-XX:+UseParallelGC
使用 Parallel 收集器+ 老年代串行
-XX:+UseParallelOldGC
使用 Parallel 收集器+ 老年代并行
它重點關注的是:可控制的吞吐量(Thoughput=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),也即比如程序運行100分鐘,垃圾收集時間1分鐘,吞吐量就是99%)。高吞吐量意味著高效利用CPU的時間,它多用于在后臺運算而不需要太多交互的任務。
自適應調節策略也是ParallelScavenge收集器與ParNew收集器的一個重要區別。(自適應調節策略:虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間(-XX:MaxGCPauseMillis)或最大的吞吐量。
常用JVM參數:-XX:+UseParaIIeIGC或-XX:+UseParaIIeIOldGC(可互相激活)使用ParallelScanvenge收集器開啟該參數后:新生代使用復制算法,老年代使用標記·整理算法
補充:
-XX:ParaIIeIGCThreads=數字N,表示啟動多少個GC線程
cpu>8,N=5/8
cpu<8,N=實際個數
4.4.Serial Old 收集器
SerialOld是 Serial垃圾收集器老年代版本,它同樣是個單線程的收集器,使用標記-整理算法,這個收集器也主要是運行在Client默認的java虛擬機默認的年老代垃圾收集器。
在Server模式下,主要有兩個用途(了解版本己經到8及以后):
4.5 Parallel Old 收集器
ParallelOld收集器是ParaScavenge的老年代版本,使用多線程的標記-整理算法,Parallel Old收集器在JDK1.6才開始提供。
在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的SerialOld收集器,只能保證新生代的吞吐量優先,無法保證整體的吞吐量。
ParallelOld正是為了在年老代同樣提供吞吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,JDK1.8后可以優先考慮新生代ParallelScavenge和年老代Parallel Old收集器的搭配策略。在JDK1.8及后(Parallel Scavenge+Parallel Old)
JVM常用參數:
-XX:+UseParallelOldGC使用ParallelOld收集器,設置該參數后,新生代Parallel+老年代ParallelOld
4.6 CMS 收集器
CMS收集器(ConcurrentMarkSweep:并發標記清除)是一種以獲取最短回收停頓時間為目標的收集器。適合應用在互聯網站或者B/S系統的服務器上,這類應用尤其重視服務器的響應速度,希望系統停頓時間最短。
CMS非常適合堆內存大、CPU核數多的服務器端應用,也是G1出現之前大型應用的首選收集器。
Concurrent Mark Sweep 并發標記清除,并發收集低停頓,并發指的是與用戶線程一起執行。
開啟該收集器的JVM參數:-XX:+UseConcMarkSreepGC,開啟該參數后會自動將-XX:+UseParNewGC打開。開啟該參數后,使用ParNew(Young區用)+CMS(Old區用)+SerialOld的收集器組合,SerialOld將作為CMS出錯的后備收集器。
從名字中的Mark Sweep這兩個詞可以看出,CMS 收集器是一種 “標記-清除”算法實現的,它的運作過程相比于前面幾種垃圾收集器來說更加復雜一些。整個過程分為四個步驟:
由于耗時最長的并發標記和并發清除過程中,垃圾收集線程可以和用戶一起并發工作,所以總體上來看CMS收集器的內存回收和用戶線程是一起并發地執行。
CMS優點:并發收集低停頓
CMS缺點:
4.7 G1 收集器
G1(Garbage-Frist)收集器,是一款面向服務端應用的收集器。
從官網的描述中,我們知道G1是一種服務器端的垃圾收集器,應用在多處理器和大容量內存環境中,在提高吞吐量的同時,盡可能的滿足垃圾收集暫停時間的要求。另外,它還具有以下特性:
G1收集器的設計目標是取代CMS收集器,它同CMS相比,在以下方面表現更出色:
CMS垃圾集器雖然減少了暫停應用程序的運行時間,但是它還是存在著內存碎片問題。于是,為了去除內存碎片問題,同時又保留CMS垃圾收集器低暫停時間的優點,JAVA7發布了一個新的垃圾收集器-G1垃圾收集器。
G1是在2012年才在jdk1.7u4中可用。oracle官方計劃在jdk9中將G1變成默認的垃圾收集器以替代CMS。它是一款面向服務端應用的收集器,主要應用在多CPU和大內存服務器環境下,極大的減少垃圾收集的停頓時間,全面提升服務器的性能,逐步替換java8以前的CMS收集器。
主要改變是Eden,Survivor和Tenured等內存區域不再是連續的了,而是變成了一個個大小一樣的region,每個region從1M到32M不等。一個region有可能屬于Eden,Survivor或者Tenured內存區域。
G1特點
準備。GI只有邏輯上的分代概念,或者說每個分區都可能隨G1的運行在不同代之間前后切換;
G1底層原理
Region區域化垃圾收集器
G1算法將堆劃分為若干個區域(Region),它仍然屬于分代收集器
這些Region的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應用線程的方式,將存活對象拷貝到老年代或者survivor空間。
這些Region的一部分包含老年代,G1收集器通過將對象從一個區域復制到另外一個區域,完成了清理工作。這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有CMS內存碎片問題的存在了。
在G1中,還有一種特殊的區域,叫Humongous(巨大的)區域
如果一個對象占用的空間超過了分區容量50%以上,G1收集器就認為這是一個巨型對象。這些巨型對象默認直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那么G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC。
G1回收步驟
G1 收集器下的 Young GC
針對 Eden 區進行收集,Eden 區耗盡后會被觸發,主要是小區域收集 + 形成連續的內存塊,避免內存碎片。
- Eden 區的數據移動到 Survivor 區,假如出現 Survivor 區空間不夠,Eden 區數據會部分晉升到 Old 區
- Survivor 區的數據移動到新的 Survivor 區,部分數據晉升到 Old
- 最后 Eden 區收拾干凈了,GC結束,用戶的應用程序繼續執行
運行過程:
和CMS相比的優勢
比起CMS有兩個優勢:
4.8 小結
15. 類加載過程
Class 文件需要加載到虛擬機中之后才能運行和使用,那么虛擬機是如何加載這些 Class 文件呢?
系統加載 Class 類型的文件主要三步:加載->連接->初始化。連接過程又可分為三步:驗證->準備->解析。
1. 加載
類加載過程的第一步,主要完成下面3件事情:
虛擬機規范多上面這3點并不具體,因此是非常靈活的。比如:“通過全類名獲取定義此類的二進制字節流” 并沒有指明具體從哪里獲取、怎樣獲取。比如:比較常見的就是從 ZIP 包中讀取(日后出現的JAR、EAR、WAR格式的基礎)、其他文件生成(典型應用就是JSP)等等。
一個非數組類的加載階段(加載階段獲取類的二進制字節流的動作)是可控性最強的階段,這一步我們可以去完成還可以自定義類加載器去控制字節流的獲取方式(重寫一個類加載器的 loadClass() 方法)。數組類型不通過類加載器創建,它由 Java 虛擬機直接創建。
類加載器、雙親委派模型也是非常重要的知識點,這部分內容會在后面的文章中單獨介紹到。
加載階段和連接階段的部分內容是交叉進行的,加載階段尚未結束,連接階段可能就已經開始了。
2. 驗證
3. 準備
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些內存都將在方法區中分配。對于該階段有以下幾點需要注意:
基本數據類型的零值:
4. 解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符7類符號引用進行。
符號引用就是一組符號來描述目標,可以是任何字面量。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。在程序實際運行時,只有符號引用是不夠的,舉個例子:在程序執行方法時,系統需要明確知道這個方法所在的位置。Java 虛擬機為每個類都準備了一張方法表來存放類中所有的方法。當需要調用一個類的方法的時候,只要知道這個方法在方發表中的偏移量就可以直接調用該方法了。通過解析操作符號引用就可以直接轉變為目標方法在類中方法表的位置,從而使得方法可以被調用。
綜上,解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,也就是得到類或者字段、方法在內存中的指針或者偏移量。
5. 初始化
初始化是類加載的最后一步,也是真正執行類中定義的 Java 程序代碼(字節碼),初始化階段是執行類構造器 <clinit> ()方法的過程。
對于<clinit>() 方法的調用,虛擬機會自己確保其在多線程環境中的安全性。因為<clinit>() 方法是帶鎖線程安全,所以在多線程環境下進行類初始化的話可能會引起死鎖,并且這種死鎖很難被發現。
對于初始化階段,虛擬機嚴格規范了有且只有5種情況下,必須對類進行初始化:
6. 類加載器總結
JVM 中內置了三個重要的 ClassLoader,除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自java.lang.ClassLoader:
- BootstrapClassLoader(啟動類加載器) :最頂層的加載類,由C++實現,負責加載 %JAVA_HOME%/lib目錄下的jar包和類或者或被 -Xbootclasspath參數指定的路徑中的所有類。
- ExtensionClassLoader(擴展類加載器) :主要負責加載目錄 %JRE_HOME%/lib/ext 目錄下的jar包和類,或被 java.ext.dirs 系統變量所指定的路徑下的jar包。
- AppClassLoader(應用程序類加載器) :面向我們用戶的加載器,負責加載當前應用classpath下的所有jar包和類。
7. 雙親委派模型介紹
每一個類都有一個對應它的類加載器。系統中的 ClassLoder 在協同工作的時候會默認使用 雙親委派模型 。即在類加載的時候,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則才會嘗試加載。加載的時候,首先會把該請求委派該父類加載器的 loadClass() 處理,因此所有的請求最終都應該傳送到頂層的啟動類加載器 BootstrapClassLoader 中。當父類加載器無法處理時,才由自己來處理。當父類加載器為null時,會使用啟動類加載器 BootstrapClassLoader 作為父類加載器。
8. 雙親委派模型的好處
雙親委派模型保證了Java程序的穩定運行,可以避免類的重復加載(JVM 區分不同類的方式不僅僅根據類名,相同的類文件被不同的類加載器加載產生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改。如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現一些問題,比如我們編寫一個稱為 java.lang.Object 類的話,那么程序運行的時候,系統就會出現多個不同的 Object 類。
總結