者:ecznlai@騰訊文檔
前段時間通過優化業務里的相關實現,將高頻調用場景性能優化到原來的十倍,使文檔核心指標耗時達到 10~15% 的下降。本文將從 V8 整體架構出發,深入淺出 V8 對象模型,從匯編細節點出其 ICs 優化細節以及原理,最后根據這些優化原理來編寫超快的 JS 代碼
js 代碼從源碼到執行 —— v8 編譯器管線:
parser 將源碼編譯為 AST,并在 AST 基礎上編譯為「字節碼 bytecode」
ignition 是 v8 的字節碼解釋器,可以運行字節碼,并在運行過程中持續收集「feedback」即綠線,給到 turbofan 做最終的機器碼編譯優化。
而由于 js 是相當動態的語言,編譯出來的「機器指令」未必能正確,因此其運行過程中有可能要回滾到 ignition 解釋器來運行,這些問題通過「紅線」反饋給 ignition 解釋器,這個過程叫做「反優化」。
—— 更具體來說:
將源碼一段線性 buffer string 解析為 Token 流,最后依據 Token 流生成 AST 樹狀構造,這是所有語言都會有的過程。
運行過程中產生并持續收集的反饋信息,比如多次調用 add(1, 2) 就會產生「add 函數的兩個參數 “大概率” 是整數」的反饋,v8 會收集這類信息,并在后續 TurboFan codegen 的時候根據這些反饋來做假設,并依據這些假設做深度優化,后文將從匯編的角度討論這個細節。
前面提到 「add 函數的兩個參數 “大概率” 是整數」 的假設,當假設被打破的時候會觸發所謂的「deoptimize」反優化,比如你在運行了很久的 add(number, number) 上突然來一個 add("123", "abc") 那么此時就會降級重新回到 ignition bytecode 執行。
前者生成 byte code,后者根據執行過程中收集的 feedback 來生成深度優化的 machine code
世界上能執行代碼的地方有很多,數軸上的兩個極端: 左邊是抽象程度最高的人腦,右邊是抽象程度最低的 CPU:
上圖中三個實體以不同的角度理解下面這樣的代碼,從源碼到字節碼再到機器碼其實就是不斷編譯為另外一個語言的過程
const a=3 + 4;
計算 3+4 存儲到 js 變量 const a 中
將代碼解析為 AST 樹(一種 JSON 結構)
iginition 會將代碼理解編譯為 bytecode :
...
LdaSmi [3] // 加載字面量 3 到棧頂
Star0 // 將棧頂 3 pop 到寄存器 r0
Add r0, [4] // 計算 r0 + 4
...
TurboFan 會將代碼理解為匯編:
...
mov ax 3 # 將 3 賦值到寄存器 ax
add ax 4 # 計算 ax=ax + 4
...
本質上來說 v8 bytecode 和 x86 匯編是一樣的,只是世界上沒有裸機能跑出 v8 所理解的 bytecode 而已,機器碼為什么快是因為 CPU 能在硬件層面上裸跑匯編,因此速度特別快。
總之為了充分表達 js 動態特性以及方便優化為 CPU 能直接裸跑的匯編,v8 引入了 bytecode 這個層次,它比 AST 更接近物理機,因為它沒有層次嵌套,是一種基于寄存器的指令集。
JIT 指的是邊運行邊優化為機器碼的編譯技術,其中的代表有 jvm / lua jit / v8,這類優化技術會在運行過程中持續收集執行信息并優化程序性能。AOT 指的是傳統的編譯行為,在靜態類型語言(如 C、C++、Rust)和某些動態類型語言(如 Go、Swift)中得到了廣泛應用,由于能提前看到完整代碼,編譯器/語言運行時可以在編譯階段進行充分的優化,從而提高程序的性能。
由于 JIT 語言并不能提前分析代碼并優化執行,因此 JIT 語言的「編譯期」很薄,而「運行時」相當厚實,諸多編譯優化都是在代碼運行的過程中實現的。
ignition 負責解釋執行 V8 引入的中間層次字節碼,上接人腦里的 js 規范,下承底層 CPU 機器指令
TurboFan 可以將字節碼編譯為最快的機器碼,讓裸機直接運行,達到最快的執行速度。
利用這個參數開啟 v8 注入的 runtime call,幫助分析和調試 v8
# node 下開啟
$ node --allow-natives-syntax
# chrome 下開啟
$ open -a Chromium --args --js-flags="--allow-natives-syntax"
下面是一些常用指令說明。
可以打印對象在 v8 的內部信息,比如打印一個函數:
告訴 v8 下次調用主動觸發優化函數 fn
獲取函數當前的優化 status,后文會詳細介紹:
對應的是 V8 源碼里的這個枚舉:
從開發視角來看,一個函數最佳的 status 應該是 00000000000001010001 (81) 即:
%HasFastProperties 可以用來打印對象是否是 Fast Properties 模式
后文會介紹這個 Fast Properties 和與之對立的 Slow Properties。
首先 Tagged Pointer 是 C/C++ 里常用的優化技術,不只在 V8 里有用,具體來說就是依據 pointer 自身的數值的某些位來決定 pointer 的行為,也就是說這類指針的特點是「其指針數值上的某些位有特殊含義」。
比如在 v8 里,js 堆指針和 SMI 小整數類型(small intergers)是通過 Tagged Pointer 來表達和引用的,區別就在于最低一位是不是 0 來決定其指針類型:
對象指針(32 位):
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxx1
SMI 小整數(32 位)其中 xxx 部分為數值部分:
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxx0
用 C 表達就是這樣:
#include <stdio.h>
void printTaggedPointer(void * p) {
// 強轉一下, 關注 p 本身的數值
unsigned int tp=((unsigned int) p);
if ((tp & 0b1)==0b0) {
printf("p 是 SMI, 數值大小為 0x%x \n", tp >> 1);
return;
}
printf("p 是堆對象指針, Object<0x%x> \n", tp);
// printObject(*p); // 假設有個方法可以打印堆對象
}
int main() {
printTaggedPointer(0x1234 << 1); // smi
printTaggedPointer(17); // object
return 0;
}
運行效果:
備注:
我們先來看這個例子,一個 add(x,y) 函數,如果運行期間出現了多種類型的傳參,那么會導致代碼變慢:
我們可以看到,L15 速度慢了非常多,比一開始的 66ms 慢了幾倍。
原因:
比如一開始傳的是 number,走到了優化過的代碼,里面走的是匯編指令 add;當傳入 string 或者 其他什么合法的 JSValue 后,編譯為匯編的 add 函數的執行真的沒問題嗎?—— 不會有問題,因為 TurboFan 在編譯后的「機器碼」里會帶上很多 checkpoint,其實這些 checkpoint 就是在做類型檢查 type guard,如果類型對不上立刻就會終止這次調用并執行「反優化」讓 ignition 走字節碼解釋執行。
上述說法可能會比較含糊,我們可以具體看看打出來的匯編是咋樣的,可以通過以下方式打印出優化后的 x86 匯編(m1 芯片的蘋果電腦應該是 arm 指令)。
$ node --print-opt-code --allow-natives-syntax --trace-opt --trace-deopt ./a.js
如下圖所示,這個 test 函數實現是將第一個入參加上 0x1234 并返回,而這個核心邏輯對應 L37 那行匯編,而其他的部分除了 v8 自身的「調用約定」外,其他的就是 checkpoint 檢查類型,以及一些 debug 斷點了:
從前面的 Tagged Pointer 的相關討論可知,L19 ~ L22 其實就是在判斷入參是不是 SMI,具體來說是 [rbx+0xf] 與 0x1 做按位與操作([rbx+0xf] 是通過棧傳遞的參數,是 v8 里 js 的調用約定)如果結果是 0 則跳轉 0x10b7cc34f 即后續的正常流程,否則走到 CompileLazyDeoptimizedCode 走反優化流程用字節碼解釋器去執行了,我這里大概寫了一個反匯編偽碼對照:
另外我們也可以看到,核心邏輯對應到匯編也就一行,剩余的指令要么是 checkpoint 要么是 v8/js 的調用約定,在這么多冗余指令的情況下執行性能依然很快,可見匯編的執行效率比起 line-by-line 的解釋器要高得多了。
通過 %DebugPrint 可以看到
當打破這個 assumption 后,會變成 Any:
不會
根據前面提到的 checkpoint,上面三個 mono 的 checkpoint 最少,而最后的 mega 將會非常多,優化性能最差,或者 V8 干脆就不會對這類函數做更深度的機器碼優化了(比如后文會提到的 ICs)
從 JS AST / bytecode 編譯到機器碼也需要開銷,毫秒級。
根據這篇文章 V8 function optimization - Blog by Kemal Erdem 如果某個函數「反優化」超過 5 次后,v8 以后就不再會對這個函數做優化了,不過我無法復現他說的這個情況,可能是老版本的 v8 的表現,node16 不會這樣,不管怎樣只要 run 了足夠多次都turbofanned,只是如果「曾經傳的參數類型太 union typed」會導致優化效果出現非常大的折損。
前面我們已經知道了「運行足夠多次」會觸發優化,而這只是其中一種情況,具體可以參考 v8 里 ShouldOptimize 的實現,里面有詳細定義何時啟動優化:
作為開發視角來看:
備注:maglev 是去年 chrome v8 團隊搞的新特性 —— 編譯層次優化,總的來說就是根據 feedback 對機器碼的編譯層次做精細控制來達到更好的優化效果,下圖是 v8 團隊發布的 benchmark 對比:
具體可參考 v8.dev/blog/maglev
會的,而且有時候這部分內存占用非常多,這也是 Chrome 經常被調侃為內存殺手的重要原因之一,以 qq.com 為例,具體對應是 heapdump 里的 (compiled code) 包含了編譯后的代碼內存占用:
本節開始是本文的重點部分,因為只有了解 V8 對象的內存構造,才能真正理解 V8 諸多優化的理由。
在正式進入之前,我們先看看 C 里面 struct 的「點讀」是怎么做的。
C 會將 struct 理解為一段連續的線性 buffer 結構,并在上面根據字段的類型來劃分好從下標的哪里到哪里是哪個字段(對齊),因此在編譯 point.x 的時候會改成 base+4 的方式進行屬性訪問,如下圖所示,時間復雜度是 O(1) 的:
也因此 C 里面沒提供從字段 key 名的方式去取 struct value 的方法,也就是不支持 point['x']這樣,需要你自己寫 getter 才能實現類似操作。
這類根據 string value 來從對象取值的技術通常在現代編程語言里都是自帶了的,通常稱為反射,可以在運行時訪問源碼信息。
但在 JS 里,對象是動態的,可以有任意多的 key-values,而且這些 kv 鍵值對還可能在運行時期間動態發生變化,比如我可以隨時 p.xxx=123 又或者 delete p.xxx 去刪掉它,這意味著一個 object 的 “shapes” 及其「內存結構」是無法被靜態分析出來的,而且這種內存結構必然不是「定長固定」的,是需要動態 malloc 變長的。
假設現在是 2008 年,你是 google 的工程師,正在 chrome v8 項目組開發,你會怎樣設計 JS 的對象的內存結構?
const obj={ x: 3, y: 5 }
// obj 的內存結構可以設計成怎樣?
一眼丁真,開搞:
一個 key 定義加一個值,然后將這個結構數組化就可以表達對象的 kv 結構,增加屬性就在后面繼續擴增,查找算法則是從頭查到尾,時間復雜度為 O(n)
但是如果按這個設計,下面兩個 obj 就會有重復的 key 定義內存消耗了:
const obj1={ x: 11, y: 22 } // "x" 11 "y" 22
const obj1={ x: 33, y: 44 } // "x" 33 "y" 44
// 會重復 "x" 和 "y"
好了就上面這樣簡單弄一下就搞出了好多問題了。從下面開始正式進入,V8 是如何描述對象,參見下文。
在 js 標準里 Array 是一類特殊的 Object,但出于性能考慮 V8 底層針對對象和數組的處理是不同的:
如下圖所示,JSObject:
在 V8 里:
嗯?對象的 Shapes?那是什么?
所謂對象的 shapes,其實就是對象上有什么 key,前面提到過 V8 的優化需要在運行時不斷收集 feedback,比如當執行下面這段代碼的時候,引擎就可以知道「obj 有兩個 key,一個是 a 一個是 b」:
const obj={}
obj.a=123;
obj.b=124;
doSomething(obj);
V8 通過 Hidden Class 結構來記錄 JSObject 在運行時的時候有哪些 key,也就是記錄對象的 shapes,由于 JSObject 是動態的,后續也可以隨意設置 obj.xxx=123,也就是對象的 shapes 會變,也因此對象持有的 Hidden Class 會隨著特定代碼的運行而變化
Hidden Class 是比較學術的說法,在 V8 源碼里的「工程命名」是 Map,在微軟 Edge Chakra (edge) 里叫做 Types,在 JavaScriptCore (WebKit Safari) 里叫做 Structure,在 SpiderMonkey (FireFox) 里叫做 Shapes .... 總之各個主流引擎都有實現追蹤「對象 shapes 變化」
后文可能會混淆上面幾個用語,它們都是指 Hidden Class,用來描述對象的 shapes。
前面提到除了 *properties 和 *elements 可以用來存儲對象成員之外,JSObject 還提供了所謂 in-object properties 的方式來存儲對象成員,也就是將對象成員保存在「JSObject 結構體」上,并配合 Hidden Class 進行鍵值描述:
上圖里 Hidden Class 里底下有個叫做 DescriptorArrays 的子結構,這個結構會記錄對象成員 key 以及其對應存儲的 in-object 下標,也就是上面的紫框。
或許你會問:
如果 Hidden Class 是靜態的,那么這圖就足夠描述 Hidden Class 了:
但是對象的 shapes 會變,也因此對象持有的 Hidden Class 會隨著特定代碼的運行而變化,V8 使用了 Transition Chain,一種基于鏈表構造的方式來描述「變化中的 Hidden Class」:
備注:為了方便討論,后文可能不會將 Hidden Class 畫成鏈表,而是畫成一起并且省略空對象的 shapes,另外 Hidden Class Node 上還有其他字段,相對不那么重要,就忽略了
由于鏈表的特性,顯然可以比較容易地讓具有相同 shapes 的對象能復用同一個 Hidden Class ,比如下面這個 case,o1 o2 均復用了地址為 0xABCD 的 Hidden Class 節點:
當出現不同走向的時候,此時會單獨開一個 branch 來描述這種情況,此時 o1 和 o2 就不再一樣了:
從前文的討論,可以得到的結論:
懸而未決的問題:
請帶著這兩個問題到下一章 Inline Caches 繼續閱讀。
引入 Hidden Class 后,為了讀取某個成員,那不還得查一次 Hidden Class 拿到 in-object 的下標,這個過程不還是 O(n) 嗎?
是的,如果事先不知道 JSObject 的 shapes 的情況下去讀取成員確實是 O(n) 的,但前面我已經提過了:
V8 的諸多優化是基于 assumption 的,那么在已知 obj 的 Shapes 的情況下,你會怎么優化下面這個 distance 函數?
如此優化就可以將「通過遍歷 *properties訪問成員的O(n) 過程」直接優化為「直接按下標偏移直接讀取 `in-object` 的 O(1)過程」了,這種優化手段就叫做 Inline Caches (ICs),有點類似 C 語言的 struct 將字段點讀編譯為偏移訪問,只不過這個過程是 JIT 的,不是 C 那樣 AOT 靜態編譯確定的,是 V8 在函數執行多次收集了足夠多的 feedback 后實現的。
你可能還會問:在調用優化后的 distance2 的時候具體要怎么確定傳入的 p1 p2 的 shapes 是否有變化?還記得前面那個 0xABCD 嗎?沒錯,編譯后的匯編 checkpoint 就是直接判斷傳入對象的 hidden classs 指針數值是不是 *0xABCD*,如果不是就觸發「反優化」兜底解釋器模式運行即可。
—— 下面這個實例將手把手介紹 ICs 的真實場景以及匯編細節
從前面 Inline Cache 的討論中可以得知,必須要確定了訪問的 key 才能做 ICs 優化,因此寫代碼的過程中,如有可能請盡量避免下面這樣通過 key string 動態查找對象屬性:
function test(obj: any, key: string) {
return obj[key];
}
如果能明確知道 key 的具體值,此時建議寫為:
function test(obj: any, key: 'a' | 'b') {
if (key==='a') return obj.a;
if (key==='b') return obj.b;
}
即使確實不得不動態查詢,但是你知道某個子 case 占了 99% 的調用次數,此時也可以這樣優化:
function test(obj, key: 'a' | 'b') {
// 為 'a' 的調用次數占了 99% 可以這樣提前優化
if (key==='a') return obj.a;
return obj[key];
}
靜態和動態兩種寫法風格可能會有幾倍甚至上百倍的差距,如果業務里有大幾百萬次的調用 test,優化后能省不少毫秒,比如下面這個「簡化的服務發現」例子有近百倍的差距:
原因是 s2.js 里那些屬性訪問都被 ICs 技術優化成 O(1) 訪問了,速度很快 —— 為了探究內部的 ICs 相關匯編邏輯,嘗試輸出 serviecMap 的 Hidden Class (V8 里 hidden class 別名是 Map) 以及匯編源碼:
首先 %DebugPrint 出 serviceMap 的 Hidden Class 的物理地址,可以看到是 0x3a8d76b74971 然后看后續編譯優化的 arm machine code 是怎么利用這個地址實現 ICs 技術優化的:(筆者這會的電腦是 mac m1 因此是 arm 匯編,不是 x86 匯編)。
可以看到,ICs 優化后匯編的 checkpoint 其實就是將 Hidden Map 的指針物理地址直接 Inline 到匯編里了,通過判等的方式來驗證假設,然后就可以直接將屬性訪問優化為 O(1) 的 in-object properties 訪問了,這也是這個技術為什么叫做 Inline Cahce (ICs) 了。
(這幾乎是 V8 里效果最好的優化了,也因此部分 benchmark 里 nodejs 對象可能比 Java 對象還快,因為 Java 里有可能濫用反射導致對象性能非常差)。
如果知道 ICs 技術內涵的話,理解 Fast Properties 和 Slow Properties (或者稱字典模式) 就不會有困難了。
下圖描述了 JSObject 的主要構造:當把對象成員存儲到 in-object properties 的時候,此時稱對象是 Fast Properties 模式,這意味著對象訪問 V8 會在合適的時候將其 Inline Cache 到優化后的匯編里;反之,當成員存儲到 *properties 的時候,此時稱為 Slow Properties,此時就不會對這類對象做 inline cache 優化了,此時對象訪問性能最差(因為要遍歷 *properties字典,通常慢幾十到幾百倍,取決于對象成員數量)。
我們可以用 %HasFastProperties 來打印對象是否是 Fast Properties 模式,如下圖所示:
delete 會將對象轉為 slow properties 模式,為什么呢?因為 delete 帶來的問題可太多了,緩存技術最怕的就是 delete,如圖所示:
我拍腦子就能想到上面四個問題,要完整的確保 delete 的安全性可太難了,因此維護 delete 后的 hidden class 非常麻煩,V8 采取的方式是直接將 in-object 釋放掉,然后將對象屬性都復制存儲到 *properties 里了,以后這個對象就不再開啟 ICs 優化了,此時這種退化后的對象就稱為 slow properties (或者稱字典模式)。
Hidden Class 是比較學術的名字,在 V8 里對應的「工程命名」是 Map,可以在 heapdump 里看到:
利用查找 Hidden Class 的方式可以快速定位大批量相同 shapes 的對象哦,很方便查找內存溢出問題。
跟 C++ 里的 inline 關鍵字一樣,將函數直接提前展開,少一次調用棧和函數作用域開銷。
基于 Sea Of Nodes 的 PL 理論進行優化,分析對象生命周期,如果對象是一次性的,那么就可以做編譯替換提升性能,比如下圖里對象 o 只用到了 a,那么就可以優化成右邊那樣,減少對象內存分配并提升尋址速度:
通過打 heapdump 的方式可以發現下面第二行的空對象的 shallow size 是 28 字節,而后一個是 16 字節:
window.arr=[]; // 打一次 heapdump
arr.push({}); // 打一次 heapdump
arr.push({ ggg: undefined });
原因:V8 假設空對象后面都會設置新的 key 上去,因此會預先 malloc 了一些 in-object 字段到 JSObject 上,最后就是 28,比 16 要大;而第三行這樣固定就只會 malloc 一個 in-object 字段了(其實看圖里還有一個 proto 字段)。
那么 new Object() 呢?一樣會;如果是 Object.create(null) 呢?這種情況就不會申請了,shallow size 此時最小,為 12 字節。
28 - 12=16 字節,而一個指針占 4 字節,因此 V8 對一個空對象會默認為其多創建 4 個 in-object 字段以備后續使用,而這類預分配的內存空間,會在下次 GC 的時候將沒用到的回收掉,這項技術叫做 「Slack Tracking 松弛追蹤」。
v8 里還有很多針對 string / Array 的優化技術,本次技術優化主要涉及 ICs 相關優化,就不展開寫了,參見后文鏈接(其實大部分對象優化技術都是圍繞 V8 對象模型來進行的)。
Safari 的 WebKit JSCore 引擎也有基于 LLVM 后端的 JIT 技術,因此很多優化手段是共通的,比如 safari 也有 type feedback 和屬性追蹤,也有自己的 hidden class / ICs 實現,可以打開 safari 的調試工具看到運行時的 type feedback:(macOS、iOS、iPadOS 上都有 JIT,在 chrome 上優化后全平臺都能受益)。
在這些優化技術的加持上,safari jscore 某些情況下甚至會比 chrome v8 還要快:
大部分業務場景里更關心可維護性,性能不是最重要的,另外就是面向引擎/底層優化邏輯寫的 js 未必是符合最佳實踐的,有時候會顯得非常臟,這里總結一下個人遇到的常見實例對照,供參考:
熱點函數會優先走 turbofan 編譯為機器碼,性能會更好,要如何利用好這個特性?將項目里的一些高頻原子操作拆成獨立函數,人為制造熱點代碼,比如計算點距離,單位換算等等這些需要高性能的地方:
除了前面提到的熱區之外,拆解后的函數如果足夠短,那么 V8 在調用的時候會做 inline 展開優化,節省一次調用棧開銷。
從前面的 add 的例子我們可以知道,V8 TurboFan 優化是基于 assumption 的,應該盡量保持函數的單態性 (Monomorphic),或者說減少函數的狀態,具體來說高頻函數不要傳 Union Types 作為參數。(這個不夠準確,最好是不要打破參數的 V8 內部類型表示以及匯編 checkpoint,比如一會傳浮點數、一會傳 SMI 這樣即使都是 number 也會打破 v8 的假設,因為 v8 內部實現的浮點數會裝箱,而小整數 SMI 不會,兩者的匯編邏輯不一樣)。
推薦使用 TypeScript 來寫 js 應用,限制函數的入參類型可以有效保證函數的單態性質,更容易編寫高性能的 js 代碼
賦值順序的不同會產生不同的 Hidden Class 鏈,不同的鏈不能做 ICs 優化。
class A {
a?: number
}
class A {
a=undefined // 或 null
}
理由跟前一點一樣,前者 A 有 shapes 鏈是 空對象+a,而后者就是確定的 a 了。
但是,賦值會多消耗一點內存,內存敏感型場景慎用。
delete 后會將對象轉為 Slow Properties 模式,這種模式下的對象不會被 inline cache 到優化后的匯編機器碼里,對性能影響比較大,另外這樣的對象如果到處傳的話就會到處觸發「反優化」將污染已經優化過的代碼。
前面的例子里提到,反優化后的函數再優化性能不會比最開始要好,換言之被「feedback 污染」了,我們應當盡量避免反優化的出現(即 checkpoint 被打破的情況)。
前面已經討論過這類情況了,靜態種寫法 V8 可以做 ICs 優化,將屬性訪問直接改為 in-object 訪問,速度可以比動態 key 查找快近百倍。
const obj={ a: 1, b: 2 };
const obj={};
obj.a=1;
obj.b=2;
從 Hidden Class 的角度來看,第二種會使 Hidden Class 變化三次,而第一種直接聲明其實就隱含了 Hidden Class 了,V8 可以直接提前靜態分析得出。
v8 會分析 ast,將左側優化成右側。
在 React / Vue 里有這種 Ref 構造來實現訪問同一個實例的操作(類似指針)
type Ref<T>={
ref: T
}
// React 的是 current 作為 key
type ReactRef<T>={ current: T }
前面提到過的 ICs 優化,因此上述這樣的構造并不會造成嚴重的性能損失,會多消耗一點內存,大多數情況下可以放心使用(多消耗 16 字節)。
這塊參考了大量資料,有的地方只有源碼里才有,這里簡單列一下:
另外特別感謝元寶對我工作的大力支持 ??
本文會按技能模塊劃分文章段落,每個模塊里的內容,從易到難依次進行排序,各模塊之間不存在互相關聯的關系,讀者可選擇文章順序閱讀或者跳躍式閱讀。
本文分為十九個模塊,分別是: Java 基礎、容器、多線程、反射、對象拷貝、Java Web 、異常、網絡、設計模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM ,如下圖所示:
共包含 208 道面試題,本文的宗旨是為讀者朋友們整理一份詳實而又權威的面試清單,下面一起進入主題吧。
具體來說 JDK 其實包含了 JRE,同時還包含了編譯 Java 源碼的編譯器 Javac,還包含了很多 Java 程序調試和分析的工具。簡單來說:如果你需要運行 Java 程序,只需安裝 JRE 就可以了,如果你需要編寫 Java 程序,需要安裝 JDK。
==解讀
對于基本類型和引用類型==的作用效果是不同的,如下所示:
代碼示例:
String x="string";
String y="string";
String z=new String("string");
System.out.println(x==y); // true
System.out.println(x==z); // false
System.out.println(x.equals(y)); // true
System.out.println(x.equals(z)); // true
代碼解讀:因為 x 和 y 指向的是同一個引用,所以==也是 true,而 new String()方法則重寫開辟了內存空間,所以==結果為 false,而 equals 比較的一直是值,所以結果都為 true。
equals 解讀
equals 本質上就是==,只不過 String 和 Integer 等重寫了 equals 方法,把它變成了值比較。看下面的代碼就明白了。
首先來看默認情況下 equals 比較一個有相同值的對象,代碼如下:
class Cat {
public Cat(String name) {
this.name=name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name=name;
}
}
Cat c1=new Cat("王磊");
Cat c2=new Cat("王磊");
System.out.println(c1.equals(c2)); // false
輸出結果出乎我們的意料,竟然是 false?這是怎么回事,看了 equals 源碼就知道了,源碼如下:
public boolean equals(Object obj) {
return (this==obj);
}
原來 equals 本質上就是==。
那問題來了,兩個相同值的 String 對象,為什么返回的是 true?代碼如下:
String s1=new String("老王");
String s2=new String("老王");
System.out.println(s1.equals(s2)); // true
同樣的,當我們進入 String 的 equals 方法,找到了答案,代碼如下:
public boolean equals(Object anObject) {
if (this==anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString=(String)anObject;
int n=value.length;
if (n==anotherString.value.length) {
char v1[]=value;
char v2[]=anotherString.value;
int i=0;
while (n-- !=0) {
if (v1[i] !=v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
原來是 String 重寫了 Object 的 equals 方法,把引用比較改成了值比較。
總結 :==對于基本類型來說是值比較,對于引用類型來說是比較的是引用;而 equals 默認情況下是引用比較,只是很多類重新了 equals 方法,比如 String、Integer 等把它變成了值比較,所以一般情況下 equals 比較的是值是否相等。
不對,兩個對象的 hashCode() 相同,equals() 不一定 true。
代碼示例:
String str1="通話";
String str2="重地";
System. out. println(String. format("str1:%d | str2:%d", str1. hashCode(),str2. hashCode()));
System. out. println(str1. equals(str2));
執行的結果:
str1:1179395 | str2:1179395
false
代碼解讀:很顯然“通話”和“重地”的 hashCode() 相同,然而 equals() 則為 false,因為在散列表中,hashCode() 相等即兩個鍵值對的哈希值相等,然而哈希值相等,并不一定能得出鍵值對相等。
等于 -1,因為在數軸上取值時,中間值(0.5)向右取整,所以正 0.5 是往上取整,負 0.5 是直接舍棄。
String 不屬于基礎類型,基礎類型有 8 種:byte、boolean、char、short、int、float、long、double,而 String 屬于對象。
操作字符串的類有:String、StringBuffer、StringBuilder。
String 和 StringBuffer、StringBuilder 的區別在于 String 聲明的是不可變的對象,每次操作都會生成新的 String 對象,然后將指針指向新的 String 對象,而 StringBuffer、StringBuilder 可以在原有對象的基礎上進行操作,所以在經常改變字符串內容的情況下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的區別在于,StringBuffer 是線程安全的,而 StringBuilder 是非線程安全的,但 StringBuilder 的性能卻高于 StringBuffer,所以在單線程環境下推薦使用 StringBuilder,多線程環境下推薦使用 StringBuffer。
不一樣,因為內存的分配方式不一樣。String str="i"的方式,Java 虛擬機會將其分配到常量池中;而 String str=new String("i") 則會被分到堆內存中。
使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
示例代碼:
// StringBuffer reverse
StringBuffer stringBuffer=new StringBuffer();
stringBuffer. append("abcdefg");
System. out. println(stringBuffer. reverse()); // gfedcba
// StringBuilder reverse
StringBuilder stringBuilder=new StringBuilder();
stringBuilder. append("abcdefg");
System. out. println(stringBuilder. reverse()); // gfedcba
不需要,抽象類不一定非要有抽象方法。
示例代碼:
abstract class Cat {
public static void sayHi() {
System. out. println("hi~");
}
}
上面代碼,抽象類并沒有抽象方法但完全可以正常運行。
不能,定義抽象類就是讓其他類繼承的,如果定義為 final 該類就不能被繼承,這樣彼此就會產生矛盾,所以 final 不能修飾抽象類,如下圖所示,編輯器也會提示錯誤信息:
按功能來分:輸入流(input)、輸出流(output)。
按類型來分:字節流和字符流。
字節流和字符流的區別是:字節流按 8 位傳輸以字節為單位輸入輸出數據,字符流按 16 位傳輸以字符為單位輸入輸出數據。
Java 容器分為 Collection 和 Map 兩大類,其下又有很多子類,如下所示:
List、Set、Map 的區別主要體現在兩個方面:元素是否有序、是否允許元素重復。
三者之間的區別,如下表:
對于在 Map 中插入、刪除、定位一個元素這類操作,HashMap 是最好的選擇,因為相對而言 HashMap 的插入會更快,但如果你要對一個 key 集合進行有序的遍歷,那 TreeMap 是更好的選擇。
HashMap 基于 Hash 算法實現的,我們通過 put(key,value)存儲,get(key)來獲取。當傳入 key 時,HashMap 會根據 key. hashCode() 計算出 hash 值,根據 hash 值將 value 保存在 bucket 里。當計算出的 hash 值相同時,我們稱之為 hash 沖突,HashMap 的做法是用鏈表和紅黑樹存儲相同 hash 值的 value。當 hash 沖突的個數比較少時,使用鏈表否則使用紅黑樹。
HashSet 是基于 HashMap 實現的,HashSet 底層使用 HashMap 來保存所有元素,因此 HashSet 的實現比較簡單,相關 HashSet 的操作,基本上都是直接調用底層 HashMap 的相關方法來完成,HashSet 不允許重復的值。
綜合來說,在需要頻繁讀取集合中的元素時,更推薦使用 ArrayList,而在插入和刪除操作較多時,更推薦使用 LinkedList。
代碼示例:
// list to array
List<String> list=new ArrayList<String>();
list. add("王磊");
list. add("的博客");
list. toArray();
// array to list
String[] array=new String[]{"王磊","的博客"};
Arrays. asList(array);
代碼示例:
Queue<String> queue=new LinkedList<String>();
queue. offer("string"); // add
System. out. println(queue. poll());
System. out. println(queue. remove());
System. out. println(queue. size());
Vector、Hashtable、Stack 都是線程安全的,而像 HashMap 則是非線程安全的,不過在 JDK 1.5 之后隨著 Java. util. concurrent 并發包的出現,它們也有了自己對應的線程安全類,比如 HashMap 對應的線程安全類就是 ConcurrentHashMap。
Iterator 接口提供遍歷任何 Collection 的接口。我們可以從一個 Collection 中使用迭代器方法來獲取迭代器實例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允許調用者在迭代過程中移除元素。
Iterator 使用代碼如下:
List<String> list=new ArrayList<>();
Iterator<String> it=list. iterator();
while(it. hasNext()){
String obj=it. next();
System. out. println(obj);
}
Iterator 的特點是更加安全,因為它可以確保,在當前遍歷的集合元素被更改的時候,就會拋出 ConcurrentModificationException 異常。
可以使用 Collections. unmodifiableCollection(Collection c) 方法來創建一個只讀集合,這樣改變集合的任何操作都會拋出 Java. lang. UnsupportedOperationException 異常。
示例代碼如下:
List<String> list=new ArrayList<>();
list. add("x");
Collection<String> clist=Collections. unmodifiableCollection(list);
clist. add("y"); // 運行時此行報錯
System. out. println(list. size());
如下圖:
并發=兩個隊列和一臺咖啡機。
并行=兩個隊列和兩臺咖啡機。
一個程序下至少有一個進程,一個進程下至少有一個線程,一個進程下也可以有多個線程來增加程序的執行速度。
守護線程是運行在后臺的一種特殊進程。它獨立于控制終端并且周期性地執行某種任務或等待處理某些發生的事件。在 Java 中垃圾回收線程就是特殊的守護線程。
創建線程有三種方式:
runnable 沒有返回值,callable 可以拿到有返回值,callable 可以看作是 runnable 的補充。
線程的狀態:
notifyAll() 會喚醒所有的線程,notify() 只會喚醒一個線程。notifyAll() 調用后,會將全部線程由等待池移到鎖池,然后參與鎖的競爭,競爭成功則繼續執行,如果不成功則留在鎖池等待鎖被釋放后再次參與競爭。而 notify()只會喚醒一個線程,具體喚醒哪一個線程由虛擬機控制。
start() 方法用于啟動線程,run() 方法用于執行線程的運行時代碼。run() 可以重復調用,而 start() 只能調用一次。
線程池創建有七種方式,最核心的是最后一種:
Callable 類型的任務可以獲取執行的返回值,而 Runnable 執行無返回值。
手動鎖 Java 示例代碼如下:
Lock lock=new ReentrantLock();
lock. lock();
try {
System. out. println("獲得鎖");
} catch (Exception e) {
// TODO: handle exception
} finally {
System. out. println("釋放鎖");
lock. unlock();
}
synchronized 鎖升級原理:在鎖對象的對象頭里面有一個 threadid 字段,在第一次訪問的時候 threadid 為空,jvm 讓其持有偏向鎖,并將 threadid 設置為其線程 id,再次進入的時候會先判斷 threadid 是否與其線程 id 一致,如果一致則可以直接使用此對象,如果不一致,則升級偏向鎖為輕量級鎖,通過自旋循環一定次數來獲取鎖,執行一定次數之后,如果還沒有正常獲取到要使用的對象,此時就會把鎖從輕量級升級為重量級鎖,此過程就構成了 synchronized 鎖的升級。
鎖的升級的目的:鎖升級是為了減低了鎖帶來的性能消耗。在 Java 6 之后優化 synchronized 的實現方式,使用了偏向鎖升級為輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的性能消耗。
當線程 A 持有獨占鎖a,并嘗試去獲取獨占鎖 b 的同時,線程 B 持有獨占鎖 b,并嘗試獲取獨占鎖 a 的情況下,就會發生 AB 兩個線程由于互相持有對方需要的鎖,而發生的阻塞現象,我們稱為死鎖。
ThreadLocal 為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
ThreadLocal 的經典使用場景是數據庫連接和 session 管理等。
synchronized 是由一對 monitorenter/monitorexit 指令實現的,monitor 對象是同步的基本實現單元。在 Java 6 之前,monitor 的實現完全是依靠操作系統內部的互斥鎖,因為需要進行用戶態到內核態的切換,所以同步操作是一個無差別的重量級操作,性能也很低。但在 Java 6 的時候,Java 虛擬機 對此進行了大刀闊斧地改進,提供了三種不同的 monitor 實現,也就是常說的三種不同的鎖:偏向鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。
synchronized 早期的實現比較低效,對比 ReentrantLock,大多數場景性能都相差較大,但是在 Java 6 中對 synchronized 進行了非常多的改進。
主要區別如下:
atomic 主要利用 CAS (Compare And Wwap) 和 volatile 和 native 方法來保證原子操作,從而避免 synchronized 的高開銷,執行效率大為提升。
反射是在運行狀態中,對于任意一個類,都能夠知道這個類的所有屬性和方法;對于任意一個對象,都能夠調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱為 Java 語言的反射機制。
Java 序列化是為了保存各種對象在內存中的狀態,并且可以把保存的對象狀態再讀出來。
以下情況需要使用 Java 序列化:
動態代理是運行時動態生成代理類。
動態代理的應用有 spring aop、hibernate 數據查詢、測試框架的后端 mock、rpc,Java注解對象獲取等。
JDK 原生動態代理和 cglib 動態代理。JDK 原生動態代理是基于接口實現的,而 cglib 是基于繼承當前類的子類實現的。
克隆的對象可能包含一些已經修改過的屬性,而 new 出來的對象的屬性都還是初始化時候的值,所以當需要一個新的對象來保存當前對象的“狀態”就靠克隆方法了。
JSP 是 servlet 技術的擴展,本質上就是 servlet 的簡易方式。servlet 和 JSP 最主要的不同點在于,servlet 的應用邏輯是在 Java 文件中,并且完全從表示層中的 html 里分離開來,而 JSP 的情況是 Java 和 html 可以組合成一個擴展名為 JSP 的文件。JSP 側重于視圖,servlet 主要用于控制邏輯。
JSP 有 9 大內置對象:
session 的工作原理是客戶端登錄完成之后,服務器會創建對應的 session,session 創建完之后,會把 session 的 id 發送給客戶端,客戶端再存儲到瀏覽器中。這樣客戶端每次訪問服務器時,都會帶著 sessionid,服務器拿到 sessionid 之后,在內存找到與之對應的 session 這樣就可以正常工作了。
可以用,session 只是依賴 cookie 存儲 sessionid,如果 cookie 被禁用了,可以使用 url 中添加 sessionid 的方式保證 session 能正常使用。
XSS 攻擊:即跨站腳本攻擊,它是 Web 程序中常見的漏洞。原理是攻擊者往 Web 頁面里插入惡意的腳本代碼(css 代碼、Javascript 代碼等),當用戶瀏覽該頁面時,嵌入其中的腳本代碼會被執行,從而達到惡意攻擊用戶的目的,如盜取用戶 cookie、破壞頁面結構、重定向到其他網站等。
預防 XSS 的核心是必須對輸入的數據做過濾處理。
CSRF:Cross-Site Request Forgery(中文:跨站請求偽造),可以理解為攻擊者盜用了你的身份,以你的名義發送惡意請求,比如:以你名義發送郵件、發消息、購買商品,虛擬貨幣轉賬等。
防御手段:
try-catch-finally 其中 catch 和 finally 都可以被省略,但是不能同時省略,也就是說有 try 的時候,必須后面跟一個 catch 或者 finally。
finally 一定會執行,即使是 catch 中 return 了,catch 中的 return 會等 finally 中的代碼執行完之后,才會執行。
301:永久重定向。
302:暫時重定向。
它們的區別是,301 對搜索引擎優化(SEO)更加有利;302 有被提示為網絡攔截的風險。
forward 是轉發 和 redirect 是重定向:
tcp 和 udp 是 OSI 模型中的運輸層中的協議。tcp 提供可靠的通信傳輸,而 udp 則常被用于讓廣播和細節控制交給應用的通信傳輸。
兩者的區別大致如下:
如果采用兩次握手,那么只要服務器發出確認數據包就會建立連接,但由于客戶端此時并未響應服務器端的請求,那此時服務器端就會一直在等待客戶端,這樣服務器端就白白浪費了一定的資源。若采用三次握手,服務器端沒有收到來自客戶端的再此確認,則就會知道客戶端并沒有要求建立請求,就不會浪費服務器的資源。
tcp 粘包可能發生在發送端或者接收端,分別來看兩端各種產生粘包的原因:
實現跨域有以下幾種方案:
jsonp:JSON with Padding,它是利用script標簽的 src 連接可以訪問不同源的特性,加載遠程返回的“JS 函數”來執行的。
aop 是面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。
簡單來說就是統一處理某一“切面”(類)的問題的編程思想,比如統一處理日志、異常等。
ioc:Inversionof Control(中文:控制反轉)是 spring 的核心,對于 spring 框架來說,就是由 spring 來負責控制對象的生命周期和對象間的關系。
簡單來說,控制指的是當前對象對內部成員的控制權;控制反轉指的是,這種控制權不由當前對象管理了,由其他(類,第三方容器)來管理。
spring 中的 bean 默認是單例模式,spring 框架并沒有對單例 bean 進行多線程的封裝處理。
實際上大部分時候 spring bean 無狀態的(比如 dao 類),所有某種程度上來說 bean 也是安全的,但如果 bean 有狀態的話(比如 view model 對象),那就要開發者自己去保證線程安全了,最簡單的就是改變 bean 的作用域,把“singleton”變更為“prototype”,這樣請求 bean 相當于 new Bean()了,所以就可以保證線程安全了。
spring 支持 5 種作用域,如下:
注意: 使用 prototype 作用域需要慎重的思考,因為頻繁創建和銷毀 bean 會帶來很大的性能開銷。
spring 有五大隔離級別,默認值為 ISOLATION_DEFAULT(使用數據庫的設置),其他四個隔離級別和數據庫的隔離級別一致:
ISOLATION_DEFAULT:用底層數據庫的設置隔離級別,數據庫設置的是什么我就用什么;
ISOLATIONREADUNCOMMITTED:未提交讀,最低隔離級別、事務未提交前,就可被其他事務讀取(會出現幻讀、臟讀、不可重復讀);
ISOLATIONREADCOMMITTED:提交讀,一個事務提交后才能被其他事務讀取到(會造成幻讀、不可重復讀),SQL server 的默認級別;
ISOLATIONREPEATABLEREAD:可重復讀,保證多次讀取同一個數據時,其值都和事務開始時候的內容是一致,禁止讀取到別的事務未提交的數據(會造成幻讀),MySQL 的默認級別;
ISOLATION_SERIALIZABLE:序列化,代價最高最可靠的隔離級別,該隔離級別能防止臟讀、不可重復讀、幻讀。
臟讀 :表示一個事務能夠讀取另一個事務中還未提交的數據。比如,某個事務嘗試插入記錄 A,此時該事務還未提交,然后另一個事務嘗試讀取到了記錄 A。
不可重復讀 :是指在一個事務內,多次讀同一數據。
幻讀 :指同一個事務內多次查詢返回的結果集不一樣。比如同一個事務 A 第一次查詢時候有 n 條記錄,但是第二次同等條件下查詢卻有 n+1 條記錄,這就好像產生了幻覺。發生幻讀的原因也是另外一個事務新增或者刪除或者修改了第一個事務結果集里面的數據,同一個記錄的數據內容被修改了,所有數據行的記錄就變多或者變少了。
將 http 請求映射到相應的類/方法上。
@Autowired 它可以對類成員變量、方法及構造函數進行標注,完成自動裝配的工作,通過@Autowired 的使用來消除 set/get 方法。
spring boot 是為 spring 服務的,是用來簡化新 spring 應用的初始搭建以及開發過程的。
spring boot 核心的兩個配置文件:
配置文件有 . properties 格式和 . yml 格式,它們主要的區別是書法風格不同。
. properties 配置如下:
spring. RabbitMQ. port=5672
. yml 配置如下:
spring:
RabbitMQ:
port: 5672
. yml 格式不支持 @PropertySource 注解導入。
jpa 全稱 Java Persistence API,是 Java 持久化接口規范,hibernate 屬于 jpa 的具體實現。
spring cloud 是一系列框架的有序集合。它利用 spring boot 的開發便利性巧妙地簡化了分布式系統基礎設施的開發,如服務發現注冊、配置中心、消息總線、負載均衡、斷路器、數據監控等,都可以用 spring boot 的開發風格做到一鍵啟動和部署。
在分布式架構中,斷路器模式的作用也是類似的,當某個服務單元發生故障(類似用電器發生短路)之后,通過斷路器的故障監控(類似熔斷保險絲),向調用方返回一個錯誤響應,而不是長時間的等待。這樣就不會使得線程因調用故障服務被長時間占用不釋放,避免了故障在分布式系統中的蔓延。
ORM(Object Relation Mapping)對象關系映射,是把數據庫中的關系數據映射成為程序中的對象。
使用 ORM 的優點:提高了開發效率降低了開發成本、開發更簡單更對象化、可移植更強。
在 Config 里面把 hibernate. show_SQL 設置為 true 就可以。但不建議開啟,開啟之后會降低程序的運行效率。
三種:hql、原生 SQL、條件查詢 Criteria。
實體類可以定義為 final 類,但這樣的話就不能使用 hibernate 代理模式下的延遲關聯提供性能了,所以不建議定義實體類為 final。
Integer 類型為對象,它的值允許為 null,而 int 屬于基礎數據類型,值不能為 null。
hibernate 常用的緩存有一級緩存和二級緩存:
一級緩存:也叫 Session 緩存,只在 Session 作用范圍內有效,不需要用戶干涉,由 hibernate 自身維護,可以通過:evict(object)清除 object 的緩存;clear()清除一級緩存中的所有緩存;flush()刷出緩存;
二級緩存:應用級別的緩存,在所有 Session 中都有效,支持配置第三方的緩存,如:EhCache。
hibernate 中每個實體類必須提供一個無參構造函數,因為 hibernate 框架要使用 reflection api,通過調用 ClassnewInstance() 來創建實體類的實例,如果沒有無參的構造函數就會拋出異常。
\#{}是預編譯處理,${}是字符替換。 在使用 #{}時,MyBatis 會將 SQL 中的 #{}替換成“?”,配合 PreparedStatement 的 set 方法賦值,這樣可以有效的防止 SQL 注入,保證程序的運行安全。
分頁方式:邏輯分頁和物理分頁。
邏輯分頁: 使用 MyBatis 自帶的 RowBounds 進行分頁,它是一次性查詢很多數據,然后在數據中再進行檢索。
物理分頁: 自己手寫 SQL 分頁或使用分頁插件 PageHelper,去數據庫查詢指定條數的分頁數據的形式。
RowBounds 表面是在“所有”數據中檢索數據,其實并非是一次性查詢出所有數據,因為 MyBatis 是對 jdbc 的封裝,在 jdbc 驅動中有一個 Fetch Size 的配置,它規定了每次最多從數據庫查詢多少條數據,假如你要查詢更多數據,它會在你執行 next()的時候,去查詢更多的數據。就好比你去自動取款機取 10000 元,但取款機每次最多能取 2500 元,所以你要取 4 次才能把錢取完。只是對于 jdbc 來說,當你調用 next()的時候會自動幫你完成查詢工作。這樣做的好處可以有效的防止內存溢出。
Fetch Size 官方相關文檔:http://t. cn/EfSE2g3
MyBatis 支持延遲加載,設置 lazyLoadingEnabled=true 即可。
延遲加載的原理的是調用的時候觸發加載,而不是在初始化的時候就加載信息。比如調用 a. getB(). getName(),這個時候發現 a. getB() 的值為 null,此時會單獨觸發事先保存好的關聯 B 對象的 SQL,先查詢出來 B,然后再調用 a. setB(b),而這時候再調用 a. getB(). getName() 就有值了,這就是延遲加載的基本原理。
開啟二級緩存數據查詢流程:二級緩存 -> 一級緩存 -> 數據庫。
緩存更新機制:當某一個作用域(一級緩存 Session/二級緩存 Mapper)進行了C/U/D 操作后,默認該作用域下所有 select 中的緩存將被 clear。
MyBatis 有三種基本的Executor執行器:
分頁插件的基本原理是使用 MyBatis 提供的插件接口,實現自定義插件,在插件的攔截方法內攔截待執行的 SQL,然后重寫 SQL,根據 dialect 方言,添加對應的物理分頁語句和物理分頁參數。
自定義插件實現原理
MyBatis 自定義插件針對 MyBatis 四大對象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)進行攔截:
自定義插件實現關鍵
MyBatis 插件要實現 Interceptor 接口,接口包含的方法,如下:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
自定義插件實現示例
官方插件實現:
@Intercepts({@Signature(type=Executor. class, method="query",
args={MappedStatement. class, Object. class, RowBounds. class, ResultHandler. class})})
public class TestInterceptor implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
Object target=invocation. getTarget(); //被代理對象
Method method=invocation. getMethod(); //代理方法
Object[] args=invocation. getArgs(); //方法參數
// do something . . . . . . 方法攔截前執行代碼塊
Object result=invocation. proceed();
// do something . . . . . . . 方法攔截后執行代碼塊
return result;
}
public Object plugin(Object target) {
return Plugin. wrap(target, this);
}
}
RabbitMQ 中重要的角色有:生產者、消費者和代理:
vhost:每個 RabbitMQ 都能創建很多 vhost,我們稱之為虛擬主機,每個虛擬主機其實都是 mini 版的RabbitMQ,它擁有自己的隊列,交換器和綁定,擁有自己的權限機制。
首先客戶端必須連接到 RabbitMQ 服務器才能發布和消費消息,客戶端和 rabbit server 之間會創建一個 tcp 連接,一旦 tcp 打開并通過了認證(認證就是你發送給 rabbit 服務器的用戶名和密碼),你的客戶端和 RabbitMQ 就創建了一條 amqp 信道(channel),信道是創建在“真實” tcp 上的虛擬連接,amqp 命令都是通過信道發送出去的,每個信道都會有一個唯一的 id,不論是發布消息,訂閱隊列都是通過這個信道完成的。
以上四個條件都滿足才能保證消息持久化成功。
持久化的缺地就是降低了服務器的吞吐量,因為使用的是磁盤而非內存存儲,從而降低了吞吐量。可盡量使用 ssd 硬盤來緩解吞吐量的問題。
延遲隊列的實現有兩種方式:
集群主要有以下兩個用途:
不是,原因有以下兩個:
如果唯一磁盤的磁盤節點崩潰了,不能進行以下操作:
唯一磁盤節點崩潰了,集群是可以保持運行的,但你不能更改任何東西。
RabbitMQ 對集群的停止的順序是有要求的,應該先關閉內存節點,最后再關閉磁盤節點。如果順序恰好相反的話,可能會造成消息的丟失。
kafka 不能脫離 zookeeper 單獨使用,因為 kafka 使用 zookeeper 管理和協調 kafka 的節點服務器。
kafka 有兩種數據保存策略:按照過期時間保留和按照存儲的消息大小保留。
這個時候 kafka 會執行數據清除工作,時間和大小不論那個滿足條件,都會清空數據。
zookeeper 是一個分布式的,開放源碼的分布式應用程序協調服務,是 google chubby 的開源實現,是 hadoop 和 hbase 的重要組件。它是一個為分布式應用提供一致性服務的軟件,提供的功能包括:配置維護、域名服務、分布式同步、組服務等。
zookeeper 有三種部署模式:
zookeeper 的核心是原子廣播,這個機制保證了各個 server 之間的同步。實現這個機制的協議叫做 zab 協議。 zab 協議有兩種模式,分別是恢復模式(選主)和廣播模式(同步)。當服務啟動或者在領導者崩潰后,zab 就進入了恢復模式,當領導者被選舉出來,且大多數 server 完成了和 leader 的狀態同步以后,恢復模式就結束了。狀態同步保證了 leader 和 server 具有相同的系統狀態。
在分布式環境中,有些業務邏輯只需要集群中的某一臺機器進行執行,其他的機器可以共享這個結果,這樣可以大大減少重復計算,提高性能,所以就需要主節點。
可以繼續使用,單數服務器只要沒超過一半的服務器宕機就可以繼續使用。
客戶端端會對某個 znode 建立一個 watcher 事件,當該 znode 發生變化時,這些客戶端會收到 zookeeper 的通知,然后客戶端可以根據 znode 變化來做出業務上的改變。
InnoDB 表只會把自增主鍵的最大 id 記錄在內存中,所以重啟之后會導致最大 id 丟失。
使用 select version() 獲取當前 MySQL 數據庫版本。
chat 優點:效率高;缺點:占用空間;適用場景:存儲密碼的 md5 值,固定長度的,使用 char 非常合適。
所以,從空間上考慮 varcahr 比較合適;從效率上考慮 char 比較合適,二者使用需要權衡。
內連接關鍵字:inner join;左連接:left join;右連接:right join。
內連接是把匹配的關聯數據顯示出來;左連接是左邊的表全部顯示出來,右邊的表顯示出符合條件的數據;右連接正好相反。
索引是滿足某種特定查找算法的數據結構,而這些數據結構會以某種方式指向數據,從而實現高效查找數據。
具體來說 MySQL 中的索引,不同的數據引擎實現有所不同,但目前主流的數據庫引擎的索引都是 B+ 樹實現的,B+ 樹的搜索效率,可以到達二分法的性能,找到數據區域之后就找到了完整的數據結構了,所有索引的性能也是更好的。
使用 explain 查看 SQL 是如何執行查詢語句的,從而分析你的索引是否滿足需求。
explain 語法:explain select * from table where type=1。
MySQL 的事務隔離是在 MySQL. ini 配置文件里添加的,在文件的最后添加:
transaction-isolation=REPEATABLE-READ
可用的配置值:READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE。
臟讀 :表示一個事務能夠讀取另一個事務中還未提交的數據。比如,某個事務嘗試插入記錄 A,此時該事務還未提交,然后另一個事務嘗試讀取到了記錄 A。
不可重復讀 :是指在一個事務內,多次讀同一數據。
幻讀 :指同一個事務內多次查詢返回的結果集不一樣。比如同一個事務 A 第一次查詢時候有 n 條記錄,但是第二次同等條件下查詢卻有 n+1 條記錄,這就好像產生了幻覺。發生幻讀的原因也是另外一個事務新增或者刪除或者修改了第一個事務結果集里面的數據,同一個記錄的數據內容被修改了,所有數據行的記錄就變多或者變少了。
MySQL 只支持表鎖,InnoDB 支持表鎖和行鎖,默認為行鎖。
數據庫的樂觀鎖需要自己實現,在表里面添加一個 version 字段,每次修改成功值加 1,這樣每次修改的時候先對比一下,自己擁有的 version 和數據庫現在的 version 是否一致,如果不一致就不修改,這樣就實現了樂觀鎖。
Redis 是一個使用 C 語言開發的高速緩存數據庫。
Redis 使用場景:
因為 cpu 不是 Redis 的瓶頸,Redis 的瓶頸最有可能是機器內存或者網絡帶寬。既然單線程容易實現,而且 cpu 又不會成為瓶頸,那就順理成章地采用單線程的方案了。
關于 Redis 的性能,官方網站也有,普通筆記本輕松處理每秒幾十萬的請求。
而且單線程并不代表就慢 nginx 和 nodejs 也都是高性能單線程的代表。
緩存穿透:指查詢一個一定不存在的數據,由于緩存是不命中時需要從數據庫查詢,查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到數據庫去查詢,造成緩存穿透。
解決方案:最簡單粗暴的方法如果一個查詢返回的數據為空(不管是數據不存在,還是系統故障),我們就把這個空結果進行緩存,但它的過期時間會很短,最長不超過五分鐘。
Redis 支持的數據類型:string(字符串)、list(列表)、hash(字典)、set(集合)、zset(有序集合)。
支持的 Java 客戶端有 Redisson、jedis、lettuce 等。
Redis 的持久化有兩種方式,或者說有兩種策略:
Redis 分布式鎖其實就是在系統里面占一個“坑”,其他程序也要占“坑”的時候,占用成功了就可以繼續執行,失敗了就只能放棄或稍后重試。
占坑一般使用 setnx(set if not exists)指令,只允許被一個程序占有,使用完調用 del 釋放鎖。
Redis 分布式鎖不能解決超時的問題,分布式鎖有一個超時時間,程序的執行如果超出了鎖的超時時間就會出現問題。
盡量使用 Redis 的散列表,把相關的信息放到散列表里面存儲,而不是把每個字段單獨存儲,這樣可以有效的減少內存使用。比如將 Web 系統的用戶對象,應該放到散列表里面再整體存儲到 Redis,而不是把用戶的姓名、年齡、密碼、郵箱等字段分別設置 key 進行存儲。
組件的作用: 首先通過類加載器(ClassLoader)會把 Java 代碼轉換成字節碼,運行時數據區(Runtime Data Area)再把字節碼加載到內存中,而字節碼文件只是 JVM 的一套指令集規范,并不能直接交給底層操作系統去執行,因此需要特定的命令解析器執行引擎(Execution Engine),將字節碼翻譯成底層系統指令,再交由 CPU 去執行,而這個過程中需要調用其他語言的本地庫接口(Native Interface)來實現整個程序的功能。
不同虛擬機的運行時數據區可能略微有所不同,但都會遵從 Java 虛擬機規范, Java 虛擬機規范規定的區域分為以下 5 個部分:
隊列和棧都是被用來預存儲數據的。
隊列允許先進先出檢索元素,但也有例外的情況,Deque 接口允許從兩端檢索元素。
棧和隊列很相似,但它運行對元素進行后進先出進行檢索。
在介紹雙親委派模型之前先說下類加載器。對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立在 JVM 中的唯一性,每一個類加載器,都有一個獨立的類名稱空間。類加載器就是根據指定全限定名稱將 class 文件加載到 JVM 內存,然后再轉化為 class 對象。
類加載器分類:
雙親委派模型:如果一個類加載器收到了類加載的請求,它首先不會自己去加載這個類,而是把這個請求委派給父類加載器去完成,每一層的類加載器都是如此,這樣所有的加載請求都會被傳送到頂層的啟動類加載器中,只有當父加載無法完成加載請求(它的搜索范圍中沒找到所需的類)時,子加載器才會嘗試去加載類。
類裝載分為以下 5 個步驟:
一般有兩種方法來判斷:
CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器。對于要求服務器響應速度的應用上,這種垃圾回收器非常適合。在啟動 JVM 的參數加上“-XX:+UseConcMarkSweepGC”來指定使用 CMS 垃圾回收器。
CMS 使用的是標記-清除的算法實現的,所以在 gc 的時候回產生大量的內存碎片,當剩余內存不能滿足程序運行要求時,系統將會出現 Concurrent Mode Failure,臨時 CMS 會采用 Serial Old 回收器進行垃圾清除,此時的性能將會被降低。
新生代垃圾回收器一般采用的是復制算法,復制算法的優點是效率高,缺點是內存利用率低;老年代回收器一般采用的是標記-整理的算法進行垃圾回收。
分代回收器有兩個分區:老生代和新生代,新生代默認的空間占比總空間的 1/3,老生代的默認占比是 2/3。
新生代使用的是復制算法,新生代里有 3 個分區:Eden、To Survivor、From Survivor,它們的默認占比是 8:1:1,它的執行流程如下:
每次在 From Survivor 到 To Survivor 移動時都存活的對象,年齡就 +1,當年齡到達 15(默認配置是 15)時,升級為老生代。大對象也會直接進入老生代。
老生代當空間占用到達某個值之后就會觸發全局垃圾收回,一般使用標記整理的執行算法。以上這些循環往復就構成了整個分代垃圾回收的整體執行流程。
JDK 自帶了很多監控工具,都位于 JDK 的 bin 目錄下,其中最常用的是 jconsole 和 jvisualvm 這兩款視圖監控工具。
在本篇文章開始之前,我想先來回答一個問題:我為什么要寫這樣一篇關于面試的文章?
原因有三個:
第一,我想為每一個為夢想時刻準備著的“有心人”,盡一份自己的力量,提供一份高度精華的 Java 面試清單;
第二,目前市面上的面試題不是答案不準確就是內容覆蓋面太窄,所以提供一份經典而又準確的面試題是非常有必要的;
第三,本文會對部分面試題提供詳細解讀和代碼案例,讓讀者知其然并知其所以然,從而學到更多的知識。
或許這份面試題還不足以囊括所有 Java 問題,但有了它,我相信你一定不會“敗”的很慘,因為有了它,足以應對目前市面上絕大部分的 Java 面試了,因為這篇文章不論是從深度還是廣度上來講,都已經囊括了非常多的知識點了。
凡事預則立,不預則廢。能讀到這里的人,我相信都是這個世界上的“有心人”,還是那句老話:上天不負有心人!我相信你的每一步努力,都會收獲意想不到的回報。
適宜閱讀人群
閱讀建議
本文會按技能模塊劃分文章段落,每個模塊里的內容,從易到難依次進行排序,各模塊之間不存在互相關聯的關系,讀者可選擇文章順序閱讀或者跳躍式閱讀。
包含的模塊
本文分為十九個模塊,分別是: Java 基礎、容器、多線程、反射、對象拷貝、Java Web 、異常、網絡、設計模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM ,如下圖所示:
共包含 208 道面試題,本文的宗旨是為讀者朋友們整理一份詳實而又權威的面試清單,下面一起進入主題吧。
1. JDK 和 JRE 有什么區別?
具體來說 JDK 其實包含了 JRE,同時還包含了編譯 Java 源碼的編譯器 Javac,還包含了很多 Java 程序調試和分析的工具。簡單來說:如果你需要運行 Java 程序,只需安裝 JRE 就可以了,如果你需要編寫 Java 程序,需要安裝 JDK。
2.==和 equals 的區別是什么?
==解讀
對于基本類型和引用類型==的作用效果是不同的,如下所示:
代碼示例:
String x="string";String y="string";String z=new String("string");System.out.println(x==y); // trueSystem.out.println(x==z); // falseSystem.out.println(x.equals(y)); // trueSystem.out.println(x.equals(z)); // true
代碼解讀:因為 x 和 y 指向的是同一個引用,所以==也是 true,而 new String()方法則重寫開辟了內存空間,所以==結果為 false,而 equals 比較的一直是值,所以結果都為 true。
equals 解讀
equals 本質上就是==,只不過 String 和 Integer 等重寫了 equals 方法,把它變成了值比較。看下面的代碼就明白了。
首先來看默認情況下 equals 比較一個有相同值的對象,代碼如下:
class Cat {public Cat(String name) {this.name=name;}private String name;public String getName() {return name;}public void setName(String name) {this.name=name;}}Cat c1=new Cat("王磊");Cat c2=new Cat("王磊");System.out.println(c1.equals(c2)); // false
輸出結果出乎我們的意料,竟然是 false?這是怎么回事,看了 equals 源碼就知道了,源碼如下:
public boolean equals(Object obj) {return (this==obj);}
原來 equals 本質上就是==。
那問題來了,兩個相同值的 String 對象,為什么返回的是 true?代碼如下:
String s1=new String("老王");String s2=new String("老王");System.out.println(s1.equals(s2)); // true
同樣的,當我們進入 String 的 equals 方法,找到了答案,代碼如下:
public boolean equals(Object anObject) {if (this==anObject) {return true;}if (anObject instanceof String) {String anotherString=(String)anObject;int n=value.length;if (n==anotherString.value.length) {char v1[]=value;char v2[]=anotherString.value;int i=0;while (n-- !=0) {if (v1[i] !=v2[i])return false;i++;}return true;}}return false;}
原來是 String 重寫了 Object 的 equals 方法,把引用比較改成了值比較。
總結 :==對于基本類型來說是值比較,對于引用類型來說是比較的是引用;而 equals 默認情況下是引用比較,只是很多類重新了 equals 方法,比如 String、Integer 等把它變成了值比較,所以一般情況下 equals 比較的是值是否相等。
3. 兩個對象的 hashCode() 相同,則 equals() 也一定為 true,對嗎?
不對,兩個對象的 hashCode() 相同,equals() 不一定 true。
代碼示例:
String str1="通話";String str2="重地";System. out. println(String. format("str1:%d | str2:%d", str1. hashCode(),str2. hashCode()));System. out. println(str1. equals(str2));
執行的結果:
str1:1179395 | str2:1179395false
代碼解讀:很顯然“通話”和“重地”的 hashCode() 相同,然而 equals() 則為 false,因為在散列表中,hashCode() 相等即兩個鍵值對的哈希值相等,然而哈希值相等,并不一定能得出鍵值對相等。
4. final 在 Java 中有什么作用?
5. Java 中的 Math. round(-1. 5) 等于多少?
等于 -1,因為在數軸上取值時,中間值(0.5)向右取整,所以正 0.5 是往上取整,負 0.5 是直接舍棄。
6. String 屬于基礎的數據類型嗎?
String 不屬于基礎類型,基礎類型有 8 種:byte、boolean、char、short、int、float、long、double,而 String 屬于對象。
7. Java 中操作字符串都有哪些類?它們之間有什么區別?
操作字符串的類有:String、StringBuffer、StringBuilder。
String 和 StringBuffer、StringBuilder 的區別在于 String 聲明的是不可變的對象,每次操作都會生成新的 String 對象,然后將指針指向新的 String 對象,而 StringBuffer、StringBuilder 可以在原有對象的基礎上進行操作,所以在經常改變字符串內容的情況下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的區別在于,StringBuffer 是線程安全的,而 StringBuilder 是非線程安全的,但 StringBuilder 的性能卻高于 StringBuffer,所以在單線程環境下推薦使用 StringBuilder,多線程環境下推薦使用 StringBuffer。
8. String str="i"與 String str=new String("i")一樣嗎?
不一樣,因為內存的分配方式不一樣。String str="i"的方式,Java 虛擬機會將其分配到常量池中;而 String str=new String("i") 則會被分到堆內存中。
9. 如何將字符串反轉?
使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
示例代碼:
// StringBuffer reverseStringBuffer stringBuffer=new StringBuffer();stringBuffer. append("abcdefg");System. out. println(stringBuffer. reverse()); // gfedcba// StringBuilder reverseStringBuilder stringBuilder=new StringBuilder();stringBuilder. append("abcdefg");System. out. println(stringBuilder. reverse()); // gfedcba
10. String 類的常用方法都有那些?
11. 抽象類必須要有抽象方法嗎?
不需要,抽象類不一定非要有抽象方法。
示例代碼:
abstract class Cat {public static void sayHi() {System. out. println("hi~");}}
上面代碼,抽象類并沒有抽象方法但完全可以正常運行。
12. 普通類和抽象類有哪些區別?
13. 抽象類能使用 final 修飾嗎?
不能,定義抽象類就是讓其他類繼承的,如果定義為 final 該類就不能被繼承,這樣彼此就會產生矛盾,所以 final 不能修飾抽象類,如下圖所示,編輯器也會提示錯誤信息:
14. 接口和抽象類有什么區別?
15. Java 中 IO 流分為幾種?
按功能來分:輸入流(input)、輸出流(output)。
按類型來分:字節流和字符流。
字節流和字符流的區別是:字節流按 8 位傳輸以字節為單位輸入輸出數據,字符流按 16 位傳輸以字符為單位輸入輸出數據。
16. BIO、NIO、AIO 有什么區別?
17. Files的常用方法都有哪些?
18. Java 容器都有哪些?
Java 容器分為 Collection 和 Map 兩大類,其下又有很多子類,如下所示:
19. Collection 和 Collections 有什么區別?
20. List、Set、Map 之間的區別是什么?
List、Set、Map 的區別主要體現在兩個方面:元素是否有序、是否允許元素重復。
三者之間的區別,如下表:
21. HashMap 和 Hashtable 有什么區別?
22. 如何決定使用 HashMap 還是 TreeMap?
對于在 Map 中插入、刪除、定位一個元素這類操作,HashMap 是最好的選擇,因為相對而言 HashMap 的插入會更快,但如果你要對一個 key 集合進行有序的遍歷,那 TreeMap 是更好的選擇。
23. 說一下 HashMap 的實現原理?
HashMap 基于 Hash 算法實現的,我們通過 put(key,value)存儲,get(key)來獲取。當傳入 key 時,HashMap 會根據 key. hashCode() 計算出 hash 值,根據 hash 值將 value 保存在 bucket 里。當計算出的 hash 值相同時,我們稱之為 hash 沖突,HashMap 的做法是用鏈表和紅黑樹存儲相同 hash 值的 value。當 hash 沖突的個數比較少時,使用鏈表否則使用紅黑樹。
24. 說一下 HashSet 的實現原理?
HashSet 是基于 HashMap 實現的,HashSet 底層使用 HashMap 來保存所有元素,因此 HashSet 的實現比較簡單,相關 HashSet 的操作,基本上都是直接調用底層 HashMap 的相關方法來完成,HashSet 不允許重復的值。
25. ArrayList 和 LinkedList 的區別是什么?
綜合來說,在需要頻繁讀取集合中的元素時,更推薦使用 ArrayList,而在插入和刪除操作較多時,更推薦使用 LinkedList。
26. 如何實現數組和 List 之間的轉換?
代碼示例:
// list to arrayList<String> list=new ArrayList<String>();list. add("王磊");list. add("的博客");list. toArray();// array to listString[] array=new String[]{"王磊","的博客"};Arrays. asList(array);
27. ArrayList 和 Vector 的區別是什么?
28. Array 和 ArrayList 有何區別?
29. 在 Queue 中 poll()和 remove()有什么區別?
代碼示例:
Queue<String> queue=new LinkedList<String>();queue. offer("string"); // addSystem. out. println(queue. poll());System. out. println(queue. remove());System. out. println(queue. size());
30. 哪些集合類是線程安全的?
Vector、Hashtable、Stack 都是線程安全的,而像 HashMap 則是非線程安全的,不過在 JDK 1.5 之后隨著 Java. util. concurrent 并發包的出現,它們也有了自己對應的線程安全類,比如 HashMap 對應的線程安全類就是 ConcurrentHashMap。
31. 迭代器 Iterator 是什么?
Iterator 接口提供遍歷任何 Collection 的接口。我們可以從一個 Collection 中使用迭代器方法來獲取迭代器實例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允許調用者在迭代過程中移除元素。
32. Iterator 怎么使用?有什么特點?
Iterator 使用代碼如下:
List<String> list=new ArrayList<>();Iterator<String> it=list. iterator();while(it. hasNext()){String obj=it. next();System. out. println(obj);}
Iterator 的特點是更加安全,因為它可以確保,在當前遍歷的集合元素被更改的時候,就會拋出 ConcurrentModificationException 異常。
33. Iterator 和 ListIterator 有什么區別?
34. 怎么確保一個集合不能被修改?
可以使用 Collections. unmodifiableCollection(Collection c) 方法來創建一個只讀集合,這樣改變集合的任何操作都會拋出 Java. lang. UnsupportedOperationException 異常。
示例代碼如下:
List<String> list=new ArrayList<>();list. add("x");Collection<String> clist=Collections. unmodifiableCollection(list);clist. add("y"); // 運行時此行報錯System. out. println(list. size());
35. 并行和并發有什么區別?
如下圖:
并發=兩個隊列和一臺咖啡機。
并行=兩個隊列和兩臺咖啡機。
36. 線程和進程的區別?
一個程序下至少有一個進程,一個進程下至少有一個線程,一個進程下也可以有多個線程來增加程序的執行速度。
37. 守護線程是什么?
守護線程是運行在后臺的一種特殊進程。它獨立于控制終端并且周期性地執行某種任務或等待處理某些發生的事件。在 Java 中垃圾回收線程就是特殊的守護線程。
38. 創建線程有哪幾種方式?
創建線程有三種方式:
39. 說一下 runnable 和 callable 有什么區別?
runnable 沒有返回值,callable 可以拿到有返回值,callable 可以看作是 runnable 的補充。
40. 線程有哪些狀態?
線程的狀態:
41. sleep() 和 wait() 有什么區別?
42. notify()和 notifyAll()有什么區別?
notifyAll()會喚醒所有的線程,notify()之后喚醒一個線程。notifyAll() 調用后,會將全部線程由等待池移到鎖池,然后參與鎖的競爭,競爭成功則繼續執行,如果不成功則留在鎖池等待鎖被釋放后再次參與競爭。而 notify()只會喚醒一個線程,具體喚醒哪一個線程由虛擬機控制。
43. 線程的 run() 和 start() 有什么區別?
start() 方法用于啟動線程,run() 方法用于執行線程的運行時代碼。run() 可以重復調用,而 start() 只能調用一次。
44. 創建線程池有哪幾種方式?
線程池創建有七種方式,最核心的是最后一種:
45. 線程池都有哪些狀態?
46. 線程池中 submit() 和 execute() 方法有什么區別?
Callable 類型的任務可以獲取執行的返回值,而 Runnable 執行無返回值。
47. 在 Java 程序中怎么保證多線程的運行安全?
手動鎖 Java 示例代碼如下:
Lock lock=new ReentrantLock();lock. lock();try {System. out. println("獲得鎖");} catch (Exception e) {// TODO: handle exception} finally {System. out. println("釋放鎖");lock. unlock();}
48. 多線程中 synchronized 鎖升級的原理是什么?
synchronized 鎖升級原理:在鎖對象的對象頭里面有一個 threadid 字段,在第一次訪問的時候 threadid 為空,jvm 讓其持有偏向鎖,并將 threadid 設置為其線程 id,再次進入的時候會先判斷 threadid 是否與其線程 id 一致,如果一致則可以直接使用此對象,如果不一致,則升級偏向鎖為輕量級鎖,通過自旋循環一定次數來獲取鎖,執行一定次數之后,如果還沒有正常獲取到要使用的對象,此時就會把鎖從輕量級升級為重量級鎖,此過程就構成了 synchronized 鎖的升級。
鎖的升級的目的:鎖升級是為了減低了鎖帶來的性能消耗。在 Java 6 之后優化 synchronized 的實現方式,使用了偏向鎖升級為輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的性能消耗。
49. 什么是死鎖?
當線程 A 持有獨占鎖a,并嘗試去獲取獨占鎖 b 的同時,線程 B 持有獨占鎖 b,并嘗試獲取獨占鎖 a 的情況下,就會發生 AB 兩個線程由于互相持有對方需要的鎖,而發生的阻塞現象,我們稱為死鎖。
50. 怎么防止死鎖?
51. ThreadLocal 是什么?有哪些使用場景?
ThreadLocal 為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
ThreadLocal 的經典使用場景是數據庫連接和 session 管理等。
52. 說一下 synchronized 底層實現原理?
synchronized 是由一對 monitorenter/monitorexit 指令實現的,monitor 對象是同步的基本實現單元。在 Java 6 之前,monitor 的實現完全是依靠操作系統內部的互斥鎖,因為需要進行用戶態到內核態的切換,所以同步操作是一個無差別的重量級操作,性能也很低。但在 Java 6 的時候,Java 虛擬機 對此進行了大刀闊斧地改進,提供了三種不同的 monitor 實現,也就是常說的三種不同的鎖:偏向鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。
53. synchronized 和 volatile 的區別是什么?
54. synchronized 和 Lock 有什么區別?
55. synchronized 和 ReentrantLock 區別是什么?
synchronized 早期的實現比較低效,對比 ReentrantLock,大多數場景性能都相差較大,但是在 Java 6 中對 synchronized 進行了非常多的改進。
主要區別如下:
56. 說一下 atomic 的原理?
atomic 主要利用 CAS (Compare And Wwap) 和 volatile 和 native 方法來保證原子操作,從而避免 synchronized 的高開銷,執行效率大為提升。
57. 什么是反射?
反射是在運行狀態中,對于任意一個類,都能夠知道這個類的所有屬性和方法;對于任意一個對象,都能夠調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱為 Java 語言的反射機制。
58. 什么是 Java 序列化?什么情況下需要序列化?
Java 序列化是為了保存各種對象在內存中的狀態,并且可以把保存的對象狀態再讀出來。
以下情況需要使用 Java 序列化:
59. 動態代理是什么?有哪些應用?
動態代理是運行時動態生成代理類。
動態代理的應用有 spring aop、hibernate 數據查詢、測試框架的后端 mock、rpc,Java注解對象獲取等。
60. 怎么實現動態代理?
JDK 原生動態代理和 cglib 動態代理。JDK 原生動態代理是基于接口實現的,而 cglib 是基于繼承當前類的子類實現的。
61. 為什么要使用克隆?
克隆的對象可能包含一些已經修改過的屬性,而 new 出來的對象的屬性都還是初始化時候的值,所以當需要一個新的對象來保存當前對象的“狀態”就靠克隆方法了。
62. 如何實現對象克隆?
63. 深拷貝和淺拷貝區別是什么?
64. JSP 和 servlet 有什么區別?
JSP 是 servlet 技術的擴展,本質上就是 servlet 的簡易方式。servlet 和 JSP 最主要的不同點在于,servlet 的應用邏輯是在 Java 文件中,并且完全從表示層中的 html 里分離開來,而 JSP 的情況是 Java 和 html 可以組合成一個擴展名為 JSP 的文件。JSP 側重于視圖,servlet 主要用于控制邏輯。
65. JSP 有哪些內置對象?作用分別是什么?
JSP 有 9 大內置對象:
66. 說一下 JSP 的 4 種作用域?
67. session 和 cookie 有什么區別?
68. 說一下 session 的工作原理?
session 的工作原理是客戶端登錄完成之后,服務器會創建對應的 session,session 創建完之后,會把 session 的 id 發送給客戶端,客戶端再存儲到瀏覽器中。這樣客戶端每次訪問服務器時,都會帶著 sessionid,服務器拿到 sessionid 之后,在內存找到與之對應的 session 這樣就可以正常工作了。
69. 如果客戶端禁止 cookie 能實現 session 還能用嗎?
可以用,session 只是依賴 cookie 存儲 sessionid,如果 cookie 被禁用了,可以使用 url 中添加 sessionid 的方式保證 session 能正常使用。
70. spring mvc 和 struts 的區別是什么?
71. 如何避免 SQL 注入?
72. 什么是 XSS 攻擊,如何避免?
XSS 攻擊:即跨站腳本攻擊,它是 Web 程序中常見的漏洞。原理是攻擊者往 Web 頁面里插入惡意的腳本代碼(css 代碼、Javascript 代碼等),當用戶瀏覽該頁面時,嵌入其中的腳本代碼會被執行,從而達到惡意攻擊用戶的目的,如盜取用戶 cookie、破壞頁面結構、重定向到其他網站等。
預防 XSS 的核心是必須對輸入的數據做過濾處理。
73. 什么是 CSRF 攻擊,如何避免?
CSRF:Cross-Site Request Forgery(中文:跨站請求偽造),可以理解為攻擊者盜用了你的身份,以你的名義發送惡意請求,比如:以你名義發送郵件、發消息、購買商品,虛擬貨幣轉賬等。
防御手段:
74. throw 和 throws 的區別?
75. final、finally、finalize 有什么區別?
76. try-catch-finally 中哪個部分可以省略?
try-catch-finally 其中 catch 和 finally 都可以被省略,但是不能同時省略,也就是說有 try 的時候,必須后面跟一個 catch 或者 finally。
77. try-catch-finally 中,如果 catch 中 return 了,finally 還會執行嗎?
finally 一定會執行,即使是 catch 中 return 了,catch 中的 return 會等 finally 中的代碼執行完之后,才會執行。
78. 常見的異常類有哪些?
79. http 響應碼 301 和 302 代表的是什么?有什么區別?
301:永久重定向。
302:暫時重定向。
它們的區別是,301 對搜索引擎優化(SEO)更加有利;302 有被提示為網絡攔截的風險。
80. forward 和 redirect 的區別?
forward 是轉發 和 redirect 是重定向:
81. 簡述 tcp 和 udp的區別?
tcp 和 udp 是 OSI 模型中的運輸層中的協議。tcp 提供可靠的通信傳輸,而 udp 則常被用于讓廣播和細節控制交給應用的通信傳輸。
兩者的區別大致如下:
82. tcp 為什么要三次握手,兩次不行嗎?為什么?
如果采用兩次握手,那么只要服務器發出確認數據包就會建立連接,但由于客戶端此時并未響應服務器端的請求,那此時服務器端就會一直在等待客戶端,這樣服務器端就白白浪費了一定的資源。若采用三次握手,服務器端沒有收到來自客戶端的再此確認,則就會知道客戶端并沒有要求建立請求,就不會浪費服務器的資源。
83. 說一下 tcp 粘包是怎么產生的?
tcp 粘包可能發生在發送端或者接收端,分別來看兩端各種產生粘包的原因:
84. OSI 的七層模型都有哪些?
85. get 和 post 請求有哪些區別?
86. 如何實現跨域?
實現跨域有以下幾種方案:
87. 說一下 JSONP 實現原理?
jsonp:JSON with Padding,它是利用script標簽的 src 連接可以訪問不同源的特性,加載遠程返回的“JS 函數”來執行的。
88. 說一下你熟悉的設計模式?
89. 簡單工廠和抽象工廠有什么區別?
90. 為什么要使用 spring?
91. 解釋一下什么是 aop?
aop 是面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。
簡單來說就是統一處理某一“切面”(類)的問題的編程思想,比如統一處理日志、異常等。
92. 解釋一下什么是 ioc?
ioc:Inversionof Control(中文:控制反轉)是 spring 的核心,對于 spring 框架來說,就是由 spring 來負責控制對象的生命周期和對象間的關系。
簡單來說,控制指的是當前對象對內部成員的控制權;控制反轉指的是,這種控制權不由當前對象管理了,由其他(類,第三方容器)來管理。
93. spring 有哪些主要模塊?
94. spring 常用的注入方式有哪些?
95. spring 中的 bean 是線程安全的嗎?
spring 中的 bean 默認是單例模式,spring 框架并沒有對單例 bean 進行多線程的封裝處理。
實際上大部分時候 spring bean 無狀態的(比如 dao 類),所有某種程度上來說 bean 也是安全的,但如果 bean 有狀態的話(比如 view model 對象),那就要開發者自己去保證線程安全了,最簡單的就是改變 bean 的作用域,把“singleton”變更為“prototype”,這樣請求 bean 相當于 new Bean()了,所以就可以保證線程安全了。
96. spring 支持幾種 bean 的作用域?
spring 支持 5 種作用域,如下:
注意: 使用 prototype 作用域需要慎重的思考,因為頻繁創建和銷毀 bean 會帶來很大的性能開銷。
97. spring 自動裝配 bean 有哪些方式?
98. spring 事務實現方式有哪些?
99. 說一下 spring 的事務隔離?
spring 有五大隔離級別,默認值為 ISOLATION_DEFAULT(使用數據庫的設置),其他四個隔離級別和數據庫的隔離級別一致:
ISOLATION_DEFAULT:用底層數據庫的設置隔離級別,數據庫設置的是什么我就用什么;
ISOLATIONREADUNCOMMITTED:未提交讀,最低隔離級別、事務未提交前,就可被其他事務讀取(會出現幻讀、臟讀、不可重復讀);
ISOLATIONREADCOMMITTED:提交讀,一個事務提交后才能被其他事務讀取到(會造成幻讀、不可重復讀),SQL server 的默認級別;
ISOLATIONREPEATABLEREAD:可重復讀,保證多次讀取同一個數據時,其值都和事務開始時候的內容是一致,禁止讀取到別的事務未提交的數據(會造成幻讀),MySQL 的默認級別;
ISOLATION_SERIALIZABLE:序列化,代價最高最可靠的隔離級別,該隔離級別能防止臟讀、不可重復讀、幻讀。
臟讀 :表示一個事務能夠讀取另一個事務中還未提交的數據。比如,某個事務嘗試插入記錄 A,此時該事務還未提交,然后另一個事務嘗試讀取到了記錄 A。
不可重復讀 :是指在一個事務內,多次讀同一數據。
幻讀 :指同一個事務內多次查詢返回的結果集不一樣。比如同一個事務 A 第一次查詢時候有 n 條記錄,但是第二次同等條件下查詢卻有 n+1 條記錄,這就好像產生了幻覺。發生幻讀的原因也是另外一個事務新增或者刪除或者修改了第一個事務結果集里面的數據,同一個記錄的數據內容被修改了,所有數據行的記錄就變多或者變少了。
100. 說一下 spring mvc 運行流程?
101. spring mvc 有哪些組件?
102. @RequestMapping 的作用是什么?
將 http 請求映射到相應的類/方法上。
103. @Autowired 的作用是什么?
@Autowired 它可以對類成員變量、方法及構造函數進行標注,完成自動裝配的工作,通過@Autowired 的使用來消除 set/get 方法。
104. 什么是 spring boot?
spring boot 是為 spring 服務的,是用來簡化新 spring 應用的初始搭建以及開發過程的。
105. 為什么要用 spring boot?
106. spring boot 核心配置文件是什么?
spring boot 核心的兩個配置文件:
107. spring boot 配置文件有哪幾種類型?它們有什么區別?
配置文件有 . properties 格式和 . yml 格式,它們主要的區別是書法風格不同。
. properties 配置如下:
spring. RabbitMQ. port=5672
. yml 配置如下:
spring:RabbitMQ:port: 5672
. yml 格式不支持 @PropertySource 注解導入。
108. spring boot 有哪些方式可以實現熱部署?
109. jpa 和 hibernate 有什么區別?
jpa 全稱 Java Persistence API,是 Java 持久化接口規范,hibernate 屬于 jpa 的具體實現。
110. 什么是 spring cloud?
spring cloud 是一系列框架的有序集合。它利用 spring boot 的開發便利性巧妙地簡化了分布式系統基礎設施的開發,如服務發現注冊、配置中心、消息總線、負載均衡、斷路器、數據監控等,都可以用 spring boot 的開發風格做到一鍵啟動和部署。
111. spring cloud 斷路器的作用是什么?
在分布式架構中,斷路器模式的作用也是類似的,當某個服務單元發生故障(類似用電器發生短路)之后,通過斷路器的故障監控(類似熔斷保險絲),向調用方返回一個錯誤響應,而不是長時間的等待。這樣就不會使得線程因調用故障服務被長時間占用不釋放,避免了故障在分布式系統中的蔓延。
112. spring cloud 的核心組件有哪些?
113. 為什么要使用 hibernate?
114. 什么是 ORM 框架?
ORM(Object Relation Mapping)對象關系映射,是把數據庫中的關系數據映射成為程序中的對象。
使用 ORM 的優點:提高了開發效率降低了開發成本、開發更簡單更對象化、可移植更強。
115. hibernate 中如何在控制臺查看打印的 SQL 語句?
在 Config 里面把 hibernate. show_SQL 設置為 true 就可以。但不建議開啟,開啟之后會降低程序的運行效率。
116. hibernate 有幾種查詢方式?
三種:hql、原生 SQL、條件查詢 Criteria。
117. hibernate 實體類可以被定義為 final 嗎?
實體類可以定義為 final 類,但這樣的話就不能使用 hibernate 代理模式下的延遲關聯提供性能了,所以不建議定義實體類為 final。
118. 在 hibernate 中使用 Integer 和 int 做映射有什么區別?
Integer 類型為對象,它的值允許為 null,而 int 屬于基礎數據類型,值不能為 null。
119. hibernate 是如何工作的?
120. get()和 load()的區別?
121. 說一下 hibernate 的緩存機制?
hibernate 常用的緩存有一級緩存和二級緩存:
一級緩存:也叫 Session 緩存,只在 Session 作用范圍內有效,不需要用戶干涉,由 hibernate 自身維護,可以通過:evict(object)清除 object 的緩存;clear()清除一級緩存中的所有緩存;flush()刷出緩存;
二級緩存:應用級別的緩存,在所有 Session 中都有效,支持配置第三方的緩存,如:EhCache。
122. hibernate 對象有哪些狀態?
123. 在 hibernate 中 getCurrentSession 和 openSession 的區別是什么?
124. hibernate 實體類必須要有無參構造函數嗎?為什么?
hibernate 中每個實體類必須提供一個無參構造函數,因為 hibernate 框架要使用 reflection api,通過調用 ClassnewInstance() 來創建實體類的實例,如果沒有無參的構造函數就會拋出異常。
125. MyBatis 中 #{}和 ${}的區別是什么?
\#{}是預編譯處理,${}是字符替換。在使用 #{}時,MyBatis 會將 SQL 中的 #{}替換成“?”,配合 PreparedStatement 的 set 方法賦值,這樣可以有效的防止 SQL 注入,保證程序的運行安全。
126. MyBatis 有幾種分頁方式?
分頁方式:邏輯分頁和物理分頁。
邏輯分頁: 使用 MyBatis 自帶的 RowBounds 進行分頁,它是一次性查詢很多數據,然后在數據中再進行檢索。
物理分頁: 自己手寫 SQL 分頁或使用分頁插件 PageHelper,去數據庫查詢指定條數的分頁數據的形式。
127. RowBounds 是一次性查詢全部結果嗎?為什么?
RowBounds 表面是在“所有”數據中檢索數據,其實并非是一次性查詢出所有數據,因為 MyBatis 是對 jdbc 的封裝,在 jdbc 驅動中有一個 Fetch Size 的配置,它規定了每次最多從數據庫查詢多少條數據,假如你要查詢更多數據,它會在你執行 next()的時候,去查詢更多的數據。就好比你去自動取款機取 10000 元,但取款機每次最多能取 2500 元,所以你要取 4 次才能把錢取完。只是對于 jdbc 來說,當你調用 next()的時候會自動幫你完成查詢工作。這樣做的好處可以有效的防止內存溢出。
Fetch Size 官方相關文檔:http://t. cn/EfSE2g3
128. MyBatis 邏輯分頁和物理分頁的區別是什么?
129. MyBatis 是否支持延遲加載?延遲加載的原理是什么?
MyBatis 支持延遲加載,設置 lazyLoadingEnabled=true 即可。
延遲加載的原理的是調用的時候觸發加載,而不是在初始化的時候就加載信息。比如調用 a. getB(). getName(),這個時候發現 a. getB() 的值為 null,此時會單獨觸發事先保存好的關聯 B 對象的 SQL,先查詢出來 B,然后再調用 a. setB(b),而這時候再調用 a. getB(). getName() 就有值了,這就是延遲加載的基本原理。
130. 說一下 MyBatis 的一級緩存和二級緩存?
開啟二級緩存數據查詢流程:二級緩存 -> 一級緩存 -> 數據庫。
緩存更新機制:當某一個作用域(一級緩存 Session/二級緩存 Mapper)進行了C/U/D 操作后,默認該作用域下所有 select 中的緩存將被 clear。
131. MyBatis 和 hibernate 的區別有哪些?
132. MyBatis 有哪些執行器(Executor)?
MyBatis 有三種基本的Executor執行器:
133. MyBatis 分頁插件的實現原理是什么?
分頁插件的基本原理是使用 MyBatis 提供的插件接口,實現自定義插件,在插件的攔截方法內攔截待執行的 SQL,然后重寫 SQL,根據 dialect 方言,添加對應的物理分頁語句和物理分頁參數。
134. MyBatis 如何編寫一個自定義插件?
自定義插件實現原理
MyBatis 自定義插件針對 MyBatis 四大對象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)進行攔截:
自定義插件實現關鍵
MyBatis 插件要實現 Interceptor 接口,接口包含的方法,如下:
public interface Interceptor {Object intercept(Invocation invocation) throws Throwable;Object plugin(Object target);void setProperties(Properties properties);}
自定義插件實現示例
官方插件實現:
@Intercepts({@Signature(type=Executor. class, method="query",args={MappedStatement. class, Object. class, RowBounds. class, ResultHandler. class})})public class TestInterceptor implements Interceptor {public Object intercept(Invocation invocation) throws Throwable {Object target=invocation. getTarget(); //被代理對象Method method=invocation. getMethod(); //代理方法Object[] args=invocation. getArgs(); //方法參數// do something . . . . . . 方法攔截前執行代碼塊Object result=invocation. proceed();// do something . . . . . . . 方法攔截后執行代碼塊return result;}public Object plugin(Object target) {return Plugin. wrap(target, this);}}
135. RabbitMQ 的使用場景有哪些?
136. RabbitMQ 有哪些重要的角色?
RabbitMQ 中重要的角色有:生產者、消費者和代理:
137. RabbitMQ 有哪些重要的組件?
138. RabbitMQ 中 vhost 的作用是什么?
vhost:每個 RabbitMQ 都能創建很多 vhost,我們稱之為虛擬主機,每個虛擬主機其實都是 mini 版的RabbitMQ,它擁有自己的隊列,交換器和綁定,擁有自己的權限機制。
139. RabbitMQ 的消息是怎么發送的?
首先客戶端必須連接到 RabbitMQ 服務器才能發布和消費消息,客戶端和 rabbit server 之間會創建一個 tcp 連接,一旦 tcp 打開并通過了認證(認證就是你發送給 rabbit 服務器的用戶名和密碼),你的客戶端和 RabbitMQ 就創建了一條 amqp 信道(channel),信道是創建在“真實” tcp 上的虛擬連接,amqp 命令都是通過信道發送出去的,每個信道都會有一個唯一的 id,不論是發布消息,訂閱隊列都是通過這個信道完成的。
140. RabbitMQ 怎么保證消息的穩定性?
141. RabbitMQ 怎么避免消息丟失?
142. 要保證消息持久化成功的條件有哪些?
以上四個條件都滿足才能保證消息持久化成功。
143. RabbitMQ 持久化有什么缺點?
持久化的缺地就是降低了服務器的吞吐量,因為使用的是磁盤而非內存存儲,從而降低了吞吐量。可盡量使用 ssd 硬盤來緩解吞吐量的問題。
144. RabbitMQ 有幾種廣播類型?
145. RabbitMQ 怎么實現延遲消息隊列?
延遲隊列的實現有兩種方式:
146. RabbitMQ 集群有什么用?
集群主要有以下兩個用途:
147. RabbitMQ 節點的類型有哪些?
148. RabbitMQ 集群搭建需要注意哪些問題?
149. RabbitMQ 每個節點是其他節點的完整拷貝嗎?為什么?
不是,原因有以下兩個:
150. RabbitMQ 集群中唯一一個磁盤節點崩潰了會發生什么情況?
如果唯一磁盤的磁盤節點崩潰了,不能進行以下操作:
唯一磁盤節點崩潰了,集群是可以保持運行的,但你不能更改任何東西。
151. RabbitMQ 對集群節點停止順序有要求嗎?
RabbitMQ 對集群的停止的順序是有要求的,應該先關閉內存節點,最后再關閉磁盤節點。如果順序恰好相反的話,可能會造成消息的丟失。
152. kafka 可以脫離 zookeeper 單獨使用嗎?為什么?
kafka 不能脫離 zookeeper 單獨使用,因為 kafka 使用 zookeeper 管理和協調 kafka 的節點服務器。
153. kafka 有幾種數據保留的策略?
kafka 有兩種數據保存策略:按照過期時間保留和按照存儲的消息大小保留。
154. kafka 同時設置了 7 天和 10G 清除數據,到第五天的時候消息達到了 10G,這個時候 kafka 將如何處理?
這個時候 kafka 會執行數據清除工作,時間和大小不論那個滿足條件,都會清空數據。
155. 什么情況會導致 kafka 運行變慢?
156. 使用 kafka 集群需要注意什么?
157. zookeeper 是什么?
zookeeper 是一個分布式的,開放源碼的分布式應用程序協調服務,是 google chubby 的開源實現,是 hadoop 和 hbase 的重要組件。它是一個為分布式應用提供一致性服務的軟件,提供的功能包括:配置維護、域名服務、分布式同步、組服務等。
158. zookeeper 都有哪些功能?
159. zookeeper 有幾種部署模式?
zookeeper 有三種部署模式:
160. zookeeper 怎么保證主從節點的狀態同步?
zookeeper 的核心是原子廣播,這個機制保證了各個 server 之間的同步。實現這個機制的協議叫做 zab 協議。zab 協議有兩種模式,分別是恢復模式(選主)和廣播模式(同步)。當服務啟動或者在領導者崩潰后,zab 就進入了恢復模式,當領導者被選舉出來,且大多數 server 完成了和 leader 的狀態同步以后,恢復模式就結束了。狀態同步保證了 leader 和 server 具有相同的系統狀態。
161. 集群中為什么要有主節點?
在分布式環境中,有些業務邏輯只需要集群中的某一臺機器進行執行,其他的機器可以共享這個結果,這樣可以大大減少重復計算,提高性能,所以就需要主節點。
162. 集群中有 3 臺服務器,其中一個節點宕機,這個時候 zookeeper 還可以使用嗎?
可以繼續使用,單數服務器只要沒超過一半的服務器宕機就可以繼續使用。
163. 說一下 zookeeper 的通知機制?
客戶端端會對某個 znode 建立一個 watcher 事件,當該 znode 發生變化時,這些客戶端會收到 zookeeper 的通知,然后客戶端可以根據 znode 變化來做出業務上的改變。
164. 數據庫的三范式是什么?
165. 一張自增表里面總共有 7 條數據,刪除了最后 2 條數據,重啟 MySQL 數據庫,又插入了一條數據,此時 id 是幾?
InnoDB 表只會把自增主鍵的最大 id 記錄在內存中,所以重啟之后會導致最大 id 丟失。
166. 如何獲取當前數據庫版本?
使用 select version() 獲取當前 MySQL 數據庫版本。
167. 說一下 ACID 是什么?
168. char 和 varchar 的區別是什么?
chat 優點:效率高;缺點:占用空間;適用場景:存儲密碼的 md5 值,固定長度的,使用 char 非常合適。
所以,從空間上考慮 varcahr 比較合適;從效率上考慮 char 比較合適,二者使用需要權衡。
169. float 和 double 的區別是什么?
170. MySQL 的內連接、左連接、右連接有什么區別?
內連接關鍵字:inner join;左連接:left join;右連接:right join。
內連接是把匹配的關聯數據顯示出來;左連接是左邊的表全部顯示出來,右邊的表顯示出符合條件的數據;右連接正好相反。
171. MySQL 索引是怎么實現的?
索引是滿足某種特定查找算法的數據結構,而這些數據結構會以某種方式指向數據,從而實現高效查找數據。
具體來說 MySQL 中的索引,不同的數據引擎實現有所不同,但目前主流的數據庫引擎的索引都是 B+ 樹實現的,B+ 樹的搜索效率,可以到達二分法的性能,找到數據區域之后就找到了完整的數據結構了,所有索引的性能也是更好的。
172. 怎么驗證 MySQL 的索引是否滿足需求?
使用 explain 查看 SQL 是如何執行查詢語句的,從而分析你的索引是否滿足需求。
explain 語法:explain select * from table where type=1。
173. 說一下數據庫的事務隔離?
MySQL 的事務隔離是在 MySQL. ini 配置文件里添加的,在文件的最后添加:
transaction-isolation=REPEATABLE-READ
可用的配置值:READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE。
臟讀 :表示一個事務能夠讀取另一個事務中還未提交的數據。比如,某個事務嘗試插入記錄 A,此時該事務還未提交,然后另一個事務嘗試讀取到了記錄 A。
不可重復讀 :是指在一個事務內,多次讀同一數據。
幻讀 :指同一個事務內多次查詢返回的結果集不一樣。比如同一個事務 A 第一次查詢時候有 n 條記錄,但是第二次同等條件下查詢卻有 n+1 條記錄,這就好像產生了幻覺。發生幻讀的原因也是另外一個事務新增或者刪除或者修改了第一個事務結果集里面的數據,同一個記錄的數據內容被修改了,所有數據行的記錄就變多或者變少了。
174. 說一下 MySQL 常用的引擎?
175. 說一下 MySQL 的行鎖和表鎖?
MyISAM 只支持表鎖,InnoDB 支持表鎖和行鎖,默認為行鎖。
176. 說一下樂觀鎖和悲觀鎖?
數據庫的樂觀鎖需要自己實現,在表里面添加一個 version 字段,每次修改成功值加 1,這樣每次修改的時候先對比一下,自己擁有的 version 和數據庫現在的 version 是否一致,如果不一致就不修改,這樣就實現了樂觀鎖。
177. MySQL 問題排查都有哪些手段?
178. 如何做 MySQL 的性能優化?
179. Redis 是什么?都有哪些使用場景?
Redis 是一個使用 C 語言開發的高速緩存數據庫。
Redis 使用場景:
180. Redis 有哪些功能?
181. Redis 和 memcache 有什么區別?
182. Redis 為什么是單線程的?
因為 cpu 不是 Redis 的瓶頸,Redis 的瓶頸最有可能是機器內存或者網絡帶寬。既然單線程容易實現,而且 cpu 又不會成為瓶頸,那就順理成章地采用單線程的方案了。
關于 Redis 的性能,官方網站也有,普通筆記本輕松處理每秒幾十萬的請求。
而且單線程并不代表就慢 nginx 和 nodejs 也都是高性能單線程的代表。
183. 什么是緩存穿透?怎么解決?
緩存穿透:指查詢一個一定不存在的數據,由于緩存是不命中時需要從數據庫查詢,查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到數據庫去查詢,造成緩存穿透。
解決方案:最簡單粗暴的方法如果一個查詢返回的數據為空(不管是數據不存在,還是系統故障),我們就把這個空結果進行緩存,但它的過期時間會很短,最長不超過五分鐘。
184. Redis 支持的數據類型有哪些?
Redis 支持的數據類型:string(字符串)、list(列表)、hash(字典)、set(集合)、zset(有序集合)。
185. Redis 支持的 Java 客戶端都有哪些?
支持的 Java 客戶端有 Redisson、jedis、lettuce 等。
186. jedis 和 Redisson 有哪些區別?
187. 怎么保證緩存和數據庫數據的一致性?
188. Redis 持久化有幾種方式?
Redis 的持久化有兩種方式,或者說有兩種策略:
189. Redis 怎么實現分布式鎖?
Redis 分布式鎖其實就是在系統里面占一個“坑”,其他程序也要占“坑”的時候,占用成功了就可以繼續執行,失敗了就只能放棄或稍后重試。
占坑一般使用 setnx(set if not exists)指令,只允許被一個程序占有,使用完調用 del 釋放鎖。
190. Redis 分布式鎖有什么缺陷?
Redis 分布式鎖不能解決超時的問題,分布式鎖有一個超時時間,程序的執行如果超出了鎖的超時時間就會出現問題。
191. Redis 如何做內存優化?
盡量使用 Redis 的散列表,把相關的信息放到散列表里面存儲,而不是把每個字段單獨存儲,這樣可以有效的減少內存使用。比如將 Web 系統的用戶對象,應該放到散列表里面再整體存儲到 Redis,而不是把用戶的姓名、年齡、密碼、郵箱等字段分別設置 key 進行存儲。
192. Redis 淘汰策略有哪些?
193. Redis 常見的性能問題有哪些?該如何解決?
194. 說一下 JVM 的主要組成部分?及其作用?
組件的作用: 首先通過類加載器(ClassLoader)會把 Java 代碼轉換成字節碼,運行時數據區(Runtime Data Area)再把字節碼加載到內存中,而字節碼文件只是 JVM 的一套指令集規范,并不能直接交個底層操作系統去執行,因此需要特定的命令解析器執行引擎(Execution Engine),將字節碼翻譯成底層系統指令,再交由 CPU 去執行,而這個過程中需要調用其他語言的本地庫接口(Native Interface)來實現整個程序的功能。
195. 說一下 JVM 運行時數據區?
不同虛擬機的運行時數據區可能略微有所不同,但都會遵從 Java 虛擬機規范, Java 虛擬機規范規定的區域分為以下 5 個部分:
196. 說一下堆棧的區別?
197. 隊列和棧是什么?有什么區別?
隊列和棧都是被用來預存儲數據的。
隊列允許先進先出檢索元素,但也有例外的情況,Deque 接口允許從兩端檢索元素。
棧和隊列很相似,但它運行對元素進行后進先出進行檢索。
198. 什么是雙親委派模型?
在介紹雙親委派模型之前先說下類加載器。對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立在 JVM 中的唯一性,每一個類加載器,都有一個獨立的類名稱空間。類加載器就是根據指定全限定名稱將 class 文件加載到 JVM 內存,然后再轉化為 class 對象。
類加載器分類:
雙親委派模型:如果一個類加載器收到了類加載的請求,它首先不會自己去加載這個類,而是把這個請求委派給父類加載器去完成,每一層的類加載器都是如此,這樣所有的加載請求都會被傳送到頂層的啟動類加載器中,只有當父加載無法完成加載請求(它的搜索范圍中沒找到所需的類)時,子加載器才會嘗試去加載類。
199. 說一下類裝載的執行過程?
類裝載分為以下 5 個步驟:
200. 怎么判斷對象是否可以被回收?
一般有兩種方法來判斷:
201. Java 中都有哪些引用類型?
202. 說一下 JVM 有哪些垃圾回收算法?
203. 說一下 JVM 有哪些垃圾回收器?
204. 詳細介紹一下 CMS 垃圾回收器?
CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器。對于要求服務器響應速度的應用上,這種垃圾回收器非常適合。在啟動 JVM 的參數加上“-XX:+UseConcMarkSweepGC”來指定使用 CMS 垃圾回收器。
CMS 使用的是標記-清除的算法實現的,所以在 gc 的時候回產生大量的內存碎片,當剩余內存不能滿足程序運行要求時,系統將會出現 Concurrent Mode Failure,臨時 CMS 會采用 Serial Old 回收器進行垃圾清除,此時的性能將會被降低。
205. 新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么區別?
新生代垃圾回收器一般采用的是復制算法,復制算法的優點是效率高,缺點是內存利用率低;老年代回收器一般采用的是標記-整理的算法進行垃圾回收。
206. 簡述分代垃圾回收器是怎么工作的?
分代回收器有兩個分區:老生代和新生代,新生代默認的空間占比總空間的 1/3,老生代的默認占比是 2/3。
新生代使用的是復制算法,新生代里有 3 個分區:Eden、To Survivor、From Survivor,它們的默認占比是 8:1:1,它的執行流程如下:
每次在 From Survivor 到 To Survivor 移動時都存活的對象,年齡就 +1,當年齡到達 15(默認配置是 15)時,升級為老生代。大對象也會直接進入老生代。
老生代當空間占用到達某個值之后就會觸發全局垃圾收回,一般使用標記整理的執行算法。以上這些循環往復就構成了整個分代垃圾回收的整體執行流程。
207. 說一下 JVM 調優的工具?
JDK 自帶了很多監控工具,都位于 JDK 的 bin 目錄下,其中最常用的是 jconsole 和 jvisualvm 這兩款視圖監控工具。
208. 常用的 JVM 調優的參數都有哪些?
以下是小編總結的面試PDF 轉發+關注+私信就可以獲取這份PDF
資料過多就不給大家一一展示了 獲取這份面試手冊的轉發+關注+私信小編(學習)免費獲取哦!
*請認真填寫需求信息,我們會在24小時內與您取得聯系。