圖解學(xué)習(xí)網(wǎng)站:https://xiaolincoding.com
大家好,我是小林。
這周已經(jīng)分享過了幾個(gè)大廠后端開發(fā)面經(jīng),有不少同學(xué)反饋也先看看小廠的面經(jīng),想感受一下區(qū)別。
有些小廠面試不會(huì)問太多,主要就考察幾個(gè)問題,可能十多分鐘就結(jié)束了,但是也有一些小廠比較例外,可能會(huì)拷打你一個(gè)小時(shí),面試考察題量相當(dāng)于大廠的題量,但是這種情況還是比較少數(shù),同學(xué)們也不需要有太大的壓力。
分析十多分鐘的小廠面經(jīng)也沒什么意思,大家能學(xué)到的內(nèi)容也比較有限,正好之前看到有同學(xué)面南京某小廠Java后端開發(fā)的時(shí)候,足足被拷打了近 30 題,還是蠻有壓力的,有些題目也是大廠會(huì)考察的內(nèi)容。
面試問題不止非常有廣度,而且有些問題問的也很有深度,今天就讓我們來看看吧!
考察的知識(shí)點(diǎn),我給大家羅列了一下:
- MySQL:索引、特性和關(guān)鍵字、存儲(chǔ)引起、索引數(shù)據(jù)結(jié)構(gòu)Redis:線程模型、持久化機(jī)制、分布式鎖、應(yīng)用場(chǎng)景Java:SpringBoot、JVM、volatile
MySQL
MySQL索引的數(shù)據(jù)結(jié)構(gòu)
從數(shù)據(jù)結(jié)構(gòu)的角度來看,MySQL 常見索引有 B+Tree 索引、HASH 索引、Full-Text 索引。
每一種存儲(chǔ)引擎支持的索引類型不一定相同,表中總結(jié)了 MySQL 常見的存儲(chǔ)引擎 InnoDB、MyISAM 和 Memory 分別支持的索引類型。
InnoDB 是在 MySQL 5.5 之后成為默認(rèn)的 MySQL 存儲(chǔ)引擎,B+Tree 索引類型也是 MySQL 存儲(chǔ)引擎采用最多的索引類型。
MySQL索引底層的數(shù)據(jù)結(jié)構(gòu)是B+樹。B+樹是一種多叉樹,葉子節(jié)點(diǎn)才存放數(shù)據(jù),非葉子節(jié)點(diǎn)只存放索引,而且每個(gè)節(jié)點(diǎn)里的數(shù)據(jù)是按主鍵順序存放的。每一層父節(jié)點(diǎn)的索引值都會(huì)出現(xiàn)在下層子節(jié)點(diǎn)的索引值中,因此在葉子節(jié)點(diǎn)中,包括了所有的索引值信息,并且每一個(gè)葉子節(jié)點(diǎn)都有兩個(gè)指針,分別指向下一個(gè)葉子節(jié)點(diǎn)和上一個(gè)葉子節(jié)點(diǎn),形成一個(gè)雙向鏈表。
主鍵索引的 B+樹 如圖所示:
MySQL有哪些存儲(chǔ)引擎
MySQL中常用的存儲(chǔ)引擎分別是:MyISAM存儲(chǔ)引擎、innoDB存儲(chǔ)引擎,他們的區(qū)別在于:
- 事務(wù):InnoDB 支持事務(wù),MyISAM 不支持事務(wù),這是 MySQL 將默認(rèn)存儲(chǔ)引擎從 MyISAM 變成 InnoDB 的重要原因之一。索引結(jié)構(gòu):InnoDB 是聚簇索引,MyISAM 是非聚簇索引。聚簇索引的文件存放在主鍵索引的葉子節(jié)點(diǎn)上,因此 InnoDB 必須要有主鍵,通過主鍵索引效率很高。但是輔助索引需要兩次查詢,先查詢到主鍵,然后再通過主鍵查詢到數(shù)據(jù)。因此,主鍵不應(yīng)該過大,因?yàn)橹麈I太大,其他索引也都會(huì)很大。而 MyISAM 是非聚簇索引,數(shù)據(jù)文件是分離的,索引保存的是數(shù)據(jù)文件的指針。主鍵索引和輔助索引是獨(dú)立的。鎖粒度:InnoDB 最小的鎖粒度是行鎖,MyISAM 最小的鎖粒度是表鎖。一個(gè)更新語(yǔ)句會(huì)鎖住整張表,導(dǎo)致其他查詢和更新都會(huì)被阻塞,因此并發(fā)訪問受限。count 的效率:InnoDB 不保存表的具體行數(shù),執(zhí)行 select count(*) from table 時(shí)需要全表掃描。而MyISAM 用一個(gè)變量保存了整個(gè)表的行數(shù),執(zhí)行上述語(yǔ)句時(shí)只需要讀出該變量即可,速度很快。
MySQL為什么用B+樹結(jié)構(gòu),和其他結(jié)構(gòu)比的優(yōu)點(diǎn)
MySQL 是會(huì)將數(shù)據(jù)持久化在硬盤,而存儲(chǔ)功能是由 MySQL 存儲(chǔ)引擎實(shí)現(xiàn)的,所以討論 MySQL 使用哪種數(shù)據(jù)結(jié)構(gòu)作為索引,實(shí)際上是在討論存儲(chǔ)引使用哪種數(shù)據(jù)結(jié)構(gòu)作為索引,InnoDB 是 MySQL 默認(rèn)的存儲(chǔ)引擎,它就是采用了 B+ 樹作為索引的數(shù)據(jù)結(jié)構(gòu)。
要設(shè)計(jì)一個(gè) MySQL 的索引數(shù)據(jù)結(jié)構(gòu),不僅僅考慮數(shù)據(jù)結(jié)構(gòu)增刪改的時(shí)間復(fù)雜度,更重要的是要考慮磁盤 I/0 的操作次數(shù)。因?yàn)樗饕陀涗浂际谴娣旁谟脖P,硬盤是一個(gè)非常慢的存儲(chǔ)設(shè)備,我們?cè)诓樵償?shù)據(jù)的時(shí)候,最好能在盡可能少的磁盤 I/0 的操作次數(shù)內(nèi)完成。
二分查找樹雖然是一個(gè)天然的二分結(jié)構(gòu),能很好的利用二分查找快速定位數(shù)據(jù),但是它存在一種極端的情況,每當(dāng)插入的元素都是樹內(nèi)最大的元素,就會(huì)導(dǎo)致二分查找樹退化成一個(gè)鏈表,此時(shí)查詢復(fù)雜度就會(huì)從 O(logn)降低為 O(n)。
為了解決二分查找樹退化成鏈表的問題,就出現(xiàn)了自平衡二叉樹,保證了查詢操作的時(shí)間復(fù)雜度就會(huì)一直維持在 O(logn) 。但是它本質(zhì)上還是一個(gè)二叉樹,每個(gè)節(jié)點(diǎn)只能有 2 個(gè)子節(jié)點(diǎn),隨著元素的增多,樹的高度會(huì)越來越高。
而樹的高度決定于磁盤 I/O 操作的次數(shù),因?yàn)闃涫谴鎯?chǔ)在磁盤中的,訪問每個(gè)節(jié)點(diǎn),都對(duì)應(yīng)一次磁盤 I/O 操作,也就是說樹的高度就等于每次查詢數(shù)據(jù)時(shí)磁盤 IO 操作的次數(shù),所以樹的高度越高,就會(huì)影響查詢性能。
B 樹和 B+ 都是通過多叉樹的方式,會(huì)將樹的高度變矮,所以這兩個(gè)數(shù)據(jù)結(jié)構(gòu)非常適合檢索存于磁盤中的數(shù)據(jù)。
B+Tree vs B Tree:
-
- B+Tree 只在葉子節(jié)點(diǎn)存儲(chǔ)數(shù)據(jù),而 B 樹 的非葉子節(jié)點(diǎn)也要存儲(chǔ)數(shù)據(jù),所以 B+Tree 的單個(gè)節(jié)點(diǎn)的數(shù)據(jù)量更小,在相同的磁盤 I/O 次數(shù)下,就能查詢更多的節(jié)點(diǎn)。另外,B+Tree 葉子節(jié)點(diǎn)采用的是雙鏈表連接,適合 MySQL 中常見的基于范圍的順序查找,而 B 樹無法做到這一點(diǎn)。
B+Tree vs 二叉樹:
-
- 對(duì)于有 N 個(gè)葉子節(jié)點(diǎn)的 B+Tree,其搜索復(fù)雜度為O(logdN),其中 d 表示節(jié)點(diǎn)允許的最大子節(jié)點(diǎn)個(gè)數(shù)為 d 個(gè)。在實(shí)際的應(yīng)用當(dāng)中, d 值是大于100的,這樣就保證了,即使數(shù)據(jù)達(dá)到千萬級(jí)別時(shí),B+Tree 的高度依然維持在 3~4 層左右,也就是說一次數(shù)據(jù)查詢操作只需要做 3~4 次的磁盤 I/O 操作就能查詢到目標(biāo)數(shù)據(jù)。而二叉樹的每個(gè)父節(jié)點(diǎn)的兒子節(jié)點(diǎn)個(gè)數(shù)只能是 2 個(gè),意味著其搜索復(fù)雜度為 O(logN),這已經(jīng)比 B+Tree 高出不少,因此二叉樹檢索到目標(biāo)數(shù)據(jù)所經(jīng)歷的磁盤 I/O 次數(shù)要更多。
B+Tree vs Hash:
- Hash 在做等值查詢的時(shí)候效率賊快,搜索復(fù)雜度為 O(1)。但是 Hash 表不適合做范圍查詢,它更適合做等值的查詢,這也是 B+Tree 索引要比 Hash 表索引有著更廣泛的適用場(chǎng)景的原因
MySQL的關(guān)鍵字in和exist
在MySQL中,IN
和 EXISTS
都是用來處理子查詢的關(guān)鍵詞,但它們?cè)诠δ堋⑿阅芎褪褂脠?chǎng)景上有各自的特點(diǎn)和區(qū)別。
IN關(guān)鍵字
IN
用于檢查左邊的表達(dá)式是否存在于右邊的列表或子查詢的結(jié)果集中。如果存在,則IN
返回TRUE
,否則返回FALSE
。
語(yǔ)法結(jié)構(gòu):
SELECT?column_name(s)
FROM?table_name
WHERE?column_name?IN?(value1,?value2,?...);
或
SELECT?column_name(s)
FROM?table_name
WHERE?column_name?IN?(SELECT?column_name?FROM?another_table?WHERE?condition);
例子:
SELECT?*?FROM?Customers
WHERE?Country?IN?('Germany',?'France');
EXISTS關(guān)鍵字
EXISTS
用于判斷子查詢是否至少能返回一行數(shù)據(jù)。它不關(guān)心子查詢返回什么數(shù)據(jù),只關(guān)心是否有結(jié)果。如果子查詢有結(jié)果,則EXISTS
返回TRUE
,否則返回FALSE
。語(yǔ)法結(jié)構(gòu):
SELECT?column_name(s)
FROM?table_name
WHERE?EXISTS?(SELECT?column_name?FROM?another_table?WHERE?condition);
例子:
SELECT?*?FROM?Customers
WHERE?EXISTS?(SELECT?1?FROM?Orders?WHERE?Orders.CustomerID?=?Customers.CustomerID);
區(qū)別與選擇
性能差異:在很多情況下,EXISTS
的性能優(yōu)于IN
,特別是當(dāng)子查詢的表很大時(shí)。這是因?yàn)?code>EXISTS一旦找到匹配項(xiàng)就會(huì)立即停止查詢,而IN
可能會(huì)掃描整個(gè)子查詢結(jié)果集。
使用場(chǎng)景:如果子查詢結(jié)果集較小且不頻繁變動(dòng),IN
可能更直觀易懂。而當(dāng)子查詢涉及外部查詢的每一行判斷,并且子查詢的效率較高時(shí),EXISTS
更為合適。
NULL值處理:IN
能夠正確處理子查詢中包含NULL值的情況,而EXISTS
不受子查詢結(jié)果中NULL值的影響,因?yàn)樗P(guān)注的是行的存在性,而不是具體值。
事務(wù)四大特性是什么?
原子性(Atomicity):一個(gè)事務(wù)中的所有操作,要么全部完成,要么全部不完成,不會(huì)結(jié)束在中間某個(gè)環(huán)節(jié),而且事務(wù)在執(zhí)行過程中發(fā)生錯(cuò)誤,會(huì)被回滾到事務(wù)開始前的狀態(tài),就像這個(gè)事務(wù)從來沒有執(zhí)行過一樣,就好比買一件商品,購(gòu)買成功時(shí),則給商家付了錢,商品到手;購(gòu)買失敗時(shí),則商品在商家手中,消費(fèi)者的錢也沒花出去。
一致性(Consistency):是指事務(wù)操作前和操作后,數(shù)據(jù)滿足完整性約束,數(shù)據(jù)庫(kù)保持一致性狀態(tài)。比如,用戶 A 和用戶 B 在銀行分別有 800 元和 600 元,總共 1400 元,用戶 A 給用戶 B 轉(zhuǎn)賬 200 元,分為兩個(gè)步驟,從 A 的賬戶扣除 200 元和對(duì) B 的賬戶增加 200 元。一致性就是要求上述步驟操作后,最后的結(jié)果是用戶 A 還有 600 元,用戶 B 有 800 元,總共 1400 元,而不會(huì)出現(xiàn)用戶 A 扣除了 200 元,但用戶 B 未增加的情況(該情況,用戶 A 和 B 均為 600 元,總共 1200 元)。
隔離性(Isolation):數(shù)據(jù)庫(kù)允許多個(gè)并發(fā)事務(wù)同時(shí)對(duì)其數(shù)據(jù)進(jìn)行讀寫和修改的能力,隔離性可以防止多個(gè)事務(wù)并發(fā)執(zhí)行時(shí)由于交叉執(zhí)行而導(dǎo)致數(shù)據(jù)的不一致,因?yàn)槎鄠€(gè)事務(wù)同時(shí)使用相同的數(shù)據(jù)時(shí),不會(huì)相互干擾,每個(gè)事務(wù)都有一個(gè)完整的數(shù)據(jù)空間,對(duì)其他并發(fā)事務(wù)是隔離的。也就是說,消費(fèi)者購(gòu)買商品這個(gè)事務(wù),是不影響其他消費(fèi)者購(gòu)買的。
持久性(Durability):事務(wù)處理結(jié)束后,對(duì)數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會(huì)丟失。
InnoDB 引擎通過什么技術(shù)來保證事務(wù)的這四個(gè)特性的呢?
- 持久性是通過 redo log (重做日志)來保證的;原子性是通過 undo log(回滾日志) 來保證的;隔離性是通過 MVCC(多版本并發(fā)控制) 或鎖機(jī)制來保證的;一致性則是通過持久性+原子性+隔離性來保證;
Redis
Redis是單線程的還是多線程的,為什么是單線程的?有了解過其特性嗎
Redis在執(zhí)行指令時(shí)是單線程操作的,但是在實(shí)際運(yùn)行過程中也出現(xiàn)多個(gè)線程并行運(yùn)行的情況。
Redis 單線程指的是「接收客戶端請(qǐng)求->解析請(qǐng)求 ->進(jìn)行數(shù)據(jù)讀寫等操作->發(fā)送數(shù)據(jù)給客戶端」這個(gè)過程是由一個(gè)線程(主線程)來完成的,這也是我們常說 Redis 是單線程的原因。
除此之外,雖然 Redis 的主要工作(網(wǎng)絡(luò) I/O 和執(zhí)行命令)一直是單線程模型,但是在 Redis 6.0 版本之后,也采用了多個(gè) I/O 線程來處理網(wǎng)絡(luò)請(qǐng)求,這是因?yàn)殡S著網(wǎng)絡(luò)硬件的性能提升,Redis 的性能瓶頸有時(shí)會(huì)出現(xiàn)在網(wǎng)絡(luò) I/O 的處理上。
所以為了提高網(wǎng)絡(luò) I/O 的并行度,Redis 6.0 對(duì)于網(wǎng)絡(luò) I/O 采用多線程來處理。但是對(duì)于命令的執(zhí)行,Redis 仍然使用單線程來處理,所以大家不要誤解 Redis 有多線程同時(shí)執(zhí)行命令。
Redis 官方表示,Redis 6.0 版本引入的多線程 I/O 特性對(duì)性能提升至少是一倍以上。
Redis 6.0 版本支持的 I/O 多線程特性,默認(rèn)情況下 I/O 多線程只針對(duì)發(fā)送響應(yīng)數(shù)據(jù)(write client socket),并不會(huì)以多線程的方式處理讀請(qǐng)求(read client socket)。要想開啟多線程處理客戶端讀請(qǐng)求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置項(xiàng)設(shè)為 yes。
//讀請(qǐng)求也使用io多線程
io-threads-do-reads?yes
同時(shí), Redis.conf 配置文件中提供了 IO 多線程個(gè)數(shù)的配置項(xiàng)。
//?io-threads?N,表示啟用?N-1?個(gè)?I/O?多線程(主線程也算一個(gè)?I/O?線程)
io-threads?4
關(guān)于線程數(shù)的設(shè)置,官方的建議是如果為 4 核的 CPU,建議線程數(shù)設(shè)置為 2 或 3,如果為 8 核 CPU 建議線程數(shù)設(shè)置為 6,線程數(shù)一定要小于機(jī)器核數(shù),線程數(shù)并不是越大越好。
因此, Redis 6.0 版本之后,Redis 在啟動(dòng)的時(shí)候,默認(rèn)情況下會(huì)額外創(chuàng)建 6 個(gè)線程(_這里的線程數(shù)不包括主線程_):
- Redis-server :Redis的主線程,主要負(fù)責(zé)執(zhí)行命令;bio_close_file、bio_aof_fsync、bio_lazy_free:三個(gè)后臺(tái)線程,分別異步處理關(guān)閉文件任務(wù)、AOF刷盤任務(wù)、釋放內(nèi)存任務(wù);io_thd_1、io_thd_2、io_thd_3:三個(gè) I/O 線程,io-threads 默認(rèn)是 4 ,所以會(huì)啟動(dòng) 3(4-1)個(gè) I/O 多線程,用來分擔(dān) Redis 網(wǎng)絡(luò) I/O 的壓力。
Redis有哪2種持久化方式,分別的優(yōu)缺點(diǎn)
Redis 的讀寫操作都是在內(nèi)存中,所以 Redis 性能才會(huì)高,但是當(dāng) Redis 重啟后,內(nèi)存中的數(shù)據(jù)就會(huì)丟失,那為了保證內(nèi)存中的數(shù)據(jù)不會(huì)丟失,Redis 實(shí)現(xiàn)了數(shù)據(jù)持久化的機(jī)制,這個(gè)機(jī)制會(huì)把數(shù)據(jù)存儲(chǔ)到磁盤,這樣在 Redis 重啟就能夠從磁盤中恢復(fù)原有的數(shù)據(jù)。Redis 共有三種數(shù)據(jù)持久化的方式:
AOF 日志:每執(zhí)行一條寫操作命令,就把該命令以追加的方式寫入到一個(gè)文件里;
RDB 快照:將某一時(shí)刻的內(nèi)存數(shù)據(jù),以二進(jìn)制的方式寫入磁盤;
AOF 日志是如何實(shí)現(xiàn)的?
Redis 在執(zhí)行完一條寫操作命令后,就會(huì)把該命令以追加的方式寫入到一個(gè)文件里,然后 Redis 重啟時(shí),會(huì)讀取該文件記錄的命令,然后逐一執(zhí)行命令的方式來進(jìn)行數(shù)據(jù)恢復(fù)。
我這里以「_set name xiaolin_」命令作為例子,Redis 執(zhí)行了這條命令后,記錄在 AOF 日志里的內(nèi)容如下圖:Redis 提供了 3 種寫回硬盤的策略, 在 Redis.conf 配置文件中的 appendfsync 配置項(xiàng)可以有以下 3 種參數(shù)可填:
Always,這個(gè)單詞的意思是「總是」,所以它的意思是每次寫操作命令執(zhí)行完后,同步將 AOF 日志數(shù)據(jù)寫回硬盤;
Everysec,這個(gè)單詞的意思是「每秒」,所以它的意思是每次寫操作命令執(zhí)行完后,先將命令寫入到 AOF 文件的內(nèi)核緩沖區(qū),然后每隔一秒將緩沖區(qū)里的內(nèi)容寫回到硬盤;
No,意味著不由 Redis 控制寫回硬盤的時(shí)機(jī),轉(zhuǎn)交給操作系統(tǒng)控制寫回的時(shí)機(jī),也就是每次寫操作命令執(zhí)行完后,先將命令寫入到 AOF 文件的內(nèi)核緩沖區(qū),再由操作系統(tǒng)決定何時(shí)將緩沖區(qū)內(nèi)容寫回硬盤。
我也把這 3 個(gè)寫回策略的優(yōu)缺點(diǎn)總結(jié)成了一張表格:
RDB 快照是如何實(shí)現(xiàn)的呢?
因?yàn)?AOF 日志記錄的是操作命令,不是實(shí)際的數(shù)據(jù),所以用 AOF 方法做故障恢復(fù)時(shí),需要全量把日志都執(zhí)行一遍,一旦 AOF 日志非常多,勢(shì)必會(huì)造成 Redis 的恢復(fù)操作緩慢。為了解決這個(gè)問題,Redis 增加了 RDB 快照。
所謂的快照,就是記錄某一個(gè)瞬間東西,比如當(dāng)我們給風(fēng)景拍照時(shí),那一個(gè)瞬間的畫面和信息就記錄到了一張照片。所以,RDB 快照就是記錄某一個(gè)瞬間的內(nèi)存數(shù)據(jù),記錄的是實(shí)際數(shù)據(jù),而 AOF 文件記錄的是命令操作的日志,而不是實(shí)際的數(shù)據(jù)。因此在 Redis 恢復(fù)數(shù)據(jù)時(shí), RDB 恢復(fù)數(shù)據(jù)的效率會(huì)比 AOF 高些,因?yàn)橹苯訉?RDB 文件讀入內(nèi)存就可以,不需要像 AOF 那樣還需要額外執(zhí)行操作命令的步驟才能恢復(fù)數(shù)據(jù)。
Redis 提供了兩個(gè)命令來生成 RDB 文件,分別是 save 和 bgsave,他們的區(qū)別就在于是否在「主線程」里執(zhí)行:
-
- 執(zhí)行了 save 命令,就會(huì)在主線程生成 RDB 文件,由于和執(zhí)行操作命令在同一個(gè)線程,所以如果寫入 RDB 文件的時(shí)間太長(zhǎng),
會(huì)阻塞主線程
-
- ;執(zhí)行了 bgsave 命令,會(huì)創(chuàng)建一個(gè)子進(jìn)程來生成 RDB 文件,這樣可以
避免主線程的阻塞;
優(yōu)缺點(diǎn)
AOF:
優(yōu)點(diǎn):首先,AOF提供了更好的數(shù)據(jù)安全性,因?yàn)樗J(rèn)每接收到一個(gè)寫命令就會(huì)追加到文件末尾。即使Redis服務(wù)器宕機(jī),也只會(huì)丟失最后一次寫入前的數(shù)據(jù)。其次,AOF支持多種同步策略(如everysec、always等),可以根據(jù)需要調(diào)整數(shù)據(jù)安全性和性能之間的平衡。同時(shí),AOF文件在Redis啟動(dòng)時(shí)可以通過重寫機(jī)制優(yōu)化,減少文件體積,加快恢復(fù)速度。并且,即使文件發(fā)生損壞,AOF還提供了redis-check-aof工具來修復(fù)損壞的文件。
缺點(diǎn):因?yàn)橛涗浟嗣恳粋€(gè)寫操作,所以AOF文件通常比RDB文件更大,消耗更多的磁盤空間。并且,頻繁的磁盤IO操作(尤其是同步策略設(shè)置為always時(shí))可能會(huì)對(duì)Redis的寫入性能造成一定影響。而且,當(dāng)問個(gè)文件體積過大時(shí),AOF會(huì)進(jìn)行重寫操作,AOF如果沒有開啟AOF重寫或者重寫頻率較低,恢復(fù)過程可能較慢,因?yàn)樗枰胤潘械牟僮髅睢?/p>
RDB:
優(yōu)點(diǎn): RDB通過快照的形式保存某一時(shí)刻的數(shù)據(jù)狀態(tài),文件體積小,備份和恢復(fù)的速度非???。并且,RDB是在主線程之外通過fork子進(jìn)程來進(jìn)行的,不會(huì)阻塞服務(wù)器處理命令請(qǐng)求,對(duì)Redis服務(wù)的性能影響較小。最后,由于是定期快照,RDB文件通常比AOF文件小得多。
缺點(diǎn): RDB方式在兩次快照之間,如果Redis服務(wù)器發(fā)生故障,這段時(shí)間的數(shù)據(jù)將會(huì)丟失。并且,如果在RDB創(chuàng)建快照到恢復(fù)期間有寫操作,恢復(fù)后的數(shù)據(jù)可能與故障前的數(shù)據(jù)不完全一致
Redis除了緩存,還有哪些應(yīng)用
Redis實(shí)現(xiàn)消息隊(duì)列
使用Pub/Sub模式:
-
- Redis的Pub/Sub是一種基于發(fā)布/訂閱的消息模式,任何客戶端都可以訂閱一個(gè)或多個(gè)頻道,發(fā)布者可以向特定頻道發(fā)送消息,所有訂閱該頻道的客戶端都會(huì)收到此消息。該方式實(shí)現(xiàn)起來比較簡(jiǎn)單,發(fā)布者和訂閱者完全解耦,支持模式匹配訂閱。但是這種方式不支持消息持久化,消息發(fā)布后若無訂閱者在線則會(huì)被丟棄;不保證消息的順序和可靠性傳輸。
使用List結(jié)構(gòu)
-
-
-
- :
- 使用List的方式通常是使用LPUSH命令將消息推入一個(gè)列表,消費(fèi)者使用BLPOP或BRPOP阻塞地從列表中取出消息(先進(jìn)先出FIFO)。這種方式可以實(shí)現(xiàn)簡(jiǎn)單的任務(wù)隊(duì)列。這種方式可以結(jié)合Redis的過期時(shí)間特性實(shí)現(xiàn)消息的TTL;通過Redis事務(wù)可以保證操作的原子性。但是需要客戶端自己實(shí)現(xiàn)消息確認(rèn)、重試等機(jī)制,相比專門的消息隊(duì)列系統(tǒng)功能較弱。
-
-
Redis實(shí)現(xiàn)分布式鎖
set nx方式:Redis提供了幾種方式來實(shí)現(xiàn)分布式鎖,最常用的是基于SET
命令的爭(zhēng)搶鎖機(jī)制。客戶端可以使用SET resource_name lock_value NX PX milliseconds
命令設(shè)置鎖,其中NX表示只有當(dāng)鍵不存在時(shí)才設(shè)置,PX指定鎖的有效時(shí)間(毫秒)。如果設(shè)置成功,則認(rèn)為客戶端獲得鎖??蛻舳送瓿刹僮骱?,解鎖的還需要先判斷鎖是不是自己,再進(jìn)行刪除,這里涉及到 2 個(gè)操作,為了保證這兩個(gè)操作的原子性,可以用 lua 腳本來實(shí)現(xiàn)。
RedLock算法:
- 為了提高分布式鎖的可靠性,Redis作者Antirez提出了RedLock算法,它基于多個(gè)獨(dú)立的Redis實(shí)例來實(shí)現(xiàn)一個(gè)更安全的分布式鎖。它的基本原理是客戶端嘗試在多數(shù)(大于半數(shù))Redis實(shí)例上同時(shí)加鎖,只有當(dāng)在大多數(shù)實(shí)例上加鎖成功時(shí)才認(rèn)為獲取鎖成功。鎖的超時(shí)時(shí)間應(yīng)該遠(yuǎn)小于單個(gè)實(shí)例的超時(shí)時(shí)間,以避免死鎖。該方式可以通過跨多個(gè)節(jié)點(diǎn)減少單點(diǎn)故障的影響,提高了鎖的可用性和安全性。
Redis分布式鎖的實(shí)現(xiàn),什么場(chǎng)景下用到分布式鎖
分布式鎖是用于分布式環(huán)境下并發(fā)控制的一種機(jī)制,用于控制某個(gè)資源在同一時(shí)刻只能被一個(gè)應(yīng)用所使用。如下圖所示:
Redis 本身可以被多個(gè)客戶端共享訪問,正好就是一個(gè)共享存儲(chǔ)系統(tǒng),可以用來保存分布式鎖,而且 Redis 的讀寫性能高,可以應(yīng)對(duì)高并發(fā)的鎖操作場(chǎng)景。Redis 的 SET 命令有個(gè) NX 參數(shù)可以實(shí)現(xiàn)「key不存在才插入」,所以可以用它來實(shí)現(xiàn)分布式鎖:
- 如果 key 不存在,則顯示插入成功,可以用來表示加鎖成功;如果 key 存在,則會(huì)顯示插入失敗,可以用來表示加鎖失敗。
基于 Redis 節(jié)點(diǎn)實(shí)現(xiàn)分布式鎖時(shí),對(duì)于加鎖操作,我們需要滿足三個(gè)條件。
- 加鎖包括了讀取鎖變量、檢查鎖變量值和設(shè)置鎖變量值三個(gè)操作,但需要以原子操作的方式完成,所以,我們使用 SET 命令帶上 NX 選項(xiàng)來實(shí)現(xiàn)加鎖;鎖變量需要設(shè)置過期時(shí)間,以免客戶端拿到鎖后發(fā)生異常,導(dǎo)致鎖一直無法釋放,所以,我們?cè)?SET 命令執(zhí)行時(shí)加上 EX/PX 選項(xiàng),設(shè)置其過期時(shí)間;鎖變量的值需要能區(qū)分來自不同客戶端的加鎖操作,以免在釋放鎖時(shí),出現(xiàn)誤釋放操作,所以,我們使用 SET 命令設(shè)置鎖變量值時(shí),每個(gè)客戶端設(shè)置的值是一個(gè)唯一值,用于標(biāo)識(shí)客戶端;
滿足這三個(gè)條件的分布式命令如下:
SET?lock_key?unique_value?NX?PX?10000
- lock_key 就是 key 鍵;unique_value 是客戶端生成的唯一的標(biāo)識(shí),區(qū)分來自不同客戶端的鎖操作;NX 代表只在 lock_key 不存在時(shí),才對(duì) lock_key 進(jìn)行設(shè)置操作;PX 10000 表示設(shè)置 lock_key 的過期時(shí)間為 10s,這是為了避免客戶端發(fā)生異常而無法釋放鎖。
而解鎖的過程就是將 lock_key 鍵刪除(del lock_key),但不能亂刪,要保證執(zhí)行操作的客戶端就是加鎖的客戶端。所以,解鎖的時(shí)候,我們要先判斷鎖的 unique_value 是否為加鎖客戶端,是的話,才將 lock_key 鍵刪除。
可以看到,解鎖是有兩個(gè)操作,這時(shí)就需要 Lua 腳本來保證解鎖的原子性,因?yàn)?Redis 在執(zhí)行 Lua 腳本時(shí),可以以原子性的方式執(zhí)行,保證了鎖釋放操作的原子性。
//?釋放鎖時(shí),先比較?unique_value?是否相等,避免鎖的誤釋放
if?redis.call("get",KEYS[1])?==?ARGV[1]?then
??return?redis.call("del",KEYS[1])
else
??return?0
end
這樣一來,就通過使用 SET 命令和 Lua 腳本在 Redis 單節(jié)點(diǎn)上完成了分布式鎖的加鎖和解鎖。
Java
SpringBoot自動(dòng)裝配原理是什么?
SpringBoot 的自動(dòng)裝配原理是基于Spring Framework的條件化配置和@EnableAutoConfiguration注解實(shí)現(xiàn)的。這種機(jī)制允許開發(fā)者在項(xiàng)目中引入相關(guān)的依賴,SpringBoot 將根據(jù)這些依賴自動(dòng)配置應(yīng)用程序的上下文和功能。
SpringBoot 定義了一套接口規(guī)范,這套規(guī)范規(guī)定:SpringBoot 在啟動(dòng)時(shí)會(huì)掃描外部引用 jar 包中的META-INF/spring.factories文件,將文件中配置的類型信息加載到 Spring 容器(此處涉及到 JVM 類加載機(jī)制與 Spring 的容器知識(shí)),并執(zhí)行類中定義的各種操作。對(duì)于外部 jar 來說,只需要按照 SpringBoot 定義的標(biāo)準(zhǔn),就能將自己的功能裝置進(jìn) SpringBoot。
通俗來講,自動(dòng)裝配就是通過注解或一些簡(jiǎn)單的配置就可以在SpringBoot的幫助下開啟和配置各種功能,比如數(shù)據(jù)庫(kù)訪問、Web開發(fā)。
SpringBoot自動(dòng)裝配原理
首先點(diǎn)進(jìn)@SpringBootApplication注解的內(nèi)部
接下來將逐個(gè)解釋這些注解的作用:
- @Target({ElementType.TYPE}): 該注解指定了這個(gè)注解可以用來標(biāo)記在類上。在這個(gè)特定的例子中,這表示該注解用于標(biāo)記配置類。@Retention(RetentionPolicy.RUNTIME): 這個(gè)注解指定了注解的生命周期,即在運(yùn)行時(shí)保留。這是因?yàn)?Spring Boot 在運(yùn)行時(shí)掃描類路徑上的注解來實(shí)現(xiàn)自動(dòng)配置,所以這里使用了 RUNTIME 保留策略。@Documented: 該注解表示這個(gè)注解應(yīng)該被包含在 Java 文檔中。它是用于生成文檔的標(biāo)記,使開發(fā)者能夠看到這個(gè)注解的相關(guān)信息。@Inherited: 這個(gè)注解指示一個(gè)被標(biāo)注的類型是被繼承的。在這個(gè)例子中,它表明這個(gè)注解可以被繼承,如果一個(gè)類繼承了帶有這個(gè)注解的類,它也會(huì)繼承這個(gè)注解。@SpringBootConfiguration: 這個(gè)注解表明這是一個(gè) Spring Boot 配置類。如果點(diǎn)進(jìn)這個(gè)注解內(nèi)部會(huì)發(fā)現(xiàn)與標(biāo)準(zhǔn)的 @Configuration 沒啥區(qū)別,只是為了表明這是一個(gè)專門用于 SpringBoot 的配置。@EnableAutoConfiguration: 這個(gè)注解是 Spring Boot 自動(dòng)裝配的核心。它告訴 Spring oot 啟用自動(dòng)配置機(jī)制,根據(jù)項(xiàng)目的依賴和配置自動(dòng)配置應(yīng)用程序的上下文。通過這個(gè)注解,SpringBoot 將嘗試根據(jù)類路徑上的依賴自動(dòng)配置應(yīng)用程序。@ComponentScan: 這個(gè)注解用于配置組件掃描的規(guī)則。在這里,它告訴 SpringBoot 在指定的包及其子包中查找組件,這些組件包括被注解的類、@Component 注解的類等。其中的 excludeFilters 參數(shù)用于指定排除哪些組件,這里使用了兩個(gè)自定義的過濾器,分別是 TypeExcludeFilter 和 AutoConfigurationExcludeFilter。
@EnableAutoConfiguration
這個(gè)注解是實(shí)現(xiàn)自動(dòng)裝配的核心注解
- @AutoConfigurationPackage,將項(xiàng)目src中main包下的所有組件注冊(cè)到容器中,例如標(biāo)注了Component注解的類等@Import({AutoConfigurationImportSelector.class}),是自動(dòng)裝配的核心,接下來分析一下這個(gè)注解
AutoConfigurationImportSelector
AutoConfigurationImportSelector 是 Spring Boot 中一個(gè)重要的類,它實(shí)現(xiàn)了 ImportSelector 接口,用于實(shí)現(xiàn)自動(dòng)配置的選擇和導(dǎo)入。具體來說,它通過分析項(xiàng)目的類路徑和條件來決定應(yīng)該導(dǎo)入哪些自動(dòng)配置類。代碼太多,選取部分主要功能的代碼
public?class?AutoConfigurationImportSelector?implements?DeferredImportSelector,?BeanClassLoaderAware,
??ResourceLoaderAware,?BeanFactoryAware,?EnvironmentAware,?Ordered?{
????
????//?...?(其他方法和屬性)
??//?獲取所有符合條件的類的全限定類名,例如RedisTemplate的全限定類名(org.springframework.data.redis.core.RedisTemplate;),這些類需要被加載到 IoC 容器中。
?@Override
?public?String[]?selectImports(AnnotationMetadata?annotationMetadata)?{
??//?掃描類路徑上的?META-INF/spring.factories?文件,獲取所有實(shí)現(xiàn)了?AutoConfiguration?接口的自動(dòng)配置類
??List<String>?configurations?=?getCandidateConfigurations(annotationMetadata,?attributes);
??//?過濾掉不滿足條件的自動(dòng)配置類,比如一些自動(dòng)裝配類
??configurations?=?filter(configurations,?annotationMetadata,?attributes);
??//?排序自動(dòng)配置類,根據(jù)?@AutoConfigureOrder?和?@AutoConfigureAfter/@AutoConfigureBefore?注解指定的順序
??sort(configurations,?annotationMetadata,?attributes);
??//?將滿足條件的自動(dòng)配置類的類名數(shù)組返回,這些類將被導(dǎo)入到應(yīng)用程序上下文中
??return?StringUtils.toStringArray(configurations);
?}
?//?...?(其他方法)
?protected?List<String>?getCandidateConfigurations(AnnotationMetadata?metadata,?AnnotationAttributes?attributes)?{
??//?獲取自動(dòng)配置類的候選列表,從?META-INF/spring.factories?文件中讀取
??//?通過類加載器加載所有候選類
??List<String>?configurations?=?SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
????getBeanClassLoader());
??//?過濾出實(shí)現(xiàn)了?AutoConfiguration?接口的自動(dòng)配置類
??configurations?=?configurations.stream()
????.filter(this::isEnabled)
????.collect(Collectors.toList());
??//?對(duì)于?Spring?Boot?1.x?版本,還需要添加?spring-boot-autoconfigure?包中的自動(dòng)配置類
??//?configurations.addAll(getAutoConfigEntry(getAutoConfigurationEntry(metadata)));
??return?configurations;
?}
?//?...?(其他方法)
?protected?List<String>?filter(List<String>?configurations,?AnnotationMetadata?metadata,
???AnnotationAttributes?attributes)?{
??//?使用條件判斷機(jī)制,過濾掉不滿足條件的自動(dòng)配置類
??configurations?=?configurations.stream()
????.filter(configuration?->?isConfigurationCandidate(configuration,?metadata,?attributes))
????.collect(Collectors.toList());
??return?configurations;
?}
?//?...?(其他方法)
?protected?void?sort(List<String>?configurations,?AnnotationMetadata?metadata,
???AnnotationAttributes?attributes)?{
??//?根據(jù)?@AutoConfigureOrder?和?@AutoConfigureAfter/@AutoConfigureBefore?注解指定的順序?qū)ψ詣?dòng)配置類進(jìn)行排序
??configurations.sort((o1,?o2)?->?{
???int?i1?=?getAutoConfigurationOrder(o1,?metadata,?attributes);
???int?i2?=?getAutoConfigurationOrder(o2,?metadata,?attributes);
???return?Integer.compare(i1,?i2);
??});
?}
??
???//?...?(其他方法)
}
梳理一下,以下是AutoConfigurationImportSelector
的主要工作:
-
- 掃描類路徑: 在應(yīng)用程序啟動(dòng)時(shí),
AutoConfigurationImportSelector
-
- 會(huì)掃描類路徑上的
META-INF/spring.factories
-
- 文件,這個(gè)文件中包含了各種 Spring 配置和擴(kuò)展的定義。在這里,它會(huì)查找所有實(shí)現(xiàn)了
AutoConfiguration
-
- 接口的類,具體的實(shí)現(xiàn)為
getCandidateConfigurations
-
- 方法。條件判斷: 對(duì)于每一個(gè)發(fā)現(xiàn)的自動(dòng)配置類,
AutoConfigurationImportSelector
-
- 會(huì)使用條件判斷機(jī)制(通常是通過
@ConditionalOnXxx
- 注解)來確定是否滿足導(dǎo)入條件。這些條件可以是配置屬性、類是否存在、Bean是否存在等等。根據(jù)條件導(dǎo)入自動(dòng)配置類: 滿足條件的自動(dòng)配置類將被導(dǎo)入到應(yīng)用程序的上下文中。這意味著它們會(huì)被實(shí)例化并應(yīng)用于應(yīng)用程序的配置。
說幾個(gè)啟動(dòng)器(starter)?
spring-boot-starter-web:這是最常用的起步依賴之一,它包含了Spring MVC和Tomcat嵌入式服務(wù)器,用于快速構(gòu)建Web應(yīng)用程序。
spring-boot-starter-security:提供了Spring Security的基本配置,幫助開發(fā)者快速實(shí)現(xiàn)應(yīng)用的安全性,包括認(rèn)證和授權(quán)功能。
mybatis-spring-boot-starter:這個(gè)Starter是由MyBatis團(tuán)隊(duì)提供的,用于簡(jiǎn)化在Spring Boot應(yīng)用中集成MyBatis的過程。它自動(dòng)配置了MyBatis的相關(guān)組件,包括SqlSessionFactory、MapperScannerConfigurer等,使得開發(fā)者能夠快速地開始使用MyBatis進(jìn)行數(shù)據(jù)庫(kù)操作。
spring-boot-starter-data-jpa或spring-boot-starter-jdbc:如果使用的是Java Persistence API (JPA)進(jìn)行數(shù)據(jù)庫(kù)操作,那么應(yīng)該使用spring-boot-starter-data-jpa。這個(gè)Starter包含了Hibernate等JPA實(shí)現(xiàn)以及數(shù)據(jù)庫(kù)連接池等必要的庫(kù),可以讓你輕松地與MySQL數(shù)據(jù)庫(kù)進(jìn)行交互。你需要在application.properties或application.yml中配置MySQL的連接信息。如果傾向于直接使用JDBC而不通過JPA,那么可以使用spring-boot-starter-jdbc,它提供了基本的JDBC支持。
spring-boot-starter-data-redis:用于集成Redis緩存和數(shù)據(jù)存儲(chǔ)服務(wù)。這個(gè)Starter包含了與Redis交互所需的客戶端(默認(rèn)是Jedis客戶端,也可以配置為L(zhǎng)ettuce客戶端),以及Spring Data Redis的支持,使得在Spring Boot應(yīng)用中使用Redis變得非常便捷。同樣地,需要在配置文件中設(shè)置Redis服務(wù)器的連接詳情。
spring-boot-starter-test:包含了單元測(cè)試和集成測(cè)試所需的庫(kù),如JUnit, Spring Test, AssertJ等,便于進(jìn)行測(cè)試驅(qū)動(dòng)開發(fā)(TDD)。
怎么搭建SpringBoot項(xiàng)目的?
用 IntelliJ IDEA工具開發(fā)的SpringBoot項(xiàng)目
voliatle關(guān)鍵字有什么作用?
volatite作用有 2 個(gè):
保證變量對(duì)所有線程的可見性。當(dāng)一個(gè)變量被聲明為volatile時(shí),它會(huì)保證對(duì)這個(gè)變量的寫操作會(huì)立即刷新到主存中,而對(duì)這個(gè)變量的讀操作會(huì)直接從主存中讀取,從而確保了多線程環(huán)境下對(duì)該變量訪問的可見性。這意味著一個(gè)線程修改了volatile變量的值,其他線程能夠立刻看到這個(gè)修改,不會(huì)受到各自線程工作內(nèi)存的影響。
禁止指令重排序優(yōu)化。volatile關(guān)鍵字在Java中主要通過內(nèi)存屏障來禁止特定類型的指令重排序。
-
-
-
- 1)
寫-寫(Write-Write)屏障
-
-
-
-
- :在對(duì)volatile變量執(zhí)行寫操作之前,會(huì)插入一個(gè)寫屏障。這確保了在該變量寫操作之前的所有普通寫操作都已完成,防止了這些寫操作被移到volatile寫操作之后。
-
-
- 2)
讀-寫(Read-Write)屏障
-
-
-
-
- :在對(duì)volatile變量執(zhí)行讀操作之后,會(huì)插入一個(gè)讀屏障。它確保了對(duì)volatile變量的讀操作之后的所有普通讀操作都不會(huì)被提前到volatile讀之前執(zhí)行,保證了讀取到的數(shù)據(jù)是最新的。
-
-
- 3)
寫-讀(Write-Read)屏障
-
-
-
- :這是最重要的一個(gè)屏障,它發(fā)生在volatile寫之后和volatile讀之前。這個(gè)屏障確保了volatile寫操作之前的所有內(nèi)存操作(包括寫操作)都不會(huì)被重排序到volatile讀之后,同時(shí)也確保了volatile讀操作之后的所有內(nèi)存操作(包括讀操作)都不會(huì)被重排序到volatile寫之前。
JVM內(nèi)存共享區(qū)域有哪些?
根據(jù) JVM8 規(guī)范,JVM 運(yùn)行時(shí)內(nèi)存共分為虛擬機(jī)棧、堆、元空間、程序計(jì)數(shù)器、本地方法棧五個(gè)部分。還有一部分內(nèi)存叫直接內(nèi)存,屬于操作系統(tǒng)的本地內(nèi)存,也是可以直接操作的。
JVM的內(nèi)存結(jié)構(gòu)主要分為以下幾個(gè)部分:
元空間
-
- :元空間并不是在堆上分配的,而是在堆外空間進(jìn)行分配的,它的大小默認(rèn)沒有上限,我們常說的方法區(qū),就在元空間中。元空間的本質(zhì)和永久代類似,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。
Java 虛擬機(jī)棧
-
- :每個(gè)線程有一個(gè)私有的棧,隨著線程的創(chuàng)建而創(chuàng)建。棧里面存著的是一種叫“棧幀”的東西,每個(gè)方法會(huì)創(chuàng)建一個(gè)棧幀,棧幀中存放了局部變量表(基本數(shù)據(jù)類型和對(duì)象引用)、操作數(shù)棧、方法出口等信息。棧的大小可以固定也可以動(dòng)態(tài)擴(kuò)展。
本地方法棧
-
- :與虛擬機(jī)棧類似,區(qū)別是虛擬機(jī)棧執(zhí)行java方法,本地方法站執(zhí)行native方法。在虛擬機(jī)規(guī)范中對(duì)本地方法棧中方法使用的語(yǔ)言、使用方法與數(shù)據(jù)結(jié)構(gòu)沒有強(qiáng)制規(guī)定,因此虛擬機(jī)可以自由實(shí)現(xiàn)它。
程序計(jì)數(shù)器:
-
- 程序計(jì)數(shù)器可以看成是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(對(duì)于多內(nèi)核來說是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要一個(gè)獨(dú)立的程序計(jì)數(shù)器,我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”內(nèi)存。
-
- :堆內(nèi)存是 JVM 所有線程共享的部分,在虛擬機(jī)啟動(dòng)的時(shí)候就已經(jīng)創(chuàng)建。所有的對(duì)象和數(shù)組都在堆上進(jìn)行分配。這部分空間可通過 GC 進(jìn)行回收。當(dāng)申請(qǐng)不到空間時(shí)會(huì)拋出 OutOfMemoryError。堆是JVM內(nèi)存占用最大,管理最復(fù)雜的一個(gè)區(qū)域。其唯一的用途就是存放對(duì)象實(shí)例:所有的對(duì)象實(shí)例及數(shù)組都在對(duì)上進(jìn)行分配。jdk1.8后,字符串常量池從永久代中剝離出來,存放在隊(duì)中。
直接內(nèi)存
- :直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java 虛擬機(jī)規(guī)范中農(nóng)定義的內(nèi)存區(qū)域。在JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O 方式,它可以使用native 函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通脫一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù)。
方法區(qū)中的方法的執(zhí)行過程?
當(dāng)程序中通過對(duì)象或類直接調(diào)用某個(gè)方法時(shí),主要包括以下幾個(gè)步驟:
解析方法調(diào)用:JVM會(huì)根據(jù)方法的符號(hào)引用找到實(shí)際的方法地址(如果之前沒有解析過的話)。
棧幀創(chuàng)建:在調(diào)用一個(gè)方法前,JVM會(huì)在當(dāng)前線程的Java虛擬機(jī)棧中為該方法分配一個(gè)新的棧幀,用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。
執(zhí)行方法:執(zhí)行方法內(nèi)的字節(jié)碼指令,涉及的操作可能包括局部變量的讀寫、操作數(shù)棧的操作、跳轉(zhuǎn)控制、對(duì)象創(chuàng)建、方法調(diào)用等。
返回處理:方法執(zhí)行完畢后,可能會(huì)返回一個(gè)結(jié)果給調(diào)用者,并清理當(dāng)前棧幀,恢復(fù)調(diào)用者的執(zhí)行環(huán)境。
方法區(qū)中還有哪些東西?
《深入理解Java虛擬機(jī)》書中對(duì)方法區(qū)(Method Area)存儲(chǔ)內(nèi)容描述如下:它用于存儲(chǔ)已被虛擬機(jī)加載的類型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等。
- 類信息:包括類的結(jié)構(gòu)信息、類的訪問修飾符、父類與接口等信息。常量池:存儲(chǔ)類和接口中的常量,包括字面值常量、符號(hào)引用,以及運(yùn)行時(shí)常量池。靜態(tài)變量:存儲(chǔ)類的靜態(tài)變量,這些變量在類初始化的時(shí)候被賦值。方法字節(jié)碼:存儲(chǔ)類的方法字節(jié)碼,即編譯后的代碼。符號(hào)引用:存儲(chǔ)類和方法的符號(hào)引用,是一種直接引用不同于直接引用的引用類型。運(yùn)行時(shí)常量池:存儲(chǔ)著在類文件中的常量池?cái)?shù)據(jù),在類加載后在方法區(qū)生成該運(yùn)行時(shí)常量池。常量池緩存:用于提升類加載的效率,將常用的常量緩存起來方便使用。
類加載器有哪些?
啟動(dòng)類加載器(Bootstrap Class Loader)
-
- :這是最頂層的類加載器,負(fù)責(zé)加載Java的核心庫(kù)(如位于jre/lib/rt.jar中的類),它是用C++編寫的,是JVM的一部分。啟動(dòng)類加載器無法被Java程序直接引用。
擴(kuò)展類加載器(Extension Class Loader)
-
- :它是Java語(yǔ)言實(shí)現(xiàn)的,繼承自ClassLoader類,負(fù)責(zé)加載Java擴(kuò)展目錄(jre/lib/ext或由系統(tǒng)變量java.ext.dirs指定的目錄)下的jar包和類庫(kù)。擴(kuò)展類加載器由啟動(dòng)類加載器加載,并且父加載器就是啟動(dòng)類加載器。
系統(tǒng)類加載器(System Class Loader)/ 應(yīng)用程序類加載器(Application Class Loader)
-
- :這也是Java語(yǔ)言實(shí)現(xiàn)的,負(fù)責(zé)加載用戶類路徑(ClassPath)上的指定類庫(kù),是我們平時(shí)編寫Java程序時(shí)默認(rèn)使用的類加載器。系統(tǒng)類加載器的父加載器是擴(kuò)展類加載器。它可以通過ClassLoader.getSystemClassLoader()方法獲取到。
自定義類加載器(Custom Class Loader)
- :開發(fā)者可以根據(jù)需求定制類的加載方式,比如從網(wǎng)絡(luò)加載class文件、數(shù)據(jù)庫(kù)、甚至是加密的文件中加載類等。自定義類加載器可以用來擴(kuò)展Java應(yīng)用程序的靈活性和安全性,是Java動(dòng)態(tài)性的一個(gè)重要體現(xiàn)。
這些類加載器之間的關(guān)系形成了雙親委派模型,其核心思想是當(dāng)一個(gè)類加載器收到類加載的請(qǐng)求時(shí),首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請(qǐng)求委派給父類加載器去完成,每一層次的類加載器都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器中。
只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請(qǐng)求(它的搜索范圍中沒有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去加載。
雙親委派模型的作用
保證類的唯一性:通過委托機(jī)制,確保了所有加載請(qǐng)求都會(huì)傳遞到啟動(dòng)類加載器,避免了不同類加載器重復(fù)加載相同類的情況,保證了Java核心類庫(kù)的統(tǒng)一性,也防止了用戶自定義類覆蓋核心類庫(kù)的可能。
保證安全性:由于Java核心庫(kù)被啟動(dòng)類加載器加載,而啟動(dòng)類加載器只加載信任的類路徑中的類,這樣可以防止不可信的類假冒核心類,增強(qiáng)了系統(tǒng)的安全性。例如,惡意代碼無法自定義一個(gè)java.lang.System類并加載到JVM中,因?yàn)檫@個(gè)請(qǐng)求會(huì)被委托給啟動(dòng)類加載器,而啟動(dòng)類加載器只會(huì)加載標(biāo)準(zhǔn)的Java庫(kù)中的類。
支持隔離和層次劃分:雙親委派模型支持不同層次的類加載器服務(wù)于不同的類加載需求,如應(yīng)用程序類加載器加載用戶代碼,擴(kuò)展類加載器加載擴(kuò)展框架,啟動(dòng)類加載器加載核心庫(kù)。這種層次化的劃分有助于實(shí)現(xiàn)沙箱安全機(jī)制,保證了各個(gè)層級(jí)類加載器的職責(zé)清晰,也便于維護(hù)和擴(kuò)展。
簡(jiǎn)化了加載流程:通過委派,大部分類能夠被正確的類加載器加載,減少了每個(gè)加載器需要處理的類的數(shù)量,簡(jiǎn)化了類的加載過程,提高了加載效率。
講一下類加載過程?
類從被加載到虛擬機(jī)內(nèi)存開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括以下 7 個(gè)階段:
加載:
-
- 通過類的全限定名(包名 + 類名),獲取到該類的.class文件的二進(jìn)制字節(jié)流,將二進(jìn)制字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu),轉(zhuǎn)化為方法區(qū)運(yùn)行時(shí)的數(shù)據(jù)結(jié)構(gòu),在內(nèi)存中生成一個(gè)代表該類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口
連接:
-
- 驗(yàn)證、準(zhǔn)備、解析 3 個(gè)階段統(tǒng)稱為連接。
-
- 驗(yàn)證:確保class文件中的字節(jié)流包含的信息,符合當(dāng)前虛擬機(jī)的要求,保證這個(gè)被加載的class類的正確性,不會(huì)危害到虛擬機(jī)的安全。驗(yàn)證階段大致會(huì)完成以下四個(gè)階段的檢驗(yàn)動(dòng)作:文件格式校驗(yàn)、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證、符號(hào)引用驗(yàn)證準(zhǔn)備:為類中的靜態(tài)字段分配內(nèi)存,并設(shè)置默認(rèn)的初始值,比如int類型初始值是0。被final修飾的static字段不會(huì)設(shè)置,因?yàn)閒inal在編譯的時(shí)候就分配了
-
解析:解析階段是虛擬機(jī)將常量池的「符號(hào)引用」直接替換為「直接引用」的過程。符號(hào)引用是以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用的時(shí)候可以無歧義地定位到目標(biāo)即可。直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄,直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的。如果有了直接引用, 那引用的目標(biāo)必定已經(jīng)存在在內(nèi)存中了。
初始化:初始化是整個(gè)類加載過程的最后一個(gè)階段,初始化階段簡(jiǎn)單來說就是執(zhí)行類的構(gòu)造器方法(() ),要注意的是這里的構(gòu)造器方法()并不是開發(fā)者寫的,而是編譯器自動(dòng)生成的。
使用:使用類或者創(chuàng)建對(duì)象
卸載:如果有下面的情況,類就會(huì)被卸載:1. 該類所有的實(shí)例都已經(jīng)被回收,也就是java堆中不存在該類的任何實(shí)例。2. 加載該類的ClassLoader已經(jīng)被回收。3. 類對(duì)應(yīng)的java.lang.Class對(duì)象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
判斷垃圾的方法有哪些?
在Java中,判斷對(duì)象是否為垃圾(即不再被使用,可以被垃圾回收器回收)主要依據(jù)兩種主流的垃圾回收算法來實(shí)現(xiàn):引用計(jì)數(shù)法和可達(dá)性分析算法。
引用計(jì)數(shù)法(Reference Counting)
原理:為每個(gè)對(duì)象分配一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器加1;當(dāng)引用失效時(shí),計(jì)數(shù)器減1。當(dāng)計(jì)數(shù)器為0時(shí),表示對(duì)象不再被任何變量引用,可以被回收。
缺點(diǎn):不能解決循環(huán)引用的問題,即兩個(gè)對(duì)象相互引用,但不再被其他任何對(duì)象引用,這時(shí)引用計(jì)數(shù)器不會(huì)為0,導(dǎo)致對(duì)象無法被回收。
可達(dá)性分析算法(Reachability Analysis)
Java虛擬機(jī)主要采用此算法來判斷對(duì)象是否為垃圾。
原理
- :從一組稱為GC Roots(垃圾收集根)的對(duì)象出發(fā),向下追溯它們引用的對(duì)象,以及這些對(duì)象引用的其他對(duì)象,以此類推。如果一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連(即從GC Roots到這個(gè)對(duì)象不可達(dá)),那么這個(gè)對(duì)象就被認(rèn)為是不可達(dá)的,可以被回收。GC Roots對(duì)象包括:虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象、方法區(qū)中類靜態(tài)屬性引用的對(duì)象、本地方法棧中JNI(Java Native Interface)引用的對(duì)象、活躍線程的引用等。
垃圾回收算法有哪些?
標(biāo)記-清除算法:標(biāo)記-清除算法分為“標(biāo)記”和“清除”兩個(gè)階段,首先通過可達(dá)性分析,標(biāo)記出所有需要回收的對(duì)象,然后統(tǒng)一回收所有被標(biāo)記的對(duì)象。標(biāo)記-清除算法有兩個(gè)缺陷,一個(gè)是效率問題,標(biāo)記和清除的過程效率都不高,另外一個(gè)就是,清除結(jié)束后會(huì)造成大量的碎片空間。有可能會(huì)造成在申請(qǐng)大塊內(nèi)存的時(shí)候因?yàn)闆]有足夠的連續(xù)空間導(dǎo)致再次 GC。
復(fù)制算法:為了解決碎片空間的問題,出現(xiàn)了“復(fù)制算法”。復(fù)制算法的原理是,將內(nèi)存分成兩塊,每次申請(qǐng)內(nèi)存時(shí)都使用其中的一塊,當(dāng)內(nèi)存不夠時(shí),將這一塊內(nèi)存中所有存活的復(fù)制到另一塊上。然后將然后再把已使用的內(nèi)存整個(gè)清理掉。復(fù)制算法解決了空間碎片的問題。但是也帶來了新的問題。因?yàn)槊看卧谏暾?qǐng)內(nèi)存時(shí),都只能使用一半的內(nèi)存空間。內(nèi)存利用率嚴(yán)重不足。
標(biāo)記-整理算法:復(fù)制算法在 GC 之后存活對(duì)象較少的情況下效率比較高,但如果存活對(duì)象比較多時(shí),會(huì)執(zhí)行較多的復(fù)制操作,效率就會(huì)下降。而老年代的對(duì)象在 GC 之后的存活率就比較高,所以就有人提出了“標(biāo)記-整理算法”。標(biāo)記-整理算法的“標(biāo)記”過程與“標(biāo)記-清除算法”的標(biāo)記過程一致,但標(biāo)記之后不會(huì)直接清理。而是將所有存活對(duì)象都移動(dòng)到內(nèi)存的一端。移動(dòng)結(jié)束后直接清理掉剩余部分。
分代回收算法:分代收集是將內(nèi)存劃分成了新生代和老年代。分配的依據(jù)是對(duì)象的生存周期,或者說經(jīng)歷過的 GC 次數(shù)。對(duì)象創(chuàng)建時(shí),一般在新生代申請(qǐng)內(nèi)存,當(dāng)經(jīng)歷一次 GC 之后如果對(duì)還存活,那么對(duì)象的年齡 +1。當(dāng)年齡超過一定值(默認(rèn)是 15,可以通過參數(shù) -XX:MaxTenuringThreshold 來設(shè)定)后,如果對(duì)象還存活,那么該對(duì)象會(huì)進(jìn)入老年代。
標(biāo)記清除算法的缺點(diǎn)是什么?
主要缺點(diǎn)有兩個(gè):
- 一個(gè)是效率問題,標(biāo)記和清除過程的效率都不高;另外一個(gè)是空間問題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致,當(dāng)程序在以后的運(yùn)行過程中需要分配較大對(duì)象時(shí)無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。
堆分為哪幾部分呢?
Java堆(Heap)是Java虛擬機(jī)(JVM)中內(nèi)存管理的一個(gè)重要區(qū)域,主要用于存放對(duì)象實(shí)例和數(shù)組。隨著JVM的發(fā)展和不同垃圾收集器的實(shí)現(xiàn),堆的具體劃分可能會(huì)有所不同,但通??梢苑譃橐韵聨讉€(gè)部分:
新生代(Young Generation):新生代分為Eden Space和Survivor Space。在Eden Space中, 大多數(shù)新創(chuàng)建的對(duì)象首先存放在這里。Eden區(qū)相對(duì)較小,當(dāng)Eden區(qū)滿時(shí),會(huì)觸發(fā)一次Minor GC(新生代垃圾回收)。在Survivor Spaces中,通常分為兩個(gè)相等大小的區(qū)域,稱為S0(Survivor 0)和S1(Survivor 1)。在每次Minor GC后,存活下來的對(duì)象會(huì)被移動(dòng)到其中一個(gè)Survivor空間,以繼續(xù)它們的生命周期。這兩個(gè)區(qū)域輪流充當(dāng)對(duì)象的中轉(zhuǎn)站,幫助區(qū)分短暫存活的對(duì)象和長(zhǎng)期存活的對(duì)象。
老年代(Old Generation/Tenured Generation):存放過一次或多次Minor GC仍存活的對(duì)象會(huì)被移動(dòng)到老年代。老年代中的對(duì)象生命周期較長(zhǎng),因此Major GC(也稱為Full GC,涉及老年代的垃圾回收)發(fā)生的頻率相對(duì)較低,但其執(zhí)行時(shí)間通常比Minor GC長(zhǎng)。老年代的空間通常比新生代大,以存儲(chǔ)更多的長(zhǎng)期存活對(duì)象。
元空間(Metaspace):從Java 8開始,永久代(Permanent Generation)被元空間取代,用于存儲(chǔ)類的元數(shù)據(jù)信息,如類的結(jié)構(gòu)信息(如字段、方法信息等)。元空間并不在Java堆中,而是使用本地內(nèi)存,這解決了永久代容易出現(xiàn)的內(nèi)存溢出問題。
大對(duì)象區(qū)(Large Object Space / Humongous Objects):在某些JVM實(shí)現(xiàn)中(如G1垃圾收集器),為大對(duì)象分配了專門的區(qū)域,稱為大對(duì)象區(qū)或Humongous Objects區(qū)域。大對(duì)象是指需要大量連續(xù)內(nèi)存空間的對(duì)象,如大數(shù)組。這類對(duì)象直接分配在老年代,以避免因頻繁的年輕代晉升而導(dǎo)致的內(nèi)存碎片化問題。