Java面試 -- JVM篇
1、記憶體模型
堆:Java虛擬機器管理記憶體中最大的一塊,執行緒共享區域。所有物件例項和陣列都在堆上分配記憶體空間。
棧:在Hotspot中虛擬機器棧和本地方法棧是在一起的。它是執行緒私有,每個執行緒都會建立一個虛擬機器棧,生命週期與執行緒相同。每個方法被執行的時候就會建立一個棧幀,用於儲存區域性變量表,運算元棧,動態連結,方法出口等資訊。一個方法執行的過程對應著一個棧幀的入棧到出棧過程。
方法區:用於儲存類資訊,常量,靜態變數等資訊,是執行緒共享區域。
程式計數器:一塊較小的記憶體空間,作用是當前執行位元組碼的行號指示器。
2、堆的分割槽
堆中儲存物件例項,是垃圾回收的主要區域。為了方便垃圾回收,將堆區域分為新生代和老年代兩個區域。
新生代:大量物件(98%)都是朝生夕死,因此在進行垃圾回收的時候採用複製演算法進行垃圾回收,因為只需付出商量存貨物件的複製成本就可以完成收集。但是由於新生代大量物件都是非存活狀態,按照常規復制演算法1:1劃分記憶體會造成大量空間的浪費。因此新生代又可以劃分為一塊較大的Eden區域和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor區域。回收時,將存活的物件一次性拷貝到另一塊Survivor空間上,再清理掉用過的Eden和Survivor空間。預設Eden區域:Survivor區域=8:1 也就是每次使用新生代容量的90%,只有10%被浪費。但是當Survivor區域不足 以儲存回收後存活的物件時,需要老年代進行空間擔保,這些物件直接通過分配擔保機制進入老年代。
老年代:判斷物件是否死亡,至少進行兩次標記過程。如果物件在Eden出生並且經過一次gc後仍然存活,並且能夠被Survivor容納的話,將會被移動到Survivor區域,並將其年齡設定為1,如果它還能熬過下一次gc收集,年齡再+1.預設情況下當年齡到達15後,就會晉升到老年代中。老年代採用標記清除和標記整理演算法進行垃圾收集。
3、物件的建立
物件建立方法,物件的記憶體分配,物件的訪問定位。
虛擬機器遇到一個new指令時,首先去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並檢查這個符號引用代表的類是否已經被載入、解析和初始化過。如果沒有,需要先執行相應的類載入過程。
物件訪問定位符:
1.控制代碼訪問方式:java堆中劃分一部分空間作為控制代碼池,棧reference中儲存物件的控制代碼地址,控制代碼中包含物件例項資料和各自物件具體地址。即物件實 例資料指標指向堆的例項池物件地址、物件型別資料指標指向方法區的物件型別資料。
優勢:reference中存在穩定的控制代碼地址。物件被移動時只改變控制代碼中例項資料指標。
2.直接指標訪問方式: reference中直接儲存物件例項地址,物件例項資料中有一小塊區域儲存物件型別資料指標,同樣指向方法區。
優勢:節省了一次指標定位的開銷,訪問速度更快。
4、GC的判定方法
GC的判定就是判斷這個物件是否存活,包含2個判斷方式:引用計數與引用鏈。
1.引用計數:給物件新增一個引用計數器,每當有一個地方引用它時,計數器加1。引用失效時,計數器減1 。當計數器為0時,物件就不可能再被使用。
殘留問題:迴圈引用
2.引用鏈(可達性分析演算法):通過Gc-Root的物件為起點,通過這個節點向下搜尋,搜尋所走過的路徑為引用鏈。當一個物件到Gc-Root沒有任何引用鏈連線時,證明物件不可用。
可作為Gc-Root的物件:
- 虛擬機器棧中的引用物件。
- 本地方法棧中引用的物件。
- 方法區類靜態屬性引用的物件。
- 方法區中常量引用的物件。
5、GC收集方法
GC的三種收集方法:標記清除、標記整理、複製演算法的原理與特點,分別用在什麼地方。
1.標記清除演算法:首先標記處所有需要回收的物件,在標記完成後統一進行回收。
缺點:標記的過程效率不高、標記清除之後產生大量不連續的記憶體碎片,當需要申請大塊連續記憶體空間時,無法找到。
2.複製演算法:將記憶體按容量費為大小相等的兩塊區域,每次只使用其中的一塊,當一塊記憶體用完了,就將還存活的物件複製到另一塊記憶體上面,然後吧使用過的那塊記憶體統一清理掉。
缺點:每次只能使用總記憶體容量的一半。在物件存活較多的情況下會進行大量複製操作,效率底下。
3.標記整理演算法:和標記清除演算法一樣,先對死亡物件進行標記,然後將存活物件向一端移動,然後直接清理掉邊界以外的記憶體。
4.分代收集演算法:根據物件存活週期的不同,將記憶體劃分為新生代和老年代,新生代使用複製演算法,老年代使用標記清除或標記整理演算法進行垃圾收集。
6、GC收集器
GC收集器有哪些?CMS收集器與G1收集器的特點。
1.Serial:一個單執行緒的收集器,在進行垃圾收集時候,必須暫停其他所有的工作執行緒直到它收集結束。
特點:CPU利用率最高,停頓時間即使用者等待時間比較長。
2.Parallel:採用多執行緒來通過掃描並壓縮
特點:停頓時間短,回收效率高,對吞吐量要求高。
3.CMS收集器:採用“標記-清除”演算法實現,使用多執行緒的演算法去掃描堆,對發現未使用的物件進行回收。
4:G1:堆被劃分成 許多個連續的區域(region)。採用G1演算法進行回收,吸收了CMS收集器特點。
特點:支援很大的堆,高吞吐量、支援多CPU和垃圾回收執行緒、在主執行緒暫停的情況下,使用並行收集、在主執行緒執行的情況下,使用併發收集
7、Minor GC與Full GC
Minor GC與Full GC分別在什麼時候發生?
Minor GC: 從年輕代空間(包括 Eden 和 Survivor 區域)回收記憶體被稱為 Minor GC。
特點:當 JVM 無法為一個新的物件分配空間時會觸發 Minor GC,比如當 Eden 區滿了。所以分配率越高,越頻繁執行 Minor G。記憶體池被填滿的時候,其中的內容全部會被複制,指標會從0開始跟蹤空閒記憶體。Eden 和 Survivor 區進行了標記和複製操作,取代了經典的標記、掃描、壓縮、清理操作。所以 Eden 和 Survivor 區不存在記憶體碎片。寫指標總是停留在所使用記憶體池的頂部。
執行 Minor GC 操作時,不會影響到永久代。從永久代到年輕代的引用被當成 GC roots,從年輕代到永久代的引用在標記階段被直接忽略掉。
質疑常規的認知,所有的 Minor GC 都會觸發“全世界的暫停(stop-the-world)”,停止應用程式的執行緒。對於大部分應用程式,停頓導致的延遲都是可以忽略不計的。其中的真相就 是,大部分 Eden 區中的物件都能被認為是垃圾,永遠也不會被複制到 Survivor 區或者老年代空間。如果正好相反,Eden 區大部分新生物件不符合 GC 條件,Minor GC 執行時暫停的時間將會長很多。
Major GC:清理永久代
Full GC:清理整個堆空間—包括年輕代和永久代
Major GC / Full GC:老年代 GC(Major GC / Full GC):指發生在老年代的 GC,出現了 Major GC,經常會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略裡就有直接進行 Major GC 的策略選擇過程) 。MajorGC 的速度一般會比 Minor GC 慢 10倍以上。
8、雙親委派模型
JDK 預設提供了幾種ClassLoader:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。
Bootstrp loader
Bootstrp載入器是用C++語言寫的,它是在Java虛擬機器啟動後初始化的,它主要負責載入%JAVA_HOME%/jre/lib,-Xbootclasspath引數指定的路徑以及%JAVA_HOME%/jre/classes中的類。ExtClassLoader
Bootstrp loader載入ExtClassLoader,並且將ExtClassLoader的父載入器設定為Bootstrploader.ExtClassLoader是用Java寫的,具體來說就是 sun.misc.LauncherExtClassLoader,ExtClassLoader主要載入AppClassLoaderBootstrploader載入完ExtClassLoader後,就會載入AppClassLoader,並且將AppClassLoader的父載入器指定為ExtClassLoader。AppClassLoader也是用Java寫成的,它的實現類是sun.misc.LauncherAppClassLoader,另外我們知道ClassLoader中有個getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要負責載入classpath所指定的位置的類或者是jar文件,它也是Java程式預設的類載入器。
綜上所述,它們之間的關係可以通過下圖形象的描述:
Java中ClassLoader的載入採用了雙親委託機制,採用雙親委託機制載入類的時候採用如下的幾個步驟:
- 當前ClassLoader首先從自己已經載入的類中查詢是否此類已經載入,如果已經載入則直接返回原來已經載入的類。
每個類載入器都有自己的載入快取,當一個類被載入了以後就會放入快取,等下次載入的時候就可以直接返回了。 - 當前classLoader的快取中沒有找到被載入的類的時候,委託父類載入器去載入,父類載入器採用同樣的策略,首先檢視自己的快取,然後委託父類的父類去載入,一直到bootstrp ClassLoader.
- 當所有的父類載入器都沒有載入的時候,再由當前的類載入器載入,並將其放入它自己的快取中,以便下次有載入請求的時候直接返回。
說到這裡大家可能會想,Java為什麼要採用這樣的委託機制?
理解這個問題,我們引入另外一個關於Classloader的概念“名稱空間”, 它是指要確定某一個類,需要類的全限定名以及載入此類的ClassLoader來共同確定。也就是說即使兩個類的全限定名是相同的,但是因為不同的ClassLoader載入了此類, 那麼在JVM中它是不同的類。
明白了名稱空間以後,我們再來看看委託模型。採用了委託模型以後加大了不同的 ClassLoader的互動能力,比如上面說的,我們JDK本生提供的類庫,比如hashmap,linkedlist等等,這些類由bootstrp 類載入器載入了以後,無論你程式中有多少個類載入器,那麼這些類其實都是可以共享的,這樣就避免了不同的類載入器載入了同樣名字的不同類以後造成混亂。
如何自定義ClassLoader
Java除了上面所說的預設提供的classloader以外,它還容許應用程式可以自定義classloader,那麼要想自定義classloader我們需要通過繼承java.lang.ClassLoader來實現,接下來我們就來看看再自定義Classloader的時候,我們需要注意的幾個重要的方法:
1.loadClass 方法
loadClass method declare
public Class<?> loadClass(String name) throws ClassNotFoundException
上面是loadClass方法的原型宣告,上面所說的雙親委託機制的實現其實就實在此方法中實現的。下面我們就來看看此方法的程式碼來看看它到底如何實現雙親委託的。
loadClass method implement
public Class<?> loadClass(String name) throws ClassNotFoundException
{
return loadClass(name, false);
}
從上面可以看出loadClass方法呼叫了loadcClass(name,false)方法,那麼接下來我們再來看看另外一個loadClass方法的實現。
Class loadClass(String name, boolean resolve)
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{ // First, check if the class has already been loaded Class c = findLoadedClass(name);
//檢查class是否已經被載入過了 if (c == null)
{
try {
if (parent != null) {
c = parent.loadClass(name, false); //如果沒有被載入,且指定了父類載入器,則委託父載入器載入。
} else {
c = findBootstrapClass0(name);//如果沒有父類載入器,則委託bootstrap載入器載入 }
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);//如果父類載入沒有載入到,則通過自己的findClass來載入。 }
}
if (resolve)
{
resolveClass(c);
}
return c;
}
9、類載入過程
類的載入分為五個過程:載入、驗證、準備、解析、初始化。