類加載
1、類的生命周期
薪資范圍:6-16K
一個類完整的生命周期,會經(jīng)歷五個階段,分別為:加載、連接、初始化、使用、和卸載。其中的連接又分為驗證、準備和解析三個步驟。如下圖所示
加載(Loading)
簡單一句話概括,類的加載階段就是:找到需要加載的類并把類的信息加載到jvm的方法區(qū)中,然后在堆區(qū)中實例化一個java.lang.Class對象,作為方法區(qū)中這個類的信息的入口。結(jié)合jvm的內(nèi)存結(jié)構(gòu)會比較好理解。
這里要區(qū)別一下接觸到的類加載。類加載其實包括加載、連接、初始化三個階段。類加載強調(diào)一個jvm能夠直接使用所需的類,所以類必須完成初始化。
不同的虛擬機對類的加載時機有不同的實現(xiàn)方式,具體要看虛擬機的實現(xiàn)方式。這里不做展開。
類的加載方式比較靈活,總結(jié)下來有如下幾種:
- 據(jù)類的全路徑名找到相應的class文件,然后從class文件中讀取文件內(nèi)容;(常用)
- 從jar文件中讀取。另外,還有下面幾種方式也比較常用:(常用)
- 從網(wǎng)絡中獲?。罕热?0年前十分流行的Applet。
- 根據(jù)一定的規(guī)則實時生成,比如設計模式中的動態(tài)代理模式,就是根據(jù)相應的類自動生成它的代理類。
- 從非class文件中獲取,其實這與直接從class文件中獲取的方式本質(zhì)
連接(Linking)
- 驗證:進行類的合法性校驗。會對比如字節(jié)碼格式、變量與方法的合法性、數(shù)據(jù)類型的有效性、繼承與實現(xiàn)的規(guī)范性等等進行檢查,確保別加載的類能夠正常的被jvm所正常運行。
- 準備:為類的靜態(tài)變量分配內(nèi)存,并設為jvm默認的初值;對于非靜態(tài)的變量,則不會為它們分配內(nèi)存。簡單說就是分內(nèi)存、賦初值。注意:設置初始值為jvm默認初值,而不是程序設定。規(guī)則如下
-
- 基本類型(int、long、short、char、byte、boolean、float、double)的默認值為0
- 引用類型的默認值為null
- 常量的默認值為我們程序中設定的值,比如我們在程序中定義final static int a = 100,則準備階段中a的初值就是100。
- 解析:這一階段的任務就是把常量池中的符號引用轉(zhuǎn)換為直接引用。
初始化(Initialization)
類初始化階段是類加載過程的最后一步。而也是到了該階段,才真正開始執(zhí)行類中定義的java程序代碼(字節(jié)碼),之前的動作都由虛擬機主導。
jvm對類的加載時機沒有明確規(guī)范,但對類的初始化時機有:只有當類被直接引用的時候,才會觸發(fā)類的初始化。類被直接引用的情況有以下幾種:
- 通過以下幾種方式:
-
- new關鍵字創(chuàng)建對象
- 讀取或設置類的靜態(tài)變量
- 調(diào)用類的靜態(tài)方法
- 通過反射方式執(zhí)行1里面的三種方式;
- 初始化子類的時候,會觸發(fā)父類的初始化;
- 作為程序入口直接運行時(調(diào)用main方法);
- 接口實現(xiàn)類初始化的時候,會觸發(fā)直接或間接實現(xiàn)的所有接口的初始化。
關于類的初始化,記住兩句話
1、類的初始化,會自上而下運行靜態(tài)代碼塊或靜態(tài)賦值語句,非靜態(tài)與非賦值的靜態(tài)語句均不執(zhí)行。
2、如果存在父類,則父類先進行初始化,是一個典型的遞歸模型。
區(qū)別于對象的初始化,類的初始化所做的一起都是基于類變量或類語句的,也就是說執(zhí)行的都是共性的抽象信息。而我們知道,類就是對象實例的抽象。
使用(Using)
類的使用分為直接引用和間接引用。
直接引用與間接引用等判別條件,是看對該類的引用是否會引起類的初始化
直接引用已經(jīng)在類的初始化中的有過闡述,不再贅述。而類的間接引用,主要有下面幾種情況:
- 當引用了一個類的靜態(tài)變量,而該靜態(tài)變量繼承自父類的話,不引起初始化
- 定義一個類的數(shù)組,不會引起該類的初始化;
- 當引用一個類的的常量時,不會引起該類的初始化
卸載((Unloading)
當類使用完了之后,類就要進入卸載階段了。那何為衡量類使用完的標準呢?
- 該類所有的實例都已經(jīng)被回收,也就是java堆中不存在該類的任何實例。
- 加載該類的ClassLoader已經(jīng)被回收。
- 該類對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
如果以上三個條件全部滿足,jvm就會在方法區(qū)垃圾回收的時候?qū)︻愡M行卸載,類的卸載過程其實就是在方法區(qū)中清空類信息,java類的整個生命周期就結(jié)束了。
2、類加載器, JVM類加載機制
薪資范圍:6-16K
上面的類加載過程主要是通過類加載器來實現(xiàn)的,Java里有如下幾種類加載器
- 引導類加載器:負責加載支撐JVM運行的位于JRE的lib目錄下的核心類庫,比如rt.jar、charsets.jar等
- 擴展類加載器:負責加載支撐JVM運行的位于JRE的lib目錄下的ext擴展目錄中的JAR類包
- 應用程序類加載器:負責加載ClassPath路徑下的類包,主要就是加載你自己寫的那些類
- 自定義加載器:負責加載用戶自定義路徑下的類包
JDK8以后廢棄擴展類加載器(Extension ClassLoader)的原因
JDK8以后,使用平臺類加載器(Platform ClassLoader)替換了原來的擴展類加載器(Extension ClassLoader)。有兩個基本的原因歸納如下:
在JDK8中的這個Extension ClassLoader,主要用于加載jre環(huán)境下的lib下的ext下的jar包。當想要擴展Java的功能的時候,把jar包放到這個ext文件夾下。然而這樣的做法并不安全,不提倡使用。
這種擴展機制被JDK9開始加入的“模塊化開發(fā)”的天然的擴展能力所取代。
總之,擴展能力被取代了又不安全,所以被廢棄。
3、能說一下JVM的內(nèi)存區(qū)域嗎?
薪資范圍:6-16K
JVM內(nèi)存區(qū)域最粗略的劃分可以分為堆和棧,當然,按照虛擬機規(guī)范,可以劃分為以下幾個、區(qū)域
Java虛擬機運行時數(shù)據(jù)區(qū)
JVM內(nèi)存分為線程私有區(qū)和線程共享區(qū),其中方法區(qū)和堆是線程共享區(qū),虛擬機棧、本地方法棧和程序計數(shù)器是線程隔離的數(shù)據(jù)區(qū)。
1、程序計數(shù)器
程序計數(shù)器(Program Counter Register)也被稱為PC寄存器,是一塊較小的內(nèi)存空間。
它可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器。
2、Java虛擬機棧
Java虛擬機棧(Java Virtual Machine Stack)也是線程私有的,它的生命周期與線程相同。
作用:主管 Java 程序的運行,它保存方法的局部變量、部分結(jié)果,并參與方法的調(diào)用和返回。
特點:
- 棧是一種快速有效的分配存儲方式,訪問速度僅次于程序計數(shù)器
- JVM 直接對虛擬機棧的操作只有兩個:每個方法執(zhí)行,伴隨著入棧(進棧/壓棧),方法執(zhí)行結(jié)束出棧
- 棧不存在垃圾回收問題
棧中可能出現(xiàn)的異常:
Java 虛擬機規(guī)范允許 Java虛擬機棧的大小是動態(tài)的或者是固定不變的
- 如果采用固定大小的 Java 虛擬機棧,那每個線程的 Java 虛擬機棧容量可以在線程創(chuàng)建的時候獨立選定。如果線程請求分配的棧容量超過 Java 虛擬機棧允許的最大容量,Java 虛擬機將會拋出一個 StackOverflowError 異常
- 如果 Java 虛擬機棧可以動態(tài)擴展,并且在嘗試擴展的時候無法申請到足夠的內(nèi)存,或者在創(chuàng)建新的線程時沒有足夠的內(nèi)存去創(chuàng)建對應的虛擬機棧,那 Java 虛擬機將會拋出一個OutOfMemoryError異常
可以通過參數(shù)-Xss來設置線程的最大??臻g,棧的大小直接決定了函數(shù)調(diào)用的最大可達深度。
3、本地方法棧
- Java 虛擬機棧用于管理 Java 方法的調(diào)用,而本地方法棧用于管理本地方法的調(diào)用
- 本地方法棧也是線程私有的
- 允許線程固定或者可動態(tài)擴展的內(nèi)存大小
- 如果線程請求分配的棧容量超過本地方法棧允許的最大容量,Java 虛擬機將會拋出一個 StackOverflowError 異常
- 如果本地方法??梢詣討B(tài)擴展,并且在嘗試擴展的時候無法申請到足夠的內(nèi)存,或者在創(chuàng)建新的線程時沒有足夠的內(nèi)存去創(chuàng)建對應的本地方法棧,那么 Java虛擬機將會拋出一個OutofMemoryError異常
- 本地方法是使用 C 語言實現(xiàn)的
- 它的具體做法是 Native Method Stack 中登記 native 方法,在 Execution Engine 執(zhí)行時加載本地方法庫當某個線程調(diào)用一個本地方法時,它就進入了一個全新的并且不再受虛擬機限制的世界。它和虛擬機擁有同樣的權(quán)限。
- 本地方法可以通過本地方法接口來訪問虛擬機內(nèi)部的運行時數(shù)據(jù)區(qū),它甚至可以直接使用本地處理器中的寄存器,直接從本地內(nèi)存的堆中分配任意數(shù)量的內(nèi)存
- 并不是所有 JVM 都支持本地方法。因為 Java 虛擬機規(guī)范并沒有明確要求本地方法棧的使用語言、具體實現(xiàn)方式、數(shù)據(jù)結(jié)構(gòu)等。如果 JVM 產(chǎn)品不打算支持 native 方法,也可以無需實現(xiàn)本地方法棧
- 在 Hotspot JVM 中,直接將本地方法棧和虛擬機棧合二為一
4、Java堆
對于Java應用程序來說,Java堆(Java Heap)是虛擬機所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,Java里“幾乎”所有的對象實例都在這里分配內(nèi)存。
Java堆是垃圾收集器管理的內(nèi)存區(qū)域,因此一些資料中它也被稱作“GC堆”(Garbage Collected Heap,)。從回收內(nèi)存的角度看,由于現(xiàn)代垃圾收集器大部分都是基于分代收集理論設計的,所以Java堆中經(jīng)常會出現(xiàn)新生代、老年代、Eden空間、From Survivor空間、To Survivor空間等名詞,需要注意的是這種劃分只是根據(jù)垃圾回收機制來進行的劃分,不是Java虛擬機規(guī)范本身制定的。
Java 堆內(nèi)存結(jié)構(gòu)
5.方法區(qū)
- 方法區(qū)(Method Area)與 Java 堆一樣,是所有線程共享的內(nèi)存區(qū)域。
- 雖然 Java 虛擬機規(guī)范把方法區(qū)描述為堆的一個邏輯部分,但是它卻有一個別名叫 Non-Heap(非堆),目的應該是與 Java 堆區(qū)分開。
- 運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類的版本/字段/方法/接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將類在加載后進入方法區(qū)的運行時常量池中存放。運行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的是 String.intern()方法。受方法區(qū)內(nèi)存的限制,當常量池無法再申請到內(nèi)存時會拋出 OutOfMemoryError 異常。
- 方法區(qū)的大小和堆空間一樣,可以選擇固定大小也可選擇可擴展,方法區(qū)的大小決定了系統(tǒng)可以放多少個類,如果系統(tǒng)類太多,導致方法區(qū)溢出,虛擬機同樣會拋出內(nèi)存溢出錯誤
- JVM 關閉后方法區(qū)即被釋放
4、對象創(chuàng)建的過程了解嗎?
薪資范圍:6-16K
在JVM中對象的創(chuàng)建,我們從一個new指令開始:
這個過程大概圖示如下:
虛擬機收到new指令觸發(fā)。
類加載檢查:如果類沒有被類加載器加載,則執(zhí)行類加載流程(將class信息加載到JVM的運行時數(shù)據(jù)區(qū)的過程),對象所需內(nèi)存大小在類加載完后可以完全確定。
對象分配內(nèi)存:從堆中劃分出一塊確定大小的內(nèi)存。
內(nèi)存空間初始化:內(nèi)存分配完后,虛擬機需要將分配到的內(nèi)存空間初始化為零值(如:int值為0,boolean值為false等),保證了對象的實例字段在Java代碼中可以直接使用。
為對象進行必要的設置:虛擬機為對象進行設置,如設置對象屬于哪個類的實例、如何找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的GC分代年齡等信息,這些信息存放在對象頭中。
從虛擬機的角度來看,一個新的對象已經(jīng)創(chuàng)建完畢。但從Java程序的角度來看,對象創(chuàng)建才剛開始,所有的字段還是零值,所以需要程序員進行初始化操作,這樣一個真正可用的對象才算完全產(chǎn)生出來。
init是對對象級別的變量或非靜態(tài)代碼塊進行初始化的
clinit靜態(tài)變量或者靜態(tài)代碼塊誰來初始化呢
5、對象內(nèi)存分配方式
薪資范圍:6-16K
虛擬機為新對象分配內(nèi)存,從堆中劃出一塊確定大小的內(nèi)存,因為對象所需內(nèi)存的大小在類加載完后可以完全確定。
堆內(nèi)存是否規(guī)整:
·
- 堆內(nèi)存規(guī)整:已使用的內(nèi)存在一邊,未使用內(nèi)存在另一邊。
- 堆內(nèi)存不規(guī)整:已使用內(nèi)存和未使用相互交錯。
堆內(nèi)存是否規(guī)整是由垃圾收集器是否帶有壓縮整理功能決定的。
內(nèi)存分配方式:
分配方式的選擇 取決于 Java堆內(nèi)存是否規(guī)整:
- 指針碰撞方式:
-
- 堆內(nèi)存絕對規(guī)整。
- 分配過程:將已使用內(nèi)存和為使用內(nèi)存之間放一個分界點的指針,分配內(nèi)存時,指針會向未使用內(nèi)存方向移動,移動一段與對象大小相等的距離。
- 空閑列表:
-
- 堆內(nèi)存不規(guī)整。
- 分配過程:虛擬機內(nèi)部維護了一個記錄可用內(nèi)存塊的列表,在分配時從列表找一塊足夠大的空間劃分給對象實例,并更新列表上的記錄。
Java堆是否規(guī)整 由所采用的垃圾收集器是否帶有壓縮整理功能決定
6、JVM 里 new 對象時,堆會發(fā)生搶占嗎?JVM是怎么設計來保證線程安全的?
薪資范圍:6-16K
對象創(chuàng)建在虛擬機中是非常頻繁的操作,即使僅僅修改一個指針所指向的位置,在并發(fā)情況下也會引起線程不安全。
解決線程安全問題有兩種方案:
- 采用CAS分配重試的方式來保證更新操作的原子性
- 每個線程在Java堆中預先分配一小塊內(nèi)存,也就是本地線程分配緩沖(Thread Local AllocationBuffer,TLAB),要分配內(nèi)存的線程,先在本地緩沖區(qū)中分配,只有本地緩沖區(qū)用完了,分配新的緩存區(qū)時才需要同步鎖定。-XX:+UseTLAB
虛擬機1.8默認使用的是 TLAB 方式來進行內(nèi)存分配的,如果想要使用CAS方式,可以通過設置 -XX:-UseTLAB 參數(shù)來關閉TLAB功能即可。默認情況下,TLAB 空間的內(nèi)存非常小,僅占有整個 Eden 空間的 1%,我們可以通過 -XX:TLABWasteTargetPercent 設置 TLAB 空間所占用 Eden 空間的百分比大小。如果通過TLAB分配失敗的時候,則會回到Eden區(qū)通過 CAS 方式進行分配。
7、對象的內(nèi)存布局
薪資范圍:6-20K
在Java虛擬機(HotSpot)中,對象在 Java 內(nèi)存中的 存儲布局 可分為三塊:
- 對象頭 存儲區(qū)域
- 實例數(shù)據(jù) 存儲區(qū)域
- 對齊填充 存儲區(qū)域
對象頭區(qū)域:
存儲對象自身的運行時數(shù)據(jù),如:哈希碼、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程ID、偏向時間戳。
存儲對象類型指針,即對象指向類元數(shù)據(jù)的指針,JVM可以確定這個對象屬于哪個類的實例。
如果是數(shù)組,對象頭中還有一塊記錄數(shù)組長度的數(shù)據(jù)。
實例數(shù)據(jù)區(qū)域:
- 代碼中定義的字段內(nèi)容。
對齊填充區(qū)域:
- 占位符。
- 非必須。
說明:占位符起占位作用,因為對象的大小必須是8字節(jié)的整數(shù)倍,而因HotSpot VM的要求對象起始地址必須是8字節(jié)的整數(shù)倍,且對象頭部分正好是8字節(jié)的倍數(shù)。因此,當對象實例數(shù)據(jù)部分沒有對齊時(即對象的大小不是8字節(jié)的整數(shù)倍),就需要通過對齊填充來補全。
8.內(nèi)存泄漏可能由哪些原因?qū)е履兀?/h3>
薪資范圍:10-20K
內(nèi)存泄漏可能的原因有很多種:
內(nèi)存泄漏可能原因
靜態(tài)集合類引起內(nèi)存泄漏
靜態(tài)集合的生命周期和 JVM 一致,所以靜態(tài)集合引用的對象不能被釋放。
public class OOM {
static List list = new ArrayList();
public void oomTests(){
Object obj = new Object();
list.add(obj);
}
}
單例模式
和上面的例子原理類似,單例對象在初始化后會以靜態(tài)變量的方式在 JVM 的整個生命周期中存在。如果單例對象持有外部的引用,那么這個外部對象將不能被 GC 回收,導致內(nèi)存泄漏。
數(shù)據(jù)連接、IO、Socket等連接
創(chuàng)建的連接不再使用時,需要調(diào)用 close 方法關閉連接,只有連接被關閉后,GC 才會回收對應的對象(Connection,Statement,ResultSet,Session)。忘記關閉這些資源會導致持續(xù)占有內(nèi)存,無法被 GC 回收。
try {
Connection conn = null;
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("url", "", "");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("....");
} catch (Exception e) {
}finally {
//不關閉連接
}
}
變量不合理的作用域
一個變量的定義作用域大于其使用范圍,很可能存在內(nèi)存泄漏;或不再使用對象沒有及時將對象設置為 null,很可能導致內(nèi)存泄漏的發(fā)生。
public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他代碼
//由于作用域原因,method1執(zhí)行完成之后,object 對象所分配的內(nèi)存不會馬上釋放
object = null;
}
}
hash值發(fā)生變化
對象Hash值改變,使用HashMap、HashSet等容器中時候,由于對象修改之后的Hah值和存儲進容器時的Hash值不同,所以無法找到存入的對象,自然也無法單獨刪除了,這也會造成內(nèi)存泄漏。說句題外話,這也是為什么String類型被設置成了不可變類型。
ThreadLocal使用不當
ThreadLocal的弱引用導致內(nèi)存泄漏也是個老生常談的話題了,使用完ThreadLocal一定要記得使用remove方法來進行清除。
9.如何判斷對象仍然存活?
薪資范圍:8-28K
1、reference count(引用計數(shù))
查看是否有引用指向該對象,有則說明該對象不是垃圾,反之就是垃圾。
我們通過下圖的引用對象案例來說明。
在這里插入圖片描述
如上圖所示,我們可以看到一共是存在四個階段。
- 第一階段,有 3 個引用指向該對象,那該對象肯定不是垃圾。
- 第二三階段,部分引用消失,分別各有 2 個和 3 個引用指向該對象,那該對象仍然不是垃圾。
- 第四階段,沒有任何引用再指向該對象,該對象淪為垃圾。這時垃圾回收器就可以將其回收。
1.2、reference count(引用計數(shù))存在的問題
當出現(xiàn)循環(huán)引用時,如下圖所示:
我們可以看到,三個對象各自指向循環(huán)中的另一個對象,但是沒有其他引用指向這三個對象,那這三個對象就屬于“一堆垃圾”。
那現(xiàn)在我們上面所說的引用計數(shù)就不能解決這個該問題,這時我們就需要使用另外一種定位方式——Root Searching(根可達算法或根搜索算法)。
2、Root Searching(根可達算法或根搜索算法)
所謂的“根”即是:所有的程序都是從 main 方法來運行,在 main 方法里面 new 出來的對象即為根對象。
例如:在 main 方法里面我們 new 了一個 list 集合,在 list 集合中我們又可以存放若干其他對象,那我們就稱 list 為根對象,我們順著根的數(shù)據(jù)結(jié)構(gòu)往下走,只要存在引用指向的對象,那該對象就不是垃圾,反之不存在引用的對象,那該對象就是垃圾。
在這里插入圖片描述
如上圖所示,對象一、二、三、四、五均是存在根對象的引用,對象五、六之間是我們上面所提到的循環(huán)引用,對象八不存在引用,故對象六、七、八是垃圾。
根對象(root)的類型
根對象不僅僅包括我們上面所說的 main 方法里面的對象,屬于根對象的還有以下這些:
可以作為GC Roots的主要有四種對象:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
-
- 比如:各個線程被調(diào)用的方法中使用到的參數(shù)、局部變量等(局部變量表)。
- 方法區(qū)中類靜態(tài)屬性引用的對象
-
- 比如: Java類的引用類型靜態(tài)變量
- 方法區(qū)中常量引用的對象
-
- 比如:字符串常量池(String Table)里的引用
- 所有被同步鎖synchroni zed持有的對象
...
10.垃圾收集算法了解嗎?
薪資范圍:12-30K
垃圾收集算法主要有三種:
- 標記-清除算法
見名知義,標記-清除(Mark-Sweep)算法分為兩個階段:
- 標記 : 標記出所有需要回收的對象
- 清除:回收所有被標記的對象
標記-清除算法
標記-清除算法比較基礎,但是主要存在兩個缺點:
- 執(zhí)行效率不穩(wěn)定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個過程的執(zhí)行效率都隨對象數(shù)量增長而降低。
- 內(nèi)存空間的碎片化問題,標記、清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導致當以后在程序運行過程中需要分配較大對象時無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。
- 標記-復制算法
標記-復制算法解決了標記-清除算法面對大量可回收對象時執(zhí)行效率低的問題。
過程也比較簡單:將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內(nèi)存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
標記-復制算法
這種算法存在一個明顯的缺點:一部分空間沒有使用,存在空間的浪費。
新生代垃圾收集主要采用這種算法,因為新生代的存活對象比較少,每次復制的只是少量的存活對象。當然,實際新生代的收集不是按照這個比例。
- 標記-整理算法
為了降低內(nèi)存的消耗,引入一種針對性的算法:標記-整理(Mark-Compact)算法。
其中的標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內(nèi)存空間一端移動,然后直接清理掉邊界以外的內(nèi)存。
標記-整理算法
標記-整理算法主要用于老年代,移動存活對象是個極為負重的操作,而且這種操作需要Stop The World才能進行,只是從整體的吞吐量來考量,老年代使用標記-整理算法更加合適。
11 三色標記算法了解嗎
薪資范圍:12-35K
三色標記算法:
1.用于垃圾回收器升級,將STW變?yōu)椴l(fā)標記。STW就是在標記垃圾的時候,必須暫停程序,而使用并發(fā)標記,就是程序一邊運行,一邊標記垃圾。
- 避免重復掃描對象,提升標記階段的效率
什么是三色:
首先我們需要知道三色標記法就是根據(jù)可達性分析,從GC Roots開始進行遍歷訪問,在遍歷對象過程中,按“是否檢查過”這個條件將對象標記成三種顏色:
白色:該對象沒有被標記過。(對象垃圾)
灰色:該對象已經(jīng)被標記過了,但該對象下的屬性沒有全被標記完。(GC需要從此對象中去尋找垃圾)
黑色:該對象已經(jīng)被標記過了,且該對象下的屬性也全部都被標記過了。(程序所需要的對象)
2.2.三色標記過程:
假設現(xiàn)在有白、灰、黑三個集合(表示當前對象的顏色),其遍歷訪問過程為:
初始時,所有對象都在【白色集合】中;
將 GC Roots直接引用到的對象挪到【灰色集合】中;
從灰色集合中獲取對象:
3.1. 將本對象引用到的其他對象全部挪到【灰色集合】中;
3.2. 將本對象挪到【黑色集合】里面。
重復步驟3,直至【灰色集合】為空時結(jié)束。
結(jié)束后,仍在【白色集合】的對象即為GC Roots不可達,可以進行回收。
需要注意,傳統(tǒng)標記方式發(fā)生Stop The World時,對象間的引用是不會發(fā)生變化的,可以輕松完成標記。
而并發(fā)標記在標記期間應用線程還在繼續(xù)跑,對象間的引用可能發(fā)生變化,就會出現(xiàn)錯標和漏標的情況就有可能發(fā)生。
3.存在的問題
浮動垃圾:并發(fā)標記的過程中,若一個已經(jīng)被標記成黑色或者灰色的對象,突然變成了垃圾,由于不會再對黑色標記過的對象重新掃描,所以不會被發(fā)現(xiàn),那么這個對象不是白色的但是不會被清除,重新標記也不能從GC Root中去找到,所以成為了浮動垃圾,浮動垃圾對系統(tǒng)的影響不大,留給下一次GC進行處理即可。
- 對象漏標問題(需要的對象被回收):并發(fā)標記的過程中,一個業(yè)務線程將一個未被掃描過的白色對象斷開引用成為垃圾(刪除引用),同時黑色對象引用了該對象(增加引用)(這兩部可以不分先后順序);因為黑色對象的含義為其屬性都已經(jīng)被標記過了,重新標記也不會從黑色對象中去找,導致該對象被程序所需要,卻又要被GC回收,此問題會導致系統(tǒng)出現(xiàn)問題,而CMS與G1,兩種回收器在使用三色標記法時,都采取了一些措施來應對這些問題,CMS對增加引用環(huán)節(jié)進行處理(Increment Update),G1則對刪除引用環(huán)節(jié)進行處理(SATB)。
4.總結(jié)
三色標記算法是根可達算法的一種實現(xiàn)方案,其目的是為了找出所有可達對象。三色標記算法會產(chǎn)生多標和漏標問題,其中漏標問題最嚴重。漏標問題會導致本該存活的對象被回收,從而導致嚴重的程序問題。
12.能詳細說一下CMS收集器的垃圾收集過程嗎?
薪資范圍:12-35K
CMS收集齊的垃圾收集分為四步:
- 初始標記(CMS initial mark):單線程運行,需要Stop The World,標記GC Roots能直達的對象。
- 并發(fā)標記((CMS concurrent mark):無停頓,和用戶線程同時運行,從GC Roots直達對象開始遍歷整個對象圖。
- 重新標記(CMS remark):多線程運行,需要Stop The World,標記并發(fā)標記階段產(chǎn)生對象。
- 并發(fā)清除(CMS concurrent sweep):無停頓,和用戶線程同時運行,清理掉標記階段標記的死亡的對象。
Concurrent Mark Sweep收集器運行示意圖如下:
Concurrent Mark Sweep收集器運行示意圖
13.G1垃圾收集器了解嗎?
薪資范圍:12-35K
Garbage First(簡稱G1)收集器是垃圾收集器的一個顛覆性的產(chǎn)物,它開創(chuàng)了局部收集的設計思路和基于Region的內(nèi)存布局形式。
雖然G1也仍是遵循分代收集理論設計的,但其堆內(nèi)存的布局與其他收集器有非常明顯的差異。以前的收集器分代是劃分新生代、老年代、持久代等。
G1把連續(xù)的Java堆劃分為多個大小相等的獨立區(qū)域(Region),每一個Region都可以根據(jù)需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠?qū)Π缪莶煌巧腞egion采用不同的策略去處理。
更精細的控制、可預測的停頓時間、內(nèi)存碎片的控制、優(yōu)先級處理
G1 Heap Regions
每個 Region 都是通過指針碰撞來分配空間
這樣就避免了收集整個堆,而是按照若干個Region集進行收集,同時維護一個優(yōu)先級列表,跟蹤各個Region回收的“價值,優(yōu)先收集價值高的Region。
G1收集器的運行過程大致可劃分為以下四個步驟:
- 初始標記(initial mark),標記了從GC Root開始直接關聯(lián)可達的對象。STW(Stop the World)執(zhí)行。
- 并發(fā)標記(concurrent marking),和用戶線程并發(fā)執(zhí)行,從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象、
- 最終標記(Remark),STW,標記再并發(fā)標記過程中產(chǎn)生的垃圾。= 重新標記(標記的范圍更?。?/li>
-
篩選回收(Live Data Counting And Evacuation),制定回收計劃,選擇多個Region 構(gòu)成回收集,把回收集中Region的存活對象復制到空的Region中,再清理掉整個舊 Region的全部空間。需要STW。
G1收集器運行示意圖
14.有了CMS,為什么還要引入G1?
優(yōu)點:CMS最主要的優(yōu)點在名字上已經(jīng)體現(xiàn)出來——并發(fā)收集、低停頓。
缺點:CMS同樣有三個明顯的缺點。
- Mark Sweep算法會導致內(nèi)存碎片比較多
- CMS的并發(fā)能力比較依賴于CPU資源,并發(fā)回收時垃圾收集線程可能會搶占用戶線程的資源,導致用戶程序性能下降。
- 并發(fā)清除階段,用戶線程依然在運行,會產(chǎn)生所謂的理“浮動垃圾”(Floating Garbage),本次垃圾收集無法處理浮動垃圾,必須到下一次垃圾收集才能處理。如果浮動垃圾太多,會觸發(fā)新的垃圾回收,導致性能降低。
G1主要解決了內(nèi)存碎片過多的問題。
15.你們線上用的什么垃圾收集器?為什么要用它?
薪資范圍:15-35K
常見的垃圾回收器:
新生代收集器(高吞吐量): Serial、ParNew、Parallel Scavenge
老年代收集器(SWT停頓時間): Serial Old、CMS、Parallel Old
新生代和老年代收集器: G1、ZGC、Shenandoah
每種垃圾回收器之間不是獨立操作的,下圖表示垃圾回收器之間有連線表示,可以協(xié)作使用:
一般的垃圾回收器搭配為:
- Serial New(復制算法。單線程,不能利用多核) + Serial Old(標記整理。單線程) (Serial系列是單線程,GC時stop the world) JDK 5 版本之前
JDK8 :
- ParNew(復制算法。并行。 單核情況下不如Serial) + CMS(標記清除。并發(fā))
-
- 適合類型:適用于需要低停頓時間的應用,如 Web 服務器、應用服務器。
- 示例應用:電商網(wǎng)站、在線游戲、高并發(fā)服務器。
- 4-8G可以用ParNew+CMS
- Parallel Scavenge(復制算法。并行,吞吐量優(yōu)先收集器) + Parallel Old(標記整理。并行)
-
- 適合類型:適用于多核處理器的高吞吐量應用。
- 示例應用:科學計算、數(shù)據(jù)分析、大規(guī)模數(shù)據(jù)處理。
- 4G以下可以用parallel
- G1 (年輕代:復制 老年代:標記-整理)JDK 9 默認的收集器 要求盡可能可控 GC 停頓時間;內(nèi)存占用較大的應用。
-
- 適合類型:適用于需要可預測停頓時間的應用,尤其是大堆內(nèi)存的應用。
- 示例應用:企業(yè)級應用、中大規(guī)模 Web 服務、應用響應時間要求高的系統(tǒng)。
- 8G以上可以用G1
zgc:適用于需要極低停頓時間(毫秒級別)的大內(nèi)存應用
-
- 適合類型:適用于需要極低停頓時間(毫秒級別)的大內(nèi)存應用。
- 示例應用:內(nèi)存密集型數(shù)據(jù)庫、金融交易系統(tǒng)、云服務。
- 幾百G以上用ZGC
怎么查默認用的GC是什么呢?
可以使用命令:
java -XX:+PrintCommandLineFlags -version
可以看到有這么一行:
-XX:+UseParallelGC
UseParallelGC = Parallel Scavenge + Parallel Old,表示的是新生代用的Parallel Scavenge收集器,老年代用的是Parallel Old 收集器。
那為什么要用這個呢?默認的唄。
當然面試肯定不能這么答。
Parallel Scavenge的特點是什么?
高吞吐,我們可以回答:因為我們系統(tǒng)是業(yè)務相對復雜,但并發(fā)并不是非常高,所以希望盡可能的利用處理器資源,出于提高吞吐量的考慮采用Parallel Scavenge + Parallel Old的組合。
當然,這個默認雖然也有說法,但不太討喜。
還可以說:
采用Parallel New+CMS的組合,我們比較關注服務的響應速度,所以采用了CMS來降低停頓時間。
或者一步到位:
我們線上采用了設計比較優(yōu)秀的G1垃圾收集器,因為它不僅滿足我們低停頓的要求,而且解決了CMS的浮動垃圾問題、內(nèi)存碎片問題。
16.垃圾收集器應該如何選擇?
垃圾收集器的選擇需要權(quán)衡的點還是比較多的——例如運行應用的基礎設施如何?使用JDK的發(fā)行商是什么?等等……
這里簡單地列一下上面提到的一些收集器的適用場景:
- Serial :如果應用程序有一個很小的內(nèi)存空間(大約100 MB)亦或它在沒有停頓時間要求的單線程處理器上運行。
- Parallel:如果優(yōu)先考慮應用程序的峰值性能,并且沒有時間要求要求,或者可以接受1秒或更長的停頓時間。
- CMS/G1:如果響應時間比吞吐量優(yōu)先級高,或者垃圾收集暫停必須保持在大約1秒以內(nèi)。
- ZGC:如果響應時間是高優(yōu)先級的,或者堆空間比較大。
17.對象一定分配在堆中嗎?有沒有了解逃逸分析技術(shù)?
薪資范圍:10-25K
對象一定分配在堆中嗎? 不一定的。
隨著JIT編譯期的發(fā)展與逃逸分析技術(shù)逐漸成熟,所有的對象都分配到堆上也漸漸變得不那么“絕對”了。其實,在編譯期間,JIT會對代碼做很多優(yōu)化。其中有一部分優(yōu)化的目的就是減少內(nèi)存堆分配壓力,其中一種重要的技術(shù)叫做逃逸分析。
什么是逃逸分析?
逃逸分析是指分析指針動態(tài)范圍的方法,它同編譯器優(yōu)化原理的指針分析和外形分析相關聯(lián)。當變量(或者對象)在方法中分配后,其指針有可能被返回或者被全局引用,這樣就會被其他方法或者線程所引用,這種現(xiàn)象稱作指針(或者引用)的逃逸(Escape)。
通俗點講,當一個對象被new出來之后,它可能被外部所調(diào)用,如果是作為參數(shù)傳遞到外部了,就稱之為方法逃逸。-xx: -DoEscapeAnalysis
逃逸
除此之外,如果對象還有可能被外部線程訪問到,例如賦值給可以在其它線程中訪問的實例變量,這種就被稱為線程逃逸。
逃逸強度
逃逸分析的好處
- 棧上分配
如果確定一個對象不會逃逸到線程之外,那么久可以考慮將這個對象在棧上分配,對象占用的內(nèi)存隨著棧幀出棧而銷毀,這樣一來,垃圾收集的壓力就降低很多。
- 同步消除
線程同步本身是一個相對耗時的過程,如果逃逸分析能夠確定一個變量不會逃逸出線程,無法被其他線程訪問,那么這個變量的讀寫肯定就不會有競爭, 對這個變量實施的同步措施也就可以安全地消除掉。
- 標量替換
如果一個數(shù)據(jù)是基本數(shù)據(jù)類型,不可拆分,它就被稱之為標量。把一個Java對象拆散,將其用到的成員變量恢復為原始類型來訪問,這個過程就稱為標量替換。假如逃逸分析能夠證明一個對象不會被方法外部訪問,并且這個對象可以被拆散,那么可以不創(chuàng)建對象,直接用創(chuàng)建若干個成員變量代替,可以讓對象的成員變量在棧上分配和讀寫。
18.了解哪些性JVM監(jiān)控和故障處理工具?
薪資范圍:10-25K
以下是一些JDK自帶的可視化性能監(jiān)控和故障處理工具:
- JConsole
Jconsole 是一個內(nèi)置 Java 性能分析器,是基于Java Management Extensions (JMX)的實時圖形化監(jiān)測工具,這個工具利用了內(nèi)建到JVM里面的JMX指令來對Java進程提供實時的性能和資源的監(jiān)控。其監(jiān)控內(nèi)容包括:內(nèi)存、線程、類、CPU使用(Java進程的內(nèi)存使用,線程的狀態(tài),類的使用)等。通過監(jiān)控信息,可以很清晰的了解到當前程序是否運行正常,如內(nèi)存泄露、死鎖、類加載異常等。
備注:Jconsole管理內(nèi)存相當于可視化的jstat命令
JConsole概覽
開啟遠程:
java -jar xxx.jar
- -Dcom.sun.management.jmxremote 遠程開啟開關
- -Dcom.sun.management.jmxremote.port=1808 jmx遠程調(diào)用端口
- -Dcom.sun.management.jmxremote.authenticate=false 不開啟驗證
- -Dcom.sun.management.jmxremote.ssl=false 不為ssl連接
- -Djava.rmi.server.hostname=34.126.141.21 服務器所在ip或者域名
- VisualVM(jvisualvm)
VisualVM 是一款免費的,集成了多個 JDK 命令行工具的可視化工具,它能為您提供強大的分析能力,對 Java 應用程序做性能分析和調(diào)優(yōu)。這些功能包括生成和分析海量數(shù)據(jù)、跟蹤內(nèi)存泄漏、監(jiān)控垃圾回收器、執(zhí)行內(nèi)存和 CPU 分析 .
JMC主要界面
- jps
-
- 查看java進程
- jstat
-
- jstat是用于監(jiān)視虛擬機各種運行狀態(tài)信息的命令行工具。它可以顯示本地或者遠程虛擬機進程中的類裝載、內(nèi)存、垃圾收集、JIT 編譯等運行數(shù)據(jù),在沒有 GUI圖形界面,只提供了純文本控制臺環(huán)境的服務器上,它將是運行期定位虛擬機性能問題的首選工具。常用形式:
- jstat [-命令選項] [vmid] [間隔時間/毫秒] [查詢次數(shù)]
- 如:jstat-gc 13616 100 8;
- 常用參數(shù):
-
-
- -class (類加載器)
-
- -compiler (JIT)
- -gc (GC 堆狀態(tài))
-
- -gccapacity (各區(qū)大小)
-
- -gccause (最近一次 GC 統(tǒng)計和原因)
-
- -gcnew (新區(qū)統(tǒng)計)
-
- -gcnewcapacity (新區(qū)大小)
- -gcold (老區(qū)統(tǒng)計)
- -gcoldcapacity (老區(qū)大小)
- -gcpermcapacity (永久區(qū)大小)
- -gcutil (GC 統(tǒng)計匯總)
- -printcompilation (HotSpot 編譯統(tǒng)計)
-
- jmap
-
- jmap用于生成堆轉(zhuǎn)儲快照(一般稱為 heapdump 或 dump 文件)。jmap 的作用并不僅僅是為了獲取 dump 文件,它還可以查詢 finalize 執(zhí)行隊列、Java 堆和永久代的詳細信息,如空間使用率、當前用的是哪種收集器等。
-
-
- heap : 顯示Java堆詳細信息
- histo : 顯示堆中對象的統(tǒng)計信息
- permstat :Java堆內(nèi)存的永久保存區(qū)域的類加載器的統(tǒng)計信息
- finalizerinfo : 顯示在F-Queue隊列等待Finalizer線程執(zhí)行 finalizer方法的對象
- dump : 生成堆轉(zhuǎn)儲快照
-
jmap -dump:file=d:\user.hprof 1246
- jhat
-
- jhat dump 文件名
后屏幕顯示“Server is ready.”的提示后,用戶在瀏覽器中鍵入 http://localhost:7000/就可以訪問詳情。
- jhat dump 文件名
- jstack
jstack [vmid]
jstack用于生成虛擬機當前時刻的線程快照。
線程快照就是當前虛擬機內(nèi)每一條線程正在執(zhí)行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現(xiàn)長時間停頓的原因,如線程間死鎖、死循環(huán)、請求外部資源導致的長時間等待等都是導致線程長時間停頓的常見原因。
一般來說 jstack 主要是用來排查是否有死鎖和某個進程的線程調(diào)用棧的情況
除此之外,還有一些第三方的工具:
- MATJava 堆內(nèi)存分析工具。
-
- GChistoGC 日志分析工具。
-
- GCViewer GC 日志分析工具。
-
- JProfiler商用的性能分析利器。
-
- arthas阿里開源診斷工具。
-
- async-profilerJava 應用性能分析工具,開源、火焰圖、跨平臺。
19.JVM的常見參數(shù)配置知道哪些?
一些常見的參數(shù)配置:
堆配置:
- -Xms:初始堆大小
- -Xms:最大堆大小
- -XX:NewSize=n:設置年輕代大小
- -XX:NewRatio=n:設置年輕代和年老代的比值。如:為3表示年輕代和年老代比值為1:3,年輕代占整個年輕代年老代和的1/4
- -XX:SurvivorRatio=n:年輕代中Eden區(qū)與兩個Survivor區(qū)的比值。注意Survivor區(qū)有兩個。如3表示Eden:3 Survivor:2,一個Survivor區(qū)占整個年輕代的1/5
- -XX:MaxPermSize=n:設置持久代大小
gc設置:
- -XX:+UseSerialGC:設置串行收集器
- -XX:+UseParallelGC:設置并行收集器
- -XX:+UseParalledlOldGC:設置并行年老代收集器
- -XX:+UseConcMarkSweepGC:設置并發(fā)收集器
- -XX:+UseG1GC
并行收集器設置
- -XX:ParallelGCThreads=n:設置并行收集器收集時使用的CPU數(shù)。并行收集線程數(shù)
- -XX:MaxGCPauseMillis=n:設置并行收集最大的暫停時間(如果到這個時間了,垃圾回收器依然沒有回收完,也會停止回收)
- -XX:GCTimeRatio=n:設置垃圾回收時間占程序運行時間的百分比。公式為:1/(1+n)
- -XX:+CMSIncrementalMode:設置為增量模式。適用于單CPU情況
- -XX:ParallelGCThreads=n:設置并發(fā)收集器年輕代手機方式為并行收集時,使用的CPU數(shù)。并行收集線程數(shù)
打印GC回收的過程日志信息
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename
20.有做過JVM調(diào)優(yōu)嗎?
薪資范圍:10-25K
JVM調(diào)優(yōu)是一件很嚴肅的事情,不是拍腦門就開始調(diào)優(yōu)的,需要有嚴密的分析和監(jiān)控機制,大概的一個JVM調(diào)優(yōu)流程圖:
JVM調(diào)優(yōu)大致流程圖
實際上,JVM調(diào)優(yōu)是不得已而為之,有那功夫,好好把爛代碼重構(gòu)一下不比瞎調(diào)JVM強。
但是,面試官非要問怎么辦?可以從處理問題的角度來回答(對應圖中事后),這是一個中規(guī)中矩的案例:電商公司的運營后臺系統(tǒng),偶發(fā)性的引發(fā)OOM異常,堆內(nèi)存溢出。
1、因為是偶發(fā)性的,所以第一次簡單的認為就是堆內(nèi)存不足導致,單方面的加大了堆內(nèi)存從4G調(diào)整到8G -Xms8g。
2、但是問題依然沒有解決,只能從堆內(nèi)存信息下手,通過開啟了-XX:+HeapDumpOnOutOfMemoryError參數(shù) 獲得堆內(nèi)存的dump文件。
3、用JProfiler 對 堆dump文件進行分析,通過JProfiler查看到占用內(nèi)存最大的對象是String對象,本來想跟蹤著String對象找到其引用的地方,但dump文件太大,跟蹤進去的時候總是卡死,而String對象占用比較多也比較正常,最開始也沒有認定就是這里的問題,于是就從線程信息里面找突破點。
4、通過線程進行分析,先找到了幾個正在運行的業(yè)務線程,然后逐一跟進業(yè)務線程看了下代碼,有個方法引起了我的注意,導出訂單信息。
5、因為訂單信息導出這個方法可能會有幾萬的數(shù)據(jù)量,首先要從數(shù)據(jù)庫里面查詢出來訂單信息,然后把訂單信息生成excel,這個過程會產(chǎn)生大量的String對象。
6、為了驗證自己的猜想,于是準備登錄后臺去測試下,結(jié)果在測試的過程中發(fā)現(xiàn)導出訂單的按鈕前端居然沒有做點擊后按鈕置灰交互事件,后端也沒有做防止重復提交,因為導出訂單數(shù)據(jù)本來就非常慢,使用的人員可能發(fā)現(xiàn)點擊后很久后頁面都沒反應,然后就一直點,結(jié)果就大量的請求進入到后臺,堆內(nèi)存產(chǎn)生了大量的訂單對象和EXCEL對象,而且方法執(zhí)行非常慢,導致這一段時間內(nèi)這些對象都無法被回收,所以最終導致內(nèi)存溢出。
7、知道了問題就容易解決了,最終沒有調(diào)整任何JVM參數(shù),只是做了兩個處理:
- 在前端的導出訂單按鈕上加上了置灰狀態(tài),等后端響應之后按鈕才可以進行點擊
- 后端代碼加分布式鎖,做防重處理
這樣雙管齊下,保證導出的請求不會一直打到服務端,問題解決!
21.線上服務CPU占用過高怎么排查?
薪資范圍:10-25K
問題分析:CPU高一定是某個程序長期占用了CPU資源。
CPU飆高
1、所以先需要找出那個進程占用CPU高。
- top 列出系統(tǒng)各個進程的資源占用情況。
2、然后根據(jù)找到對應進行里哪個線程占用CPU高。
- top -Hp 進程ID 列出對應進程里面的線程占用資源情況
3、找到對應線程ID后,再打印出對應線程的堆棧信息
- printf "%x\n" PID 把線程ID轉(zhuǎn)換為16進制。
- jstack PID 打印出進程的所有線程信息,從打印出來的線程信息中找到上一步轉(zhuǎn)換為16進制的線程ID對應的線程信息。
4、最后根據(jù)線程的堆棧信息定位到具體業(yè)務方法,從代碼邏輯中找到問題所在。
查看是否有線程長時間的watting 或blocked,如果線程長期處于watting狀態(tài)下, 關注watting on xxxxxx,說明線程在等待這把鎖,然后根據(jù)鎖的地址找到持有鎖的線程。
22.內(nèi)存飆高問題怎么排查?
薪資范圍:10-25K
分析:內(nèi)存飚高如果是發(fā)生在java進程上,一般是因為創(chuàng)建了大量對象所導致,持續(xù)飚高說明垃圾回收跟不上對象創(chuàng)建的速度,或者內(nèi)存泄露導致對象無法回收。
1、先觀察垃圾回收的情況
- jstat -gc PID 1000 查看GC次數(shù),時間等信息,每隔一秒打印一次。
- jmap -histo PID | head -20 查看堆內(nèi)存占用空間最大的前20個對象類型,可初步查看是哪個對象占用了內(nèi)存。
如果每次GC次數(shù)頻繁,而且每次回收的內(nèi)存空間也正常,那說明是因為對象創(chuàng)建速度快導致內(nèi)存一直占用很高;如果每次回收的內(nèi)存非常少,那么很可能是因為內(nèi)存泄露導致內(nèi)存一直無法被回收。
2、導出堆內(nèi)存文件快照
- jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump堆內(nèi)存信息到文件。
如果會掛掉
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/crashes/my-heap-dump.hprof
3、使用visualVM對dump文件進行離線分析,找到占用內(nèi)存高的對象,再找到創(chuàng)建該對象的業(yè)務代碼位置,從代碼和業(yè)務場景中定位具體問題。
23.頻繁 minor gc 怎么辦?
優(yōu)化Minor GC頻繁問題:通常情況下,由于新生代空間較小,Eden區(qū)很快被填滿,就會導致頻繁Minor GC,因此可以通過增大新生代空間-Xmn來降低Minor GC的頻率。
24.頻繁Full GC怎么辦?
Full GC的排查思路大概如下:
- 清楚從程序角度,有哪些原因?qū)е翭GC?
- 大對象:系統(tǒng)一次性加載了過多數(shù)據(jù)到內(nèi)存中(比如SQL查詢未做分頁),導致大對象進入了老年代。
- 內(nèi)存泄漏:頻繁創(chuàng)建了大量對象,但是無法被回收(比如IO對象使用完后未調(diào)用close方法釋放資源),先引發(fā)FGC,最后導致OOM.
- 程序頻繁生成一些長生命周期的對象,當這些對象的存活年齡超過分代年齡時便會進入老年代,最后引發(fā)FGC.
- 程序BUG
- 代碼中顯式調(diào)用了gc方法,包括自己的代碼甚至框架中的代碼。
- JVM參數(shù)設置問題:包括總內(nèi)存大小、新生代和老年代的大小、Eden區(qū)和S區(qū)的大小、元空間大小、垃圾回收算法等等。
- 清楚排查問題時能使用哪些工具
- 公司的監(jiān)控系統(tǒng):大部分公司都會有,可全方位監(jiān)控JVM的各項指標。
- JDK的自帶工具,包括jmap、jstat等常用命令:
# 查看堆內(nèi)存各區(qū)域的使用率以及GC情況
jstat -gcutil -h20 pid 1000
# 查看堆內(nèi)存中的存活對象,并按空間排序
jmap -histo pid | head -n20
# dump堆內(nèi)存文件
jmap -dump:format=b,file=heap pid
- 可視化的堆內(nèi)存分析工具:JVisualVM、MAT等
- 排查指南
- 查看監(jiān)控,以了解出現(xiàn)問題的時間點以及當前FGC的頻率(可對比正常情況看頻率是否正常)
- 了解該時間點之前有沒有程序上線、基礎組件升級等情況。
- 了解JVM的參數(shù)設置,包括:堆空間各個區(qū)域的大小設置,新生代和老年代分別采用了哪些垃圾收集器,然后分析JVM參數(shù)設置是否合理。
- 再對步驟1中列出的可能原因做排除法,其中元空間被打滿、內(nèi)存泄漏、代碼顯式調(diào)用gc方法比較容易排查。
- 針對大對象或者長生命周期對象導致的FGC,可通過 jmap -histo 命令并結(jié)合dump堆內(nèi)存文件作進一步分析,需要先定位到可疑對象。
- 通過可疑對象定位到具體代碼再次分析,這時候要結(jié)合GC原理和JVM參數(shù)設置,弄清楚可疑對象是否滿足了進入到老年代的條件才能下結(jié)論。
25.有沒有處理過內(nèi)存溢出(OOM)問題?是如何定位的?
內(nèi)存泄漏是內(nèi)在病源,外在病癥表現(xiàn)可能有:
- 應用程序長時間連續(xù)運行時性能嚴重下降
- CPU 使用率飆升,甚至到 100%
- 頻繁 Full GC,各種報警,例如接口超時報警等
- 應用程序拋出 OutOfMemoryError 錯誤
- 應用程序偶爾會耗盡連接對象
嚴重內(nèi)存泄漏往往伴隨頻繁的 Full GC,所以分析排查內(nèi)存泄漏問題首先還得從查看 Full GC 入手。主要有以下操作步驟:文章來源:http://www.zghlxwxcb.cn/news/detail-800983.html
- 使用 jps 查看運行的 Java 進程 ID
- 使用top -p [pid] 查看進程使用 CPU 和 MEM 的情況
- 使用 top -Hp [pid] 查看進程下的所有線程占 CPU 和 MEM 的情況
- 將線程 ID 轉(zhuǎn)換為 16 進制:printf "%x\n" [pid],輸出的值就是線程棧信息中的 nid。例如:printf "%x\n" 29471,換行輸出 731f。
- 抓取線程棧:jstack 29452 > 29452.txt,可以多抓幾次做個對比。在線程棧信息中找到對應線程號的 16 進制值,如下是 731f 線程的信息。線程棧分析可使用 Visualvm 插件 TDA。
"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fbe2c164000 nid=0x731f runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
- 使用jstat -gcutil [pid] 5000 10 每隔 5 秒輸出 GC 信息,輸出 10 次,查看 YGC 和 Full GC 次數(shù)。通常會出現(xiàn) YGC 不增加或增加緩慢,而 Full GC 增加很快?;蚴褂?jstat -gccause [pid] 5000 ,同樣是輸出 GC 摘要信息。或使用 jmap -heap [pid] 查看堆的摘要信息,關注老年代內(nèi)存使用是否達到閥值,若達到閥值就會執(zhí)行 Full GC。
- 如果發(fā)現(xiàn) Full GC 次數(shù)太多,就很大概率存在內(nèi)存泄漏了
- 使用 jmap -histo:live [pid] 輸出每個類的對象數(shù)量,內(nèi)存大小(字節(jié)單位)及全限定類名。
- 生成 dump 文件,借助工具分析哪 個對象非常多,基本就能定位到問題在那了使用 jmap 生成 dump 文件:
# jmap -dump:live,format=b,file=29471.dump 29471
Dumping heap to /root/dump ...
Heap dump file created
可以使用 jhat 命令分析:jhat -port 8000 29471.dump,瀏覽器訪問 jhat 服務,端口是 8000。通常使用圖形化工具分析,如 JDK 自帶的 jvisualvm,從菜單 > 文件 > 裝入 dump 文件?;蚴褂玫谌绞骄叻治龅模?JProfiler 也是個圖形化工具,GCViewer 工具。Eclipse 或以使用 MAT 工具查看。或使用在線分析平臺 GCEasy。注意:如果 dump 文件較大的話,分析會占比較大的內(nèi)存?;旧暇涂梢远ㄎ坏酱a層的邏輯了。文章來源地址http://www.zghlxwxcb.cn/news/detail-800983.html
-
- 在 dump 文析結(jié)果中查找存在大量的對象,再查對其的引用。
- dump 文件分析
到了這里,關于大廠面試題一文講通jvm,Java虛擬機高頻面試題的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!