?本篇文章總結(jié)了目前JVM面試中最常問(wèn)到的問(wèn)題,以下是問(wèn)題,答案附在后面
Java 內(nèi)存分配
簡(jiǎn)述 Java 垃圾回收機(jī)制
垃圾回收的優(yōu)點(diǎn)和原理并考慮 2 種回收機(jī)制
System.gc() 和 Runtime.gc() 會(huì)做什么事情?
Java 堆的結(jié)構(gòu)是什么樣子的?什么是堆中的永久代(****Perm Gen space****)****?
Java 中會(huì)存在內(nèi)存泄漏嗎,請(qǐng)簡(jiǎn)單描述
Java 中的內(nèi)存泄露的情況
finalize() 方法什么時(shí)候被調(diào)用?析構(gòu)函數(shù) (finalization) 的目的是什么?
JVM 的永久代中會(huì)發(fā)生垃圾回收么?
什么是類(lèi)加載器,類(lèi)加載器有哪些?
Java 類(lèi)加載過(guò)程?
類(lèi)加載的主要步驟?
Java 內(nèi)存分配
? 寄存器:程序計(jì)數(shù)器,是線(xiàn)程私有的,就是一個(gè)指針,指向方法區(qū)中的方法字節(jié)碼。
? 靜態(tài)域:static 定義的靜態(tài)成員。
? 常量池:編譯時(shí)被確定并保存在 .class 文件中的(final)
常量值和一些文本修飾的符號(hào)引用(類(lèi)和接口的全限定名,字段的名稱(chēng)和描述符,方法和名稱(chēng)和描述符)。
? 非 RAM 存儲(chǔ):硬盤(pán)等永久存儲(chǔ)空間。
? 堆內(nèi)存:new 創(chuàng)建的對(duì)象和數(shù)組,由 Java 虛擬機(jī)自動(dòng)垃圾回收器管理,存取速度慢。
? 棧內(nèi)存:基本類(lèi)型的變量和對(duì)象的引用變量(堆內(nèi)存空間的訪(fǎng)問(wèn)地址),速度快,可以共享,但是大小與生存期必須確定,缺乏靈活性。
串行(serial)收集器和吞吐量(throughput)收集器的區(qū)別是什么?
吞吐量收集器使用并行版本的新生代垃圾收集器,它用于中等規(guī)模和大規(guī)模數(shù)據(jù)的應(yīng)用程序。
而串行收集器對(duì)大多數(shù)的小應(yīng)用(在現(xiàn)代處理器上需要大概 100M 左右的內(nèi)存)就足夠了。
在 Java 中,對(duì)象什么時(shí)候可以被垃圾回收?
當(dāng)對(duì)象對(duì)當(dāng)前使用這個(gè)對(duì)象的應(yīng)用程序變得不可觸及的時(shí)候,這個(gè)對(duì)象就可以被回收了。
GC 是什么? 為什么要有 GC?
GC 是垃圾收集的意思(GabageCollection),內(nèi)存處理是編程人員容易出現(xiàn)問(wèn)題的地方,忘記或者錯(cuò)誤的內(nèi)存回收會(huì)導(dǎo)致程序或
系統(tǒng)的不穩(wěn)定甚至崩潰,Java 提供的 GC 功能可以自動(dòng)監(jiān)測(cè)對(duì)象是否超過(guò)作用域從而達(dá)到自動(dòng)回收內(nèi)存的目的,Java 語(yǔ)言沒(méi)有提
供釋放已分配內(nèi)存的顯示操作方法。
簡(jiǎn)述 Java 垃圾回收機(jī)制
在 Java 中,程序員是不需要顯示的去釋放一個(gè)對(duì)象的內(nèi)存的,而是由虛擬機(jī)自行執(zhí)行。在 JVM 中,有一個(gè)垃圾回收線(xiàn)程,它是低優(yōu)先級(jí)的,在正常情況下是不會(huì)執(zhí)行的,只有在虛擬機(jī)空閑或者當(dāng)前堆內(nèi)存不足時(shí),才會(huì)觸發(fā)執(zhí)行,掃面那些沒(méi)有被任何引用的對(duì)象,并將它們添加到要回收的集合中,進(jìn)行回收。
如何判斷一個(gè)對(duì)象是否存活?(或者 GC 對(duì)象的判定方法)
判斷一個(gè)對(duì)象是否存活有兩種方法:
引用計(jì)數(shù)法
所謂引用計(jì)數(shù)法就是給每一個(gè)對(duì)象設(shè)置一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用這個(gè)對(duì)象時(shí),就將計(jì)數(shù)器加一,引用失效時(shí),計(jì)數(shù)器就
減一。當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)器為零時(shí),說(shuō)明此對(duì)象沒(méi)有被引用,也就是“死對(duì)象”,將會(huì)被垃圾回收.引用計(jì)數(shù)法有一個(gè)缺陷就是無(wú)法解決循環(huán)引用問(wèn)題,也就是說(shuō)當(dāng)對(duì)象 A 引用對(duì)象 B,對(duì)象 B 又引用者對(duì)象 A,那么此時(shí) A、B 對(duì)象的引用計(jì)數(shù)器都不為零,也就造成無(wú)法完成垃圾回收,所以主流的虛擬機(jī)都沒(méi)有采用這種算法。
可達(dá)性算法(引用鏈法)
該算法的思想是:從一個(gè)被稱(chēng)為 GC Roots 的對(duì)象開(kāi)始向下搜索,如果一個(gè)對(duì)象到 GC Roots 沒(méi)有任何引用鏈相連時(shí),則說(shuō)明此對(duì)
象不可用。
在 Java 中可以作為 GC Roots 的對(duì)象有以下幾種:
? 虛擬機(jī)棧中引用的對(duì)象
? 方法區(qū)類(lèi)靜態(tài)屬性引用的對(duì)象
? 方法區(qū)常量池引用的對(duì)象
? 本地方法棧 JNI 引用的對(duì)象
雖然這些算法可以判定一個(gè)對(duì)象是否能被回收,但是當(dāng)滿(mǎn)足上述條件時(shí),一個(gè)對(duì)象并不一定會(huì)被回收。當(dāng)一個(gè)對(duì)象不可達(dá) GC Root
時(shí),這個(gè)對(duì)象并不會(huì)立馬被回收,而是處于一個(gè)死緩的階段,若要被真正的回收需要經(jīng)歷兩次標(biāo)記.如果對(duì)象在可達(dá)性分析中沒(méi)有與 GC Root 的引用鏈,那么此時(shí)就會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是是否有必要執(zhí)行finalize() 方法。當(dāng)對(duì)象沒(méi)有覆蓋 finalize() 方法或者已被虛擬機(jī)調(diào)用過(guò),那么就認(rèn)為是沒(méi)必要的。如果該對(duì)象有必要執(zhí)行finalize() 方法,那么這個(gè)對(duì)象將會(huì)放在一個(gè)稱(chēng)為 F-Queue 的對(duì)
隊(duì)列中,虛擬機(jī)會(huì)觸發(fā)一個(gè) Finalize() 線(xiàn)程去執(zhí)行,此線(xiàn)程是低優(yōu)先級(jí)的,并且虛擬機(jī)不會(huì)承諾一直等待它運(yùn)行完,這是因?yàn)槿绻鹒inalize() 執(zhí)行緩慢或者發(fā)生了死鎖,那么就會(huì)造成 F-Queue 隊(duì)列一直等待,造成了內(nèi)存回收系統(tǒng)的崩潰。GC 對(duì)處于 F-Queue 中的對(duì)象進(jìn)行第二次被標(biāo)記,這時(shí),該對(duì)象將被移除” 即將回收”集合,等待回收。
垃圾回收的優(yōu)點(diǎn)和原理并考慮 2 種回收機(jī)制
Java 語(yǔ)言中一個(gè)顯著的特點(diǎn)就是引入了垃圾回收機(jī)制,使 C++ 程序員最頭疼的內(nèi)存管理的問(wèn)題迎刃而解,它使得 Java 程序員在編寫(xiě)程序的時(shí)候不再需要考慮內(nèi)存管理。由于有個(gè)垃圾回收機(jī)制,Java 中的對(duì)象不再有“作用域”的概念,只有對(duì)象的引用才有"作用域"。垃圾回收可以有效的防止內(nèi)存泄露,有效的使用可以使用的內(nèi)存。垃圾回收器通常是作為一個(gè)單獨(dú)的低級(jí)別的線(xiàn)程運(yùn)行,不可預(yù)知的情況下對(duì)內(nèi)存堆中已經(jīng)死亡的或者長(zhǎng)時(shí)間沒(méi)有使用的對(duì)象進(jìn)行清楚和回收,程序員不能實(shí)時(shí)的調(diào)用垃圾回收器對(duì)某個(gè)對(duì)象或所有對(duì)象進(jìn)行垃圾回收。
回收機(jī)制有分代復(fù)制垃圾回收和標(biāo)記垃圾回收,增量垃圾回收。
垃圾回收器的基本原理是什么?垃圾回收器可以馬上回收內(nèi)存嗎?有什么辦法主動(dòng)通知虛擬機(jī)進(jìn)行垃圾回收?
對(duì)于 GC 來(lái)說(shuō),當(dāng)程序員創(chuàng)建對(duì)象時(shí),GC 就開(kāi)始監(jiān)控這個(gè)對(duì)象的地址、大小以及使用情況。通常,GC 采用有向圖的方式記錄和管理堆(heap)中的所有對(duì)象。通過(guò)這種方式確定哪些對(duì)象是”可達(dá)的”,哪些對(duì)象是”不可達(dá)的”。當(dāng) GC 確定一些對(duì)象為“不可達(dá)”時(shí),GC 就有責(zé)任回收這些內(nèi)存空間。可以。程序員可以手動(dòng)執(zhí)行 System.gc(),通知 GC 運(yùn)行,但是 Java 語(yǔ)言規(guī)范并不保證 GC 一定會(huì)執(zhí)行。
System.gc() 和 Runtime.gc() 會(huì)做什么事情?
這兩個(gè)方法用來(lái)提示 JVM 要進(jìn)行垃圾回收。但是,立即開(kāi)始還是延遲進(jìn)行垃圾回收是取決于 JVM 的。
Java 堆的結(jié)構(gòu)是什么樣子的?什么是堆中的永久代(****Perm Gen space****)****?
JVM 的堆是運(yùn)行時(shí)數(shù)據(jù)區(qū),所有類(lèi)的實(shí)例和數(shù)組都是在堆上分配內(nèi)存。它在 JVM 啟動(dòng)的時(shí)候被創(chuàng)建。對(duì)象所占的堆內(nèi)存是由自動(dòng)
內(nèi)存管理系統(tǒng)也就是垃圾收集器回收。堆內(nèi)存是由存活和死亡的對(duì)象組成的。存活的對(duì)象是應(yīng)用可以訪(fǎng)問(wèn)的,不會(huì)被垃圾回收。死亡的對(duì)象是應(yīng)用不可訪(fǎng)問(wèn)尚且還沒(méi)有被垃圾收集器回收掉的對(duì)象。一直到垃圾收集器把這些 對(duì)象回收掉之前,他們會(huì)一直占據(jù)堆內(nèi)存空間。
Java 中會(huì)存在內(nèi)存泄漏嗎,請(qǐng)簡(jiǎn)單描述。
所謂內(nèi)存泄露就是指一個(gè)不再被程序使用的對(duì)象或變量一直被占據(jù)在內(nèi)存中。Java 中有垃圾回收機(jī)制,它可以保證一對(duì)象不再被引用的時(shí)候,即對(duì)象變成了孤兒的時(shí)候,對(duì)象將自動(dòng)被垃圾回收器從內(nèi)存中清除掉。由于 Java 使用有向圖的方式進(jìn)行垃圾回收管理,可以消除引用循環(huán)的問(wèn)題,例如有兩個(gè)對(duì)象,相互引用,只要它們和根進(jìn)程不可達(dá)的,那么 GC 也是可以回收它們的,
下面的代碼可以看到這種情況的內(nèi)存回收:
import java.io.IOException; **public** **class** **GarbageTest** {**public** **static** **void** **main**(String[] args) throws IOException {
// TODO Auto-generated method stub **try** {
gcTest();
} **catch** (IOException e) {// TODO Auto-generated catch block e.printStackTrace();
}
System.**out**.println("has exited gcTest!");
System.**in**.read();
System.**in**.read();
System.**out**.println("out begin gc!");
**for**(**int** i=0;i<100;i++){
System.gc();
System.**in**.read();
System.**in**.read();
}
}
**private** **static** **void** **gcTest**() throws IOException {
System.**in**.read();
System.**in**.read();
Person p1 = **new** Person();
System.**in**.read();
System.**in**.read();
Person p2 = **new** Person();
p1.setMate(p2);
p2.setMate(p1);
System.**out**.println("before exit gctest!");
System.**in**.read();
System.**in**.read();
System.gc();
System.**out**.println("exit gctest!");
}
**private** **static** **class** **Person**{ **byte**[] data = **new** **byte**[20000000];
Person mate = null;
**public** **void** **setMate**(Person other){
mate = other;
}
}
}
Java 中的內(nèi)存泄露的情況:
長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象的引用就很可能發(fā)生內(nèi)存泄露,盡管短生命周期對(duì)象已經(jīng)不再需要,但是因?yàn)殚L(zhǎng)生命周期對(duì)象持有它的引用而導(dǎo)致不能被回收,這就是 Java 中內(nèi)存泄露的發(fā)生場(chǎng)景,通俗地說(shuō),就是程序員可能創(chuàng)建了一個(gè)對(duì)象,以后一直不再使用這個(gè)對(duì)象,這個(gè)對(duì)象卻一直被引用,即這個(gè)對(duì)象無(wú)用但是卻無(wú)法被垃圾回收器回收的,這就是 java 中可能出現(xiàn)內(nèi)存泄露的情況,例如,緩存系統(tǒng),我們加載了一個(gè)對(duì)象放在緩存中 (例如放在一個(gè)全局 map 對(duì)象中),然后一直不再使用它,這個(gè)對(duì)象一直被緩存引用,但卻不再被使用。檢查 Java 中的內(nèi)存泄露,一定要讓程序?qū)⒏鞣N分支情況都完整執(zhí)行到程序結(jié)束,然后看某個(gè)對(duì)象是否被使用過(guò),如果沒(méi)有,則才能
判定這個(gè)對(duì)象屬于內(nèi)存泄露。如果一個(gè)外部類(lèi)的實(shí)例對(duì)象的方法返回了一個(gè)內(nèi)部類(lèi)的實(shí)例對(duì)象,這個(gè)內(nèi)部類(lèi)對(duì)象被長(zhǎng)期引用了,即使那個(gè)外部類(lèi)實(shí)例對(duì)象不再被使用,但由于內(nèi)部類(lèi)持久外部類(lèi)的實(shí)例對(duì)象,這個(gè)外部類(lèi)對(duì)象將不會(huì)被垃圾回收,這也會(huì)造成內(nèi)存泄露。
**public** **class** Stack {**private** Object[] elements=**new** Object[10];
**private** **int** **size** = 0;
**public** **void** **push**(Object e){
ensureCapacity();
elements[**size**++] = e;
}
**public** Object **pop**(){
**if**( **size** == 0) **throw** **new** EmptyStackException();**return** elements[--**size**];
}
**private** **void** ensureCapacity(){**if**(elements.length == **size**){
Object[] oldElements = elements;
elements = **new** Object[2 * elements.length+1];
System.arraycopy(oldElements,0, elements, 0, **size**);
}
} }
上面的原理應(yīng)該很簡(jiǎn)單,假如堆棧加了 10 個(gè)元素,然后全部彈出來(lái),雖然堆棧是空的,沒(méi)有我們要的東西,但是這是個(gè)對(duì)象是無(wú)法回收的,這個(gè)才符合了內(nèi)存泄露的兩個(gè)條件:無(wú)用,無(wú)法回收。但是就是存在這樣的東西也不一定會(huì)導(dǎo)致什么樣的后果,如果這個(gè)
堆棧用的比較少,也就浪費(fèi)了幾個(gè) K 內(nèi)存而已,反正我們的內(nèi)存都上 G 了,哪里會(huì)有什么影響,再說(shuō)這個(gè)東西很快就會(huì)被回收的,
有什么關(guān)系。下面看兩個(gè)例子。
public class Bad{public static Stack s=Stack();static{
s.push(new Object());
s.pop(); //這里有一個(gè)對(duì)象發(fā)生內(nèi)存泄露 s.push(new Object()); //上面的對(duì)象可以被回收了,等于是自 愈了 } }
因?yàn)槭?static,就一直存在到程序退出,但是我們也可以看到它有自愈功能,就是說(shuō)如果你的 Stack 最多有 100 個(gè)對(duì)象,那么最
多也就只有 100 個(gè)對(duì)象無(wú)法被回收其實(shí)這個(gè)應(yīng)該很容易理解,Stack 內(nèi)部持有 100 個(gè)引用,最壞的情況就是他們都是無(wú)用的,
因?yàn)槲覀円坏┓判碌倪M(jìn)去,以前的引用自然消失!內(nèi)存泄露的另外一種情況:當(dāng)一個(gè)對(duì)象被存儲(chǔ)進(jìn) HashSet 集合中以后,就不能修改這對(duì)象中的那些參與計(jì)算哈希值的字段了,否則,對(duì)象修改后的哈希值與最初存儲(chǔ)進(jìn) HashSet 集合中時(shí)的哈希值就不同了,在這種情況下,即使在 contains 方法使用該對(duì)象的當(dāng)前引用作為的參數(shù)去 HashSet 集合中檢索對(duì)象,也將返回找不到對(duì)象的結(jié)果,這也會(huì)導(dǎo)致無(wú)法從HashSet 集合中單獨(dú)刪除當(dāng)前對(duì)象,造成內(nèi)存泄露。
深拷貝和淺拷貝。
簡(jiǎn)單來(lái)講就是復(fù)制、克隆。
Person p=new Person(“張三”);
淺拷貝就是對(duì)對(duì)象中的數(shù)據(jù)成員進(jìn)行簡(jiǎn)單賦值,如果存在動(dòng)態(tài)成員或者指針就會(huì)報(bào)錯(cuò)。深拷貝就是對(duì)對(duì)象中存在的動(dòng)態(tài)成員或指針重新開(kāi)辟內(nèi)存空間。
finalize() 方法什么時(shí)候被調(diào)用?析構(gòu)函數(shù) (finalization) 的目的是什么?
垃圾回收器(garbage colector)決定回收某對(duì)象時(shí),就會(huì)運(yùn)行該對(duì)象的 finalize() 方法 但是在 Java 中很不幸,如果內(nèi)存總是充
足的,那么垃圾回收可能永遠(yuǎn)不會(huì)進(jìn)行,也就是說(shuō) filalize() 可能永遠(yuǎn)不被執(zhí)行,顯然指望它做收尾工作是靠不住的。
那么****finalize() 究竟是做什么的呢?
它最主要的用途是回收特殊渠道申請(qǐng)的內(nèi)存。Java 程序有垃圾回收器,所以一般情況下內(nèi)存問(wèn)題不用程序員操心。但有一種 JNI(Java Native Interface)調(diào)用non-Java 程序(C 或 C++), finalize() 的工作就是回收這部分的內(nèi)存。
如果對(duì)象的引用被置為 null****,垃圾收集器是否會(huì)立即釋放對(duì)象占用的內(nèi)存?
不會(huì),在下一個(gè)垃圾回收周期中,這個(gè)對(duì)象將是可被回收的。
什么是分布式垃圾回收(****DGC****)?它是如何工作的?
DGC 叫做分布式垃圾回收。RMI 使用 DGC 來(lái)做自動(dòng)垃圾回收。因?yàn)?RMI 包含了跨虛擬機(jī)的遠(yuǎn)程對(duì)象的引用,垃圾回收是很困難的。DGC 使用引用計(jì)數(shù)算法來(lái)給遠(yuǎn)程對(duì)象提供自動(dòng)內(nèi)存管理。
簡(jiǎn)述 Java 內(nèi)存分配與回收策率以及 Minor GC 和 Major GC****。
? 對(duì)象優(yōu)先在堆的 Eden 區(qū)分配
? 大對(duì)象直接進(jìn)入老年代
? 長(zhǎng)期存活的對(duì)象將直接進(jìn)入老年代
當(dāng) Eden 區(qū)沒(méi)有足夠的空間進(jìn)行分配時(shí),虛擬機(jī)會(huì)執(zhí)行一次Minor GC。Minor GC 通常發(fā)生在新生代的 Eden 區(qū),在這個(gè)區(qū)的對(duì)象生存期短,往往發(fā)生 Gc 的頻率較高,回收速度比較快;
Full GC/Major GC 發(fā)生在老年代,一般情況下,觸發(fā)老年代 GC 的時(shí)候不會(huì)觸發(fā) Minor GC,但是通過(guò)配置,可以在 Full GC 之
前進(jìn)行一次 Minor GC 這樣可以加快老年代的回收速度。
JVM 的永久代中會(huì)發(fā)生垃圾回收么?
垃圾回收不會(huì)發(fā)生在永久代,如果永久代滿(mǎn)了或者是超過(guò)了臨界值,
會(huì)觸發(fā)完全垃圾回收(Full GC)。
注:Java 8 中已經(jīng)移除了永久代,新加了一個(gè)叫做元數(shù)據(jù)區(qū)的
native 內(nèi)存區(qū)。
Java 中垃圾收集的方法有哪些?
標(biāo)記 - 清除:這是垃圾收集算法中最基礎(chǔ)的,根據(jù)名字就可以知
道,它的思想就是標(biāo)記哪些要被回收的對(duì)象,然后統(tǒng)一回收。這種
方法很簡(jiǎn)單,但是會(huì)有兩個(gè)主要問(wèn)題:
效率不高,標(biāo)記和清除的效率都很低;
會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,導(dǎo)致以后程序在分配較大的 對(duì)象時(shí),由于沒(méi)有充足的連續(xù)內(nèi)存而提前觸發(fā)一次 GC 動(dòng)作。
復(fù)制算法:
為了解決效率問(wèn)題,復(fù)制算法將可用內(nèi)存按容量劃分為相等的兩部分,然后每次只使用其中的一塊,當(dāng)一塊內(nèi)存用完時(shí),就將還存活的對(duì)象復(fù)制到第二塊內(nèi)存上,然后一次性清楚完第一塊內(nèi)存,再將第二塊上的對(duì)象復(fù)制到第一塊。但是這種方式,內(nèi)存的代價(jià)太高,每次基本上都要浪費(fèi)一般的內(nèi)存。于是將該算法進(jìn)行了改進(jìn),內(nèi)存區(qū)域不再是按照 1:1 去劃分,而是將內(nèi)存劃分為 8:1:1 三部分,較大那份內(nèi)存交 Eden 區(qū),其余是兩塊較小的內(nèi)存區(qū)叫 Survior 區(qū)。每次都會(huì)優(yōu)先使用 Eden 區(qū),若 Eden 區(qū)滿(mǎn),就將對(duì)象復(fù)制到第二塊內(nèi)存區(qū)上,然后清除 Eden區(qū),如果此時(shí)存活的對(duì)象太多,以至于 Survivor 不夠時(shí),會(huì)將這些對(duì)象通過(guò)分配擔(dān)保機(jī)制復(fù)制到老年代中。(java 堆又分為新生代和老年代)
標(biāo)記 - 整理:
該算法主要是為了解決標(biāo)記 - 清除,產(chǎn)生大量?jī)?nèi)存碎片的問(wèn)題;當(dāng)對(duì)象存活率較高時(shí),也解決了復(fù)制算法的效率問(wèn)題。它的不同之處就是在清除對(duì)象的時(shí)候現(xiàn)將可回收對(duì)象移動(dòng)到一端,然后清除掉端邊界以外的對(duì)象,這樣就不會(huì)產(chǎn)生內(nèi)存碎片了。
分代收集:
現(xiàn)在的虛擬機(jī)垃圾收集大多采用這種方式,它根據(jù)對(duì)象的生存周期,將堆分為新生代和老年代。在新生代中,由于對(duì)象生存期短,每次回收都會(huì)有大量對(duì)象死去,那么這時(shí)就采用復(fù)制算法。老年代里的對(duì)象存活率較高,沒(méi)有額外的空間進(jìn)行分配擔(dān)保。
什么是類(lèi)加載器,類(lèi)加載器有哪些?
實(shí)現(xiàn)通過(guò)類(lèi)的權(quán)限定名獲取該類(lèi)的二進(jìn)制字節(jié)流的代碼塊叫做類(lèi)加載器。
主要有以下四種類(lèi)加載器:
? 啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader)用來(lái)加載 Java 核心類(lèi)庫(kù),無(wú)法被 Java 程序直接引用。
? 擴(kuò)展類(lèi)加載器(extensions class loader):它用來(lái)加載 Java 的擴(kuò)展庫(kù)。Java 虛擬機(jī)的實(shí)現(xiàn)會(huì)提供一個(gè)擴(kuò)展庫(kù)目錄。該類(lèi)加載器在此目錄里面查找并加載 Java 類(lèi)。
? 系統(tǒng)類(lèi)加載器(system class loader):它根據(jù) Java 應(yīng)用的類(lèi)路徑(CLASSPATH)來(lái)加載 Java 類(lèi)。一般來(lái)說(shuō),Java 應(yīng)用的類(lèi)都是由它來(lái)完成加載的??梢酝ㄟ^(guò)ClassLoader.getSystemClassLoader() 來(lái)獲取它。
? 用戶(hù)自定義類(lèi)加載器,通過(guò)繼承 java.lang.ClassLoader 類(lèi)的方式實(shí)現(xiàn)。
類(lèi)加載器雙親委派模型機(jī)制?
當(dāng)一個(gè)類(lèi)收到了類(lèi)加載請(qǐng)求時(shí),不會(huì)自己先去加載這個(gè)類(lèi),而是將其委派給父類(lèi),由父類(lèi)去加載,如果此時(shí)父類(lèi)不能加載,反饋給子類(lèi),由子類(lèi)去完成類(lèi)的加載。
1. 內(nèi)存模型以及分區(qū),需要詳細(xì)到每個(gè)區(qū)放什么。
JVM 分為堆區(qū)和棧區(qū),還有方法區(qū),初始化的對(duì)象放在堆里面,引用放在棧里面,class 類(lèi)信息常量池(static 常量和 static 變量)等放在方法區(qū)
· 方法區(qū):主要是存儲(chǔ)類(lèi)信息,常量池(static 常量和 static 變量),編譯后的代碼(字節(jié)碼)等數(shù)據(jù)
· 堆:初始化的對(duì)象,成員變量 (那種非 static 的變量),所有的對(duì)象實(shí)例和數(shù)組都要在堆上分配
· 棧:棧的結(jié)構(gòu)是棧幀組成的,調(diào)用一個(gè)方法就壓入一幀,幀上面存儲(chǔ)局部變量表,操作數(shù)棧,方法出
· 本地方法棧:主要為 Native 方法服務(wù)
· 程序計(jì)數(shù)器:記錄當(dāng)前線(xiàn)程執(zhí)行的行號(hào)
2. 堆里面的分區(qū):Eden,survival (from+ to),老年代,各自的特點(diǎn)。
堆里面分為新生代和老生代(java8 取消了永久代,采用了 Metaspace),新生代包含 Eden+Survivor 區(qū),survivor 區(qū)里面分為 from 和 to 區(qū),內(nèi)存回收時(shí),如果用的是復(fù)制算法,從 from 復(fù)制到 to,當(dāng)經(jīng)過(guò)一次或者多次 GC 之后,存活下來(lái)的對(duì)象會(huì)被移動(dòng)到老年區(qū),當(dāng) JVM 內(nèi)存不夠用的時(shí)候,會(huì)觸發(fā) Full GC,清理 JVM 老年區(qū)當(dāng)新生區(qū)滿(mǎn)了之后會(huì)觸發(fā) YGC,先把存活的對(duì)象放到其中一個(gè) Survice區(qū),然后進(jìn)行垃圾清理。因?yàn)槿绻麅H僅清理需要?jiǎng)h除的對(duì)象,這樣會(huì)導(dǎo)致內(nèi)存碎片,因此一般會(huì)把 Eden 進(jìn)行完全的清理,然后整理內(nèi)存。那么下次 GC 的時(shí)候,就會(huì)使用下一個(gè) Survive,這樣循環(huán)使用。如果有特別大的對(duì)象,新生代放不下,就會(huì)使用老年代的擔(dān)保,直接放到老年代里面。因?yàn)?JVM 認(rèn)為,一般大對(duì)象的存活時(shí)間一般比較久遠(yuǎn)。
3. 對(duì)象創(chuàng)建方法,對(duì)象的內(nèi)存分配,對(duì)象的訪(fǎng)問(wèn)定位。new 一個(gè)對(duì)象
4. GC 的兩種判定方法:
引用計(jì)數(shù)法:指的是如果某個(gè)地方引用了這個(gè)對(duì)象就+1,如果失效了就-1,當(dāng)為 0 就會(huì)回收但是 JVM 沒(méi)有用這種方式,因?yàn)闊o(wú)法判定相互循環(huán)引用(A 引用 B,B 引用 A)的情況
引用鏈法:通過(guò)一種 GC ROOT 的對(duì)象(方法區(qū)中靜態(tài)變量引用的對(duì)象等-static 變量)來(lái)判斷,如果有一條鏈能夠到達(dá) GC ROOT 就說(shuō)明,不能到達(dá) GC ROOT 就說(shuō)明可以回收
SafePoint 是什么
比如 GC 的時(shí)候必須要等到 Java 線(xiàn)程都進(jìn)入到 safepoint 的時(shí)候 VMThread 才能開(kāi)始執(zhí)行 GC,
循環(huán)的末尾 (防止大循環(huán)的時(shí)候一直不進(jìn)入 safepoint,而其他線(xiàn)程在等待它進(jìn)入safepoint)
方法返回前
調(diào)用方法的 call 之后
拋出異常的位置
6. GC 的三種收集方法:標(biāo)記清除、標(biāo)記整理、復(fù)制算法的原理與特點(diǎn),分別用在什么地方,如果讓你優(yōu)化收集方法,有什么思路?
先標(biāo)記,標(biāo)記完畢之后再清除,效率不高,會(huì)產(chǎn)生碎片復(fù)制算法:分為 8:1 的 Eden 區(qū)和 survivor 區(qū),就是上面談到的 YGC標(biāo)記整理:標(biāo)記完畢之后,讓所有存活的對(duì)象向一端移動(dòng)
GC 收集器有哪些?CMS 收集器與 G1 收集器的特點(diǎn)。
并行收集器:串行收集器使用一個(gè)單獨(dú)的線(xiàn)程進(jìn)行收集,GC 時(shí)服務(wù)有停頓時(shí)間
串行收集器:次要回收中使用多線(xiàn)程來(lái)執(zhí)行CMS 收集器是基于“標(biāo)記—清除”算法實(shí)現(xiàn)的,經(jīng)過(guò)多次標(biāo)記才會(huì)被清除
G1 從整體來(lái)看是基于“標(biāo)記—整理”算法實(shí)現(xiàn)的收集器,從局部(兩個(gè) Region 之間)上來(lái)看是基于“復(fù)制”算法實(shí)現(xiàn)的
Minor GC 與 Full GC 分別在什么時(shí)候發(fā)生
新生代內(nèi)存不夠用時(shí)候發(fā)生 MGC 也叫 YGC,JVM 內(nèi)存不夠的時(shí)候發(fā)生 FGC
幾種常用的內(nèi)存調(diào)試工具:jmap、jstack、jconsole、jhat
jstack 可以看當(dāng)前棧的情況,jmap 查看內(nèi)存,jhat 進(jìn)行 dump 堆的信息
簡(jiǎn)述 java 內(nèi)存分配與回收策率以及 Minor GC 和
Major GC
對(duì)象優(yōu)先在堆的 Eden 區(qū)分配。
大對(duì)象直接進(jìn)入老年代.
長(zhǎng)期存活的對(duì)象將直接進(jìn)入老年代. 當(dāng) Eden 區(qū)沒(méi)有足夠的空間進(jìn)行分配時(shí),虛擬機(jī)會(huì)執(zhí)行一次 Minor GC.Minor Gc 通 常發(fā)生在新生代的 Eden 區(qū),在這個(gè)區(qū)的對(duì)象生存期短,往往發(fā)生 Gc 的頻率較高,回收速度比較快;Full Gc/Major GC 發(fā)生在老年代,一般情況下,觸發(fā)老年代 GC的時(shí)候不會(huì)觸發(fā) Minor GC,但是通過(guò)配置,可以在 Full GC 之前進(jìn)行一次 Minor GC 這樣可以加快老年代的回收速度。
Java 類(lèi)加載過(guò)程?
Java 類(lèi)加載需要經(jīng)歷一下 7 個(gè)過(guò)程:
1. 加載
加載是類(lèi)加載的第一個(gè)過(guò)程,在這個(gè)階段,將完成一下三件事情:
? 通過(guò)一個(gè)類(lèi)的全限定名獲取該類(lèi)的二進(jìn)制流。
? 將該二進(jìn)制流中的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法去運(yùn)行時(shí)數(shù)據(jù)結(jié)
構(gòu)。
? 在內(nèi)存中生成該類(lèi)的 Class 對(duì)象,作為該類(lèi)的數(shù)據(jù)訪(fǎng)問(wèn)入口。
2. 驗(yàn)證
驗(yàn)證的目的是為了確保 Class 文件的字節(jié)流中的信息不會(huì)危害到虛擬機(jī).在該階段主要完成以下四種驗(yàn)證: ? 文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合 Class 文件的規(guī)范,如主次版本號(hào)是否在當(dāng)前虛擬機(jī)范圍內(nèi),常量池中的常量是否有不被支持的類(lèi)型. ? 元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,如這個(gè)類(lèi)是否有父類(lèi),是否集成了不被繼承的類(lèi)等。
? 字節(jié)碼驗(yàn)證:是整個(gè)驗(yàn)證過(guò)程中最復(fù)雜的一個(gè)階段,通過(guò)驗(yàn)證數(shù)據(jù)流和控制流的分析,確定程序語(yǔ)義是否正確,主要針對(duì)方法體的驗(yàn)證。如:方法中的類(lèi)型轉(zhuǎn)換是否正確,跳轉(zhuǎn)指令是否正確等。
? 符號(hào)引用驗(yàn)證:這個(gè)動(dòng)作在后面的解析過(guò)程中發(fā)生,主要是為了確保解析動(dòng)作能正確執(zhí)行。
3. 準(zhǔn)備
準(zhǔn)備階段是為類(lèi)的靜態(tài)變量分配內(nèi)存并將其初始化為默認(rèn)值,這些內(nèi)存都將在方法區(qū)中進(jìn)行分配。準(zhǔn)備階段不分配類(lèi)中的實(shí)例變量的內(nèi)存,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在 Java 堆中。
public static int value=123;//在準(zhǔn)備階段 value 初始值為 0 。在初始化階段才會(huì)變?yōu)?123 。
4. 解析
該階段主要完成符號(hào)引用到直接引用的轉(zhuǎn)換動(dòng)作。解析動(dòng)作并不一定在初始化動(dòng)作完成之前,也有可能在初始化之后。
5. 初始化
初始化時(shí)類(lèi)加載的最后一步,前面的類(lèi)加載過(guò)程,除了在加載階段用戶(hù)應(yīng)用程序可以通過(guò)自定義類(lèi)加載器參與之外,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制。到了初始化階段,才真正開(kāi)始執(zhí)行類(lèi)中定義的Java 程序代碼。
6. 使用
7. 卸載
描述一下 JVM 加載 Class 文件的原理機(jī)制****?
Java 語(yǔ)言是一種具有動(dòng)態(tài)性的解釋型語(yǔ)言,類(lèi)(Class)只有被加載到 JVM 后才能運(yùn)行。當(dāng)運(yùn)行指定程序時(shí),JVM 會(huì)將編譯生成
的 .class 文件按照需求和一定的規(guī)則加載到內(nèi)存中,并組織成為一個(gè)完整的 Java 應(yīng)用程序。這個(gè)加載過(guò)程是由類(lèi)加載器完成,具
體來(lái)說(shuō),就是由 ClassLoader 和它的子類(lèi)來(lái)實(shí)現(xiàn)的。類(lèi)加載器本身也是一個(gè)類(lèi),其實(shí)質(zhì)是把類(lèi)文件從硬盤(pán)讀取到內(nèi)存中。
類(lèi)的加載方式分為隱式加載和顯示加載。
隱式加載指的是程序在使用 new 等方式創(chuàng)建對(duì)象時(shí),會(huì)隱式地調(diào)用類(lèi)的加載器把對(duì)應(yīng)的類(lèi)加載到 JVM 中。顯示加載指的是通過(guò)直接調(diào)用 class.forName() 方法來(lái)把所需的類(lèi)加載到 JVM 中。任何一個(gè)工程項(xiàng)目都是由許多類(lèi)組成的,當(dāng)程序啟動(dòng)時(shí),只把需要的類(lèi)加載到 JVM 中,其他類(lèi)只有被使用到的時(shí)候才會(huì)被加載,采用這種方法一方面可以加快加載速度,另一方面可以節(jié)約程序運(yùn)行時(shí)對(duì)內(nèi)存的開(kāi)銷(xiāo)。此外,在 Java 語(yǔ)言中,每個(gè)類(lèi)或接口都對(duì)應(yīng)一個(gè) .class 文件,這些文件可以被看成是一個(gè)個(gè)可以被動(dòng)態(tài)加載的單元,因此當(dāng)只有部分類(lèi)被修改時(shí),只需要重新編譯變化的類(lèi)即可,而不需要重新編譯所有文件,因此加快了編譯速度。在 Java 語(yǔ)言中,類(lèi)的加載是動(dòng)態(tài)的,它并不會(huì)一次性將所有類(lèi)全部加載后再運(yùn)行,而是保證程序運(yùn)行的基礎(chǔ)類(lèi)(例如基類(lèi))完全加載到 JVM 中,至于其他類(lèi),則在需要的時(shí)候才加載。
類(lèi)加載的主要步驟:
? 裝載。根據(jù)查找路徑找到相應(yīng)的 class 文件,然后導(dǎo)入。
? 鏈接。鏈接又可分為 3 個(gè)小步:
? 檢查,檢查待加載的 class 文件的正確性。
? 準(zhǔn)備,給類(lèi)中的靜態(tài)變量分配存儲(chǔ)空間。
? 解析,將符號(hào)引用轉(zhuǎn)換為直接引用(這一步可選)
? 初始化。對(duì)靜態(tài)變量和靜態(tài)代碼塊執(zhí)行初始化工作。