我肝了這份高頻面試題解 |《麪霸系列》

麪霸系列之 Java 基礎篇第一版肝出來了,說實話每個問題其實都能展開來寫一篇文章....

這個系列的文章不會是背誦版,不是那種貼上標準答案,到時候照着答就行的面試題彙總。

我會用大白話儘量用解釋性、理解性的語言來回答,但是肯定沒有比平時通過一篇文章來講解清晰,不過我儘量。

暫時我先放 20 題出來,字數實在太多了,放一些之後看反饋,然後再修訂,之後再搞個 pdf。

還有,雖說看着只有 20 題,其實不止,因爲有些題目我沒拆解,回答中會有延伸問題,所以題目數不止我列出的這些,內容還是非常多的。

剩下還有幾十題,先亮一亮題目,嘿嘿。

之後還有會網絡篇、操作系統篇、Spring 篇、Netty 篇等等之類的,感覺滿滿當當的,好充實.....

好了不多嗶嗶,gogogo!

1.你覺得 Java 好在哪兒?

這種籠統的問題如果對某些知識點沒有深入、系統地認識絕對會蒙!

所以爲什麼經常碰到面試官問你一些空、大的問題?其實就是考察你是否有形成體系的理解。

回到問題本身。我覺得可以從跨平臺、垃圾回收、生態三個方面來闡述。

首先 Java 是跨平臺的,不同平臺執行的機器碼是不一樣的,而 Java 因爲加了一層中間層 JVM ,所以可以做到一次編寫多平臺運行,即 「Write once,Run anywhere」。

編譯執行過程是先把 Java 源代碼編譯成字節碼,字節碼再由 JVM 解釋或 JIT 編譯執行,而因爲 JIT 編譯時需要預熱的,所以還提供了 AOT(Ahead-of-Time Compilation),可以直接把字節碼轉成機器碼,來讓程序重啓之後能迅速拉滿戰鬥力。

(解釋執行比編譯執行效率差,你想想每次給你英語讓你翻譯閱讀,還是直接給你看中文,哪個快?)

Java 還提供垃圾自動回收功能,雖說手動管理內存意味着自由、精細化地掌控,當時很容易出錯。

在內存較充裕的當下,將內存的管理交給 GC 來做,減輕了程序員編程的負擔,提升了開發效率,更加划算!

然後現在 Java 生態圈太全了,豐富的第三方類庫、網上全面的資料、企業級框架、各種中間件等等,總之你要的都有。

基本上這樣答差不多了,之後等着面試官延伸。

當然這種開放性問題沒有固定答案,我的回答僅供參考。

2.如果讓你設計一個 HashMap 如何設計?

這個問題我覺得可以從 HashMap 的一些關鍵點入手,例如 hash函數、如何處理衝突、如何擴容。

可以先說下你對 HashMap 的理解。

比如:HashMap 無非就是一個存儲 <key,value> 格式的集合,用於通過 key 就能快速查找到 value。

基本原理就是將 key 經過 hash 函數進行散列得到散列值,然後通過散列值對數組取模找到對應的 index 。

所以 hash 函數很關鍵,不僅運算要快,還需要分佈均勻,減少 hash 碰撞。

而因爲輸入值是無限的,而數組的大小是有限的所以肯定會有碰撞,因此可以採用拉鍊法來處理衝突。

爲了避免惡意的 hash 攻擊,當拉鍊超過一定長度之後可以轉爲紅黑樹結構。

當然超過一定的結點還是需要擴容的,不然碰撞就太嚴重了。

而普通的擴容會導致某次 put 延時較大,特別是 HashMap 存儲的數據比較多的時候,所以可以考慮和 redis 那樣搞兩個 table 延遲移動,一次可以只移動一部分。

不過這樣內存比較喫緊,所以也是看場景來 trade off 了。

不過最好使用之前預估準數據大小,避免頻繁的擴容。

基本上這樣答下來差不多了,HashMap 幾個關鍵要素都包含了,接下來就看面試官怎麼問了。

可能會延伸到線程安全之類的問題,反正就照着 currentHashMap 的設計答。

3.併發類庫提供的線程池實現有哪些?

雖說阿里巴巴Java 開發手冊禁止使用這些實現來創建線程池,但是這問題我被問過好幾次,也是熱點。

問着問着就會延伸到線程池是怎麼設計的。

我先來說下線程池的內部邏輯,這樣才能理解這幾個實現。

首先線程池有幾個關鍵的配置:核心線程數、最大線程數、空閒存活時間、工作隊列、拒絕策略。

  1. 默認情況下線程不會預創建,所以是來任務之後纔會創建線程(設置prestartAllCoreThreads可以預創建核心線程)。
  2. 當核心線程滿了之後不會新建線程,而是把任務堆積到工作隊列中。
  3. 如果工作隊列放不下了,然後纔會新增線程,直至達到最大線程數。
  4. 如果工作隊列滿了,然後也已經達到最大線程數了,這時候來任務會執行拒絕策略。
  5. 如果線程空閒時間超過空閒存活時間,並且線程線程數是大於核心線程數的則會銷燬線程,直到線程數等於核心線程數(設置allowCoreThreadTimeOut 可以回收核心線程)。

我們再回到面試題來,這個實現指的就是 Executors 的 5 個靜態工廠方法:

  • newFixedThreadPool
  • newWorkStealingPool
  • newSingleThreadExecutor
  • newCachedThreadPool
  • newScheduledThreadPool

newFixedThreadPool

這個線程池實現特點是核心線程數和最大線程數是一致的,然後  keepAliveTime 的時間是 0 ,隊列是無界隊列。

按照這幾個設定可以得知它任務線程數是固定,如其名 Fixed。

然後可能出現 OOM 的現象,因爲隊列是無界的,所以任務可能擠爆內存。

它的特性就是我就固定出這麼多線程,多餘的任務就排隊,就算隊伍排爆了我也不管

因此不建議用這個方式來創建線程池。

newWorkStealingPool

這個是1.8纔有的,從代碼可以看到返回的就是  ForkJoinPool,我們1.8用的並行流就是這個線程池。

比如users.parallelStream().filter(...).sum();用的就是 ForkJoinPool 。

從圖中可以看到線程數會參照當前服務器可用的處理核心數,我記得並行數是核心數-1。

這個線程池的特性從名字就可以看出 Stealing,會竊取任務

每個線程都有自己的雙端隊列,當自己隊列的任務處理完畢之後,會去別的線程的任務隊列尾部拿任務來執行,加快任務的執行速率。

至於 ForkJoin 的話,就是分而治之,把大任務分解成一個個小任務,然後分配執行之後再總和結果,再詳細就自行查閱資料啦~

newSingleThreadExecutor

這個線程池很有個性,一個線程池就一個線程,一個人一座城,配備的也是無界隊列。

它的特性就是能保證任務是按順序執行的

newCachedThreadPool

這個線程池是急性子,核心線程數是 0 ,最大線程數看作無限,然後任務隊列是沒有存儲空間的,簡單理解成來個任務就必須找個線程接着,不然就阻塞了。

cached 意思就是會緩存之前執行過的線程,緩存時間是 60 秒,這個時候如果有任務進來就可以用之前的線程來執行。

所以它適合用在短時間內有大量短任務的場景。如果暫無可用線程,那麼來個任務就會新啓一個線程去執行這個任務,快速響應任務

但是如果任務的時間很長,那存在的線程就很多,上下文切換就很頻繁,切換的消耗就很明顯,並且存在太多線程在內存中,也有 OOM 的風險。

newScheduledThreadPool

其實就是定時執行任務,重點就是那個延時隊列。

關於 Java 的幾個定時任務調度相關的:Timer、DelayQueue 和 ScheduledThreadPool,我之前文章都分析過了,還介紹了時間輪在netty和kafka中的應用,有興趣的可以看看。

4.如果讓你設計一個線程池如何設計?

這種設計類問題還是一樣,先說下理解,表明你是知道這個東西的用處和原理的,然後開始 BB。基本上就是按照現有的設計來說,再添加一些個人見解。

線程池講白了就是存儲線程的一個容器,池內保存之前建立過的線程來重複執行任務,減少創建和銷燬線程的開銷,提高任務的響應速度,並便於線程的管理。

我個人覺得如果要設計一個線程池的話得考慮池內工作線程的管理、任務編排執行、線程池超負荷處理方案、監控。

初始化線程數、核心線程數、最大線程池都暴露出來可配置,包括超過核心線程數的線程空閒消亡配置。

任務的存儲結構可配置,可以是無界隊列也可以是有界隊列,也可以根據配置分多個隊列來分配不同優先級的任務,也可以採用 stealing 的機制來提高線程的利用率。

再提供配置來表明此線程池是 IO 密集還是 CPU 密集型來改變任務的執行策略。

超負荷的方案可以有多種,包括丟棄任務、拒絕任務並拋出異常、丟棄最舊的任務或自定義等等。

線程池埋好點暴露出用於監控的接口,如已處理任務數、待處理任務數、正在運行的線程數、拒絕的任務數等等信息。

我覺得基本上這樣答就差不多了,等着面試官的追問就好。

注意不需要跟面試官解釋什麼叫核心線程數之類的,都懂的沒必要。

當然這種開放型問題還是仁者見仁智者見智,我這個不是標準答案,僅供參考。

5. GC 如何調優?

GC 調優這種問題肯定是具體場景具體分析,但是在面試中就不要講太細,大方向說清楚就行,不需要涉及具體的垃圾收集器比如 CMS 調什麼參數,G1 調什麼參數之類的。

GC 調優的核心思路就是儘可能的使對象在年輕代被回收,減少對象進入老年代。

具體調優還是得看場景根據 GC 日誌具體分析,常見的需要關注的指標是 Young GC  和 Full GC 觸發頻率、原因、晉升的速率 、老年代內存佔用量等等。

比如發現頻繁會產生 Full GC,分析日誌之後發現沒有內存泄漏,只是 Young GC 之後會有大量的對象進入老年代,然後最終觸發 Ful GC。所以就能得知是 Survivor 空間設置太小,導致對象過早進入老年代,因此調大 Survivor 。

或者是晉升年齡設置的太小,也有可能分析日誌之後發現是內存泄漏、或者有第三方類庫調用了 System.gc等等。

反正具體場景具體分析,核心思想就是儘量在新生代把對象給回收了。

基本上這樣答就行了,然後就等着面試官延伸了。

6.動態代理是什麼?

動態代理就是一個代理機制,動態是相對於靜態來說的。

代理可以看作是調用目標的一個包裝,通常用來在調用真實的目標之前進行一些邏輯處理,消除一些重複的代碼。

靜態代理指的是我們預先編碼好一個代理類,而動態代理指的是運行時生成代理類。

動態更加方便,可以指定一系列目標來動態生成代理類(AOP),而不像靜態代理需要爲每個目標類寫對應的代理類。

代理也是一種解耦,目標類和調用者之間的解耦,因爲多了代理類這一層。

常見的動態代理有 JDK 動態代理 和 CGLIB。

7.JDK 動態代理與 CGLIB 區別?

JDK 動態代理是基於接口的,所以要求代理類一定是有定義接口的

CGLIB 基於ASM字節碼生成工具,它是通過繼承的方式來實現代理類,所以要注意 final 方法

之間的性能隨着 JDK 版本的不同而不同,以下內容取自:haiq的博客

  • jdk6 下,在運行次數較少的情況下,jdk動態代理與 cglib 差距不明顯,甚至更快一些;而當調用次數增加之後, cglib 表現稍微更快一些
  • jdk7 下,情況發生了逆轉!在運行次數較少(1,000,000)的情況下,jdk動態代理比 cglib 快了差不多30%;而當調用次數增加之後(50,000,000), 動態代理比 cglib 快了接近1倍
  • jdk8 表現和 jdk7 基本一致

基本上這樣答差不多了,我們再看看 JDK 動態代理實現原理:

  1. 首先通過實現 InvocationHandler 接口得到一個切面類。
  2. 然後利用 Proxy 根據目標類的類加載器、接口和切面類得到一個代理類。
  3. 代理類的邏輯就是把所有接口方法的調用轉發到切面類的 invoke() 方法上,然後根據反射調用目標類的方法。

再深一點點就是代理類會現在靜態塊中通過反射把所有方法都拿到存在靜態變量中,我之前反編譯看過代理類,我忙寫了一下,大致長這樣:

這一套下來 JDK 動態代理原理應該就很清晰了。

再來看下 CGLIB,其實和 JDK 動態代理的實現邏輯是一致,只是實現方式不同。

        Enhancer en = new Enhancer();
        //2.設置父類,也就是代理目標類,上面提到了它是通過生成子類的方式
        en.setSuperclass(target.getClass());
        //3.設置回調函數,這個this其實就是代理邏輯實現類,也就是切面,可以理解爲JDK 動態代理的handler
        en.setCallback(this);
        //4.創建代理對象,也就是目標類的子類了。
        return en.create();

然後它是通過字節碼生成技術而不是反射來實現調用的邏輯,具體就不再深入了。

8.註解是什麼原理?

註解其實就是一個標記,可以標記在類上、方法上、屬性上等,標記自身也可以設置一些值。

有了標記之後,我們就可以在解析的時候得到這個標記,然後做一些特別的處理,這就是註解的用處。

比如我們可以定義一些切面,在執行一些方法的時候看下方法上是否有某個註解標記,如果是的話可以執行一些特殊邏輯(RUNTIME類型的註解)。

註解生命週期有三大類,分別是:

  • RetentionPolicy.SOURCE:給編譯器用的,不會寫入 class 文件
  • RetentionPolicy.CLASS:會寫入 class 文件,在類加載階段丟棄,也就是運行的時候就沒這個信息了
  • RetentionPolicy.RUNTIME:會寫入 class 文件,永久保存,可以通過反射獲取註解信息

所以我上文寫的是解析的時候,沒寫具體是解析啥,因爲不同的生命週期的解析動作是不同的。

像常見的:

就是給編譯器用的,編譯器編譯的時候檢查沒問題就over了,class文件裏面不會有 Override 這個標記。

再比如 Spring 常見的 Autowired ,就是 RUNTIME 的,所以在運行的時候可以通過反射得到註解的信息,還能拿到標記的值 required 。

所以註解就是一個標記,可以給編譯器用、也能運行時候用。

9.反射用過嗎?

如果你用過那就不用我多說啥了,場景說一下,然後等着面試官繼續挖。

如果沒用過那就說生產上沒用過,不過私下研究過反射的原理。

反射其實就是Java提供的能在運行期可以得到對象信息的能力,包括屬性、方法、註解等,也可以調用其方法。

一般的編碼不會用到反射,在框架上用的較多,因爲很多場景需要很靈活,所以不確定目標對象的類型,屆時只能通過反射動態獲取對象信息。

PS:對反射不瞭解的,可以網上查查,這裏不深入了。

10.能說下類加載過程嗎?

類加載顧名思義就是把類加載到 JVM 中,而輸入一段二進制流到內存,之後經過一番解析、處理轉化成可用的 class 類,這就是類加載要做的事情。

二進制流可以來源於 class 文件,或者通過字節碼工具生成的字節碼或者來自於網絡都行,只要符合格式的二進制流,JVM 來者不拒。

類加載流程分爲加載、連接、初始化三個階段,連接還能拆分爲:驗證、準備、解析三個階段。

所以總的來看可以分爲 5 個階段:

  • 加載:將二進制流搞到內存中來,生成一個 Class 類。

  • 驗證:主要是驗證加載進來的二進制流是否符合一定格式,是否規範,是否符合當前 JVM 版本等等之類的驗證。

  • 準備:爲靜態變量(類變量)賦初始值,也即爲它們在方法區劃分內存空間。這裏注意是靜態變量,並且是初始值,比如 int 的初始值是 0。

  • 解析:將常量池的符號引用轉化成直接引用。符號引用可以理解爲只是個替代的標籤,比如你此時要做一個計劃,暫時還沒有人選,你設定了個 A 去做這個事。然後等計劃真的要落地的時候肯定要找到確定的人選,到時候就是小明去做一件事。

    解析就是把 A(符號引用) 替換成小明(直接引用)。符號引用就是一個字面量,沒有什麼實質性的意義,只是一個代表。直接引用指的是一個真實引用,在內存中可以通過這個引用查找到目標。

  • 初始化:這時候就執行一些靜態代碼塊,爲靜態變量賦值,這裏的賦值纔是代碼裏面的賦值,準備階段只是設置初始值佔個坑。

這個問題我覺得回答可以比我寫的更粗,幾個階段一說,大致做的說一說就 ok 了。

想要知道更詳細的流程可以看下《深入理解虛擬機Java》虛擬機的類加載章節。

11.雙親委派知道不?來說說看?

類加載機制一問基本上就會接着問雙親委派。

雙親委派的意思是

如果一個類加載器需要加載類,那麼首先它會把這個類加載請求委派給父類加載器去完成,如果父類還有父類則接着委託,每一層都是如此。

一直遞歸到頂層,當父加載器無法完成這個請求時,子類纔會嘗試去加載。

這裏的雙親其實就指的是父類,沒有mother。

父類也不是我們平日所說的那種繼承關係,只是調用邏輯是這樣。

關於雙親委派我之前寫過文章,我把一些比較重要的內容拷過來:

Java 自身提供了 3 種類加載器:

  1. 啓動類加載器(Bootstrap ClassLoader),它是屬於虛擬機自身的一部分,用 C++ 實現的,主要負責加載<JAVA_HOME>\lib目錄中或被-Xbootclasspath指定的路徑中的並且文件名是被虛擬機識別的文件。它是所有類加載器的爸爸。

  2. 擴展類加載器(Extension ClassLoader),它是Java實現的,獨立於虛擬機,主要負責加載<JAVA_HOME>\lib\ext目錄中或被java.ext.dirs系統變量所指定的路徑的類庫。

  3. 應用程序類加載器(Application ClassLoader),它是Java實現的,獨立於虛擬機。主要負責加載用戶類路徑(classPath)上的類庫,如果我們沒有實現自定義的類加載器那這玩意就是我們程序中的默認加載器。

所以一般情況類加載會從應用程序類加載器委託給擴展類再委託給啓動類,啓動類找不到然後擴展類找,擴展類加載器找不到再應用程序類加載器找。

雙親委派模型不是一種強制性約束,也就是你不這麼做也不會報錯怎樣的,它是一種JAVA設計者推薦使用類加載器的方式

爲什麼要雙親委派?

它使得類有了層次的劃分。就拿 java.lang.Object 來說,加載它經過一層層委託最終是由Bootstrap ClassLoader來加載的,也就是最終都是由Bootstrap ClassLoader去找<JAVA_HOME>\lib中rt.jar裏面的java.lang.Object加載到JVM中。

這樣如果有不法分子自己造了個java.lang.Object,裏面嵌了不好的代碼,如果我們是按照雙親委派模型來實現的話,最終加載到JVM中的只會是我們rt.jar裏面的東西,也就是這些核心的基礎類代碼得到了保護。

因爲這個機制使得系統中只會出現一個java.lang.Object。不會亂套了。你想想如果我們JVM裏面有兩個Object,那豈不是天下大亂了。

那你知道有違反雙親委派的例子嗎?

典型的例子就是:JDBC。

JDBC 的接口是類庫定義的,但實現是在各大數據庫廠商提供的 jar 包中,那通過啓動類加載器是找不到這個實現類的,所以就需要應用程序加載器去完成這個任務,這就違反了自下而上的委託機制了。

具體做法是搞了個線程上下文類加載器,通過 setContextClassLoader() 默認設置了應用程序類加載器,然後通過 Thread.current.currentThread().getContextClassLoader() 獲得類加載器來加載。

12.JDK 和 JRE 的區別?

JRE(Java Runtime Environment)指的是 Java 運行環境,包含了 JVM 和 Java 類庫等。

JDK(Java Development Kit) 可以視爲 JRE 的超集,還提供了一些工具比如各種診斷工具:jstack,jmap,jstat 等。

13.用過哪些 JDK 提供的工具?

這個就考察你平日裏面有沒有通過一些工具進行問題的分析、排查。

如果你用過肯定很好說,比如之前排查內存異常的時候用 jmap dump下來內存文件用 MAT 進行分析之類的。

如果沒用過的話可以試試,自己找場景試驗一下。

我列幾個之前寫過文章的工具,建議自己用用,還是很簡單的。

  • jps:虛擬機進程狀況工具
  • jstat:虛擬機統計信息監視工具
  • jmap:Java內存映像工具
  • jhat:虛擬機堆轉儲快照分析工具
  • jstack:Java堆棧跟蹤工具
  • jinfo:Java配置信息工具
  • VisualVM:圖形化工具,可以得到虛擬機運行時的一些信息:內存分析、CPU 分析等等,在 jdk9 開始不再默認打包進 jdk 中。

工具其實還有很多,看看下面這個截圖。

jdk/bin中部分工具截圖

更詳細的可以去《深入理解虛擬機Java》第四章查看。

總之就是自己找機會用用,沒機會就自己給自己創造機會,防範於未然。

14.接口和抽象類有什麼區別?

接口:只能包含抽象方法,不能包含成員變量,當 has a 的情況下用接口。

接口是對行爲的抽象,類似於條約。在 Java 中接口可以多實現,從 has a 角度來說接口先行,也就是先約定接口,再實現。

抽象類: 可以包含成員變量和一般方法和抽象方法,當 is a 並且主要用於代碼複用的場景下使用抽象類繼承的方式,子類必須實現抽象類中的抽象方法。

在 Java 中只支持單繼承。從 is a 角度來看一般都是先寫,然後發現代碼能複用,然後抽象一個抽象類。

15.什麼是序列化?什麼是反序列化?

序列化其實就是將對象轉化成可傳輸的字節序列格式,以便於存儲和傳輸。

因爲對象在 JVM 中可以認爲是“立體”的,會有各種引用,比如在內存地址Ox1234 引用了某某對象,那此時這個對象要傳輸到網絡的另一端時候就需要把這些引用“壓扁”。

因爲網絡的另一端的內存地址 Ox1234 可以沒有某某對象,所以傳輸的對象需要包含這些信息,然後接收端將這些扁平的信息再反序列化得到對象。

所以反序列化就是將字節序列格式轉換成對象的過程

我再擴展一下 Java 序列化。

首先說一下 Serializable,這個接口沒有什麼實際的含義,就是起標記作用。

來看下源碼就很清楚了,除了 String、數組和枚舉之外,如果實現了這個接口就走writeOrdinaryObject,否則就序列化就拋錯。

serialVersionUID 又有什麼用?

private static final long serialVersionUID = 1L;

想必經常會看到這樣的代碼,這個 ID 其實就是用來驗證序列化的對象和反序列化對應的對象ID 是否一致。

所以這個 ID 的數字其實不重要,無論是 1L 還是 idea 自動生成的,只要序列化時候對象的 serialVersionUID 和反序列化時候對象的 serialVersionUID 一致的話就行。

如果沒有顯示指定 serialVersionUID ,則編譯器會根據類的相關信息自動生成一個,可以認爲是一個指紋。

所以如果你沒有定義一個 serialVersionUID 然後序列化一個對象之後,在反序列化之前把對象的類的結構改了,比如增加了一個成員變量,則此時的反序列化會失敗。

因爲類的結構變了,生成的指紋就變了,所以 serialVersionUID 就不一致了。

所以 serialVersionUID 就是起驗證作用。

Java 序列化不包含靜態變量

簡單地說就是序列化之後存儲的內容不包含靜態變量的值,看下下面的代碼就很清晰了。

16.什麼是不可變類?

不可變類指的是無法修改對象的值,比如 String 就是典型的不可變類,當你創建一個 String 對象之後,這個對象就無法被修改。

因爲無法被修改,所以像執行s += "a"; 這樣的方法,其實返回的是一個新建的 String 對象,老的 s 指向的對象不會發生變化,只是 s 的引用指向了新的對象而已。

所以纔會有不要在字符串拼接頻繁的場景不要使用 + 來拼接,因爲這樣會頻繁的創建對象。

不可變類的好處就是安全,因爲知曉這個對象不可能會被修改,因此可以放心大膽的用,在多線程環境下也是線程安全的。

如何實現一個不可變類?

這個問題我被面試官問過,其實就參考 String 的設計就行。

String 類用 final 修飾,表示無法被繼承。

String 本質是一個 char 數組,然後用 final 修飾,不過 final 限制不了數組內部的數據,所以這還不夠。

所以 value 是用 private 修飾的,並且沒有暴露出 set 方法,這樣外部其實就接觸不到 value 所以無法修改。

當然還是有修改的需求,比如 replace 方法,所以這時候就需要返回一個新對象來作爲結果。

總結一下就是私有化變量,然後不要暴露 set 方法,即使有修改的需求也是返回一個新對象。

17.Java 按值傳遞還是按引用傳遞?

Java 只有按值傳遞,不論是基本類型還是引用類型。

基本類型是值傳遞很好理解,引用類型有些同學可能有點理解不了,特別是初學者。

JVM 內存有劃分爲棧和堆,局部變量和方法參數是在棧上分配的,基本類型和引用類型都佔 4 個字節,當然 long 和 double 佔 8 個字節。

而對象所佔的空間是在堆中開闢的,引用類型的變量存儲對象在堆中地址來訪問對象,所以傳遞的時候可以理解爲把變量存儲的地址給傳遞過去,因此引用類型也是值傳遞。

18.泛型有什麼用?泛型擦除是什麼?

泛型可以把類型當作參數一樣傳遞,使得像一些集合類可以明確存儲的對象類型,不用顯示地強制轉化(在沒泛型之前只能是Object,然後強轉)。

並且在編譯期能識別類型,類型錯誤則會提醒,增加程序的健壯性和可讀性。

泛型擦除指的指參數類型其實在編譯之後就被抹去了,也就是生成的 class 文件是沒有泛型信息的,所以稱之爲擦除。

不過這個擦除有個細節,我們來看下代碼就很清晰了,代碼如下:

然後我們再來看看編譯後的 class 文件。

可以看到 yess 是有類型信息的,所以在代碼裏寫死的泛型類型是不會被擦除的!

這也解釋了爲什麼根據反射是可以拿到泛型信息的,因爲這種寫死的就沒有被擦除!

至於泛型擦除是爲了向後兼容,因爲在 JDK 5 之前是沒有泛型的,所以要保證 JDK 5 之前編譯的代碼可以在之後的版本上跑,而類型擦除就是能達到這一目標的一個實現手段。

其實 Java 也可以搞別的手段來實現泛型兼容,只是擦除比較容易實現。

19.說說強、軟、弱、虛引用?

Java 根據其生命週期的長短將引用類型又分爲強引用、軟引用、弱引用、幻象引用。

  • 強引用:就是我們平時 new 一個對象的引用。當 JVM 的內存空間不足時,寧願拋出 OutOfMemoryError 使得程序異常終止,也不願意回收具有強引用的存活着的對象。
  • 軟引用:生命週期比強引用短,當 JVM 認爲內存空間不足時,會試圖回收軟引用指向的對象,也就是說在 JVM 拋出 OutOfMemoryError 之前,會去清理軟引用對象,適合用在內存敏感的場景。
  • 弱引用:比軟引用還短,在 GC 的時候,不管內存空間足不足都會回收這個對象,ThreadLocal中的 key 就用到了弱引用,適合用在內存敏感的場景。-虛引用:也稱幻象引用,之所以這樣叫是因爲虛引用的 get 永遠都是 null ,稱爲get 了個寂寞,所以叫虛。

虛引用的唯一作用就是配合引用隊列來監控引用的對象是否被加入到引用隊列中,也就是可以準確的讓我們知曉對象何時被回收。

還有一點有關虛引用的需要提一下,之前看文章都說虛引用對 gc 回收不會有任何的影響,但是看 1.8 doc 上面說

簡單翻譯下就是:與軟引用和弱引用不同,虛引用在排隊時不會被垃圾回收器自動清除。通過虛引用可訪問的對象將保持這種狀態,直到所有這些引用被清除或者它們本身變得不可訪問

簡單的說就是被虛引用引用的對象不能被 gc,然而在 JDK9 又有個變更記錄:

鏈接:https://bugs.openjdk.java.net/browse/JDK-8071507

按照這上面說的 JDK9 之前虛引用的對象是在虛引用自身被銷燬之前是無法被 gc 的,而 JDK9 之後改了。

我沒下 JDK9 ,不過我有 JDK11 ,所以看了下 11 doc 的確實改了。

看起來是把那段刪了。所以 JDK9 之前虛引用對引用對象的GC是有影響的,9及之後的版本沒影響。

20.Integer 緩存池知道嗎?

因爲根據實踐發現大部分的數據操作都集中在值比較小的範圍,因此 Integer 搞了個緩存池,默認範圍是 -128 到 127,可以根據通過設置JVM-XX:AutoBoxCacheMax=<size>來修改緩存的最大值,最小值改不了。

實現的原理是int 在自動裝箱的時候會調用Integer.valueOf,進而用到了 IntegerCache。

沒什麼花頭,就是判斷下值是否在範圍之內,如果是的話去 IntegerCache 中取。

IntegerCache 在靜態塊中會初始化好緩存值。

所以這裏還有個面試題,就是啥 Integer 127 之內的相等,而超過 127 的就不等了,因爲 127 之內的就是同一個對象,所以當然相等。

不僅 Integer 有,Long 也是有的,不過範圍是寫死的 -128 到 127。

對了 Float 和 Double 是沒有滴,畢竟是小數,能存的數太多了。


本文分享自微信公衆號 - 武培軒(wupeixuan404)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章