【Java常见面试题】JVM篇
導航:
【黑馬Java筆記+踩坑匯總】JavaSE+JavaWeb+SSM+SpringBoot+瑞吉外賣+SpringCloud+黑馬旅游+谷粒商城+學成在線+牛客面試題
目錄
1、說說你了解的JVM內存模型
2、簡單說下你對JVM的了解
3、說說類加載機制
4、說說對象的實例化過程
5、說說JVM的雙親委派模型
6、說說JVM調優思路
7、項目中有沒有實際的JVM調優經驗?
8、請你說說內存溢出
9、請你說說內存泄漏
10、JVM中一次完整的GC流程是怎樣的
11、說說JVM的垃圾回收機制
12、說說GC的可達性分析算法
13、說說JVM的垃圾回收算法
14、說說七個垃圾回收器
15、請你講下CMS(并發標記清除)回收器
16、請你講下G1垃圾優先回收器
1、說說你了解的JVM內存模型
得分點?
類加載子系統、運行時數據區、執行引擎
JVM由三部分組成:類加載子系統、運行時數據區、執行引擎
類加載子系統:通過類加載機制加載類的class文件,如果該類是第一次加載,會執行加載、驗證、解析。只負責class文件的加載,至于是否可運行,則由執行引擎決定。
類加載過程是在類加載子系統完成的:加載 --> 鏈接(驗證 --> 準備 --> 解析) --> 初始化
運行時數據區:
在程序運行時,存儲程序的內容(例如字節碼、對象、參數、返回值等)。運行時數據區包括本地方法棧、虛擬機棧、方法區、堆、程序計數器。
只有方法區和堆是各線程共享的進程內存區域,其他運行區都是每個線程可以獨立擁有的。
- 本地方法棧:存放本地方法調用過程中的棧幀。用于管理本地方法的調用,本地方法是C語言寫的。不是所有虛擬機都支持本地方法棧,例如Hotspot虛擬機就是將本地方法棧和虛擬機棧合二為一。棧解決程序的運行問題,即程序如何執行、如何處理數據。
- 棧幀:棧幀是棧的元素,由三部分組成,即局部變量表(存方法參數和局部變量)、操作數棧(存方法執行過程中的中間結果,或者其他暫存數據)和幀數據區(存方法返回地址、線程引用等附加信息)。
- 虛擬機棧:存放Java方法調用過程中的棧幀。用于管理Java方法的調用,Java方法是開發時寫的Java方法。
- 方法區:可以看作是一塊獨立于Java堆的內存空間,存儲已被虛擬機加載的類型信息(類、接口、枚舉、注解等類型的全名、父類名、父接口名列表、修飾符)、常量、靜態變量、即時編譯器編譯后的代碼緩存等。方法區是各線程共享的內存區域。
- 方法區和永久代、元空間的關系:方法區是一個抽象概念,永久代和元空間是方法區的實現方式。
- 永久代:屬于JVM方法區的內存,用來存儲類的元數據,如類名、方法信息、字段信息等一些靜態的數據。JDK7及之前方法區也叫永久代。缺點是內存大小固定,容易出現oom問題。可以通過-XX:PermSize設置永久代大小。永久代對象只能通過Major GC(又稱Full GC)進行垃圾回收。
- 元空間:是Hotspot在JDK8引入的,用于取代永久代。元空間屬于本地內存,由操作系統直接管理,不再受JVM管理。同時內存空間可以自動擴容,避免內存溢出。默認情況下元空間可以無限使用本地內存,也可以通過-XX:MetaspaceSize限制內存大小。
- 常量池:就是一張表,JVM根據這張常量表找到要執行的類信息和方法信息
- 類常量池:是.class字節碼文件中的資源倉庫,主要存放字面量(表示字符串值和數值,例如文本字符串、final常量、靜態變量)和符號引用(類或接口、方法、成員變量的符號引用,具體包括類全限定名、字段名和描述符、方法名和描述符)。
- 運行時常量池:類加載的“加載”階段會創建運行時常量池,存放類常量池一部分符號引用。在類加載的“解析”階段JVM會把運行時常量池的這些符號引用轉為直接引用。類常量池。類常量池在字節碼文件中的,運行時常量池在內存中。
- 字符串常量池:專門針對String類型設計的常量池。是當前應用程序里所有線程共享的,每個jvm只有一個字符串常量池。存儲字符串對象的引用。在創建String對象時,JVM會先在字符串常量池尋找是否已存在相同字符串的引用,如果有的話就直接返回引用,沒的話就在堆中創建一個對象,然后常量池保存這個引用并返回引用。
- 方法區和永久代、元空間的關系:方法區是一個抽象概念,永久代和元空間是方法區的實現方式。
- 堆:存放對象實例、實例變量、數組,包括新生代(伊甸園區、幸存區S0和S1)和老年代。堆是垃圾收集器管理的內存區域。堆解決的是數據存儲的問題,即數據怎么放、放在哪兒。堆實際內存空間可以不連續,大小可以選擇固定大小或可擴展,堆是各線程共享的內存區域。
- 程序計數器(PC寄存器):存放下一條字節碼指令的地址,由執行引擎讀取下一條字節碼指令并轉為本地機器指令進行執行。是程序控制流(分支、循環、跳轉、線程恢復)的指示器,只有它不會拋出OutOfMemoryError。每個線程有自己獨立的程序計數器,以便于線程在切換回來時能知道下一條指令是什么。程序計數器生命周期與線程一致。
執行引擎:將字節碼指令解釋/編譯為對應平臺上的本地機器指令。充當了將高級語言翻譯為機器語言的譯者。執行引擎在執行過程中需要執行什么樣的字節碼指令依賴于PC寄存器。每當執行完一項指令操作后,PC寄存器就會更新下一條需要被執行的指令地址。
- 字節碼指令(JVM指令):字節碼文件中的指令,內部只包含一些能夠被JVM所識別的字節碼指令、符號表,以及其他輔助信息,不能夠直接運行在操作系統之上。
- 本地機器指令:可以直接運行在操作系統之上。
內存模型:
內存模型里的運行時數據區:
JVM由三部分組成:類加載子系統、執行引擎、運行時數據區。
1. 類加載子系統,可以根據指定的全限定名來載入類或接口。
2. 運行時數據區。當程序運行時,JVM需要內存來存儲許多內容,例如:字節碼、對象、參數、返回值、局部變量、運算的中間結果,等等,JVM會把這些東西都存儲到運行時數據區中,以便于管理。而運行時數據區又可以分為方法區、堆、虛擬機棧、本地方法棧、程序計數器。
3. 執行引擎,負責執行那些包含在被載入類的方法中的指令。
加分回答-運行時數據區
運行時數據區是開發者重點要關注的部分,因為程序的運行與它密不可分,很多錯誤的排查也需要基于對運行時數據區的理解。在運行時數據區所包含的幾塊內存空間中,方法區和堆是線程之間共享的內存區域,而虛擬機棧、本地方法棧、程序計數器則是線程私有的區域,就是說每個線程都有自己的這個區域。?
2、簡單說下你對JVM的了解
得分點
Java跨平臺、HotSpot、熱點代碼探測技術、內存模型、垃圾回收算法、垃圾回收器
跨平臺?
Java跨平臺,JVM不跨平臺。?
JVM是Java語言跨平臺的關鍵,Java在虛擬機層面隱藏了底層技術的復雜性以及機器與操作系統的差異性。運行程序的物理機千差萬別,而JVM則在千差萬別的物理機上面建立了統一的運行平臺,實現了在任意一臺JVM上編譯的程序,都能在任何其他JVM上正常運行。這一極大的優勢使得Java應用的開發比傳統C/C++應用的開發更高效快捷,程序員可以把主要精力放在具體業務邏輯,而不是放在保障物理硬件的兼容性上。通常情況下,一個程序員只要了解了必要的Java類庫、Java語法,學習適當的第三方開發框架,就已經基本滿足日常開發的需要了,JVM會在用戶不知不覺中完成對硬件平臺的兼容及對內存等資源的管理工作。
默認Java虛擬機HotSpot?
HotSpot是Sun/OracleJDK和OpenJDK中的默認Java虛擬機,也是目前使用范圍最廣的Java虛擬機。HotSpot既繼承了Sun之前兩款商用虛擬機的優點,也有許多自己新的技術優勢,如它名稱中的HotSpot指的就是它的熱點代碼探測技術。HotSpot的熱點代碼探測能力可以通過執行計數器找出最具有編譯價值的代碼,然后通知即時編譯器以方法為單位進行編譯。如果一個方法被頻繁調用,或方法中有效循環次數很多,將會分別觸發標準即時編譯和棧上替換編譯行為。通過編譯器與解釋器恰當地協同工作,可以在最優化的程序響應時間與最佳執行性能中取得平衡,而且無須等待本地代碼輸出才能執行程序,即時編譯的時間壓力也相對減小,這樣有助于引入更復雜的代碼優化技術,輸出質量更高的本地代碼。本地方法棧和Java方法棧是合并的。
內存模型?
JVM由三部分組成:類加載子系統、運行時數據區、執行引擎。
類加載子系統:根據指定的全限定名來載入類或接口。
運行時數據區:在程序運行時,存儲程序的內容,例如:字節碼、對象、參數、返回值等。而運行時數據區又可以分為方法區、堆、虛擬機棧、本地方法棧、程序計數器。
執行引擎:負責執行那些包含在被載入類的方法中的指令。
3、說說類加載機制
得分點
加載、驗證、準備、解析、初始化
標準回答?
類加載過程:加載、鏈接(驗證、準備、解析)、初始化。這個過程是在類加載子系統完成的。
加載:生成類的Class對象。
鏈接:將類的二進制數據合并到JRE中。該過程分為以下3個階段:
- 驗證:確保代碼符合JAVA虛擬機規范和安全約束。包括文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。
- 文件格式驗證:驗證字節流是否符合Class文件格式規范。例如版本號是否在JVM兼容范圍、常量池的常量是否有不被支持的常量類型、是否魔數0xCAFEBABE開頭。
- 元數據驗證:元數據是類的全名、方法信息、字段信息、繼承關系等。例如驗證類名接口名標識符有沒有符合規范、有沒有實現接口的所有方法、有沒有實現抽象類的所有抽象方法、是不是繼承了final類等。
- 字節碼驗證:主要校驗類的方法體,通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。如:保證方法體中的類型轉換是有效的;保證跳轉指令不會跳轉到方法體以外的字節碼指令上;保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作。
- 符號引用驗證:驗證類全限定名是否能找到對應的類、引用的對象是否存在,是否有權引用等。
- 準備:為類變量(即static變量)分配內存并賦零值。
- 解析:將方法區-運行時常量池內的符號引用(類的名字、成員名、標識符)轉為直接引用(實際內存地址,不包含任何抽象信息,因此可以直接使用)。
初始化:類變量賦初值、執行靜態語句塊。
?
一個類型從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期將會經歷加載、驗證、準備、解析、初始化、使用、卸載七個階段,其中驗證、準備、解析三個部分統稱為連接,而前五個階段則是類加載的完整過程。
?
1. 在加載階段JVM需要在內存中生成一個代表這個類的Class對象,作為方法區這個類的各種數據的訪問入口。
2. 驗證階段大致上會完成下面四個階段的檢驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。
3. 準備階段是正式為類中定義變量(靜態變量)分配到內存并設置類變量初始值的階段,這些變量所使用的內存都應當在方法區中進行分配,但必須注意到方法區本身是一個邏輯上的區域。
4. 解析階段是Java虛擬機將常量池內的符號替換為直接引用的過程,符號引用以一組符號來描述所引用的目標,直接引用是可以直接指向目標的指針、相對偏移量或者一個能間接定位到目標的句柄。
5. 類的初始化階段是類加載過程的最后一個步驟,直到初始化階段,Java虛擬機才真正開始執行類中編寫的Java程序代碼,將主導權移交給應用程序。本質上,初始化階段就是執行類構造器的過程。并不是程序員在Java代碼中直接編寫的方法,它是Javac編譯器的自動生成物。
加分回答
關于在什么情況下需要開始類加載過程的第一個階段“加載”,《Java虛擬機規范》中并沒有進行強制約束,這點可以交給虛擬機的具體實現來自由把握。但是對于初始化階段,《Java虛擬機規范》則是嚴格規定了有且只有六種情況必須立即對類進行“初始化”:
1. 使用new實例化對象、讀寫類的靜態字段、調用類的靜態方法時。
2. 使用java.lang.reflect包的方法對類型進行反射調用時。
3. 當初始化類時,若發現其父類還沒有進行過初始化,則先初始化這個父類。
4. 虛擬機啟動時,需要指定一個要執行的主類,虛擬機會先初始化這個主類。
5. 當使用JDK 7新加入的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種類型的方法句柄,并且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化。
6. 當一個接口中定義了JDK 8新加入的默認方法(被default關鍵字修飾的接口方法)時,如果有這個接口的實現類發生了初始化,那該接口要在其之前被初始化。
4、說說對象的實例化過程
得分點
類加載、分配內存(內存規整和不規整)、處理并發安全問題、設置對象頭、成員變量賦初值、執行構造方法
對象的實例化過程:
指針碰撞法:?指針一直在空閑和已用內存中間,分配空間時,指針往空閑內存方向移動一段距離,使這段距離剛好滿足新對象內存大小。
元空間:是Hotspot在JDK8引入的,用于取代永久代。元空間屬于本地內存,由操作系統直接管理,不再受JVM管理。同時也可以自動擴容內存空間,避免內存溢出。默認情況下元空間可以無限使用本地內存,也可以通過-XX:MetaspaceSize限制內存大小。
指針碰撞法:?所有用過的內存在一邊,空閑的內存在另外一邊,中間放著一個指針作為分界點的指示器,分配內存就僅僅是把指針向空閑那邊挪動一段與對象大小相等的距離罷了。
如果垃圾收集器選擇的是Serial、ParNew這種基于壓縮算法的,虛擬機采用這種分配方式。 一般使用帶有compact( 整理)過程的收集器時,使用指針碰撞。
回顧synchronized用到的對象頭:
前面多線程篇有提到,synchronized鎖基于對象頭的Mark Word,鎖升級四個狀態里,偏向鎖和輕量級鎖基于CAS原子替換,重量級鎖基于Monitor對象。對象頭里Mark Word存哈希碼、GC標記、鎖信息。對象頭里類型指針指向當前對象所在的類。
鎖信息:
-
鎖標志位:01未鎖定、01可偏向、00輕量級鎖、10重量級鎖、11垃圾回收標記
-
偏向鎖線程ID、時間戳等
-
輕量級鎖的指針:指向鎖記錄的指針
-
重量級鎖的指針:指向Monitor鎖的指針
【Java面試八股文】Java多線程篇_java多線程八股文_vincewm的博客-CSDN博客
在JVM中,對象的創建遵循如下過程:
當JVM遇到一條字節碼new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
在類加載檢查通過后,接下來虛擬機將為新生對象分配內存。對象所需內存的大小在類加載完成后便可完全確定,為對象分配空間的任務實際上便等同于把一塊確定大小的內存塊從Java堆中劃分出來。
內存分配完成之后,虛擬機必須將分配到的內存空間都初始化為零值,如果使用了TLAB的話,這一項工作也可以提前至TLAB分配時順便進行。這步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,使程序能訪問到這些字段的數據類型所對應的零值。
接下來,虛擬機還要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭之中。根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。
在上面工作都完成之后,從虛擬機的視角來看,一個新的對象已經產生了。但是從Java程序的視角看來,對象創建才剛剛開始——構造函數,即Class文件中的`<init>()`方法還沒有執行,所有的字段都為默認的零值,對象需要的其他資源和狀態信息也還沒有按照預定的意圖構造好。
一般來說,new指令之后會接著執行`<init>()`方法,按照程序員的意愿對對象進行初始化,這樣一個真正可用的對象才算完全被構造出來。
5、說說JVM的雙親委派模型
得分點
三個默認類加載器、工作過程、作用
JVM三個默認類加載器
- 啟動類加載器BootStrapClassLoader(最頂端):無法被Java程序直接引用,只能加載委派過來的請求。
- 擴展類加載器ExtClassLoader:可以直接用來加載類,也可以通過委派加載類。Ext是Extract縮寫,譯為擴展、提取。
- 應用程序類加載器AppClassLoader(最低端):負責加載類路徑所有的類庫,開發者同樣可以直接在代碼中使用這個類加載器。
雙親委派模型的工作過程
雙親委派模型的工作過程是,如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去完成加載。
具體的流程:
JVM在加載一個類時,會調用應用程序類加載器的loadClass()方法來加載這個類,不過在這方法中,會先使用擴展類加載器的loadClass()方法來加載類,同樣擴展類加載器的loadClass()方法中會先使用啟動類加載器來加載類;
如果啟動類加載器加載到了就直接成功,如果啟動類加載器沒有加載到,那擴展類加載器就會自己嘗試加載該類,如果沒有加載到,那么則會由應用程序類加載器來加載這個類。
作用:
- 避免類的重復加載:無論哪一個類加載器要加載某類,最終都是委派最頂端的啟動類加載器。
- 防止核心API被篡改:如果沒有使用雙親委派模型,都由各個類加載器自行去加載的話,如果用戶自己也編寫了一個名為java.lang.Object的類,并放在程序的ClassPath中,那系統中就會出現多個不同的Object類,Java類型體系中最基礎的行為也就無從保證,應用程序將會變得一片混亂。
類路徑:
classpath:類路徑classpath是編譯之后的target文件夾下的WEB-INF/class文件夾。內容等同于打包前的src.main.java和src.main.resource下的目錄和文件
classpath* :不僅包含class路徑,還包括jar文件中(class路徑)進行查找.?
對于JDK8及其之前版本的Java應用,都會使用到以下3個系統提供的類加載器來進行加載:
1.啟動類加載器
這個類加載器負責加載存放在`<java_home>\lib`目錄,或者被-Xbootclasspath參數所指定的路徑中存放的,而且是Java虛擬機能夠識別的類庫加載到虛擬機的內存中。注意,Java虛擬機會按照文件名識別類庫,例如rt.jar、tools.jar,對于名字不符合的類庫即使放在lib目錄中也不會被加載。啟動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導類加載器去處理,那直接使用null代替即可,即讓java.lang.ClassLoader.getClassLoader()返回null。
2.擴展類加載器
這個類加載器是在類sun.misc.Launcher$ExtClassLoader中以Java代碼的形式實現的。它負責加載`<java_home>\lib\ext`目錄中,或者被java.ext.dirs系統變量所指定的路徑中所有的類庫。由于擴展類加載器是由Java代碼實現的,開發者可以直接在程序中使用擴展類加載器來加載Class文件。
3.應用程序類加載器
這個類加載器由sun.misc.Launcher$AppClassLoader來實現。它負責加載用戶類路徑(ClassPath)上所有的類庫,開發者同樣可以直接在代碼中使用這個類加載器。
用戶還可以加入自定義的類加載器來進行拓展,這些類加載器之間的協作關系“通常”如下圖所示。圖中展示的各種類加載器之間的層次關系被稱為類加載器的“雙親委派模型”。雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應有自己的父類加載器。不過這里類加載器之間的父子關系一般不是以繼承的關系來實現的,而是通常使用組合關系來復用父加載器的代碼。
?
工作過程
雙親委派模型的工作過程是,如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去完成加載。
作用:避免類的重復加載、防止核心API被篡改
使用雙親委派模型來組織類加載器之間的關系,一個顯而易見的好處就是Java中的類隨著它的類加載器一起具備了一種帶有優先級的層次關系。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都能夠保證是同一個類。
反之,如果沒有使用雙親委派模型,都由各個類加載器自行去加載的話,如果用戶自己也編寫了一個名為java.lang.Object的類,并放在程序的ClassPath中,那系統中就會出現多個不同的Object類,Java類型體系中最基礎的行為也就無從保證,應用程序將會變得一片混亂。
加分回答-雙親委派模型的3次被破壞
雙親委派模型并不是一個具有強制性約束的模型,而是Java設計者推薦給開發者們的類加載器實現方式。在Java的世界中大部分的類加載器都遵循這個模型,但也有例外的情況,雙親委派模型主要出現過3次較大規模的“被破壞”的情況。
1.雙親委派模型的第一次“被破壞”發生在雙親委派模型出現之前
雙親委派模型在JDK1.2之后才被引入,但是類加載器的概念和抽象類ClassLoader則在Java的第一個版本中就已經存在,面對已經存在的用戶自定義類加載器的代碼,Java設計者們引入雙親委派模型時不得不做出一些妥協。為了兼容這些已有代碼,只能在之后的ClassLoader中添加一個protected方法findClass(),并引導用戶編寫的類加載邏輯時盡可能去重寫這個方法,而不是在loadClass()中編寫代碼。雙親委派的具體邏輯就實現在這里面,按照loadClass()的邏輯,如果父類加載失敗,會自動調用自己的findClass()來完成加載,這樣既不影響用戶按照自己的意愿去加載類,又可以保證新寫出來的類加載器符合雙親委派規則。
2.雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷導致的
雙親委派很好地解決了各個類加載器協作時基礎類型的一致性問題,基礎類型之所以被稱為“基礎”,是因為它們總是作為被用戶代碼繼承、調用的API存在,但程序設計往往沒有絕對不變的完美規則,如果有基礎類型又要調用回用戶的代碼,那該怎么辦呢?
一個典型的例子便是JNDI服務,JNDI現在已經是Java的標準服務,它的代碼由啟動類加載器來完成加載,肯定屬于Java中很基礎的類型了。但JNDI存在的目的就是對資源進行查找和集中管理,它需要調用由其他廠商實現并部署在應用程序的ClassPath下的JNDI服務提供者接口的代碼,現在問題來了,啟動類加載器是絕不可能認識、加載這些代碼的,那該怎么辦?
為了解決這個困境,Java的設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。
有了線程上下文類加載器,程序就可以做一些“舞弊”的事情了。JNDI服務使用這個線程上下文類加載器去加載所需的SPI服務代碼,這是一種父類加載器去請求子類加載器完成類加載的行為,這種行為實際上是打通了雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型的一般性原則,但也是無可奈何的事情。
3.雙親委派模型的第三次“被破壞”是由于用戶對程序動態性的追求而導致的
這里所說的“動態性”指的是一些非常“熱”門的名詞:代碼熱替換、模塊熱部署等。說白了就是希望Java應用程序能像我們的電腦外設那樣,接上鼠標、U盤,不用重啟機器就能立即使用,鼠標有問題或要升級就換個鼠標,不用關機也不用重啟。
早在2008年,在Java社區關于模塊化規范的第一場戰役里,由Sun/Oracle公司所提出的JSR-294、JSR-277規范提案就曾敗給以IBM公司主導的JSR-291(即OSGi R4.2)提案。盡管Sun/Oracle并不甘心就此失去Java模塊化的主導權,隨即又再拿出Jigsaw項目迎戰,但此時OSGi已經站穩腳跟,成為業界“事實上”的Java模塊化標準。
OSGi實現模塊化熱部署的關鍵是它自定義的類加載器機制的實現,每一個程序模塊(OSGi中稱為Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現代碼的熱替換。在OSGi環境下,類加載器不再雙親委派模型推薦的樹狀結構,而是進一步發展為更加復雜的網狀結構。</java_home></java_home>
6、說說JVM調優思路
JVM調優三步驟、性能監控、性能分析、性能調優
JVM調優三步驟:
性能監控:看服務器有沒有以下情況,有的話需要調優:
- GC頻繁
- CPU負載過高
- OOM
- 內存泄露
- 死鎖
- 程序響應時間較長
性能分析:使用分析工具定位oom、內存泄漏等問題
- GC日志:使用GCViewer、VisualVM、GCeasy等日志分析工具打印GC日志;
- JDK自帶的命令行調優工具:
- jps:查看正在運行的 Java 進程。jps -v查看進程啟動時的JVM參數;
- jstat:查看指定進程的 JVM 統計信息。jstat -gc查看堆各分區大小、YGC,FGC次數和時長。如果服務器沒有 GUI 圖形界面,只提供了純文本控制臺環境,它是運行期定位虛擬機性能問題的首選工具。
- jinfo:實時查看和修改指定進程的 JVM 配置參數。jinfo -flag查看和修改具體參數。
- jstack:打印指定進程此刻的線程快照。定位線程長時間停頓的原因,例如死鎖、等待資源、阻塞。如果有死鎖會打印線程的互相占用資源情況。
- 線程快照:該進程內每條線程正在執行的方法堆棧的集合。
- JDK自帶的可視化監控工具:例如jconsole、Visual VM。Visual VM可以監視應用程序的 CPU、GC、堆、方法區、線程快照,查看JVM進程、JVM 參數、系統屬性。
- MAT:解析Heap Dump(堆轉儲)文件dump.hprof,查看GC Roots、對象信息、類信息、引用鏈、線程信息。可以快速生成內存泄漏報表。
- 生成dump文件方式:
- jmap
- JVM參數:OOM后生成、FGC前生成
- Visual VM
- MAT直接從Java進程導出dump文件
- 生成dump文件方式:
性能調優:
- 調整JVM參數:
- 減少停頓時間:垃圾收集器做垃圾回收中斷應用執行的時間。 可以通過-XX:MaxGCPauseMillis參數進行設置,以毫秒為單位,至少大于1
- 提高吞吐量:垃圾收集的時間和總時間的占比:1/(1+n),吞吐量為1-1/(1+n) 。通過-XX:GCTimeRatio=n參數進行設置,99的話代表吞吐量為99%?
- 調整堆內存大小:-Xms、-Xmx、-Xmn分別設置初始堆(最大堆大小的1/4到1/2)、最大堆(服務器內存一半到三分之二)、年輕代大小(最大堆大小的1/4到1/3)
- 根據場景選擇垃圾回收器,升級垃圾回收器:不考慮應付面試的因素,升級垃圾回收器確實會是最有效的方式之一,例如:CMS 升級到 G1,甚至 ZGC。JDK8默認Parallel+Parallel Old,我們改成ParNew+CMS,吞吐量降低但是低停頓。
- 優化代碼:優化代碼,控制內存使用
- 增加機器:增加機器,分散節點壓力
- 調整線程池參數:合理設置線程池線程數量
- 緩存、MQ等中間件優化:使用中間件提高程序效率,比如緩存、消息隊列等
7、項目中有沒有實際的JVM調優經驗?
CPU飆升:?
原因:CPU利用率過高,大量線程并發執行任務導致CPU飆升。例如鎖等待(例如CAS不斷自旋)、死循環、Redis被攻擊、網站被攻擊、文件IO、網絡IO。
定位步驟:?
GC調優:?
上午8點是我們的業務高峰,一到高峰的時候,監控工具發現TP99(滿足百分之九十九的網絡請求所需要的最低耗時值)耗時會變高,有明顯的的毛刺,通過排查發現內存的使用率也會增大,然后再釋放,其他各項指標正常,于是懷疑是GC導致。
通過jstat -gc或Visual VM觀察服務器的GC情況,發現youngGC的情況如下:大概每5分鐘,YGC55次,峰值最高可以達到220次。FullGC比較頻繁,每5分鐘大概0.5次,峰值8次。
那么問題在于FullGC頻繁,而且youngGC峰值也會高。
FullGC頻繁,那么會觸發stop the world。此時會導致我們的系統進行停頓,這個可能是導致我們的系統tp99耗時上升的主要原因。由于并發很高,我們的YoungGC頻繁,本應該在YoungGC就回收的對象沒有回收成功,直接進入了老年代,由于對象的晉升,導致了我們的老年代繼續觸發FUIIGC。于是峰值變高。
目標:
- YoungGC次數減少
- YoungGC耗時減少
- FullGC不超過6次一天
- FullGC耗時減少
分析:
① 垃圾收集器選擇低停頓組合:ParNew+CMS,或G1
我們的jdk版本為8,并且這個服務未指定特定的收集器,走的是我們默認的收集器組合,年輕代為Parallel Scavenge,老年代為Parallel Old。這兩種并行收集器的組臺提高了系統的吞吐量,而不是低延遲配比,我們首先應該換一個低延遲的收集器。低延遲的組臺,我們選擇ParNew與CMS的組臺,如果jdk的版本高,也可以選擇G1或者ZGC。
② 年輕代擴大
一些堆空間其實是空閑的。那么當我們年輕代的空間小,而且并發大的時候,年輕代的對象會激增,并且晉升到老年代。
③ 元數據區
jdk 1.8后,原來的永久代變為元數據區,如果我們沒有指定元數據區的大小。其默認的初始值只有21M,那么如果動態代理的對象比較多,就會導致元數據區進行GC回收,元數據區的回收也會觸發FullGC,再次導致我們的stw。所以我們觀看了一下元數據的常駐對象的大小,大概是100M左右,所以我們直接用參數指定元數據區的大小為256M。我們的元數據區的最大容量也同時指定為256M,防止其進行動態凋整。
④ 并發預處理
在FullGC發生時,會產生我們的GC root追蹤。老年代與年輕代之間又會存在跨年齡引用,如果我們在CMS收集器進行收集前,進行一次重新標記,其實會減少我們的對象掃描,減少我們的FullGC時間。所以我們就讓進行FullGC前,強制做一次MinorGC,
XX:+CMSScavengeBeforeRemark,這樣就減少了我們要掃描的對象,減少了重新標記時間。
最終方案:
- 指定收集器為ParNew +CMS
- 擴充年輕代的占比為之前的1.5倍
- 指定元數據區的最大容量,防止無限動態擴容
- FullGC前,強制做一次MinorGC。因為MinorGC回收率高,這樣可以減少重新標記階段對象的掃描,縮短CMS重新標記時間。
- 根據以上四種方式配置后,我們重新進行了一次壓測,發現TP99耗時較之前降低60%。FullGC耗時降低80%。YoungGC次數減少30 %。TP99耗時基本持平,完全符臺預期。
8、請你說說內存溢出
得分點
內存溢出、溢出原因、解決方案
內存溢出:?申請的內存大于系統能提供的內存。
溢出原因:
- 本地直接內存溢出:本地直接內存設的太小導致溢出。設置直接內存最大值-XX:MaxDirectMemorySize,若不指定則默認與Java堆最大值一致
- 虛擬機棧和本地方法棧溢出:如果虛擬機的棧內存允許動態擴展,并且方法遞歸層數太深時,導致擴展棧容量時無法申請到足夠內存。
- 方法區溢出:運行時生成大量動態類時會內存溢出。例如CGlib動態代理產生大量類填滿了整個方法區(方法區存常量池、類信息、方法信息),直到溢出。CGlib動態代理是在內存中構建子類對象實現對目標對象功能擴展。例如大量JSP或動態產生JSP文件的應用。
- 堆溢出:
- 死循環創建過多對象;
- 集合類中有對對象的引用,使用完后未清空,使得JVM不能回收;
- 內存中加載的數據量過于龐大,如一次從數據庫取出過多數據;
- 程序計數器不會內存溢出。
使用JDK自帶的命令行調優工具?,判斷是否有OOM:
解決方案:
MAT分析OOM問題:
標準回答
內存溢出,簡單地說內存溢出就是指程序運行過程中申請的內存大于系統能夠提供的內存,導致無法申請到足夠的內存,于是就發生了內存溢出。
引起內存溢出的原因有很多種,常見的有以下幾種:
1. 內存中加載的數據量過于龐大,如一次從數據庫取出過多數據;
2. 集合類中有對對象的引用,使用完后未清空,使得JVM不能回收;
3. 代碼中存在死循環或循環產生過多重復的對象實體;
4. 使用的第三方軟件中的BUG;
5. 啟動參數內存值設定的過小。
加分回答
除了程序計數器外,虛擬機內存的其他幾個運行時區域都有發生OOM異常的可能。
1. Java堆溢出
Java堆用于儲存對象實例,我們只要不斷地創建對象,并且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那么隨著對象數量的增加,總容量觸及最大堆的容量限制后就會產生內存溢出異常。
2. 虛擬機棧和本地方法棧溢出
HotSpot虛擬機中并不區分虛擬機棧和本地方法棧,如果虛擬機的棧內存允許動態擴展,當擴展棧容量無法申請到足夠的內存時,將拋出OutOfMemoryError異常。
3. 方法區和運行時常量池溢出
方法區溢出也是一種常見的內存溢出異常,在經常運行時生成大量動態類的應用場景里,就應該特別關注這些類的回收狀況。這類場景常見的包括:程序使用了CGLib字節碼增強和動態語言、大量JSP或動態產生JSP文件的應用、基于OSGi的應用等。 在JDK 6或更早之前的HotSpot虛擬機中,常量池都是分配在永久代中,即常量池是方法區的一部分,所以上述問題在常量池中也同樣會出現。而HotSpot從JDK 7開始逐步“去永久代”的計劃,并在JDK 8中完全使用元空間來代替永久代,所以上述問題在JDK 8中會得到避免。
4. 本地直接內存溢出
直接內存的容量大小可通過`-XX:MaxDirectMemorySize`參數來指定,如果不去指定,則默認與Java堆最大值一致。如果直接通過反射獲取Unsafe實例進行內存分配,并超出了上述的限制時,將會引發OOM異常。
9、請你說說內存泄漏
得分點
內存泄漏、內存泄露的9種情況、性能分析工具判斷是否有內存泄漏、解決辦法
內存泄漏:?不再使用的對象仍然被引用,導致GC無法回收;
內存泄露的9種情況:
- 靜態容器里的對象:靜態集合類的生命周期與 JVM 程序一致,容器里的對象引用也將一直被引用得不到GC;Java里不準靜態方法引用非靜態方法也是防止內存泄漏。
- 單例對象引用的外部對象:單例模式里,如果單例對象如果持有外部對象的引用,因為單例對象不會被回收,那么這個外部對象也不會被回收
- 外部類跟隨內部類被引用:內部類持有外部類,這個內部類對象被長期引用了,即使那個外部類實例對象不再被使用,但由于內部類持有外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會造成內存泄漏。
- 數據庫、網絡、IO等連接忘記關閉:在對數據庫進行操作的過程中,首先需要建立與數據庫的連接,當不再使用時,需要調用 close 方法來釋放與數據庫的連接。如果對 Connection、Statement 或 ResultSet 不顯性地關閉,將會造成大量的對象無法被回收,從而引起內存泄漏。
- 變量作用域不合理:例如一個變量只會在某個方法中使用,卻聲明為成員變量,并且被使用后沒有被賦值為null,將會導致這個變量明明已經沒用了,生命周期卻還跟對象一致。
- HashSet中對象改變哈希值:當一個對象被存儲進 HashSet 集合中以后,就不能修改這個對象中的那些參與計算哈希值的字段了。否則對象哈希值改變,找不到對應的value。
- 緩存引用忘刪除:一旦你把對象引用放入到緩存中,他就很容易遺忘,緩存忘了刪除,將導致引用一直存在。
- 邏輯刪除而不是真實刪除:監聽器和其他回調:如果客戶端在你實現的 API 中注冊回調,卻沒有顯示的取消,那么就會積聚。需要確保回調立即被當作垃圾回收的最佳方法是只保存它的弱引用,例如將他們保存成為 軟WeakHashMap 中的鍵。例如出棧只是移動了指針,而沒有將出棧的位置賦值null,導致已出棧的位置還存在引用。
- 線程池時,ThreadLocal忘記remove():使用線程池的時候,ThreadLocal?需要在使用完線程中的線程變量手動?remove(),否則會內存泄漏。因為線程執行完后沒有銷毀而是被線程池回收,導致ThreadLocal中的對象不能被自動垃圾回收。?
性能分析工具判斷是否有內存泄漏:
- JDK自帶的命令行調優工具:
- 每隔一段較長的時間通過jstat命令采樣多組 OU(老年代內存量) 的最小值;
- 如果這些最小值在上漲,說明無法回收對象在不斷增加,可能是內存泄漏導致的。
- MAT監視診斷內存泄漏:先生成堆轉儲文件,然后用MAT監視診斷內存泄漏;
- GC詳細日志:啟動參數開啟GC詳細日志,設置日志地址;-XX:+PrintGCDetails;
- 編譯器警告:查看Eclipse等編譯器的內存泄漏警告;
- Java基準測試工具:分析代碼性能;?
解決辦法:
內存泄漏,是指不再使用的對象仍然被引用,導致垃圾收集器無法回收它們的內存。由于不再使用的對象仍然無法清理,甚至這種情況可能會越積越多,最終導致致命的OutOfMemoryError。
可以按照如下的思路來分析和解決內存泄漏問題:
1. 啟用分析器
Java分析器是通過應用程序監視和診斷內存泄漏的工具,它可以分析我們的應用程序內部發生的事情,例如如何分配內存。使用分析器,我們可以比較不同的方法并找到可以最佳利用資源的方式。
2. 啟用詳細垃圾收集日志
通過啟用詳細垃圾收集日志,我們可以跟蹤GC的詳細進度。要啟用該功能,我們需要將以下內容添加到JVM的配置當中:`-verbose:gc`。通過這個參數,我們可以看到GC內部發生的細節。
3. 使用引用對象
我們還可以借助java.lang.ref包內置的Java引用對象來規避問題,使用java.lang.ref包,而不是直接引用對象,即使用對象的特殊引用,使得它們可以輕松地被垃圾收集。
4. Eclipse內存泄漏警告
對于JDK1.5以及更高的版本中,Eclipse會在遇到明顯的內存泄漏情況時顯示警告和錯誤。因此,在Eclipse中開發時,我們可以定期地訪問“問題”選項卡,并更加警惕內存泄漏警告。
5. 基準測試
我們可以通過執行基準測試來衡量和分析Java代碼的性能。通過這種方式,我們可以比較執行相同任務的替代方法的性能。這可以幫助我們選擇更好的方法,并可以幫助我們節約內存。
6. 代碼審查
最后,我們總是采用經典的老方式來進行簡單的代碼演練。在某些情況下,即使這種看似微不足道的方法也有助于消除一些常見的內存泄漏問題。
加分回答-沒有一刀切的解決方案,具體問題具體分析
通俗地說,我們可以將內存泄漏視為一種疾病,它通過阻塞重要的內存資源來降低應用程序的性能。和所有其他疾病一樣,如果不治愈,隨著時間的推移,它可能導致致命的應用程序崩潰。
內存泄漏很難解決,找到它們需要對Java語言有很深的理解并掌握復雜的命令。在處理內存泄漏時,沒有一刀切的解決方案,因為泄漏可能通過各種不同的事件發生。 但是,如果我們采用最佳實踐并定期執行嚴格的代碼演練和分析,那么我們就可以將應用程序中內存泄漏的風險降到最低。
10、JVM中一次完整的GC流程是怎樣的
堆分為哪幾個區、GC流程、注意大對象和年齡15
11、說說JVM的垃圾回收機制
得分點
新生代收集、老年代收集、混合收集、整堆收集
依據分代假說理論,垃圾回收可以分為:?新生代收集、老年代收集、混合收集、整堆收集
當前商業虛擬機的垃圾收集器,大多數都遵循了“分代收集”的理論進行設計,分代收集名為理論,實質是一套符合大多數程序運行實際情況的經驗法則。而分代收集理論,建立在如下三個分代假說之上,即弱分代假說、強分代假說、跨代引用假說。依據分代假說理論,垃圾回收可以分為如下幾類:
1. 新生代收集:目標為新生代的垃圾收集。
2. 老年代收集:目標為老年代的垃圾收集,目前只有CMS收集器會有這種行為。
3. 混合收集:目標為整個新生代及部分老年代的垃圾收集,目前只有G1收集器會有這種行為。
4. 整堆收集:目標為整個堆和方法區的垃圾收集。
加分回答-垃圾收集器
HotSpot虛擬機內置了很多垃圾收集器,其中針對新生代的垃圾收集器有Serial、ParNew、Parallel Scavenge,針對老年代的垃圾收集器有CMS、Serial Old、Parallel Old。此外,HotSpot還內置了面向整堆的G1收集器。
在上述收集器中,常見的組合方式有:
1. Serial + Serial Old,是客戶端模式下常用的收集器。
2. ParNew + CMS,是服務端模式下常用的收集器。
3. Parallel Scavenge + Parallel Old,適用于后臺運算而不需要太多交互的分析任務。
12、說說GC的可達性分析算法
得分點
概念、GC Roots、引用鏈、非可達對象兩次標記
可達性分析算法:
以根對象集合(GC Roots)的每個跟對象為起始點,根據引用關系向下搜索,將所有與GC Roots直接或間接有引用關系的對象在對象頭的Mark Word里標記為可達對象,即不需要回收的有引用關系對象。搜索過程所走過的路徑稱為“引用鏈” 。
GC Roots:即GC根節點集合,是一組必須活躍的引用。可作為GC Roots的對象:
- 棧引用的對象:Java方法棧、本地方法棧中的參數引用、局部變量引用、臨時變量引用等。臨時變量是方法里的中間操作結果。
- 方法區中常量、靜態變量引用的對象;
- 所有被同步鎖持有的對象;
- 所有線程對象;
- 所有跨代引用對象;
- JVM內部的引用:如基本數據類型對應的Class對象,常駐的異常對象,以及應用程序類類加載器;?
非可達對象被回收需要兩次標記:
finalize()方法:?
finalize()方法是對象逃脫死亡命運的最后一次機會,需要注意的是,任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行。
另外,finalize()方法的運行代價高昂,不確定性大,無法保證各個對象的調用順序,如今已被官方明確聲明為不推薦使用的語法。
當前主流的商用程序語言的內存管理子系統,都是通過可達性分析算法來判定對象是否存活的。
這個算法的基本思路就是通過一系列稱為“GC Roots”的根對象作為起始節點集,從這些節點開始,根據引用關系向下搜索,搜索過程所走過的路徑稱為“引用鏈”,如果某個對象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。
?
GC Roots?到底是什么東西呢,哪些對象可以作為 GC Root 呢?
?是一組必須活躍的引用。在Java技術體系里面,固定可作為GC Roots的對象包括以下幾種:
- 在虛擬機棧中引用的對象,譬如各個線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等;
- 在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量;
- 在方法區中常量引用的對象,譬如字符串常量池里的引用;
- 在本地方法棧中引用的對象;
- JVM內部的引用,如基本數據類型對應的Class對象,常駐的異常對象,以及系統類加載器;
- 所有被同步鎖持有的對象;
- 反映Java虛擬機內部情況的JMXBean、JVMTI中注冊的回調、本地代碼緩存等。
加分回答-宣告對象死亡要經歷兩次標記
真正宣告一個對象死亡,至少要經歷兩次標記過程:
1. 第一次標記
如果對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記,隨后進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,那么虛擬機將這兩種情況都視為“沒有必要執行”。反之,該對象將會被放置在一個名為F-Queue的隊列之中,并在稍后由一條由虛擬機自動建立的、低調度優先級的Finalizer線程去執行它們的finalize()方法。
2. 第二次標記
稍后,收集器將對F-Queue中的對象進行第二次小規模的標記。如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移出“即將回收”的集合。如果對象這時候還沒有逃脫,那基本上它就真的要被回收了。
????finalize()方法是對象逃脫死亡命運的最后一次機會,需要注意的是,任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行。另外,finalize()方法的運行代價高昂,不確定性大,無法保證各個對象的調用順序,如今已被官方明確聲明為不推薦使用的語法。
13、說說JVM的垃圾回收算法
得分點
標記清除、標記復制、標記整理,比較優缺點(效率、空間浪費、調整引用、stw)、使用場景
標記清除算法、標記復制算法、標記整理算法。
標記清除算法(Mark-Sweep):
- 標記、清除:當堆中有效內存空間被耗盡時,會STW(stop the world,暫停其他所有工作線程),然后先標記,再清除。
- 標記:可達性分析法,從GC Roots開始遍歷,找到可達對象,并在對象頭中進行標記。
- 清除:堆內存內從頭到尾進行線性遍歷,“清除”非可達對象。注意清除并不是真的置空,垃圾還在原來的位置。實際是把垃圾對象的地址維護在空閑列表,創建新對象時通過空閑列表找到合適大小的空閑內存分配給新對象。
- 優點:簡單
- 缺點:
- 效率不高:需要可達性遍歷和線性遍歷,效率差。
- STW導致用戶體驗差:GC時需要暫停其他所有工作線程,用戶體驗差。
- 有內存碎片,要維護空閑列表:回收垃圾對象后沒有整理,導致堆中出現一塊塊不連續的內存碎片。
- 適用場景:適合小型應用程序,內存空間不大的情況。應用程序越大越不適用這種回收算法。
標記復制算法(Copying) :
- 標記、復制、清除:將內存空間分為兩塊,每次只使用一塊。在進行垃圾回收時,先可達性分析法標記可達對象,然后將可達對象復制到沒有被使用的那個內存塊中,最后再清除當前內存塊中的所有對象。后續再按同樣的流程來回復制和清除。
- 優點:
- 垃圾多時效率高:只需可達性遍歷,效率很高。
- 無內存碎片:因為有移動操作,所以內存規整。
- 缺點:
- 內存利用率低,浪費內存:始終有一半以上的空閑內存。
- 需要調整引用地址:可達對象移動后,內存地址發生了變化,需要調整所有引用,指向移動后的地址。
- 垃圾少時效率相對差,但還是比其他算法強:如果可達對象比較多,垃圾對象比較少,那么復制算法的效率就會比較低。只為了一點垃圾而移動所有對象未免有些小題大做。所以垃圾對象多的情況下,復制算法比較適合。
- 適用場景:適合垃圾對象多,可達對象少的情況,這樣復制耗時短。非常適合新生代的垃圾回收,因為新生代要頻繁地把可達對象從伊甸園區移動到幸存區,而且是新生代滿了適合再Minor GC,垃圾對象占比高,所以回收性價比非常高,一次通常可以回收70-90%的內存空間,現在的商業虛擬機都是用這種GC算法回收新生代。
標記整理算法(Mark-Compact) :
- 標記、整理、清除:首先可達性分析法標記可達對象,然后將可達對象按順序整理到內存的一端,最后清理邊界外的垃圾對象。相當于內存碎片優化版的標記清楚算法,不用維護空閑列表。
- 優點:
- 無內存碎片:內存規整。
- 內存利用率最高:內存既規整又不用浪費一般空間。
- 缺點:
- 效率最低:效率比其他兩種算法都低
- 需要調整引用地址:可達對象移動后,內存地址發生了變化,需要調整所有引用,指向移動后的地址。
- STW導致用戶體驗差:移動時需要暫停其他所有工作線程,用戶體驗差。
分代收集算法:將堆分為新生代、老年代不同生命周期的對象放在不同的代,采用不同的收集算法,以提高回收效率。
引用計數法
每個對象都保存一個引用計數器屬性,用戶記錄對象被引用的次數。
可達性分析法
可達性分析法會以GC Roots作為起始點,然后一層一層找到所引用的對象,被找到的對象就是存活對象,那么其他不可達的對象就是垃圾對象。
1. 標記清除算法
算法分為“標記”和“清除”兩個階段,首先標記出所有需要回收的對象,在標記完成后,統一回收掉所有被標記的對象,也可以反過來,標記存活的對象,統一回收所有未被標記的對象。它主要有如下兩個缺點: 第一個是執行效率不穩定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個過程的執行效率都隨對象數量增長而降低。 第二個是內存空間碎片化問題,標記、清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致當程序在運行過程中需要分配較大對象時無法找到足夠的連續的內存而不得不提前觸發另一次垃圾收集。
2. 標記復制算法
將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。對于多數對象都是可回收的情況,算法需要復制的就是占少數的存活對象,而且每次都是針對整個半區進行內存回收,分配內存時也就不用考慮有空間碎片的復雜情況,只要移動堆頂指針,按順序分配即可。
這種復制回收算法的代價是將可用內存縮小為了原來的一半,空間浪費未免太多了一點。另外,如果內存中多數對象都是存活的,這種算法將會產生大量的內存間復制的開銷。所以,現在的商用Java虛擬機大多都優先采用了這種收集算法去回收新生代。
3. 標記整理算法
針對老年代對象的存亡特征,1974年Edward Lueders提出了另外一種有針對性的“標記-整理”算法,其中的標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內存空間一端移動,然后直接清理掉邊界以外的內存。
如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區域,移動存活對象并更新所有引用這些對象的地方將會是一種極為負重的操作,而且這種對象移動操作必須全程暫停用戶應用程序才能進行,像這樣的停頓被最初的虛擬機設計者形象地描述為“Stop The World”。
加分回答
目前,新生代的垃圾回收采用標記復制算法比較多,老年代的垃圾回收采用標記整理算法比較多。而標記復制算法浪費一半內存的缺點長期以來被人詬病,所以業界也有人針對該算法給出了改進的方案。
IBM公司曾有一項專門研究對新生代“朝生夕滅”的特點做了更量化的詮釋——新生代中的對象有98%熬不過第一輪收集。因此并不需要按照1∶1的比例來劃分新生代的內存空間。在1989年,Andrew
Appel針對具備“朝生夕滅”特點的對象,提出了一種更優化的半區復制分代策略,現在稱為“Appel式回收”。
Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配內存只使用Eden和其中一塊Survivor。發生垃圾搜集時,將Eden和Survivor中仍然存活的對象一次性復制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空間。
HotSpot虛擬機的Serial、ParNew等新生代收集器均采用了這種策略來設計新生代的內存布局。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1:1,也即每次新生代中可用內存空間為整個新生代容量的90%(Eden的80%加上一個Survivor的10%),只有一個Survivor空間,即10%的新生代是會被“浪費”的。
98%的對象可被回收僅僅是“普通場景”下測得的數據,任何人都沒有辦法百分百保證每次回收都只有不多于10%的對象存活,因此Appel式回收還有一個充當罕見情況的“逃生門”的安全設計,當Survivor空間不足以容納一次Minor
GC之后存活的對象時,就需要依賴其他內存區域(實際上大多就是老年代)進行分配擔保。
對比三種垃圾回收算法:
14、說說七個垃圾回收器
得分點
Serial、Serial Old、PawNew、CMS、Parallel Scavenge、Parallel Old、G1
各版本默認回收器:JDK8默認回收器是Parallel+Parallel Old。
各區域對應算法:?
- 新生代回收算法:標記復制算法;
- 老年代回收算法:標記清除/整理算法
- 整堆回收算法:分區算法。
Serial(串行收集器):
- 介紹:單線程、單處理器回收新生代,回收時會STW。
- STW:Stop The World,暫停其他所有工作線程直到收集結束。
- 算法:標記復制算法
- 回收區域:新生代
- 優點:簡單、比其他單線程收集器效率高:單線程,不用線程切換,可以專心進行垃圾回收。
- 應用場景:適用于內存小的桌面應用,可以在較短時間完成收集。Serial GC是最基礎、歷史最悠久的收集器,曾是HotSpot虛擬機新生代收集器的唯一選擇。
- 命令:指定新生代用Serial GC,老年代用Serial Old GC:-XX:+UseSerialGC
Serial Old(老年代串行收集器):
- 介紹:Serial收集器的老年代版本。單線程、單處理器回收老年代,回收時會STW。
- 算法:標記-整理算法
ParNew(并行新生代收集器):Par是Parallel(并行,平行)的縮寫,New:只能處理的是新生代
- 介紹:Serial收集器的多線程并行版本。多線程并行回收新生代,回收時會STW。
- 算法:標記復制算法
- 回收區域:新生代
- 優點:多CPU場景下性能高,吞吐量大
- 缺點:單CPU場景下性能差,不如串行收集器
- 應用場景:多CPU場景下。
Parallel Scavenge(并行收集器):
- 介紹:可控制高吞吐量,多線程并行回收新生代,回收時會STW。
- 算法:標記復制算法
- 回收區域:新生代
- 應用場景:后臺運算量大而不需要太多交互的任務。JDK8默認回收器是Parallel+Parallel Old
Parallel Old(老年代并行收集器):
- 介紹:Parallel Scavenge收集器的老年代版本。可控制高吞吐量,多線程并行回收老生代,回收時會STW。
- 算法:標記整理算法
- 回收區域:老年代
CMS(并發標記清除收集器):
- 介紹:以最短停頓時間為目標,JDK1.5推出,第一次實現了垃圾收集線程和用戶線程同時工作。多線程并行回收老生代,低stw。初始標記和重新標記需要stw,但耗時很短。
- 算法:標記清除算法。不使用標記整理算法是為了保證清除時不影響用戶線程中的工作線程,如果使用標記整理算法的話工作線程引用指向的對象地址就都變了。
- 回收區域:老年代
- 步驟:
- 初始標記:標記GC Roots直接關聯的對象。單線程且停頓用戶線程,速度很快。
- 并發標記:從直接關聯對象并發遍歷整個圖,標記可達對象。并發不停頓。
- 重新標記:修正上一步用戶線程變動的標記。并發停頓。速度遠比并發標記階段快。注意只能修正原有對象不能修正新增對象,即只能修正原有對象非可達變可達、可達變非可達。
- 并發清除:并發線性遍歷并清理未被標記的對象。并發不停頓。
- 優點:
- 并發速度快;
- 低停頓:用戶線程和垃圾回收器同時執行,僅初始標記和重新標記階段需要停頓,這兩個階段運行速度很快。
- 缺點:
- 并發占線程
- 有內存碎片:內存不規整,需要維護空閑列表。
- 無法處理浮動垃圾:并發標記階段會產生新對象,重新標記階段又只能修正不能新增,所以會出現浮動垃圾。
- 回收時要確保用戶線程有足夠內存:不能等老年代滿了再回收,而是內存到達某個閾值后回收,防止用戶線程在并發執行過程中新創建對象導致內存不夠,導致虛擬機補償使用Serial Old收集器進行回收并處理內存碎片,從而浪費更多時間。
- 應用場景:因為底層是標記清除算法,所以有內存碎片,適合小應用。
??G1(Garbage-First,垃圾優先收集器):
- 介紹:以延遲可控并保證高吞吐量為目標,為了適應內存大小和處理器數量不斷擴大而在JDK7推出的垃圾回收器。開創了收集器面向局部收集的設計思路和基于Region(區域)的內存布局形式。JDK8支持并發類卸載后被Oracle官方稱為“全功能的垃圾收集器”。并行低停頓,除了并發標記外需要stw,但耗時很短(初始標記和最終標記是真短,篩選回收是有指定STW)。
- 實現機制:不再把堆劃分為連續的分代,而是將堆內存分割成2048個大小相等的Region,各Region根據需要扮演伊甸園區、幸存區、老年代區、巨大區。垃圾優先收集器跟蹤各Region里垃圾的回收價值(回收空間大小和預計回收時長),在后臺維護一個優先級列表,每次根據用戶設定允許的收集停頓時間,回收優先級最高的那些Region,以達到垃圾優先的效果。
- 設置最大停頓時間:-XX:MaxGCPauseMillis=默認0.2s
- Humongous Region(巨大區):存儲大小超過Region一半空間的大對象,如果大對象的內存大小超過了Region大小,將會被存在幾個連續的巨大區里。G1的大多數行為把巨大區看作老年代的一部分。
- 算法:分區收集算法(整體是標記整理算法、Region之間標記復制算法)
- 回收區域:整堆。整堆里哪個Region垃圾最多,回收收益最大。
- 步驟:
- 初始標記:標記GC Roots直接關聯的可達對象。單線程且停頓用戶線程,速度很快。
- 并發標記:從直接關聯對象并發遍歷整個圖,標記可達對象。并發不停頓。
- 最終標記:重新標記所有存活的對象。并發停頓。采用SATB算法,效率比CMS重新標記高。并發停頓。
- 篩選回收:根據優先級列表,回收價值高的一些Region,將存活對象通過標記復制算法復制到同類型的空閑Region。根據指定的最大停頓時間回收,因此可能來不及回收所有垃圾對象,但能保證回收到最高回收價值的垃圾。并發停頓。
- 記憶集:是一個抽象概念。每個Region都維護一個記憶集Rset,用來記錄其他Region對象對本Region對象的引用。本Region在回收后對象地址會改變,用記憶集就能直接知道直接找到對應引用修改指向的地址,從而不用全局掃描。
- 卡表(CardTable):是記憶集的一種實現方式。卡表是一個字節數組,每個元素對應一個內存塊,每個內存塊大小都是2^n字節(Hotspot是2^9=512字節)。
- 寫屏障:當前對象被其他Region對象通過引用關系賦值時,賦值前后會插入寫前屏障和寫后屏障中斷當前Region垃圾回收。
- CMS的記憶集和寫屏障:其他回收器也用到了記憶集和寫后屏障,用來防止回收導致位置改變時,不用為了更正引用地址而掃描整個堆。例如CMS記憶集記錄老年代指向年輕代的引用。但只有G1用到了寫前屏障。
- 優點:無內存碎片:因為整體和局部是整理和復制,都不會產生內存碎片。
- 缺點:
- 比CMS更耗費內存和負載。
- 可能來不及回收所有垃圾:根據指定的STW時間(默認0.2s)回收,因此可能來不及回收所有垃圾對象,但能保證回收到最高回收價值的垃圾。
- 比CMS更耗費內存和負載:因為使用寫前屏障和寫后屏障維護記憶集,而cms只用寫后屏障。
- 應用場景:適合多核CPU且內存大的大應用,小應用不及其他回收器,但未來會越來越適合。
標準回答?
《Java虛擬機規范》中對垃圾收集器應該如何實現并沒有做出任何規定,因此不同的廠商、不同版本的虛擬機所包含的垃圾收集器都可能會有很大差別,不同的虛擬機一般也都會提供各種參數供用戶根據自己的應用特點和要求組合出各個內存分代所使用的收集器。下圖是HotSpot虛擬機中包含的垃圾收集器,圖中展示了七種作用于不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用,圖中收集器所處的區域,則表示它是屬于新生代收集器抑或是老年代收集器。
?
串行收集器Serial
Serial收集器是最基礎、歷史最悠久的收集器,使用復制算法,曾經是HotSpot虛擬機新生代收集器的唯一選擇。這個收集器是一個單線程工作的收集器,但它的“單線程”的意義并不僅僅是說明它只會使用一個處理器或一條收集線程去完成垃圾收集工作,更重要的是強調在它進行垃圾收集時,必須暫停其他所有工作線程,直到它收集結束。也就是說它在進行垃圾收集時,會發生“Stop The World”
老年代串行收集器Serial Old
Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用標記-整理算法,這個收集器的主要意義也是供客戶端模式下的HotSpot虛擬機使用。
ParNew收集器
ParNew收集器實質上是Serial收集器的多線程并行版本,除了同時使用多條線程進行垃圾收集之外,其余的行為包括Serial收集器可用的所有控制參數、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一致,在實現上這兩種收集器也共用了相當多的代碼。
Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同樣是基于標記-復制算法實現的收集器,也是能夠并行收集的多線程收集器。它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。
Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線程并發收集,基于標記-整理算法實現。直到Parallel Old收集器出現后,“吞吐量優先”收集器終于有了比較名副其實的搭配組合,在注重吞吐量或者處理器資源較為稀缺的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器這個組合。
CMS
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,從名字上就可以看出CMS收集器是基于標記-清除算法實現的,它的運作過程相對于前面幾種收集器來說要更復雜一些,整個過程分為四個步驟,包括:初始標記、并發標記、重新標記、并發清除。
G1
Garbage First(簡稱G1)收集器是垃圾收集器技術發展歷史上的里程碑式的成果,它開創了收集器面向局部收集的設計思路和基于Region的內存布局形式。到了JDK 8 Update 40的時候,G1提供并發的類卸載的支持,補全了其計劃功能的最后一塊拼圖。這個版本以后的G1收集器才被Oracle官方稱為“全功能的垃圾收集器”。
加分回答
通常,Serial收集器搭配Serial Old使用,ParNew收集器搭配CMS使用,Parallel Scavenge收集器搭配Parallel Old使用。此外,G1是整堆收集器,它無需搭配其他的垃圾收集器。
15、請你講下CMS(并發標記清除)回收器
得分點
介紹、算法、回收區域、四個步驟、優缺點(并發、停頓、內存碎片、浮動垃圾、回收條件)、應用場景
CMS(并發標記清除收集器):
- 介紹:以最短停頓時間為目標,JDK1.5推出,第一次實現了垃圾收集線程和用戶線程同時工作。多線程并行回收老生代,低stw。初始標記和重新標記需要stw,但耗時很短。
- 算法:標記清除算法。不使用標記整理算法是為了保證清除時不影響用戶線程中的工作線程,如果使用標記整理算法的話工作線程引用指向的對象地址就都變了。
- 回收區域:老年代
- 步驟:
- 初始標記:標記GC Roots直接關聯的對象。單線程且停頓用戶線程,速度很快。
- 并發標記:從直接關聯對象并發遍歷整個圖,標記可達對象。并發不停頓。
- 重新標記:修正上一步用戶線程變動的標記。并發停頓。速度遠比并發標記階段快。注意只能修正原有對象不能修正新增對象,即只能修正原有對象非可達變可達、可達變非可達。
- 并發清除:并發線性遍歷并清理未被標記的對象。并發不停頓。
- 優點:
- 并發速度快;
- 低停頓:用戶線程和垃圾回收器同時執行,僅初始標記和重新標記階段需要停頓,這兩個階段運行速度很快。
- 缺點:
- 并發占線程拖慢速度
- 有內存碎片:內存不規整,需要維護空閑列表。
- 無法處理浮動垃圾:并發標記階段會產生新對象,重新標記階段又只能修正不能新增,所以會出現浮動垃圾。
- 回收時要確保用戶線程有足夠內存:不能等老年代滿了再回收,而是內存到達某個閾值后回收,防止用戶線程在并發執行過程中新創建對象導致內存不夠,導致虛擬機補償使用Serial Old收集器進行回收并處理內存碎片,從而浪費更多時間。
- 應用場景:因為底層是標記清除算法,所以有內存碎片,適合小應用。
?CMS收集器是一種以獲取最短回收停頓時間為目標的收集器,從名字上就可以看出CMS收集器是基于標記清除算法實現的,它的運作過程相對于前面幾種收集器來說要更復雜一些,整個過程分為四個步驟,包括:初始標記、并發標記、重新標記、并發清除。其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。?
STW:Stop-The-World是在垃圾回收算法執行過程中,將jvm內存凍結,停頓的一種狀態。即暫停用戶線程。
?1. 初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快。
?2. 并發標記階段就是從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起并發運行。
?3. 重新標記階段則是為了修正并發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比并發標記階段的時間短。
?4. 并發清除階段,清理刪除掉標記階段判斷的已經死亡的對象,由于不需要移動存活對象,所以這個階段也是可以與用戶線程同時并發的。
指定老年代使用CMS GC:
-XX:+UseConcMarkSweepGC加分回答-優缺點
CMS是一款優秀的收集器,它最主要的優點在名字上已經體現出來:并發收集、低停頓(單位時間內占用用戶線程的時間更少了),一些官方公開文檔里面也稱之為“并發低停頓收集器”。
CMS收集器是HotSpot虛擬機追求低停頓的第一次成功嘗試,但是它還遠達不到完美的程度,至少有以下三個明顯的缺點:
?1. 并發階段,它雖然不會導致用戶線程停頓,卻因為占用一部分線程而導致應用程序變慢,降低總吞吐量。
?2. 它無法處理“浮動垃圾”,有可能會出現“并發失敗”進而導致另一次Full GC的發生。
?3. 它是一款基于標記清除算法實現的收集器,這意味著收集結束時會有大量內存碎片產生。
16、請你講下G1垃圾優先回收器
得分點
整堆、Region、標記整理、四個步驟
???G1(Garbage-First,垃圾優先收集器):
- 介紹:以延遲可控并保證高吞吐量為目標,為了適應內存大小和處理器數量不斷擴大而在JDK7推出的垃圾回收器。開創了收集器面向局部收集的設計思路和基于Region(區域)的內存布局形式。JDK8支持并發類卸載后被Oracle官方稱為“全功能的垃圾收集器”。并行低停頓,除了并發標記外需要stw,但耗時很短(初始標記和最終標記是真短,篩選回收是有指定STW)。
- 實現機制:不再把堆劃分為連續的分代,而是將堆內存分割成2048個大小相等的Region,各Region根據需要扮演伊甸園區、幸存區、老年代區、巨大區。垃圾優先收集器跟蹤各Region里垃圾的回收價值(回收空間大小和預計回收時長),在后臺維護一個優先級列表,每次根據用戶設定允許的收集停頓時間,回收優先級最高的那些Region,以達到垃圾優先的效果。
- 設置最大停頓時間:-XX:MaxGCPauseMillis=默認0.2s
- Humongous Region(巨大區):存儲大小超過Region一半空間的大對象,如果大對象的內存大小超過了Region大小,將會被存在幾個連續的巨大區里。G1的大多數行為把巨大區看作老年代的一部分。
- 算法:分區收集算法(整體是標記整理算法、Region之間標記復制算法)
- 回收區域:整堆。整堆里哪個Region垃圾最多,回收收益最大。
- 步驟:
- 初始標記:標記GC Roots直接關聯的可達對象。單線程且停頓用戶線程,速度很快。
- 并發標記:從直接關聯對象并發遍歷整個圖,標記可達對象。并發不停頓。
- 最終標記:重新標記所有存活的對象。并發停頓。采用SATB算法,效率比CMS重新標記高。并發停頓。
- 篩選回收:根據優先級列表,回收價值高的一些Region,將存活對象通過標記復制算法復制到同類型的空閑Region。根據指定的最大停頓時間回收,因此可能來不及回收所有垃圾對象,但能保證回收到最高回收價值的垃圾。并發停頓。
- 記憶集:是一個抽象概念。每個Region都維護一個記憶集Rset,用來記錄其他Region對象對本Region對象的引用。本Region在回收后對象地址會改變,用記憶集就能直接知道直接找到對應引用修改指向的地址,從而不用全局掃描。
- 卡表(CardTable):是記憶集的一種實現方式。卡表是一個字節數組,每個元素對應一個內存塊,每個內存塊大小都是2^n字節(Hotspot是2^9=512字節)。
- 寫屏障:當前對象被其他Region對象通過引用關系賦值時,賦值前后會插入寫前屏障和寫后屏障中斷當前Region垃圾回收。
- CMS的記憶集和寫屏障:其他回收器也用到了記憶集和寫后屏障,用來防止回收導致位置改變時,不用為了更正引用地址而掃描整個堆。例如CMS記憶集記錄老年代指向年輕代的引用。但只有G1用到了寫前屏障。
- 優點:無內存碎片:因為整體和局部是整理和復制,都不會產生內存碎片。
- 缺點:
- 比CMS更耗費內存和負載。
- 可能來不及回收所有垃圾:根據指定的STW時間(默認0.2s)回收,因此可能來不及回收所有垃圾對象,但能保證回收到最高回收價值的垃圾。
- 比CMS更耗費內存和負載:因為使用寫前屏障和寫后屏障維護記憶集,而cms只用寫后屏障。
- 應用場景:適合多核CPU且內存大的大應用,小應用不及其他回收器,但未來會越來越適合。
Garbage First(G1)垃圾優先收集器開創了收集器面向局部收集的設計思路和基于Region的內存布局形式。在G1收集器出現之前的所有其他收集器,垃圾收集的目標范圍要么是整個新生代,要么就是整個老年代,再要么就是整個Java堆。而G1跳出了這個限制,它可以面向堆內存任何部分來組成回收集進行回收,衡量標準不再是它屬于哪個分代,而是哪塊內存中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式。
G1也仍是遵循分代收集理論設計的,但其堆內存的布局與其他收集器有非常明顯的差異:G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。
巨大區:此外,還有一類專門用來存儲大對象的特殊區域(Humongous Region)。G1認為只要超過了Region一半的對象即可判定為大對象。而對于那些超過了整個Region容量的超級大對象,將會被存放在N個連續的Humongous Region之中,G1的大多數行為都把Humongous Region作為老年代的一部分來進行看待。
更具體的處理思路是,讓G1收集器去跟蹤各個Region里面的垃圾堆積的“價值”大小(垃圾數量),價值即回收所獲得的空間大小以及回收所需時間的經驗值,然后在后臺維護一個優先級列表,每次根據用戶設定允許的收集停頓時間,優先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來。
G1收集器的運作過程大致可劃分為以下四個步驟:初始標記、并發標記、最終標記、篩選回收。其中,初始標記和最終標記階段仍然需要停頓所有的線程,但是耗時很短。
加分回答-G1與CMS的對比:
G1從整體來看是基于標記整理算法實現的收集器,但從局部上看又是基于標記復制算法實現。無論如何,這兩種算法都意味著G1運作期間不會產生內存空間碎片,垃圾收集完成之后能提供規整的可用內比起CM存。S,G1的弱項也可以列舉出不少。例如在用戶程序運行過程中,G1無論是為了垃圾收集產生的內存占用還是程序運行時的額外執行負載都要比CMS要高。
G1與CMS的選擇:
目前在小內存應用上CMS的表現大概率仍然要會優于G1,而在大內存應用上G1則大多能發揮其優勢,這個優劣勢的Java堆容量平衡點通常在6GB至8GB之間。以上這些也僅是經驗之談,隨著HotSpot的開發者對G1的不斷優化,也會讓對比結果繼續向G1傾斜。
G1比CMS更耗費內存和負載:因為使用寫前屏障和寫后屏障維護記憶集,而cms只用寫后屏障。
總結
以上是生活随笔為你收集整理的【Java常见面试题】JVM篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 监听支付宝、微信 二合一 个人免签最新,
- 下一篇: ECharts数据可视化项目