1.HashMap的底層原理
JDK1.7版本之前,HashMap的底層數據結構是數組+鏈表,HashMap通過哈希算法會將元素的key映射待數組的的槽位(Bucket)。如果多個鍵映射到同一個槽位,就會以鏈表的形式存儲在同一個槽位上。但是鏈表的查詢的復雜度O(n),所有沖突比較嚴重,當鏈表的長度很長時,效率就很低了;
所以在JDK1.8之后,一個鏈表的長度超過8的時候就轉換數據結構,不再使用鏈表存儲,而是使用紅黑樹,查找時使用紅黑樹,時間復雜度O(log n),可以提高查詢性能,但是在數量較少時,即數量小于6時,會將紅黑樹轉換回鏈表。
2.Synchronized關鍵字
1. 基本概念
synchronized?是 Java 中實現線程同步的核心機制,通過內置鎖(監視器鎖,Monitor Lock)確保多線程環境下對共享資源的互斥訪問。其底層實現涉及?對象頭、鎖狀態升級、JVM 指令?等關鍵機制。
2. 對象頭與 Monitor
對象頭結構:
Java 對象在內存中分為三部分:
- 對象頭(Header)
- 實例數據(Instance Data)
- 對齊填充(Padding)。
對象頭中的鎖信息:存儲鎖標志位(Lock Flag)、偏向線程ID、輕量級鎖指針、重量級鎖指針等。
鎖標志位(Mark Word):標識當前對象的鎖狀態(無鎖、偏向鎖、輕量級鎖、重量級鎖)。
Monitor 機制:
每個 Java 對象都與一個 Monitor 關聯。
線程執行?synchronized?代碼塊時,需先通過?monitorenter?指令獲取對象的 Monitor。
執行完畢時,通過?monitorexit?指令釋放 Monitor。
Monitor內部具體的存儲結構:
- Owner:存儲當前獲取鎖的線程的,只能有一個線程可以獲取
- EntryList:關聯沒有搶到鎖的線程,處于Blocked狀態的線程
- WaitSet:關聯調用了wait方法的線程,處于Waiting狀態的線程
具體的流程:
- 代碼進入synchorized代碼塊,先讓lock(對象鎖)關聯的monitor,然后判斷Owner是否有線程持有
- 如果沒有線程持有,則讓當前線程持有,表示該線程獲取鎖成功
- 如果有線程持有,則讓當前線程進入entryList進行阻塞,如果Owner持有的線程已經釋放了鎖,在EntryList中的線程去競爭鎖的持有權(非公平)
- 如果代碼塊中調用了wait()方法,則會進去WaitSet中進行等待
3.回答
synchronized 底層使用的JVM級別中的Monitor 來決定當前線程是否獲得了鎖,如果某一個線程獲得了鎖,在沒有釋放鎖之前,其他線程是不能或得到鎖的。synchronized 屬于悲觀鎖。synchronized 因為需要依賴于JVM級別的Monitor ,相對性能也比較低。
monitor對象存在于每個Java對象的對象頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因
monitor內部維護了三個變量
- WaitSet:保存處于Waiting狀態的線程
- EntryList:保存處于Blocked狀態的線程
- Owner:持有鎖的線程
只有一個線程獲取到的標志就是在monitor中設置成功了Owner,一個monitor中只能有一個Owner
在上鎖的過程中,如果有其他線程也來搶鎖,則進入EntryList 進行阻塞,當獲得鎖的線程執行完了,釋放了鎖,就會喚醒EntryList 中等待的線程競爭鎖,競爭的時候是非公平的
syncronized鎖升級的過程講一下
無鎖:這是沒有開啟偏向鎖的時候的狀態,在JDK1.6之后偏向鎖的默認開啟的,但是有一個偏向延遲,需要在JVM啟動之后的多少秒之后才能開啟,這個可以通過JVM參數進行設置,同時是否開啟偏向鎖也可以通過JVM參數設置。
偏向鎖:這個是在偏向鎖開啟之后的鎖的狀態,如果還沒有一個線程拿到這個鎖的話,這個狀態叫做匿名偏向,當一個線程拿到偏向鎖的時候,下次想要競爭鎖只需要拿線程ID跟MarkWord當中存儲的線程ID進行比較,如果線程ID相同則直接獲取鎖(相當于鎖偏向于這個線程),不需要進行CAS操作和將線程掛起的操作。
輕量級鎖:在這個狀態下線程主要是通過CAS操作實現的。將對象的MarkWord存儲到線程的虛擬機棧上,然后通過CAS將對象的MarkWord的內容設置為指向Displaced Mark Word的指針,如果設置成功則獲取鎖。在線程出臨界區的時候,也需要使用CAS,如果使用CAS替換成功則同步成功,如果失敗表示有其他線程在獲取鎖,那么就需要在釋放鎖之后將被掛起的線程喚醒。
重量級鎖:當有兩個以上的線程獲取鎖的時候輕量級鎖就會升級為重量級鎖,因為CAS如果沒有成功的話始終都在自旋,進行while循環操作,這是非常消耗CPU的,但是在升級為重量級鎖之后,線程會被操作系統調度然后掛起,這可以節約CPU資源。
最開始系統時無鎖的狀態,當有第一個線程來訪問時,jvm將對象頭的MarkWord鎖標志設置為偏向鎖,然后將線程id記錄到MarkWord里面,這個時候這個線程進入這個同步代碼塊就不需要其他的同步操作了,就非常的快了;
當第二個線程來搶鎖,就會升級到輕量級鎖,第二個線程拿不到鎖,就會采用cas自旋的方式不斷重新嘗試獲取鎖,輕量級鎖考慮的時競爭鎖線程不多,而且線程持有鎖的時間也不長的一個情景。
當第二個線程自旋到一定的次數之后還是沒有拿到鎖或者有更多線程來搶鎖了就會升級為重量級鎖,重量級鎖加鎖就需要調用操作系統的底層mutex,所以每次切換線程都需要操作系統切換內核態,開銷很大。
這時候MarkWord的指針就會指向鎖監視器monitor。
鎖監視器主要是用來負責記錄鎖的擁有者,記錄鎖的重入次數,負責線程的阻塞喚醒,可以來說鎖監視器就是一個對象有這幾個字段:
owner(持有鎖的線程)
waitSet(等待池)
Eetrylist(鎖池--管理競爭鎖失敗而阻塞的線程)
recursions(記錄鎖的重入次數)
其他的不重要
3.強引用、軟引用、弱引用、虛引用的區別?
強引用: new一個對象就是強引用
軟引用: 被掃描到并且內存不足的時候就會被回收,用于實現內存敏感的緩存。比如圖片緩存
弱引用: 被掃描到就直接回收的可以避免內存泄漏。比如ThearLocal
虛引用: 用于監控對象回收過程
4.TheadLocal
ThreadLocal可以理解為線程本地變量,他會在每個線程都創建一個副本,那么在線程之間訪問內部副本變量就行了,做到了線程之間互相隔離,相比于synchronized的做法是用空間來換時間。
ThreadLocal有一個靜態內部類ThreadLocalMap,ThreadLocalMap又包含了一個Entry數組,Entry本身是一個弱引用,他的key是指向ThreadLocal的弱引用,Entry具備了保存key value鍵值對的能力。
弱引用的目的是為了防止內存泄露,如果是強引用那么ThreadLocal對象除非線程結束否則始終無法被回收,弱引用則會在下一次GC的時候被回收。 但是這樣還是會存在內存泄露的問題,假如key,ThreadLocal對象被回收之后,entry中就存在key為null,但是value有值的entry對象,但是永遠沒辦法被訪問到,同樣除非線程結束運行。
但是只要ThreadLocal使用恰當,在使用完之后調用remove方法刪除Entry對象,實際上是不會出現這個問題的。
ThreadLocal 是一個線程的本地變量,也就意味著這個變量是線程獨有的,是不能與其他線程共享的,這樣就可以避免資源競爭帶來的多線程的問題,這種解決多線程的安全問題和lock(這里的lock 指通過synchronized 或者Lock 等實現的鎖) 是有本質的區別的:
- lock 的資源是多個線程共享的,所以訪問的時候需要加鎖。
- ThreadLocal 是每個線程都有一個副本,是不需要加鎖的。
- lock 是通過時間換空間的做法。
- ThreadLocal 是典型的通過空間換時間的做法
5.線程池
- corePoolSize 核心線程數目
- maximumPoolSize 最大線程數目 = (核心線程+救急線程的最大數目)
- keepAliveTime 生存時間 - 救急線程的生存時間,生存時間內沒有新任務,此線程資源會釋放
- unit 時間單位 - 救急線程的生存時間單位,如秒、毫秒等
- workQueue - 當沒有空閑核心線程時,新來任務會加入到此隊列排隊,隊列滿會創建救急線程執行任務
- threadFactory 線程工廠 - 可以定制線程對象的創建,例如設置線程名字、是否是守護線程等
- handler 拒絕策略 - 當所有線程都在繁忙,workQueue 也放滿時,會觸發拒絕策略
6.拒絕策略有哪些?
1. AbortPolicy (默認策略)
直接拋出異常,拒絕新任務
2.CallerRunsPolicy
將任務回退給提交任務的線程執行
3.DiscardPolicy
·靜默丟棄新提交的任務,不拋出異常,也不執行任務。
4.DiscardOldestPolicy
丟棄隊列中最舊的任務(即隊列頭部的任務),然后嘗試重新提交當前任務。
5.自定義拒絕策略
一般可以把任務持久化到數據庫/消息隊列
記錄日志并觸發警告
7.JVM內存模型
- 程序計數器:可以看作是當前線程所執行的字節碼的行號指示器,用于存儲當前線程正在執行的 Java 方法的 JVM 指令地址。如果線程執行的是 Native 方法,計數器值為 null。是唯一一個在 Java 虛擬機規范中沒有規定任何 OutOfMemoryError 情況的區域,生命周期與線程相同。
- Java 虛擬機棧:每個線程都有自己獨立的 Java 虛擬機棧,生命周期與線程相同。每個方法在執行時都會創建一個棧幀,用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。可能會拋出 StackOverflowError 和 OutOfMemoryError 異常。
- 本地方法棧:與 Java 虛擬機棧類似,主要為虛擬機使用到的 Native 方法服務,在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。本地方法執行時也會創建棧幀,同樣可能出現 StackOverflowError 和 OutOfMemoryError 兩種錯誤。
- Java 堆:是 JVM 中最大的一塊內存區域,被所有線程共享,在虛擬機啟動時創建,用于存放對象實例。從內存回收角度,堆被劃分為新生代和老年代,新生代又分為 Eden 區和兩個 Survivor 區(From Survivor 和 To Survivor)。如果在堆中沒有內存完成實例分配,并且堆也無法擴展時會拋出 OutOfMemoryError 異常。
- 方法區(元空間):在 JDK 1.8 及以后的版本中,方法區被元空間取代,使用本地內存。用于存儲已被虛擬機加載的類信息、常量、靜態變量等數據。雖然方法區被描述為堆的邏輯部分,但有 “非堆” 的別名。方法區可以選擇不實現垃圾收集,內存不足時會拋出 OutOfMemoryError 異常。
- 運行時常量池:是方法區的一部分,用于存放編譯期生成的各種字面量和符號引用,具有動態性,運行時也可將新的常量放入池中。當無法申請到足夠內存時,會拋出 OutOfMemoryError 異常。
- 直接內存:不屬于 JVM 運行時數據區的一部分,通過 NIO 類引入,是一種堆外內存,可以顯著提高 I/O 性能。直接內存的使用受到本機總內存的限制,若分配不當,可能導致 OutOfMemoryError 異常。
8.雙親委派機制
1. 概念解釋
雙親委派機制(Parent Delegation Model)是 Java 類加載器(ClassLoader)中的一種工作機制。它規定了類加載器在加載類時,優先將加載任務委托給父類加載器,只有當父類加載器無法完成加載時,子類加載器才會嘗試自己加載。
這種機制的核心思想是通過層級關系來管理和隔離類加載過程,從而確保類的唯一性和安全性。
2. 工作原理
雙親委派機制的工作流程可以分為以下幾個步驟:
- 委托父類加載器:當一個類加載器收到加載類的請求時,它首先不會自己嘗試去加載這個類,而是將請求委派給父類加載器。
- 遞歸向上委托:父類加載器繼續將請求向上傳遞給它的父類加載器,直到到達頂層的啟動類加載器(Bootstrap ClassLoader)。
- 嘗試加載:如果父類加載器能夠加載該類,則直接返回加載結果;如果父類加載器無法加載(例如找不到類),則子類加載器會嘗試自己加載。
- 加載失敗:如果所有類加載器都無法加載目標類,則拋出
ClassNotFoundException
。
3. 優點與作用
雙親委派機制的設計具有以下優點和作用:
- 類的唯一性:通過雙親委派機制,確保同一個類只會被加載一次,避免重復加載和沖突。例如,核心類庫(如
java.lang.String
)由啟動類加載器加載,保證其全局唯一性。 - 安全性:防止用戶自定義的類冒充核心類庫中的類。例如,用戶不能通過自定義類加載器加載一個名為
java.lang.String
的惡意類,因為這類請求會被委派到啟動類加載器處理。 - 模塊化隔離:不同類加載器負責加載不同的模塊或應用,避免類之間的沖突,增強了系統的模塊化設計。
4. 實際應用場景
- Java 核心類庫加載:啟動類加載器負責加載核心類庫(如
rt.jar
中的類),而擴展類加載器和應用類加載器分別負責加載擴展類庫和應用程序類。 - Web 容器中的類加載:在 Tomcat 等 Web 容器中,每個 Web 應用都有自己獨立的類加載器,遵循雙親委派機制,既能加載共享的類庫(如 Servlet API),又能隔離各個應用的私有類。
- 插件化開發:在一些插件化系統中,主程序和插件使用不同的類加載器,通過雙親委派機制實現類的隔離和共享。
5. 可能的擴展問題
面試官可能會進一步提問一些相關問題,提前準備可以幫助你更好地應對:
- 如何打破雙親委派機制?
- 自定義類加載器,重寫
loadClass
方法,改變默認的加載邏輯。 - 例如,OSGi 框架中通過自定義類加載器實現模塊間的動態加載和卸載。
- 自定義類加載器,重寫
- 為什么需要打破雙親委派機制?
- 在某些場景下,需要支持熱部署、模塊化加載或動態更新,這時需要繞過雙親委派機制。
- 常見的類加載器有哪些?
- 啟動類加載器(Bootstrap ClassLoader)
- 擴展類加載器(Extension ClassLoader)
- 應用類加載器(Application ClassLoader)
- 自定義類加載器(Custom ClassLoader)
9.JVM垃圾回收算法
在面試中回答“JVM垃圾回收算法”時,建議從以下幾個方面入手:先概述垃圾回收的基本概念,然后詳細介紹主流的垃圾回收算法,最后結合實際應用場景進行分析。這樣可以讓面試官感受到你對JVM垃圾回收機制有全面且深入的理解。
1. 垃圾回收的基本概念
垃圾回收(Garbage Collection, GC)是JVM自動管理內存的一種機制,用于釋放不再使用的對象所占用的內存空間。它的主要目標是:
- 自動管理堆內存,減少內存泄漏和手動管理內存的復雜性。
- 提高程序的穩定性和性能。
判定對象是否可回收: - 引用計數法:通過記錄對象被引用的次數來判斷是否可回收。這種方法簡單但容易產生循環引用問題,因此現代JVM通常不使用。
- 可達性分析(GC Roots Tracing):通過一組稱為“GC Roots”的對象作為起點,遍歷對象圖,標記所有可達的對象,其余不可達的對象即為垃圾。
常見的GC Roots包括: - 虛擬機棧中的局部變量表。
- 方法區中的類靜態屬性。
- 本地方法棧中的JNI引用。
2. 主流的垃圾回收算法
JVM中常用的垃圾回收算法主要包括以下幾種:
(1)標記-清除算法(Mark-Sweep)
- 過程:
- 標記:從GC Roots開始,遍歷并標記所有存活對象。
- 清除:遍歷整個堆,將未標記的對象回收。
- 優點:實現簡單。
- 缺點:
- 效率較低,尤其是對象較多時。
- 會產生內存碎片,導致后續大對象分配失敗。
(2)復制算法(Copying)
- 過程:
- 將堆內存分為兩個相等的區域:From Space 和 To Space。
- 每次只使用其中一個區域(From Space),當該區域滿時,將存活對象復制到另一個區域(To Space),并清空原區域。
- 優點:
- 解決了內存碎片問題。
- 回收效率高,適合存活對象較少的場景。
- 缺點:
- 需要雙倍的內存空間。
- 如果存活對象較多,復制成本較高。
(3)標記-整理算法(Mark-Compact)
- 過程:
- 標記:與標記-清除類似,標記所有存活對象。
- 整理:將存活對象向一端移動,清理掉邊界外的垃圾。
- 優點:
- 避免了內存碎片問題。
- 適合老年代(Old Generation)這種存活對象較多的場景。
- 缺點:
- 由于需要整理內存,效率比標記-清除低。
(4)分代收集算法(Generational Collection)
- 背景:根據對象的生命周期特點,將堆內存劃分為新生代(Young Generation)和老年代(Old Generation)。
- 新生代:
- 大部分對象都是“朝生夕死”,采用復制算法。
- 細分為Eden區和兩個Survivor區(S0、S1)。
- 老年代:
- 存活時間較長的對象,采用標記-清除或標記-整理算法。
- 優點:
- 針對不同生命周期的對象選擇不同的回收策略,提升效率。
- 減少不必要的掃描和復制操作。
3. 常見的垃圾回收器
JVM提供了多種垃圾回收器,每種回收器適用于不同的場景。以下是幾種主流的垃圾回收器及其特點:
- Serial GC:單線程回收,適合小型應用或單核環境。
- Parallel GC(吞吐量優先):多線程并行回收,適合對吞吐量要求較高的場景。
- CMS(Concurrent Mark-Sweep):以最短停頓時間為目標,適合對響應時間敏感的應用。
- G1(Garbage First):基于分區的垃圾回收器,兼顧吞吐量和停頓時間。
- ZGC 和 Shenandoah:低延遲垃圾回收器,適合超大規模內存的應用。
10.IOC和AOP
IOC:即控制反轉的意思,它是一種創建和獲取對象的技術思想,依賴注入(DI)是實現這種技術的一種方式。傳統開發過程中,我們需要通過new關鍵字來創建對象。使用IoC思想開發方式的話,我們不通過new關鍵字創建對象,而是通過IoC容器來幫我們實例化對象。 通過IoC的方式,可以大大降低對象之間的耦合度。
AOP: 面向切面編程,用于將那些與業務無關,但卻對多個對象產生影響的公共行為和邏輯,抽取并封裝為一個可重用的模塊,這個模塊被命名為“切面”(Aspect),減少系統中的重復代碼,降低了模塊間的耦合度,同時提高了系統的可維護性
11.Mysql事務
事務就是一組操作的集合,他是不可分割的一部分,這些操作要么同時成功,要么同時失敗。
- 原子性:是不可分割的最小操作單元,要么全部成功,要么全部失敗。
- 一致性:事務完成時,必須使所有的數據都保持一致狀態。
- 持久性:一旦提交或回滾,它對數據庫中的數據的改變就是永久的。
- 隔離性:事務在不受外部并發操作影響的獨立環境下運行
12.索引
索引的定義:
索引時數據庫在存儲引擎中,幫助存儲引擎高效獲取數據的一種數據結構。。例如書中的目錄,如果沒有索引要找內容就需要全書查找,有了目錄(也就輸索引)就可以提高查找效率
為什么InnoDb存儲引擎中索引使用B+樹?
首先對于B樹:B+Tree 只在葉子節點存儲數據,而 B 樹的非葉子節點也要存儲數據,所以在相同的磁盤I/O 次數下,就能查詢更多的節點。另外,B+Tree 葉子節點采用的是雙鏈表連接,適合 MySQL 中常見的基于范圍的順序查找,而 B 樹無法做到這一點;
然后二叉樹而言:對于N個葉子節點的B+樹,他的搜索復雜度為O(log dN),d表示節點允許的最大節點數。當d為100是,干萬數據,B+樹的高度維持在3,4層,一般3,4次磁盤IO就可以得到數據。而二叉樹高度很高,需要的磁盤IO次數要更多
最后對于Hash:Hash不能做范圍查找和排序,更適合等值查詢
13.MVCC
MVCC主要依賴以下機制實現:
- 數據庫行記錄中的隱藏字段:最近修改本行數據的事務id、回滾指針指向本行數據的上個版本(其實是指向undo,通過undo計算上一個版本)
- undo log:回滾日志,在insert、delete、update時產生,記載了與操作相反的操作,比如delete操作對應了一條insert日志
- readview:快照讀SQL執行時產生的讀視圖,生成的一個快照,記錄了以下字段,用于數據的可見性判斷:
- 當前活躍的事務id集合
- 最小活躍事務id
- 預分配事務id,即應該分配的下一個事務id
- readview創建者id,即本事務id
MVCC如何判斷數據的可見性?
首先根據本條數據的是否是由本事務生成的,如果是,那可見,畢竟自己做的修改自己肯定是能看見的
再判斷本條數據的事務id是否比最小活躍事務id要小,如果是,那可見,因為這說明這條數據是已經被其它事務提交的,本事務應該可見
再判斷本條數據的事務id是否不在活躍事務id集合中,如果不在,那可見,因為這說明也是其它事務提交了的數據
如果本條數據對本事務不可見,就根據隱藏字段 + undo log計算出上一個版本的數據,然后繼續判斷,直到找到某條對本事務可見的數據
14.Redis常見的數據結構
String(字符串):
Hash(哈希表)
List(列表)
Set(集合)
Sorted Set(有序集合)
HyperLogLog(基數統計)
Bitmaps(位圖)
Geospatial(地理位置)
Stream(流)
15.redis三大件
16.消息可靠性投遞,消息丟失和重復消費問題
消息可靠性:
1.消息持久化,確保消息隊列的持久化
2.消息確認,消費者成功處理消息后,會向消息隊列發送確認。消息隊列只有收到確認消息才將消息刪除。沒有收到消息,消息隊列要在一定時間重發。
3.消息重試,消息者處理消息失敗后,要合理的重試策略
消息丟失:
1.消息生產階段: 生產消息到MQ的過程中,只要能正常收到MQ的ACK確認響應。
2.消息存儲階段: 可以部署多個集群,MQ也要開啟持久化功能保證消息不丟失
3.消息消費階段: 消費者接收消息+消息處理之后,才回復ACK。
重復消費:
唯一標識:客戶端為每一個請求都生成一個全局唯一id,服務端校驗id是否被處理(接口調用)
數據庫事務/樂觀鎖:通過版本號或者狀態字段來控制并發更新,確保多次更新等同一次操作(余額扣減)
分布式鎖:通過分布式鎖保證同一時刻只能有一個請求執行(秒殺業務)
消息去重:消息隊列生產者為每條消息生成唯一id,消費者處理消息先校驗id是否處理過。
17.什么情況會產生死鎖問題?如何解決?
互斥條件:多個線程不能同時使用同一個資源
持有并等待條件:線程在持有至少一個資源的同時,請求獲取其他被占用的資源,并等待而不釋放已持有的資源
不可剝奪條件:線程已獲取的資源沒有使用完之前,不能被其他線程強行剝奪,只能等持有者主動釋放
循環等待條件:倆個線程獲取資源的順序構成了環形鏈
解決方法
最常用:使用資源有序分配法,來破環環路等待條件
線程 A 和 線程 B 獲取資源的順序要一樣,當線程 A 是先嘗試獲取資源 A,然后嘗試獲取資源 B 的時候,線程 B 同樣也是先嘗試獲取資源 A,然后嘗試獲取資源 B。也就是說,線程 A 和 線程 B 總是以相同的順序申請自己想要的資源
Redis的持久化
RDB()
AOF()