字節面試體驗很棒!你去感受了嗎?
大家好,我是小林。
今天分享秋招的字節、快手 Java 后端面經,我篩選了Java+MySQL+Redis+MQ+網絡+操作系統共性的面試題,排除了項目和實習經歷的問題,同學反饋字節面試體驗很好,遇到不會的,面試官會一步一步引導,還會詳細解釋下,返回環節還介紹了部門情況。
整體難度不算太難,還算比較基礎,很多都是高頻面試題,可以收藏起來,反復復習。
網絡
從輸入域名到瀏覽器看見頁面經歷了什么過程?
簡單的網絡模型
- 解析URL:分析 URL 所需要使用的傳輸協議和請求的資源路徑。如果輸入的 URL 中的協議或者主機名不合法,將會把地址欄中輸入的內容傳遞給搜索引擎。如果沒有問題,瀏覽器會檢查 URL 中是否出現了非法字符,則對非法字符進行轉義后在進行下一過程。
- 緩存判斷:瀏覽器會判斷所請求的資源是否在緩存里,如果請求的資源在緩存里且沒有失效,那么就直接使用,否則向服務器發起新的請求。
- DNS解析:如果資源不在本地緩存,首先需要進行DNS解析。瀏覽器會向本地DNS服務器發送域名解析請求,本地DNS服務器會逐級查詢,最終找到對應的IP地址。
- 獲取MAC地址:當瀏覽器得到 IP 地址后,數據傳輸還需要知道目的主機 MAC 地址,因為應用層下發數據給傳輸層,TCP 協議會指定源端口號和目的端口號,然后下發給網絡層。網絡層會將本機地址作為源地址,獲取的 IP 地址作為目的地址。然后將下發給數據鏈路層,數據鏈路層的發送需要加入通信雙方的 MAC 地址,本機的 MAC 地址作為源 MAC 地址,目的 MAC 地址需要分情況處理。通過將 IP 地址與本機的子網掩碼相結合,可以判斷是否與請求主機在同一個子網里,如果在同一個子網里,可以使用 APR 協議獲取到目的主機的 MAC 地址,如果不在一個子網里,那么請求應該轉發給網關,由它代為轉發,此時同樣可以通過 ARP 協議來獲取網關的 MAC 地址,此時目的主機的 MAC 地址應該為網關的地址。
- 建立TCP連接:主機將使用目標 IP地址和目標MAC地址發送一個TCP SYN包,請求建立一個TCP連接,然后交給路由器轉發,等路由器轉到目標服務器后,服務器回復一個SYN-ACK包,確認連接請求。然后,主機發送一個ACK包,確認已收到服務器的確認,然后 TCP 連接建立完成。
- HTTPS 的 TLS 四次握手:如果使用的是 HTTPS 協議,在通信前還存在 TLS 的四次握手。
- 發送HTTP請求:連接建立后,瀏覽器會向服務器發送HTTP請求。請求中包含了用戶需要獲取的資源的信息,例如網頁的URL、請求方法(GET、POST等)等。
- 服務器處理請求并返回響應:服務器收到請求后,會根據請求的內容進行相應的處理。例如,如果是請求網頁,服務器會讀取相應的網頁文件,并生成HTTP響應。
TCP的三次握手過程?三次握手的原因是什么?
圖片
在這里插入圖片描述
- 第一次握手(SYN):客戶端向服務器發送一個帶有SYN標志的數據包,請求建立連接。客戶端會選擇一個隨機的初始序列號(ISN)作為起始序號。
- 第二次握手(SYN+ACK):服務器收到客戶端的請求后,會發送一個帶有SYN和ACK(確認)標志的數據包作為響應。服務器也會選擇一個隨機的初始序列號,并將客戶端的初始序列號加1作為確認號。同時,服務器也表示自己已經收到了客戶端的請求。
- 第三次握手(ACK):客戶端收到服務器的響應后,會發送一個帶有ACK標志的數據包作為確認。客戶端會將服務器的初始序列號加1作為確認號,并向服務器表示自己已經收到了服務器的響應。
完成了這三次握手后,TCP連接就建立起來了,雙方可以開始進行數據的傳輸。
三次握手的目的是確保雙方都能夠收到對方的請求和確認,并且雙方都同意建立連接。這樣可以防止因為網絡延遲或丟包等問題導致連接建立失敗或不穩定。同時,三次握手也能夠防止已經失效的連接請求報文段在網絡中重新出現,避免了資源的浪費。
TCP為什么可靠?
- 序列號與確認機制:TCP將每個數據包分配一個唯一的序列號,并且接收方會發送確認消息來確認已經接收到的數據。發送方會根據接收到的確認消息判斷是否需要重新發送丟失的數據包。
- 數據校驗和:TCP使用校驗和來驗證數據在傳輸過程中是否發生了損壞。接收方會計算校驗和并與發送方發送的校驗和進行比較,如果不一致,則說明數據包發生了損壞,需要重新發送。
- 滑動窗口機制:TCP使用滑動窗口來控制發送方發送數據的速度和接收方接收數據的速度,以避免因發送速度過快或接收速度過慢而導致的數據丟失或堵塞。
- 重傳機制:如果發送方沒有收到接收方的確認消息,或者接收方收到的數據包校驗和不一致,發送方會重新發送數據包,確保數據的可靠傳輸。
- 擁塞控制:TCP具有擁塞控制機制,可以根據網絡的擁塞情況來調整發送數據的速率,避免網絡擁塞導致的數據丟失和延遲。
HTTP狀態碼有哪些?
圖片
五大類 HTTP 狀態碼
1xx:信息性狀態碼,表示服務器已接收了客戶端請求,客戶端可繼續發送請求。
- 100 Continue
- 101 Switching Protocols
- 2xx:成功狀態碼,表示服務器已成功接收到請求并進行處理。
200 OK 表示客戶端請求成功
- 204 No Content 成功,但不返回任何實體的主體部分
- 206 Partial Content 成功執行了一個范圍(Range)請求
3xx:重定向狀態碼,表示服務器要求客戶端重定向。
- 301 Moved Permanently 永久性重定向,響應報文的Location首部應該有該資源的新URL
- 302 Found 臨時性重定向,響應報文的Location首部給出的URL用來臨時定位資源
- 303 See Other 請求的資源存在著另一個URI,客戶端應使用GET方法定向獲取請求的資源
- 304 Not Modified 服務器內容沒有更新,可以直接讀取瀏覽器緩存
- 307 Temporary Redirect 臨時重定向。與302 Found含義一樣。302禁止POST變換為GET,但實際使用時并不一定,307則更多瀏覽器可能會遵循這一標準,但也依賴于瀏覽器具體實現
4xx:客戶端錯誤狀態碼,表示客戶端的請求有非法內容。
- 400 Bad Request 表示客戶端請求有語法錯誤,不能被服務器所理解
- 401 Unauthonzed 表示請求未經授權,該狀態代碼必須與 WWW-Authenticate 報頭域一起使用
- 403 Forbidden 表示服務器收到請求,但是拒絕提供服務,通常會在響應正文中給出不提供服務的原因
- 404 Not Found 請求的資源不存在,例如,輸入了錯誤的URL
5xx:服務器錯誤狀態碼,表示服務器未能正常處理客戶端的請求而出現意外錯誤。
- 500 Internel Server Error 表示服務器發生不可預期的錯誤,導致無法完成客戶端的請求
- 503 Service Unavailable 表示服務器當前不能夠處理客戶端的請求,在一段時間之后,服務器可能會恢復正常
操作系統
進程間通信有哪些?
- 管道(Pipe):管道是一種半雙工的通信方式,可以在具有親緣關系的進程之間進行通信。它可以分為匿名管道和命名管道。匿名管道只能在具有共同祖先的進程之間使用,而命名管道可以在不具有親緣關系的進程之間使用。
優點:簡單易用,無需額外的系統調用和復雜的設置。
缺點:只能在具有親緣關系的進程之間進行通信,且只能實現單向通信。
- 信號(Signal):信號是一種異步的通信方式,用于通知進程發生了某種事件。一個進程可以向另一個進程發送信號,接收信號的進程可以選擇采取相應的行動。
優點:簡單、快速,適用于簡單的通信需求。
缺點:信號的發送和接收是異步的,無法傳遞大量數據,且不支持雙向通信。
- 共享內存(Shared Memory):共享內存是一種高效的通信方式,多個進程可以將同一塊內存空間映射到各自的地址空間中,從而實現共享數據。
優點:傳輸效率高,適用于大量數據的共享。
缺點:需要額外的同步機制來保證數據的一致性和互斥訪問,容易造成數據競爭和死鎖。
- 信號量(Semaphore):信號量是一種用于進程間同步的機制,可以用來保護共享資源的互斥訪問。
優點:可以用于進程間的同步和互斥。
缺點:只提供了同步和互斥的功能,無法傳遞大量數據。
- 消息隊列:消息隊列是一種消息傳遞的機制,可以在不同進程之間傳遞特定格式的消息。
優點:支持多對多的進程通信,每個消息都有特定的格式。
缺點:消息的發送和接收是同步的,且不支持實時性要求較高的通信。
- 套接字(Socket):套接字是一種通用的進程間通信機制,可以在不同主機上的進程之間進行通信。
優點:支持網絡通信,可以在不同主機上的進程之間進行通信。
缺點:相對于其他IPC方式,套接字的使用和編程復雜度較高。
電腦 4GB內存,我申請 5GB內存可以嗎?
應用程序通過 malloc 函數申請內存的時候,實際上申請的是虛擬內存,此時并不會分配物理內存。
虛擬內存的最大值首先操作系統的位數,32 位操作系統和 64 位操作系統的虛擬地址空間大小是不同的,在 Linux 操作系統中,虛擬地址空間的內部又被分為內核空間和用戶空間兩部分,如下所示:
圖片
- 32 位系統的內核空間占用 1G,位于最高處,剩下的 3G 是用戶空間,所以在 32 位操作系統場景下,執行malloc申請5G內存,會失敗。
- 64 位系統的內核空間和用戶空間都是 128T,所以在 64 位操作系統場景下,即使物理內存只有 4G,但是還是可以申請5G虛擬內存,能申請成功。
申請成功之后,在使用這5G內存時候會有問題嗎?
會有問題,會發生 OOM 的錯誤,內存溢出。
當應用程序讀寫了這塊虛擬內存,CPU 就會去訪問這個虛擬內存, 這時會發現這個虛擬內存沒有映射到物理內存, CPU 就會產生缺頁中斷,進程會從用戶態切換到內核態,并將缺頁中斷交給內核的 Page Fault Handler (缺頁中斷函數)處理。
缺頁中斷處理函數會看是否有空閑的物理內存:
- 如果有,就直接分配物理內存,并建立虛擬內存與物理內存之間的映射關系。
- 如果沒有空閑的物理內存,那么內核就會開始進行的工作,如果回收內存工作結束后,空閑的物理內存仍然無法滿足此次物理內存的申請,那么內核就會放最后的大招了觸發 OOM (Out of Memory)機制。
MySQL
索引的底層是怎么實現的?
MySQL 默認存儲引擎是 InnoDB,InnoDB 默認是使用 B+樹 作為索引的數據結構。
主鍵索引 B+Tree
為什么用B+樹呢?
- B+Tree vs 二叉樹:對于有 N 個葉子節點的 B+Tree,其搜索復雜度為O(logdN),其中 d 表示節點允許的最大子節點個數為 d 個。在實際的應用當中, d 值是大于100的,這樣就保證了,即使數據達到千萬級別時,B+Tree 的高度依然維持在 3~4 層左右,也就是說一次數據查詢操作只需要做 3~4 次的磁盤 I/O 操作就能查詢到目標數據。而二叉樹的每個父節點的兒子節點個數只能是 2 個,意味著其搜索復雜度為 O(logN),這已經比 B+Tree 高出不少,因此二叉樹檢索到目標數據所經歷的磁盤 I/O 次數要更多。
- B+Tree vs Hash:Hash 在做等值查詢的時候效率賊快,搜索復雜度為 O(1)。但是 Hash 表不適合做范圍查詢,它更適合做等值的查詢,這也是 B+Tree 索引要比 Hash 表索引有著更廣泛的適用場景的原因。
- B+Tree vs B Tree:B+Tree 只在葉子節點存儲數據,而 B 樹 的非葉子節點也要存儲數據,所以 B+Tree 的單個節點的數據量更小,在相同的磁盤 I/O 次數下,就能查詢更多的節點。另外,B+Tree 葉子節點采用的是雙鏈表連接,適合 MySQL 中常見的基于范圍的順序查找,而 B 樹無法做到這一點。
你是如何選擇什么字段來做索引的?
- 字段有唯一性限制的,比如商品編碼;
- 經常用于 WHERE 查詢條件的字段,這樣能夠提高整個表的查詢速度,如果查詢條件不是一個字段,可以建立聯合索引。
- 經常用于 GROUP BY 和 ORDER BY 的字段,這樣在查詢的時候就不需要再去做一次排序了,因為我們都已經知道了建立索引之后在 B+Tree 中的記錄都是排序好的。
假如現在有三個普通索引a,b,c,sql查詢where a = xx and b = xx and c == xx會怎么樣?
會進行索引合并,對多個索引分別進行條件掃描,然后將它們各自的結果進行合并。
MySQL5.0之前,一個表一次只能使用一個索引,無法同時使用多個索引分別進行條件掃描。但是從5.1開始,引入了索引合并優化技術,對同一個表可以使用多個索引分別進行條件掃描。
那如果不想索引合并呢?怎么解決?
如果出現了索引合并,那么一般同時也意味著我們的索引建立得不太合理,因為索引合并 是可以通過建立聯合索引進行更一步優化的,減少索引掃描的次數。
比如,建立聯合索引(a, b, c),這條查詢語句遵循最左匹配原則,所以三個字段都可以利用聯合索引。
圖片
Redis
Redis在項目中的應用?
主要用做緩存,提升查詢的性能,避免請求查詢mysql。
Redis過期淘汰策略有哪些?
Redis 內存淘汰策略共有八種,這八種策略大體分為「不進行數據淘汰」和「進行數據淘汰」兩類策略。
1、不進行數據淘汰的策略
noeviction(Redis3.0之后,默認的內存淘汰策略) :它表示當運行內存超過最大設置內存時,不淘汰任何數據,這時如果有新的數據寫入,會報錯通知禁止寫入,不淘汰任何數據,但是如果沒用數據寫入的話,只是單純的查詢或者刪除操作的話,還是可以正常工作。
2、進行數據淘汰的策略
針對「進行數據淘汰」這一類策略,又可以細分為「在設置了過期時間的數據中進行淘汰」和「在所有數據范圍內進行淘汰」這兩類策略。
在設置了過期時間的數據中進行淘汰:
- volatile-random:隨機淘汰設置了過期時間的任意鍵值;
- volatile-ttl:優先淘汰更早過期的鍵值。
- volatile-lru(Redis3.0 之前,默認的內存淘汰策略):淘汰所有設置了過期時間的鍵值中,最久未使用的鍵值;
- volatile-lfu(Redis 4.0 后新增的內存淘汰策略):淘汰所有設置了過期時間的鍵值中,最少使用的鍵值;
在所有數據范圍內進行淘汰:
- allkeys-random:隨機淘汰任意鍵值;
- allkeys-lru:淘汰整個鍵值中最久未使用的鍵值;
- allkeys-lfu(Redis 4.0 后新增的內存淘汰策略):淘汰整個鍵值中最少使用的鍵值。
Java
HashMap底層原理
- 數據結構:在 JDK 1.7 版本之前, HashMap 數據結構是數組和鏈表,HashMap通過哈希算法將元素的鍵(Key)映射到數組中的槽位(Bucket)。如果多個鍵映射到同一個槽位,它們會以鏈表的形式存儲在同一個槽位上,因為鏈表的查詢時間是O(n),所以沖突很嚴重,一個索引上的鏈表非常長,效率就很低了,所以在 JDK 1.8版本的時候做了優化,當一個鏈表的長度超過8的時候就轉換數據結構,不再使用鏈表存儲,而是使用紅黑樹,查找時使用紅黑樹,時間復雜度O(log n),可以提高查詢性能,但是在數量較少時,即數量小于6時,會將紅黑樹轉換回鏈表。
圖片
- 插入鍵值對的put方法的區別:JDK 1.8中會將節點插入到鏈表尾部,而1.7中是采用頭插;
- 哈希算法:JDK 1.7中的 hash() 擾動函數需要進行4次異或運算;而 JDK 1.8中則是將 (h = key.hashCode()) ^ (h >>> 16) 的結果作為計算下標的hash值,只需要一次異或運算就可以讓hashCode的高位和低位同時參與下標值的計算,更具有隨機性,可以使元素分布更均勻;
// JDK 1.7 hash 方法源碼.
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// JDK 1.8 hash 方法源碼.
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^:按位異或
// >>>:無符號右移,忽略符號位,空位都以0補齊
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap的擴容過程,說一下
hashMap默認的負載因子是0.75,即如果hashmap中的元素個數超過了總容量75%,則會觸發擴容,擴容分為兩個步驟:
- 新的數組擴容 2 倍大小
- 計算每個元素新的位置,然后遷移元素
JDK 1.8 在擴容 2 倍容量后,在遷移數據時,不需要重新計算元素的hash進行元素遷移,而是用原先位置key的hash值與舊數組的長度(oldCap)進行"與"操作。
圖片
舉一個例子,假設元素 e 存儲在舊數據的桶位 i 中:
- 如果(e.hash & oldCap) == 0 ,那么當前元素的桶位置不變。
- 如果(e.hash & oldCap) == 1,那么桶的位置就是原位置+原數組長度(oldCap)
為什么用 e.hash & oldCap 計算新位置?
首先我們要明確三點:
- HashMap的數組大小一定是2的N次冪
- HashMap擴容時一般為增大一倍,即size = size * 2
- HashMap的索引計算方式為 hash(key) & (size - 1),由1可知,size-1的二進制「低位都為1」
我們假設 oldCap = 16, 即 2^4,16 - 1 = 15, 二進制表示為 0000 0000 0000 0000 0000 0000 0000 1111,可見除了低4位, 其他位置都是0,則 (16-1) & hash 自然就是取hash值的低4位,我們假設它為 abcd(abcd 各自的值可能是 0 或者 1)。
當我們將oldCap擴大兩倍后, 新的index的位置就變成了 (32-1) & hash, 其實就是取 hash值的低5位.。那么對于同一個Node, 低5位的值無外乎下面兩種情況:
0abcd
1abcd
其中, 0abcd與原來的index值一致, 而1abcd = 0abcd + 10000 = 0abcd + oldCap(這里的oldCap是 16,二進制數:10000 )
故雖然數組大小擴大了一倍,但是同一個key在新舊table中對應的index卻存在一定聯系:要么一致,要么相差一個 oldCap。
而新舊index是否一致就體現在hash值的第4位(我們把最低為稱作第0位), 怎么拿到這一位的值呢, 只要:
hash & 0000 0000 0000 0000 0000 0000 0001 0000
上式就等效于
hash & oldCap
故得出結論:
- 如果(e.hash & oldCap) == 0 ,那么當前元素的桶位置不變。
- 如果(e.hash & oldCap) == 1,那么桶的位置就是原位置+原數組長度(oldCap)
ArrayList底層是怎么實現擴容的?
- 當創建ArrayList對象時,如果使用的是無參構造器,則初始elementData容量為0,第1次添加,則擴容elementData為10,如需要再次擴容,則擴容elementData:為1.5倍
- 如果使用的是指定大小的構造器,則初始elementData容量為指定大小,如果需要擴容,則直接擴容elementData為1.5倍。
ArrayList和LinkedList的區別
- 是否保證線程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保證線程安全;
- 底層數據結構: Arraylist 底層使用的是 Object 數組;LinkedList 底層使用的是 雙向鏈表 數據結構(JDK1.6 之前為循環鏈表,JDK1.7 取消了循環。注意雙向鏈表和雙向循環鏈表的區別,下面有介紹到!)
- 插入和刪除是否受元素位置的影響: ① ArrayList 采用數組存儲,所以插入和刪除元素的時間復雜度受元素位置的影響。 比如:執行add(E e)方法的時候, ArrayList 會默認在將指定的元素追加到此列表的末尾,這種情況時間復雜度就是 O(1)。但是如果要在指定位置 i 插入和刪除元素的話(add(int index, E element))時間復雜度就為 O(n-i)。因為在進行上述操作的時候集合中第 i 和第 i 個元素之后的(n-i)個元素都要執行向后位/向前移一位的操作。② LinkedList 采用鏈表存儲,所以對于add(E e)方法的插入,刪除元素時間復雜度不受元素位置的影響,近似 O(1),如果是要在指定位置i插入和刪除元素的話((add(int index, E element)) 時間復雜度近似為o(n))因為需要先移動到指定位置再插入。
- 是否支持快速隨機訪問: LinkedList 不支持高效的隨機元素訪問,而 ArrayList 支持。快速隨機訪問就是通過元素的序號快速獲取元素對象(對應于get(int index)方法)。
- 內存空間占用: ArrayList 的空 間浪費主要體現在在 list 列表的結尾會預留一定的容量空間,而 LinkedList 的空間花費則體現在它的每一個元素都需要消耗比 ArrayList 更多的空間(因為要存放直接后繼和直接前驅以及數據)。
消息隊列
MQ如何防止消息不丟失?
使用一個消息隊列,其實就分為三大塊:生產者、中間件、消費者,所以要保證消息就是保證三個環節都不能丟失數據。
圖片
- 消息生產階段:生產者會不會丟消息,取決于生產者對于異常情況的處理是否合理。從消息被生產出來,然后提交給 MQ 的過程中,只要能正常收到 ( MQ 中間件) 的 ack 確認響應,就表示發送成功,所以只要處理好返回值和異常,如果返回異常則進行消息重發,那么這個階段是不會出現消息丟失的。
- 消息存儲階段:RabbitMQ 或 Kafka 這類專業的隊列中間件,在使用時是部署一個集群,生產者在發布消息時,隊列中間件通常會寫「多個節點」,也就是有多個副本,這樣一來,即便其中一個節點掛了,也能保證集群的數據不丟失。
- 消息消費階段:消費者接收消息+消息處理之后,才回復 ack 的話,那么消息階段的消息不會丟失。不能收到消息就回 ack,否則可能消息處理中途掛掉了,消息就丟失了。
MQ消息大量堆積怎么辦?
- 從生產者端解決:一般我們的系統容量或者處理能力都是規劃好的,出現消息堆積的情況,大部分是由于流量暴增引起,這個時候可以考慮控制生產者的速率,對前端機器流量進行限速限流。
- 從消費者端解決。消費者端解決的思路有兩種:
假如消費者數還有增加的空間,那么我們加消費者解決。
假如沒有拓展的可能,但吞吐量還沒達到MQ的上限,只是消費者消費能力不足,比如消費者總體消費能力已經到達上線(數據庫寫入能力等),或者類似Kafka的消費者數量與partition數有關,如果前期設計沒有做好水平拓展的設計,這個時候多少個partition就只能對應多少個消費者。這個時候我們可以先把一部分消息先打到另外一個MQ中或者先落到日志文件中,再拓展消費者進行消費,優先恢復上游業。
- 從整體系統上進行解決:第2點有提到就是有些MQ的設計限制,導致的消費者數是沒法動態拓展的,這個時候可以考慮將原先隊列進行拆分,比如新建一個topic 分擔一部分消息,這個方式需要對系統的上下游都要進行調整,在實際操作難度可能比較高,處理起來可能也比較耗時,如果在事前有做好這個設計那事發后就能很好進行調整。
算法
算法題:最長公共子串
算法題:K個一組翻轉鏈表