前言
題目彙總來源 史上最全各類面試題彙總,沒有之一,不接受反駁
- 面試題彙總一 Java 語言基礎篇
- 面試題彙總二 Java 多線程篇
- 面試題彙總三 Java 集合篇
- 面試題彙總四 JVM 篇
- 面試題彙總五 Spring 篇
- 面試題彙總六 數據庫篇
- 面試題彙總七 計算機網絡篇
目錄
你知道哪些或者你們線上使⽤什麼GC策略?它有什麼優勢,適⽤於什麼場景?
Java類加載器包括⼏種?它們之間的⽗⼦關係是怎麼樣的?雙親委派機制是什麼意思?有什麼好處?
如何⾃定義⼀個類加載器?你使⽤過哪些或者你在什麼場景下需要⼀個⾃定義的類加載器嗎?
Perm Space中保存什麼數據?會引起OutOfMemory嗎?
做GC時,⼀個對象在內存各個Space中被移動的順序是什麼?
你有沒有遇到過OutOfMemory問題?你是怎麼來處理這個問題的?處理 過程中有哪些收穫?
StackOverflow異常有沒有遇到過?⼀般你猜測會在什麼情況下被觸發?如何指定⼀個線程的堆棧⼤⼩?⼀般你們寫多少?
請解釋StackOverflowError和OutOfMemeryError的區別?
JVM
談談你對解析與分派的認識。
分派:靜態分派與動態分派。
解析和分派屬於方法調用的內容。
方法調用階段唯一的任務就是確定被調用方法的版本(即調用哪一個方法),暫時還不涉及方法內部具體的運行過程。
解析
將一部分(編譯期可知,運行期不可變)的符號引用轉爲直接引用
調用方法的四種字節碼:
invokestatic 調用靜態方法
invokespacial 調用<init>、私有方法和父類方法
invokevirtual 調用所有虛方法
invokeinterface 調用接口方法,運行時確定實現此接口的方法
能被 invokestatic 和 invokespecial 調用的方法可以在解析時確定唯一版本調用。
Java中符合的有靜態方法、私有方法、實例構造器和父類方法,它們不可繼承或重寫,稱爲非虛方法。
另外,final方法也是非虛方法,雖然是 invokevirtual 指令調用,但無法被覆蓋,對多態來說選擇結果唯一。
分派
分派與面向對象的三個基本特徵之一:多態有關,如重載和重寫。
解析和分派不是對立關係,如重載靜態方法,既發生瞭解析,又發生了靜態分派。
靜態分派
依據靜態類型來定位方法執行版本的分派動作稱爲靜態分派。
和方法重載有關,重載方法的參數類型是依據靜態類型,而靜態類型編譯期已知。
動態分派
運行期根據實際類型確定方法執行版本的分派過程稱爲動態分派。
單分派和多分派
Java靜態多分派,動態單分派
靜態分派,根據靜態類型和參數類型兩個宗量進行選擇,多分派
動態分派,傳入參數已確定,只要判斷最終執行對象類型,單分派。
你知道哪些或者你們線上使⽤什麼GC策略?它有什麼優勢,適⽤於什麼場景?
Minor GC:指發生在新生代的垃圾收集動作
MajorGC / Full GC:指發生在老年代的GC
對象優先分配在Eden區
eden區空間不足時Minor GC
大對象直接進入老年代
避免短命大對象
長期存活對象進入老年代
每個對象有一個對象年齡(Age)計數器
Eden區一個對象第一次Minor GC存活並能被Survivor區容納,則移至Survivor且對象年齡設爲1
在Survivor中每經過一次Minor GC對象年齡+1
到達一定年齡(默認15)移至老年代
動態對象年齡判斷
Survivor中相同年齡所有對象大小的總和大於Survivor空間的一半,大於等於該年齡的對象直接進入老年代
空間分配擔保
Minor GC時,若之前每次晉升入老年代的平均大小>老年代剩餘空間,則直接Full GC
否則若設置允許擔保失敗,則只Minor GC,否則Full GC
Java類加載器包括⼏種?它們之間的⽗⼦關係是怎麼樣的?雙親委派機制是什麼意思?有什麼好處?
Java類加載器包括幾種?父子關係?
這裏的層次關係爲組合,即子加載器持有父加載器的引用,而非繼承關係。
Application ClassLoader 應該和 System ClassLoader 是一個意思
雙親委派機制
雙親委派模型如上圖所示。
工作過程:一個類加載器收到類加載請求,先把這個請求委託給父類加載器完成,因此所有加載請求都會傳送到頂層啓動加載器中,只有當父加載器反饋自己無法完成加載請求(其搜索範圍沒有找到所需類),子類才嘗試自己加載。
雙親委派機制好處
保證java核心庫的安全性(例如:如果用戶自己寫了一個java.lang.String類就會因爲雙親委派機制不能被加載,不會破壞原生的String類的加載)
如何⾃定義⼀個類加載器?你使⽤過哪些或者你在什麼場景下需要⼀個⾃定義的類加載器嗎?
- 加載特定路徑的class文件
- 加載一個加密的網絡class文件
- 熱部署加載class文件
雙親委派流程
loadClass
找緩存 → 找父加載器 loadClass → 自己嘗試 findClass → 根據條件解析 class
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
findClass
JDK1.2後不建議覆蓋 loadClass,將自定義加載類的過程放在 findClass 裏。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
雙親委派機制打破
服務提供者接口SPI存在於rt.jar,由Bootstrap類加載器加載,但服務方提供的SPI實現類無法由Bootstrap類加載器加載,需由App類加載器加載。Thread類中的contextClassLoader默認保存App類加載器的引用,Bootstrap類加載器以此委託App類加載器加載SPI實現類。但這就打破了雙親委派模型。
堆內存設置的參數是什麼?
類加載過程
加載:通過類的全限定名獲取二進制流,加載入方法區,生成一個代表這個類的java.lang.Class對象。
驗證:確保class字節流符合規定且安全。
準備:類變量賦初值(準確說是置零)。
解析:符號引用轉換爲直接引用。
初始化:調用類的<clinit>()方法,初始化類的資源和變量。
類加載時機
- new新對象 / 讀取或設置static屬性 / 調用static方法;
- java.lang.reflect對類反射;
- 子類初始化時先初始化父類;
- 虛擬機啓動時先初始化main方法所在的主類;
- jdk1.7動態語言支持
Perm Space中保存什麼數據?會引起OutOfMemory嗎?
Java8內存模型—永久代(PermGen)和元空間(Metaspace)
JAVA 方法區與堆--java7前,java7,java8各不相同
方法區存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等
java7之前,方法區位於永久代(PermGen),永久代和堆相互隔離,永久代的大小在啓動JVM時可以設置一個固定值,不可變;
java7中,存儲在永久代的部分數據就已經轉移到Java Heap或者Native memory。但永久代仍存在於JDK 1.7中,並沒有完全移除,譬如符號引用(Symbols)轉移到了native memory;字符串常量池(interned strings)轉移到了Java heap;類的靜態變量(class statics)轉移到了Java heap。
java8中,取消永久代,方法存放於元空間(Metaspace),元空間仍然與堆不相連,但與堆共享物理內存,邏輯上可認爲在堆中
由於方法區主要存儲類的相關信息,所以對於動態生成類的情況比較容易出現永久代的內存溢出。最典型的場景就是,在 jsp 頁面比較多的情況,容易出現永久代內存溢出。
垃圾回收算法
標記清除算法 Mark-Sweep
第一遍掃描內存標記出無用對象,第二遍掃描內存清除無用對象。
效率低且會造成內存碎片化。
複製算法 Copying
將存活對象複製到新內存空間中,清空舊內存空間。
適用於對象存活率低的情況。
JVM中,新生代的Survivor區用到該算法。
標記整理算法 Mark-Compact
在標記清除算法基礎上,把對象移向內存一端,留出連續的內存空間,解決碎片化問題。
適用於對象較大、存活率較高的情況。
JVM中,老年代使用該算法。
分代收集算法 Generational Collection
JVM中,新生代使用Copying,老年代使用Mark-Compact。
做GC時,⼀個對象在內存各個Space中被移動的順序是什麼?
參照上面GC策略的過程。
普通對象生於Eden區,然後在兩個Survivor區中往返,最後進入老年代。中途死於各種GC。
大對象或通過動態對象年齡判定的對象直接進入老年代。死於Major GC。
吞吐量和暫停時間
吞吐量:應用程序線程用時佔程序總用時的比例。GC線程會導致應用程序線程暫停,吞吐量越高,程序執行越快。
暫停時間:GC線程導致應用程序線程暫停的時長。過長的暫停時間會影響用戶體驗。
垃圾收集器
你有沒有遇到過OutOfMemory問題?你是怎麼來處理這個問題的?處理 過程中有哪些收穫?
常見的原因如下:
1)內存加載的數據量太大:一次性從數據庫取太多數據;
2)集合類中有對對象的引用,使用後未清空,GC不能進行回收;
3)代碼中存在循環產生過多的重複對象;
4)啓動參數堆內存值小。
StackOverflow異常有沒有遇到過?⼀般你猜測會在什麼情況下被觸發?如何指定⼀個線程的堆棧⼤⼩?⼀般你們寫多少?
觸發情況:
棧內存溢出,一般由棧內存的局部變量過爆了,導致內存溢出。出現在遞歸方法,參數個數過多,遞歸過深,遞歸沒有出口。
棧大小設置:
JVM優化系列之一(-Xss調整Stack Space的大小)
-Xss:如-Xss128k
JDK5.0以後每個線程堆 棧大小爲1M,以前每個線程堆棧大小爲256K。根據應用的線程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。
線程棧的大小是個雙刃劍,如果設置過小,可能會出現棧溢出,特別是在該線程內有遞歸、大的循環時出現溢出的可能性更大,如果該值設置過大,就有影響到創建棧的數量,如果是多線程的應用,就會出現內存溢出的錯誤。
內存模型以及分區,需要詳細到每個區放什麼。
程序計數器
較小的內存空間,當前線程所執行的字節碼的行號指示器。
線程私有。
Java虛擬機棧
線程私有。
描述java方法執行的內存模型。
每個方法執行時創建一個棧幀用於存儲局部變量表,操作棧,動態鏈接,方法出口等。
局部變量表:基本數據類型+reference,long和double兩個slot,其餘1個。
局部變量表的大小在編譯期確定。
異常情況:棧幀過大 StackOverflowError;虛擬機棧擴展無法申請足夠內存 OutOfMemoryError。
本地方法棧
與虛擬機棧類似,不過是執行Native方法服務。
java堆
線程共享。
對象實例和數組在此分配。
垃圾收集器管理的主要區域。
可以劃分線程私有的分配緩衝區(TLAB)。
方法區
線程共享。
存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等。
運行時常量池
方法區的一部分。
存放編譯期生成的各種字面量和符號引用。
直接內存
不屬於Java虛擬機內存。
某些io操作直接操控native堆。
虛擬機在運行時有哪些優化策略
請解釋StackOverflowError和OutOfMemeryError的區別?
根據《深入理解Java虛擬機》裏的說法,StackOverflowError出現在虛擬機棧和本地方法棧中,對虛擬機棧來說,StackOverflowError在線程請求的棧深度大於虛擬機所允許的深度時拋出;OutOfMemoryError出現在除程序計數器之外的其他區域,基本都是因爲無法滿足內存分配且無法擴展而產生的。
在JVM中,如何判斷一個對象是否死亡?
首先使用根搜索算法判定對象是否存活。
若根搜索算法不可達,還要經歷兩次標記:
- 判定對象是否有必要執行 finalize() 方法。若對象沒有覆蓋 finalize() 或已被虛擬機執行過則會被判定“不必要執行”。
- 若被判定要執行 finalize() 方法,對象會被移進 F-Queue等待方法執行,若在方法中對象被引用鏈上的任意對象引用,在第二次標記中會被移出“即將回收”集合。
對象被回收後,判定真正死亡。