吐血推薦,想進BAT必看
不必太糾結於當下,也不必太憂慮未來,人生沒有無用的經歷,當你經歷過一些事情後,眼前的風景已經和從前不一樣了。
–村上春樹
一、包含如下內容
- ActiveMQ消息中間件面試專題
- BAT80道面試題
- BAT面試的Mysql面試55題
- Dubbo面試專題
- JVM面試專題
- Kafka面試專題
- Linux面試專題
- memcached面試專題
- MongoDB面試專題
- MyBatis面試專題
- mysql面試專題
- Mysql性能優化面試專題
- Netty面試專題
- Nginx面試專題
- RabbitMQ消息中間件面試專題
- Redis面試專題
- Spring Cloud面試專題
- SpringBoot面試專題
- SpringMVC面試專題
- Spring面試專題
- SQL優化面試專題
- Tomcat面試專題
- zookeeper面試專題
- 並發編程面試專題
- 多執行緒面試專題
- 開源框架面試專題
- 設計模式面試專題
- 資料庫面試專題
- 微服務面試專題
- 消息中間件面試專題
- 性能優化面試專題
- 持續更新……
二、JVM部分
1. Java 類載入過程?
Java 類載入需要經歷一下 7 個過程:
1. 載入
載入是類載入的第一個過程,在這個階段,將完成一下三件事情:
- 通過一個類的全限定名獲取該類的二進位流。
- 將該二進位流中的靜態存儲結構轉化為方法去運行時數據結構。
- 在記憶體中生成該類的 Class 對象,作為該類的數據訪問入口。
2. 驗證
驗證的目的是為了確保 Class 文件的位元組流中的資訊不回危害到虛擬機.在該階段主要完成以下四鍾驗證:
- 文件格式驗證:驗證位元組流是否符合 Class 文件的規範,如主次版本號是否在當前虛擬機範圍內,常量池中的常量是否有不被支援的類型.
- 元數據驗證:對位元組碼描述的資訊進行語義分析,如這個類是否有父類,是否集成了不被繼承的類等。
- 位元組碼驗證:是整個驗證過程中最複雜的一個階段,通過驗證數據流和控制流的分析,確定程式語義是否正確,主要針對方法體的驗證。如:方法中的類型轉換是否正確,跳轉指令是否正確等。
- 符號引用驗證:這個動作在後面的解析過程中發生,主要是為了確保解析動作能正確執行。
3. 準備
準備階段是為類的靜態變數分配記憶體並將其初始化為默認值,這些記憶體都將在方法區中進行分配。準備階段不分配類中的實例變數的記憶體,實例變數將會在對象實例化時隨著對象一起分配在 Java 堆中。
public static int value=123;//在準備階段value初始值為0 。在初始化階段才會變為123 。
4. 解析
該階段主要完成符號引用到直接引用的轉換動作。解析動作並不一定在初始化動作完成之前,也有可能在初始化之後。
5. 初始化
初始化時類載入的最後一步,前面的類載入過程,除了在載入階段用戶應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的 Java 程式程式碼。
6. 使用
7. 卸載
2.描述一下 JVM 載入 Class 文件的原理機制?
Java 語言是一種具有動態性的解釋型語言,類(Class)只有被載入到 JVM 後才能運行。當運行指定程式時,JVM 會將編譯生成的 .class 文件按照需求和一定的規則載入到記憶體中,並組織成為一個完整的 Java 應用程式。這個載入過程是由類載入器完成,具體來說,就是由 ClassLoader 和它的子類來實現的。類載入器本身也是一個類,其實質是把類文件從硬碟讀取到記憶體中。
類的載入方式分為隱式載入和顯示載入。隱式載入指的是程式在使用 new 等方式創建對象時,會隱式地調用類的載入器把對應的類載入到 JVM 中。顯示載入指的是通過直接調用 class.forName() 方法來把所需的類載入到 JVM 中。
任何一個工程項目都是由許多類組成的,當程式啟動時,只把需要的類載入到 JVM 中,其他類只有被使用到的時候才會被載入,採用這種方法一方面可以加快載入速度,另一方面可以節約程式運行時對記憶體的開銷。此外,在 Java 語言中,每個類或介面都對應一個 .class 文件,這些文件可以被看成是一個個可以被動態載入的單元,因此當只有部分類被修改時,只需要重新編譯變化的類即可,而不需要重新編譯所有文件,因此加快了編譯速度。
在 Java 語言中,類的載入是動態的,它並不會一次性將所有類全部載入後再運行,而是保證程式運行的基礎類(例如基類)完全載入到 JVM 中,至於其他類,則在需要的時候才載入。
類載入的主要步驟:
- 裝載。根據查找路徑找到相應的 class 文件,然後導入。
- 鏈接。鏈接又可分為 3 個小步:
- 檢查,檢查待載入的 class 文件的正確性。
- 準備,給類中的靜態變數分配存儲空間。
- 解析,將符號引用轉換為直接引用(這一步可選)
- 初始化。對靜態變數和靜態程式碼塊執行初始化工作。
Java 記憶體分配
• 暫存器:我們無法控制。
• 靜態域:static定義的靜態成員。
• 常量池:編譯時被確定並保存在 .class 文件中的(final)常量值和一些文本修飾的符號引用(類和介面的全限定名,欄位的名稱和描述符,方法和名稱和描述符)。
• 非 RAM 存儲:硬碟等永久存儲空間。
• 堆記憶體:new 創建的對象和數組,由 Java 虛擬機自動垃圾回收器管理,存取速度慢。
• 棧記憶體:基本類型的變數和對象的引用變數(堆記憶體空間的訪問地址),速度快,可以共享,但是大小與生存期必須確定,缺乏靈活性。
Java 堆的結構是什麼樣子的?什麼是堆中的永久代(Perm Gen space)?
JVM 的堆是運行時數據區,所有類的實例和數組都是在堆上分配記憶體。它在 JVM 啟動的時候被創建。對象所佔的堆記憶體是由自動記憶體管理系統也就是垃圾收集器回收。
堆記憶體是由存活和死亡的對象組成的。存活的對象是應用可以訪問的,不會被垃圾回收。死亡的對象是應用不可訪問尚且還沒有被垃圾收集器回收掉的對象。一直到垃圾收集器把這些 對象回收掉之前,他們會一直佔據堆記憶體空間。
4.GC 是什麼? 為什麼要有 GC?
GC 是垃圾收集的意思(GabageCollection),記憶體處理是編程人員容易出現問題的地方,忘記或者錯誤的記憶體回收會導致程式或系統的不穩定甚至崩潰,Java 提供的 GC 功能可以自動監測對象是否超過作用域從而達到自動回收記憶體的目的,Java 語言沒有提供釋放已分配記憶體的顯示操作方法。
5. 簡述 Java 垃圾回收機制。
在 Java 中,程式設計師是不需要顯示的去釋放一個對象的記憶體的,而是由虛擬機自行執行。在 JVM 中,有一個垃圾回收執行緒,它是低優先順序的,在正常情況下是不會執行的,只有在虛擬機空閑或者當前堆記憶體不足時,才會觸發執行,掃面那些沒有被任何引用的對象,並將它們添加到要回收的集合中,進行回收。
6. 如何判斷一個對象是否存活?(或者 GC 對象的判定方法)
判斷一個對象是否存活有兩種方法:
- 引用計數法
所謂引用計數法就是給每一個對象設置一個引用計數器,每當有一個地方引用這個對象時,就將計數器加一,引用失效時,計數器就減一。當一個對象的引用計數器為零時,說明此對象沒有被引用,也就是「死對象」,將會被垃圾回收.
引用計數法有一個缺陷就是無法解決循環引用問題,也就是說當對象 A 引用對象 B,對象 B 又引用者對象 A,那麼此時 A、B 對象的引用計數器都不為零,也就造成無法完成垃圾回收,所以主流的虛擬機都沒有採用這種演算法。
- 可達性演算法(引用鏈法)該演算法的思想是:從一個被稱為 GC Roots 的對象開始向下搜索,如果一個對象到 GC Roots 沒有任何引用鏈相連時,則說明此對象不可用。
在 Java 中可以作為 GC Roots 的對象有以下幾種:
• 虛擬機棧中引用的對象
• 方法區類靜態屬性引用的對象
• 方法區常量池引用的對象
• 本地方法棧JNI引用的對象
雖然這些演算法可以判定一個對象是否能被回收,但是當滿足上述條件時,一個對象比不一定會被回收。當一個對象不可達 GC Root 時,這個對象並不會立馬被回收,而是出於一個死緩的階段,若要被真正的回收需要經歷兩次標記.
如果對象在可達性分析中沒有與 GC Root 的引用鏈,那麼此時就會被第一次標記並且進行一次篩選,篩選的條件是是否有必要執行 finalize() 方法。當對象沒有覆蓋 finalize() 方法或者已被虛擬機調用過,那麼就認為是沒必要的。 如果該對象有必要執行 finalize() 方法,那麼這個對象將會放在一個稱為 F-Queue 的對隊列中,虛擬機會觸發一個 Finalize() 執行緒去執行,此執行緒是低優先順序的,並且虛擬機不會承諾一直等待它運行完,這是因為如果 finalize() 執行緩慢或者發生了死鎖,那麼就會造成 F-Queue 隊列一直等待,造成了記憶體回收系統的崩潰。GC 對處於 F-Queue 中的對象進行第二次被標記,這時,該對象將被移除」 即將回收」 集合,等待回收。
7. 垃圾回收的優點和原理。並考慮 2 種回收機制。
Java 語言中一個顯著的特點就是引入了垃圾回收機制,使 C++ 程式設計師最頭疼的記憶體管理的問題迎刃而解,它使得 Java 程式設計師在編寫程式的時候不再需要考慮記憶體管理。由於有個垃圾回收機制,Java 中的對象不再有「作用域」的概念,只有對象的引用才有”作用域”。垃圾回收可以有效的防止記憶體泄露,有效的使用可以使用的記憶體。垃圾回收器通常是作為一個單獨的低級別的執行緒運行,不可預知的情況下對記憶體堆中已經死亡的或者長時間沒有使用的對象進行清楚和回收,程式設計師不能實時的調用垃圾回收器對某個對象或所有對象進行垃圾回收。
回收機制有分代複製垃圾回收和標記垃圾回收,增量垃圾回收。
8. 垃圾回收器的基本原理是什麼?垃圾回收器可以馬上回收記憶體嗎?有什麼辦法主動通知虛擬機進行垃圾回收?
對於 GC 來說,當程式設計師創建對象時,GC 就開始監控這個對象的地址、大小以及使用情況。通常,GC 採用有向圖的方式記錄和管理堆(heap)中的所有對象。通過這種方式確定哪些對象是」可達的」,哪些對象是」不可達的」。當 GC 確定一些對象為「不可達」時,GC 就有責任回收這些記憶體空間。可以。程式設計師可以手動執行 System.gc(),通知 GC 運行,但是 Java 語言規範並不保證 GC 一定會執行。
9. Java 中會存在記憶體泄漏嗎,請簡單描述。
所謂記憶體泄露就是指一個不再被程式使用的對象或變數一直被佔據在記憶體中。Java 中有垃圾回收機制,它可以保證一對象不再被引用的時候,即對象變成了孤兒的時候,對象將自動被垃圾回收器從記憶體中清除掉。由於 Java 使用有向圖的方式進行垃圾回收管理,可以消除引用循環的問題,例如有兩個對象,相互引用,只要它們和根進程不可達的,那麼 GC 也是可以回收它們的,例如下面的程式碼可以看到這種情況的記憶體回收:
import java.io.IOException; public class GarbageTest { /** @param args @throws IOException */ 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 中的記憶體泄露的情況:長生命周期的對象持有短生命周期對象的引用就很可能發生記憶體泄露,儘管短生命周期對象已經不再需要,但是因為長生命周期對象持有它的引用而導致不能被回收,這就是 Java 中記憶體泄露的發生場景,通俗地說,就是程式設計師可能創建了一個對象,以後一直不再使用這個對象,這個對象卻一直被引用,即這個對象無用但是卻無法被垃圾回收器回收的,這就是 java 中可能出現記憶體泄露的情況,例如,快取系統,我們載入了一個對象放在快取中 (例如放在一個全局 map 對象中),然後一直不再使用它,這個對象一直被快取引用,但卻不再被使用。
檢查 Java 中的記憶體泄露,一定要讓程式將各種分支情況都完整執行到程式結束,然後看某個對象是否被使用過,如果沒有,則才能判定這個對象屬於記憶體泄露。
如果一個外部類的實例對象的方法返回了一個內部類的實例對象,這個內部類對象被長期引用了,即使那個外部類實例對象不再被使用,但由於內部類持久外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會造成記憶體泄露。
下面內容來自於網上(主要特點就是清空堆棧中的某個元素,並不是徹底把它從數組中拿掉,而是把存儲的總數減少,本人寫得可以比這個好,在拿掉某個元素時,順便也讓它從數組中消失,將那個元素所在的位置的值設置為 null 即可):
我實在想不到比那個堆棧更經典的例子了,以致於我還要引用別人的例子,下面的例子不是我想到的,是書上看到的,當然如果沒有在書上看到,可能過一段時間我自己也想的到,可是那時我說是我自己想到的也沒有人相信的。
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); } } }
上面的原理應該很簡單,假如堆棧加了 10 個元素,然後全部彈出來,雖然堆棧是空的,沒有我們要的東西,但是這是個對象是無法回收的,這個才符合了記憶體泄露的兩個條件:無用,無法回收。但是就是存在這樣的東西也不一定會導致什麼樣的後果,如果這個堆棧用的比較少,也就浪費了幾個K記憶體而已,反正我們的記憶體都上 G 了,哪裡會有什麼影響,再說這個東西很快就會被回收的,有什麼關係。下面看兩個例子。
public class Bad{ public static Stack s=Stack(); static{ s.push(new Object()); s.pop(); //這裡有一個對象發生記憶體泄露 s.push(new Object()); //上面的對象可以被回收了,等於是自愈了 } }
因為是 static,就一直存在到程式退出,但是我們也可以看到它有自愈功能,就是說如果你的 Stack 最多有 100 個對象,那麼最多也就只有 100 個對象無法被回收其實這個應該很容易理解,Stack 內部持有 100 個引用,最壞的情況就是他們都是無用的,因為我們一旦放新的進取,以前的引用自然消失!
記憶體泄露的另外一種情況:當一個對象被存儲進 HashSet 集合中以後,就不能修改這個對象中的那些參與計算哈希值的欄位了,否則,對象修改後的哈希值與最初存儲進 HashSet 集合中時的哈希值就不同了,在這種情況下,即使在 contains 方法使用該對象的當前引用作為的參數去 HashSet 集合中檢索對象,也將返回找不到對象的結果,這也會導致無法從 HashSet 集合中單獨刪除當前對象,造成記憶體泄露。
10. 深拷貝和淺拷貝。
簡單來講就是複製、克隆。
Person p=new Person(「張三」);
淺拷貝就是對對象中的數據成員進行簡單賦值,如果存在動態成員或者指針就會報錯。
深拷貝就是對對象中存在的動態成員或指針重新開闢記憶體空間。
11. System.gc() 和 Runtime.gc() 會做什麼事情?
這兩個方法用來提示 JVM 要進行垃圾回收。但是,立即開始還是延遲進行垃圾回收是取決於 JVM 的。
12. finalize() 方法什麼時候被調用?析構函數 (finalization) 的目的是什麼?
垃圾回收器(garbage colector)決定回收某對象時,就會運行該對象的 finalize() 方法 但是在 Java 中很不幸,如果記憶體總是充足的,那麼垃圾回收可能永遠不會進行,也就是說 filalize() 可能永遠不被執行,顯然指望它做收尾工作是靠不住的。 那麼 finalize() 究竟是做什麼的呢? 它最主要的用途是回收特殊渠道申請的記憶體。Java 程式有垃圾回收器,所以一般情況下記憶體問題不用程式設計師操心。但有一種 JNI(Java Native Interface)調用 non-¬Java 程式(C 或 C++), finalize() 的工作就是回收這部分的記憶體。
13. 如果對象的引用被置為 null,垃圾收集器是否會立即釋放對象佔用的記憶體?
不會,在下一個垃圾回收周期中,這個對象將是可被回收的。
14. 什麼是分散式垃圾回收(DGC)?它是如何工作的?
DGC 叫做分散式垃圾回收。RMI 使用 DGC 來做自動垃圾回收。因為 RMI 包含了跨虛擬機的遠程對象的引用,垃圾回收是很困難的。DGC 使用引用計數演算法來給遠程對象提供自動記憶體管理。
15. 串列(serial)收集器和吞吐量(throughput)收集器的區別是什麼?
吞吐量收集器使用並行版本的新生代垃圾收集器,它用於中等規模和大規模數據的應用程式。 而串列收集器對大多數的小應用(在現代處理器上需要大概 100M 左右的記憶體)就足夠了。
16. 在 Java 中,對象什麼時候可以被垃圾回收?
當對象對當前使用這個對象的應用程式變得不可觸及的時候,這個對象就可以被回收了。
17. 簡述 Java 記憶體分配與回收策率以及 Minor GC 和 Major GC。
• 對象優先在堆的 Eden 區分配
• 大對象直接進入老年代
• 長期存活的對象將直接進入老年代
當 Eden 區沒有足夠的空間進行分配時,虛擬機會執行一次 Minor GC。Minor GC 通常發生在新生代的 Eden 區,在這個區的對象生存期短,往往發生 Gc 的頻率較高,回收速度比較快;Full GC/Major GC 發生在老年代,一般情況下,觸發老年代 GC 的時候不會觸發 Minor GC,但是通過配置,可以在 Full GC 之前進行一次 Minor GC 這樣可以加快老年代的回收速度。
18. JVM 的永久代中會發生垃圾回收么?
垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。
註:Java 8 中已經移除了永久代,新加了一個叫做元數據區的 native 記憶體區。
19. Java 中垃圾收集的方法有哪些?
標記 – 清除:這是垃圾收集演算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被回收的對象,然後統一回收。這種方法很簡單,但是會有兩個主要問題:
效率不高,標記和清除的效率都很低;
會產生大量不連續的記憶體碎片,導致以後程式在分配較大的對象時,由於沒有充足的連續記憶體而提前觸發一次 GC 動作。
複製演算法:為了解決效率問題,複製演算法將可用記憶體按容量劃分為相等的兩部分,然後每次只使用其中的一塊,當一塊記憶體用完時,就將還存活的對象複製到第二塊記憶體上,然後一次性清楚完第一塊記憶體,再將第二塊上的對象複製到第一塊。但是這種方式,記憶體的代價太高,每次基本上都要浪費一般的記憶體。
於是將該演算法進行了改進,記憶體區域不再是按照 1:1 去劃分,而是將記憶體劃分為 8:1:1 三部分,較大那份記憶體交 Eden 區,其餘是兩塊較小的記憶體區叫 Survior 區。每次都會優先使用 Eden 區,若 Eden 區滿,就將對象複製到第二塊記憶體區上,然後清除 Eden 區,如果此時存活的對象太多,以至於 Survivor 不夠時,會將這些對象通過分配擔保機制複製到老年代中。(java 堆又分為新生代和老年代)
標記 – 整理:該演算法主要是為了解決標記 – 清除,產生大量記憶體碎片的問題;當對象存活率較高時,也解決了複製演算法的效率問題。它的不同之處就是在清除對象的時候現將可回收對象移動到一端,然後清除掉端邊界以外的對象,這樣就不會產生記憶體碎片了。
分代收集:現在的虛擬機垃圾收集大多採用這種方式,它根據對象的生存周期,將堆分為新生代和老年代。在新生代中,由於對象生存期短,每次回收都會有大量對象死去,那麼這時就採用複製演算法。老年代裡的對象存活率較高,沒有額外的空間進行分配擔保。
20. 什麼是類載入器,類載入器有哪些?
實現通過類的許可權定名獲取該類的二進位位元組流的程式碼塊叫做類載入器。
主要有一下四種類載入器:
• 啟動類載入器(Bootstrap ClassLoader)用來載入 Java 核心類庫,無法被 Java 程式直接引用。
• 擴展類載入器(extensions class loader):它用來載入 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類載入器在此目錄裡面查找並載入 Java 類。
• 系統類載入器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來載入 Java 類。一般來說,Java 應用的類都是由它來完成載入的。可以通過 ClassLoader.getSystemClassLoader() 來獲取它。
• 用戶自定義類載入器,通過繼承 java.lang.ClassLoader 類的方式實現。
21. 類載入器雙親委派模型機制?
當一個類收到了類載入請求時,不會自己先去載入這個類,而是將其委派給父類,由父類去載入,如果此時父類不能載入,回饋給子類,由子類去完成類的載入。
三、領取方式一
關注公眾號 soft張三丰 ,
後台回復「面試」,就可以獲得網盤鏈接地址。
四、領取方式二
添加微信
並且關注公眾號 soft張三丰
觀注後,任意文章點贊和再看,
然後截圖給上面微信,也可領取。