天天看點

Java基礎面試題整理

0. Java基礎

0.1 Java類加載器

0.1.1 面試官:請說說你了解的類加載器

0.1.2 面試官:說說有哪幾種類加載器,他們的職責分别是什麼,他們之前存在什麼樣的約定。

0.1.3 面試官插嘴:ExtClassLoader為什麼沒有設定parent?

0.1.4 面試官:雙親委派的好處是什麼呢?

0.1.5 面試官:那自己怎麼去實作一個ClassLoader呢?請舉個實際的例子。

0.1.6 面試官:為什麼不繼承AppClassLoader呢?

0.1.7 面試官:有什麼應用場景呢?

0.1.8 面試官插嘴:為什麼需要o.getClass().getMethod("printVersion").invoke(o);這樣通過反射擷取method調用,不能先強轉成Test,然後test.printVersion()嗎?

0.1.9 面試官:會發生什麼?

0.2 HashMap和LinkedHashMap底層原理

0.3 ArrayList 底層原理

1.String面試題

1.1 String 對象的兩種建立方式?

1.2 String 類型的常量池?

1.3 String 字元串拼接

1.4 String s1 = new String("abc");這句話建立了幾個對象?

2.多線程面試題

2.1 說下volatile吧,了解多少說多少?

2.2 解釋下Java中的線程池及其用法?

2.3 Java 阻塞隊列?

3. JVM面試題

3.1 請解釋一下對象的建立過程?(半初始化)

3.2 加問DCL與volatile問題?(指令重排序)

3.3 對象在記憶體中的存儲布局?(對象與數組的存儲不同)

3.4 對象頭具體包括什麼?(markword、klasspointer、synchronized鎖資訊)

3.5 對象怎麼定位?(直接 間接)

3.5.1 句柄

3.5.2 直接指針

3.6 對象怎麼配置設定?(棧上-線程本地-Eden-Old)

3.7 Object o = new Object()在記憶體中占用多少位元組?

3.8 JVM記憶體模型

3.8.1 堆記憶體(Heap)

3.8.2 方法區(Method Area)

3.8.3 虛拟機棧(JVM Stack) —— 我們平常說的棧

3.8.4 本地方法棧(Native Stack)

3.9 JVM記憶體參數設定

3.10 JVM中,常用的GC Root有哪些?

4. Java動态代理是什麼?有什麼作用?

4.1 靜态代理

4.1.1 靜态代理的缺陷

4.2 對象建立

4.3 動态代理

4.3.1 Proxy.newProxyInstance

參考文獻:

好怕怕的類加載器

通過一個類的<code>全限定名</code>來擷取描述此類的二進制位元組<code>在這裡插入代碼片</code>流這個動作放到Java虛拟機外部去實作,以便讓應用程式自己決定如何去擷取所需要的類。實作這個動作的代碼子產品稱為“類加載<code>在這裡插入代碼片</code>器”。

Java基礎面試題整理

<code>BootstrapClassLoader</code> 啟動類類加載器

它用來加載&lt;<code>JAVA_HOME&gt;/jre/lib路徑</code>,-Xbootclasspath參數指定的路徑以&lt;JAVA_HOME&gt;/jre/classes中的類。BootStrapClassLoader是由c++實作的。

<code>ExtClassLoader</code>拓展類類加載器

用來加載&lt;<code>JAVA_HOME&gt;/jre/lib/ext</code>路徑以及<code>java.ext.dirs</code>系統變量指定的類路徑下的類。

<code>AppClassLoader</code>‘ 應用程式類類加載器

主要加載應用程式<code>ClassPath</code>下的類(包含jar包中的類)。它是<code>java應用程式預設的類加載器</code>。

<code>使用者自定義類加載器</code>

使用者根據自定義需求,自由的<code>定制</code>加載的邏輯,繼承AppClassLoader,僅僅覆寫<code>findClass</code>()即将繼續遵守<code>雙親委派</code>模型。

在虛拟機啟動的時候會初始化BootstrapClassLoader,然後在<code>Launcher</code>類中去加載ExtClassLoader、AppClassLoader,并将AppClassLoader的parent設定為ExtClassLoader,并設定線程上下文類加載器。

Launcher是JRE中用于啟動程式入口main()的類

因為BootstrapClassLoader是由c++實作的,是以并不存在一個Java的類。

雙親委派模型能保證<code>基礎類僅加載一次</code>,不會讓jvm中存在重名的類。比如String.class,每次加載都委托給父加載器,最終都是BootstrapClassLoader,都保證java核心類都是BootstrapClassLoader加載的,保證了java的安全與穩定性。

自己實作ClassLoader時隻需要繼承ClassLoader類,然後覆寫findClass(String name)方法即可完成一個帶有雙親委派模型的類加載器。

我們看下ClassLoader#loadClass的代碼

子類隻需要實作findClass,關心從哪裡加載即可。

因為它和ExtClassLoader都是Launcher的靜态類,都是包通路路徑權限的。

代碼熱替換,在不重新開機伺服器的情況下可以修改類的代碼并使之生效。

省略一堆代碼,請參考文獻。

Test.class會隐性的被加載目前類的ClassLoader加載,目前Main方法預設的ClassLoader為AppClassLoader,而不是我們自定義的MyClassLoader。

會抛出ClassCastException,因為一個類,就算包路徑完全一緻,但是加載他們的ClassLoader不一樣,那麼這兩個類也會被認為是兩個不同的類。

Hash也稱為<code>散列、哈希</code>。對應的英文就是<code>Hash</code>。基本原理就是把<code>任意長度</code>的輸入,通過Hash算法轉出<code>固定長度</code>的輸出。

Hash的特點:

從Hash值不能<code>反向推導</code>出原始的資料

輸入資料的<code>微小變化</code>會得到完全不同的hash值,相同的資料會得到相同的值

雜湊演算法的<code>執行效率</code>要高效,長的文本也能快速地計算出哈希值

雜湊演算法的<code>沖突</code>機率要小

圖解HashMap原理

Java基礎面試題整理

圖解LinkedHashMap原理

Java基礎面試題整理

<code>LinkedHashMap其實就是可以看成HashMap的基礎上,多了一個雙向連結清單來維持順序</code>。

圖解ArrayList

第一種方式是在常量池中拿對象,第二種方式是直接在堆記憶體空間建立一個新的對象。

Java基礎面試題整理

直接使用<code>雙引号聲明</code>出來的 String 對象會直接存儲在常量池中。

如果不是用雙引号聲明的 String 對象,可以使用 String 提供的 intern 方<code>String.intern()</code> 是一個 Native 方法,它的作用是:如果運作時常量池中已經包含一個等于此 String 對象内容的字元串,則傳回常量池中該字元串的引用;如果沒有,則在常量池中建立與此 String 内容相同的字元串,并傳回常量池中建立的字元串的引用。

Java基礎面試題整理

盡量避免多個字元串拼接,因為這樣會重新建立對象。如果需要改變字元串的花,可以使用 <code>StringBuilder</code> 或者 <code>StringBuffer</code>。

先有字元串"abc"放入常量池,然後 new 了一份字元串"abc"放入Java堆(<code>字元串常量"abc"在編譯期就已經确定放入常量池,而 Java 堆上的"abc"是在運作期初始化階段才确定</code>),然後 Java 棧的 str1 指向Java堆上的"abc"。

volatile是JVM(Java Memory Model)中用于保證可見性和有序性的輕量級同步機制。

它主要是有兩個作用,一是保證被修飾共享變量的可見性,也就是多個線程操作讀寫時,能被其他線程感覺到,在讀取變量時會強制将主記憶體中的變量值讀取到自己的工作記憶體中,寫入變量時又會強制自己的新值重新整理回主記憶體;另外一個重要作用在于阻止指令重排序。我們所熟知的雙檢測單例中,instance必須要用volatile修飾,原因是new SingleTon時,一般說有三個步驟(位元組碼):

配置設定一塊記憶體 在記憶體上初始化SingleTon對象 把這塊記憶體位址傳回值指派給 instance

但是經過編譯器的優化,2、3的順序有可能是颠倒的,也就是說可能你拿到的instance可能還沒有被初始化,通路instance的成員變量就可能發生空指針異常,而volatile可以阻止這種情況的發生。

使用線程分三步驟:

建立線程 任務執行 銷毀線程

備注:建立和銷毀線程都會消耗系統資源,影響性能。

線程池:

已經提前建立好線程,用的時候直接執行任務,通過阻塞隊列保證線程不會結束退出。

使用:

主要涉及到三大參數:

核心線程數

阻塞隊列

線程允許的最大線程數

當然還有三個不太重要的參數:

線程空閑的存活時間

建立線程的工廠

拒絕政策

相對線程而言,有以下幾點優勢:

降低資源消耗,通過重複利用已經建立的線程來降低建立線程和銷毀線程所造成的性能消耗。

提高響應速度,當任務到達時,任務可以不需要等待線程建立就能立即執行。

提高線程的可管理性,線程是屬于稀缺資料,如果無限制建立,不僅會消耗系統資源,還會降低系統的文檔性。使用線程池可以統一配置設定和控制。

從0到1實作自己的阻塞隊列(上)

從0到1實作自己的阻塞隊列(下)

多線程中那些看不見的陷阱

Java基礎面試題整理
Java基礎面試題整理

new 申請記憶體空間(半初始化)

Java基礎面試題整理

invokespecial 初始化

Java基礎面試題整理

astore_1 對象和變量建立關聯關系,也就是指派

Java基礎面試題整理

volatile作用:

保證線程可見性

禁止指令重排序

注意:<code>建立對象非原子性操作</code>。

thread1半初始化狀态

Java基礎面試題整理

發生指令重排序

Java基礎面試題整理

thread2使用半初始化狀态的對象,異常

Java基礎面試題整理
Java基礎面試題整理

對象頭(<code>markword</code> 8個位元組)—— 鎖資訊、HashCode、分代年齡等

類型指針(<code>class pointer</code> 4個位元組)

執行個體資料 (<code>instance</code> <code>data</code>)

對齊 (<code>padding</code>)

數組長度(<code>length</code> 4位元組)- 數組特有

Java基礎面試題整理

偏向鎖

自旋鎖(無鎖、lock-free)輕量級鎖

重量級鎖

Java基礎面試題整理

句柄,效率偏低(兩次通路),但是對于垃圾回收不用頻繁修改t

直接指針,效率高(直接通路),但是垃圾回收需要頻繁修改t

Java基礎面試題整理

如果使用句柄的話,那麼Java堆中将會劃分出一塊記憶體來作為句柄池,reference 中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊;

Java基礎面試題整理

如果使用直接指針通路,那麼 Java 堆對像的布局中就必須考慮如何防止通路類型資料的相關資訊,reference 中存儲的直接就是對象的位址。

Java基礎面試題整理

棧上配置設定(Java逃逸分析)

old區配置設定

ThreadLocalAllocateBuffer 線程本地配置設定

Eden區

S1、S2區

涉及到Age(4個bit,也就是0-15)

Java基礎面試題整理
Java基礎面試題整理

JVM的記憶體空間分為3大部分:

堆記憶體

堆記憶體可以劃分為<code>新生代</code>和<code>老年代</code>,新生代中還可以再次劃分為<code>Eden</code>區、From Survivor區和To Survivor區。

方法區

棧記憶體

棧記憶體可以再細分為<code>java虛拟機棧</code>和<code>本地方法棧</code>

堆是被所有線程共享的區域,實在虛拟機啟動時建立的。

<code>幾乎</code>所有的new對象都是存在heap中(請注意逃逸分析棧上配置設定)

堆記憶體分為兩個部分:年輕代和老年代。我們平常所說的垃圾回收,主要回收的就是堆區。更細一點劃分新生代又可劃分為Eden區和2個Survivor區(From Survivor和To Survivor)。

Java基礎面試題整理

新生代 ( Young ) 與老年代 ( Old ) 的比例的值為 1:2 ( 該值可以通過參數 –<code>XX:NewRatio</code> 來指定 )

預設的,Eden : from : to = <code>8 : 1 : 1</code> ( 可以通過參數 –<code>XX:SurvivorRatio</code> 來設定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。

方法區用于存儲虛拟機加載的<code>類資訊、常量、靜态變量</code>、是各個線程<code>共享</code>的記憶體區域。

在JDK8之前的<code>HotSpot</code> JVM,區域叫做“永久代(<code>permanent</code> generation)”。永久代是一片連續的堆空間,在JVM啟動之前通過在指令行設定參數-<code>XX:MaxPermSize</code>來設定永久代最大可配置設定的記憶體空間,預設大小是<code>64M</code>(64位JVM預設是85M)。

随着JDK8的到來,JVM不再有 永久代(PermGen)。但類的中繼資料資訊(<code>metadata</code>)還在,隻不過不再是存儲在連續的堆空間上,而是移動到叫做“Metaspace”的本地記憶體(Native memory。

方法區或永生代相關設定

-XX:PermSize=64MB 最小尺寸,初始配置設定 -XX:MaxPermSize=256MB 最大允許配置設定尺寸,按需配置設定 XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled 設定垃圾不回收 預設大小 -server選項下預設MaxPermSize為64m -client選項下預設MaxPermSize為32m

java虛拟機棧是<code>線程私有</code>,生命周期與線程相同。建立線程的時候就會建立一個java虛拟機棧。虛拟機執行java程式的時候,每個方法都會建立一個棧幀,棧幀存放在java虛拟機棧中,通過壓棧出棧的方式進行方法調用。

棧幀又分為一下幾個區域:<code>局部變量表</code>、<code>操作數棧</code>、<code>動态連接配接</code>、<code>方法出口</code>等。

平時我們所說的變量存在棧中,這句話說的不太嚴謹,應該說<code>局部變量存放在java虛拟機棧的局部變量表中</code>。

java的8中基本類型的局部變量的值存放在虛拟機棧的局部變量表中,如果是引用型的變量,則隻存儲對象的引用位址。

本地方法棧(Native Method Stacks)與虛拟機棧所發揮的作用是非常相似的,其差別不過是虛拟機棧為虛拟機執行Java方法(也就是位元組碼)服務,而<code>本地方法棧則是為虛拟機使用到的Native方法服務</code>。

Java基礎面試題整理

-<code>Xms</code>設定堆的最小空間大小。

-<code>Xmx</code>設定堆的最大空間大小。

-<code>Xmn</code>:設定年輕代大小

-<code>XX:NewSize</code>設定新生代最小空間大小。

-<code>XX:MaxNewSize</code>設定新生代最大空間大小。

-<code>XX:PermSize</code>設定永久代最小空間大小。

-<code>XX:MaxPermSize</code>設定永久代最大空間大小。

-<code>Xss</code>設定每個線程的堆棧大小

-XX:+UseParallelGC:選擇垃圾收集器為并行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用并發收集,而年老代仍舊使用串行收集。

-XX:ParallelGCThreads=20:配置并行收集器的線程數,即:同時多少個線程一起進行垃圾回收。此值最好配置與處理器數目相等。

虛拟機棧中引用的對象

方法區中 靜态屬性引用的對象

方法區中 常量引用的對象

JNI引用的對象

Java動态代理

要說動态代理,必須先聊聊靜态代理。

假設現在項目經理有一個需求:在項目現有所有類的方法前後列印日志。

你如何在<code>不修改已有代碼</code>的前提下,完成這個需求?

靜态代理的做法:

為現有的每一個類都編寫一個對應的代理類,并且讓它實作和目标類相同的接口

Java基礎面試題整理

在建立代理對象的時候,通過構造器塞入被代理對象,然後在代理對象的方法内部調用目标對象同名方法,并在調用前後列印日志。也就是說,<code>代理對象 = 增強代碼 + 目标對象(原對象)</code>。有了代理對象後,就不用原對象了

Java基礎面試題整理

程式員要手動為每一個目标類編寫對應的代理類。如果目前系統已經有成百上千個類,工作量太大了。

現在我們的努力方向是:<code>如何少寫或者不寫代理類</code>,卻能完成代理功能?

Java基礎面試題整理

所謂的Class對象,是Class類的執行個體,而Class類是描述所有類的,比如Person類,Student類

Java基礎面試題整理

可以看出,<code>要建立一個執行個體,最關鍵的就是得到對應的Class對象</code>。

那麼這裡就會抛出一個問題:

<code>能否不寫代理類,而直接得到代理Class對象,然後根據它建立代理執行個體(反射)</code>。

代理類和目标類理應實作同一組接口。

<code>之是以實作相同接口,是為了盡可能保證代理對象的内部結構和目标對象一緻,這樣我們對代理對象的操作最終都可以轉移到目标對象身上,代理對象隻需專注于增強代碼的編寫</code>。

Java基礎面試題整理

但是别忘了,接口是無法建立對象的,怎麼辦?

JDK提供了java.lang.reflect.<code>InvocationHandler</code>接口和 java.lang.reflect.<code>Proxy</code>類。

Proxy有個靜态方法:<code>getProxyClass(ClassLoader, interfaces)</code>,隻要你給它傳入類加載器和一組接口,它就給你傳回代理Class對象。

用通俗的話說,getProxyClass()這個方法,會從你傳入的接口Class中,“拷貝”類結構資訊到一個新的Class對象中,但<code>新的Class對象帶有構造器,是可以建立對象</code>的。

一旦我們明确接口,完全可以通過接口的Class對象,建立一個代理Class,通過代理Class即可建立代理對象。

Java基礎面試題整理
Java基礎面試題整理
Java基礎面試題整理

是以,按我了解,Proxy.getProxyClass()這個方法的本質就是:<code>以Class造Class</code>。

根據代理Class的構造器建立對象時,需要傳入InvocationHandler。

<code>通過構造器傳入一個引用,那麼必然有個成員變量去接收</code>。

代理對象的内部确實有個成員變量invocationHandler,而且代理對象的每個方法内部都會調用handler.invoke()!而且代理對象的每個方法内部都會調用handler.invoke()!