加入星計劃,您可以享受以下權(quán)益:

  • 創(chuàng)作內(nèi)容快速變現(xiàn)
  • 行業(yè)影響力擴散
  • 作品版權(quán)保護
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質(zhì)創(chuàng)作者
  • 5000+ 長期合作伙伴
立即加入
  • 正文
    • MySQL
    • Redis
    • Java
  • 推薦器件
  • 相關(guān)推薦
  • 電子產(chǎn)業(yè)圖譜
申請入駐 產(chǎn)業(yè)圖譜

面試 | Java后端開發(fā)面試:希望渺茫了,被南京某小廠足足拷打 62 分鐘

07/19 10:08
1456
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

圖解學(xué)習(xí)網(wǎng)站:https://xiaolincoding.com

大家好,我是小林。

這周已經(jīng)分享過了幾個大廠后端開發(fā)面經(jīng),有不少同學(xué)反饋也先看看小廠的面經(jīng),想感受一下區(qū)別。

有些小廠面試不會問太多,主要就考察幾個問題,可能十多分鐘就結(jié)束了,但是也有一些小廠比較例外,可能會拷打你一個小時,面試考察題量相當(dāng)于大廠的題量,但是這種情況還是比較少數(shù),同學(xué)們也不需要有太大的壓力。

分析十多分鐘的小廠面經(jīng)也沒什么意思,大家能學(xué)到的內(nèi)容也比較有限,正好之前看到有同學(xué)面南京某小廠Java后端開發(fā)的時候,足足被拷打了近 30 題,還是蠻有壓力的,有些題目也是大廠會考察的內(nèi)容。

面試問題不止非常有廣度,而且有些問題問的也很有深度,今天就讓我們來看看吧!

考察的知識點,我給大家羅列了一下:

    MySQL:索引、特性和關(guān)鍵字、存儲引起、索引數(shù)據(jù)結(jié)構(gòu)Redis:線程模型、持久化機制、分布式鎖、應(yīng)用場景Java:SpringBoot、JVM、volatile

MySQL

MySQL索引的數(shù)據(jù)結(jié)構(gòu)

從數(shù)據(jù)結(jié)構(gòu)的角度來看,MySQL 常見索引有 B+Tree 索引、HASH 索引、Full-Text 索引。

每一種存儲引擎支持的索引類型不一定相同,表中總結(jié)了 MySQL 常見的存儲引擎 InnoDB、MyISAM 和 Memory 分別支持的索引類型。

InnoDB 是在 MySQL 5.5 之后成為默認的 MySQL 存儲引擎,B+Tree 索引類型也是 MySQL 存儲引擎采用最多的索引類型。

MySQL索引底層的數(shù)據(jù)結(jié)構(gòu)是B+樹。B+樹是一種多叉樹,葉子節(jié)點才存放數(shù)據(jù),非葉子節(jié)點只存放索引,而且每個節(jié)點里的數(shù)據(jù)是按主鍵順序存放的。每一層父節(jié)點的索引值都會出現(xiàn)在下層子節(jié)點的索引值中,因此在葉子節(jié)點中,包括了所有的索引值信息,并且每一個葉子節(jié)點都有兩個指針,分別指向下一個葉子節(jié)點和上一個葉子節(jié)點,形成一個雙向鏈表。

主鍵索引的 B+樹 如圖所示:

MySQL有哪些存儲引擎

MySQL中常用的存儲引擎分別是:MyISAM存儲引擎、innoDB存儲引擎,他們的區(qū)別在于:

    事務(wù):InnoDB 支持事務(wù),MyISAM 不支持事務(wù),這是 MySQL 將默認存儲引擎從 MyISAM 變成 InnoDB 的重要原因之一。索引結(jié)構(gòu):InnoDB 是聚簇索引,MyISAM 是非聚簇索引。聚簇索引的文件存放在主鍵索引的葉子節(jié)點上,因此 InnoDB 必須要有主鍵,通過主鍵索引效率很高。但是輔助索引需要兩次查詢,先查詢到主鍵,然后再通過主鍵查詢到數(shù)據(jù)。因此,主鍵不應(yīng)該過大,因為主鍵太大,其他索引也都會很大。而 MyISAM 是非聚簇索引,數(shù)據(jù)文件是分離的,索引保存的是數(shù)據(jù)文件的指針。主鍵索引和輔助索引是獨立的。鎖粒度:InnoDB 最小的鎖粒度是行鎖,MyISAM 最小的鎖粒度是表鎖。一個更新語句會鎖住整張表,導(dǎo)致其他查詢和更新都會被阻塞,因此并發(fā)訪問受限。count 的效率:InnoDB 不保存表的具體行數(shù),執(zhí)行 select count(*) from table 時需要全表掃描。而MyISAM 用一個變量保存了整個表的行數(shù),執(zhí)行上述語句時只需要讀出該變量即可,速度很快。

MySQL為什么用B+樹結(jié)構(gòu),和其他結(jié)構(gòu)比的優(yōu)點

MySQL 是會將數(shù)據(jù)持久化在硬盤,而存儲功能是由 MySQL 存儲引擎實現(xiàn)的,所以討論 MySQL 使用哪種數(shù)據(jù)結(jié)構(gòu)作為索引,實際上是在討論存儲引使用哪種數(shù)據(jù)結(jié)構(gòu)作為索引,InnoDB 是 MySQL 默認的存儲引擎,它就是采用了 B+ 樹作為索引的數(shù)據(jù)結(jié)構(gòu)。

要設(shè)計一個 MySQL 的索引數(shù)據(jù)結(jié)構(gòu),不僅僅考慮數(shù)據(jù)結(jié)構(gòu)增刪改的時間復(fù)雜度,更重要的是要考慮磁盤 I/0 的操作次數(shù)。因為索引和記錄都是存放在硬盤,硬盤是一個非常慢的存儲設(shè)備,我們在查詢數(shù)據(jù)的時候,最好能在盡可能少的磁盤 I/0 的操作次數(shù)內(nèi)完成。

二分查找樹雖然是一個天然的二分結(jié)構(gòu),能很好的利用二分查找快速定位數(shù)據(jù),但是它存在一種極端的情況,每當(dāng)插入的元素都是樹內(nèi)最大的元素,就會導(dǎo)致二分查找樹退化成一個鏈表,此時查詢復(fù)雜度就會從 O(logn)降低為 O(n)。

為了解決二分查找樹退化成鏈表的問題,就出現(xiàn)了自平衡二叉樹,保證了查詢操作的時間復(fù)雜度就會一直維持在 O(logn) 。但是它本質(zhì)上還是一個二叉樹,每個節(jié)點只能有 2 個子節(jié)點,隨著元素的增多,樹的高度會越來越高。

而樹的高度決定于磁盤 I/O 操作的次數(shù),因為樹是存儲在磁盤中的,訪問每個節(jié)點,都對應(yīng)一次磁盤 I/O 操作,也就是說樹的高度就等于每次查詢數(shù)據(jù)時磁盤 IO 操作的次數(shù),所以樹的高度越高,就會影響查詢性能。

B 樹和 B+ 都是通過多叉樹的方式,會將樹的高度變矮,所以這兩個數(shù)據(jù)結(jié)構(gòu)非常適合檢索存于磁盤中的數(shù)據(jù)。

B+Tree vs B Tree:

    • B+Tree 只在葉子節(jié)點存儲數(shù)據(jù),而 B 樹 的非葉子節(jié)點也要存儲數(shù)據(jù),所以 B+Tree 的單個節(jié)點的數(shù)據(jù)量更小,在相同的磁盤 I/O 次數(shù)下,就能查詢更多的節(jié)點。另外,B+Tree 葉子節(jié)點采用的是雙鏈表連接,適合 MySQL 中常見的基于范圍的順序查找,而 B 樹無法做到這一點。

B+Tree vs 二叉樹:

    • 對于有 N 個葉子節(jié)點的 B+Tree,其搜索復(fù)雜度為O(logdN),其中 d 表示節(jié)點允許的最大子節(jié)點個數(shù)為 d 個。在實際的應(yīng)用當(dāng)中, d 值是大于100的,這樣就保證了,即使數(shù)據(jù)達到千萬級別時,B+Tree 的高度依然維持在 3~4 層左右,也就是說一次數(shù)據(jù)查詢操作只需要做 3~4 次的磁盤 I/O 操作就能查詢到目標數(shù)據(jù)。而二叉樹的每個父節(jié)點的兒子節(jié)點個數(shù)只能是 2 個,意味著其搜索復(fù)雜度為 O(logN),這已經(jīng)比 B+Tree 高出不少,因此二叉樹檢索到目標數(shù)據(jù)所經(jīng)歷的磁盤 I/O 次數(shù)要更多。

B+Tree vs Hash:

    Hash 在做等值查詢的時候效率賊快,搜索復(fù)雜度為 O(1)。但是 Hash 表不適合做范圍查詢,它更適合做等值的查詢,這也是 B+Tree 索引要比 Hash 表索引有著更廣泛的適用場景的原因

MySQL的關(guān)鍵字in和exist

在MySQL中,INEXISTS 都是用來處理子查詢的關(guān)鍵詞,但它們在功能、性能和使用場景上有各自的特點和區(qū)別。

IN關(guān)鍵字

IN 用于檢查左邊的表達式是否存在于右邊的列表或子查詢的結(jié)果集中。如果存在,則IN 返回TRUE,否則返回FALSE

語法結(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。語法結(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)子查詢的表很大時。這是因為EXISTS一旦找到匹配項就會立即停止查詢,而IN可能會掃描整個子查詢結(jié)果集。

使用場景:如果子查詢結(jié)果集較小且不頻繁變動,IN可能更直觀易懂。而當(dāng)子查詢涉及外部查詢的每一行判斷,并且子查詢的效率較高時,EXISTS更為合適。

NULL值處理IN能夠正確處理子查詢中包含NULL值的情況,而EXISTS不受子查詢結(jié)果中NULL值的影響,因為它關(guān)注的是行的存在性,而不是具體值。

事務(wù)四大特性是什么?

原子性(Atomicity):一個事務(wù)中的所有操作,要么全部完成,要么全部不完成,不會結(jié)束在中間某個環(huán)節(jié),而且事務(wù)在執(zhí)行過程中發(fā)生錯誤,會被回滾到事務(wù)開始前的狀態(tài),就像這個事務(wù)從來沒有執(zhí)行過一樣,就好比買一件商品,購買成功時,則給商家付了錢,商品到手;購買失敗時,則商品在商家手中,消費者的錢也沒花出去。

一致性(Consistency):是指事務(wù)操作前和操作后,數(shù)據(jù)滿足完整性約束,數(shù)據(jù)庫保持一致性狀態(tài)。比如,用戶 A 和用戶 B 在銀行分別有 800 元和 600 元,總共 1400 元,用戶 A 給用戶 B 轉(zhuǎn)賬 200 元,分為兩個步驟,從 A 的賬戶扣除 200 元和對 B 的賬戶增加 200 元。一致性就是要求上述步驟操作后,最后的結(jié)果是用戶 A 還有 600 元,用戶 B 有 800 元,總共 1400 元,而不會出現(xiàn)用戶 A 扣除了 200 元,但用戶 B 未增加的情況(該情況,用戶 A 和 B 均為 600 元,總共 1200 元)。

隔離性(Isolation):數(shù)據(jù)庫允許多個并發(fā)事務(wù)同時對其數(shù)據(jù)進行讀寫和修改的能力,隔離性可以防止多個事務(wù)并發(fā)執(zhí)行時由于交叉執(zhí)行而導(dǎo)致數(shù)據(jù)的不一致,因為多個事務(wù)同時使用相同的數(shù)據(jù)時,不會相互干擾,每個事務(wù)都有一個完整的數(shù)據(jù)空間,對其他并發(fā)事務(wù)是隔離的。也就是說,消費者購買商品這個事務(wù),是不影響其他消費者購買的。

持久性(Durability):事務(wù)處理結(jié)束后,對數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會丟失。

InnoDB 引擎通過什么技術(shù)來保證事務(wù)的這四個特性的呢?

    持久性是通過 redo log (重做日志)來保證的;原子性是通過 undo log(回滾日志) 來保證的;隔離性是通過 MVCC(多版本并發(fā)控制) 或鎖機制來保證的;一致性則是通過持久性+原子性+隔離性來保證;

Redis

Redis是單線程的還是多線程的,為什么是單線程的?有了解過其特性嗎

Redis在執(zhí)行指令時是單線程操作的,但是在實際運行過程中也出現(xiàn)多個線程并行運行的情況。

Redis 單線程指的是「接收客戶端請求->解析請求 ->進行數(shù)據(jù)讀寫等操作->發(fā)送數(shù)據(jù)給客戶端」這個過程是由一個線程(主線程)來完成的,這也是我們常說 Redis 是單線程的原因。

除此之外,雖然 Redis 的主要工作(網(wǎng)絡(luò) I/O 和執(zhí)行命令)一直是單線程模型,但是在 Redis 6.0 版本之后,也采用了多個 I/O 線程來處理網(wǎng)絡(luò)請求,這是因為隨著網(wǎng)絡(luò)硬件的性能提升,Redis 的性能瓶頸有時會出現(xiàn)在網(wǎng)絡(luò) I/O 的處理上

所以為了提高網(wǎng)絡(luò) I/O 的并行度,Redis 6.0 對于網(wǎng)絡(luò) I/O 采用多線程來處理。但是對于命令的執(zhí)行,Redis 仍然使用單線程來處理,所以大家不要誤解 Redis 有多線程同時執(zhí)行命令。

Redis 官方表示,Redis 6.0 版本引入的多線程 I/O 特性對性能提升至少是一倍以上。

Redis 6.0 版本支持的 I/O 多線程特性,默認情況下 I/O 多線程只針對發(fā)送響應(yīng)數(shù)據(jù)(write client socket),并不會以多線程的方式處理讀請求(read client socket)。要想開啟多線程處理客戶端讀請求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置項設(shè)為 yes。

//讀請求也使用io多線程
io-threads-do-reads?yes

同時, Redis.conf 配置文件中提供了 IO 多線程個數(shù)的配置項。

//?io-threads?N,表示啟用?N-1?個?I/O?多線程(主線程也算一個?I/O?線程)
io-threads?4

關(guān)于線程數(shù)的設(shè)置,官方的建議是如果為 4 核的 CPU,建議線程數(shù)設(shè)置為 2 或 3,如果為 8 核 CPU 建議線程數(shù)設(shè)置為 6,線程數(shù)一定要小于機器核數(shù),線程數(shù)并不是越大越好。

因此, Redis 6.0 版本之后,Redis 在啟動的時候,默認情況下會額外創(chuàng)建 6 個線程(_這里的線程數(shù)不包括主線程_):

    Redis-server :Redis的主線程,主要負責(zé)執(zhí)行命令;bio_close_file、bio_aof_fsync、bio_lazy_free:三個后臺線程,分別異步處理關(guān)閉文件任務(wù)、AOF刷盤任務(wù)、釋放內(nèi)存任務(wù);io_thd_1、io_thd_2、io_thd_3:三個 I/O 線程,io-threads 默認是 4 ,所以會啟動 3(4-1)個 I/O 多線程,用來分擔(dān) Redis 網(wǎng)絡(luò) I/O 的壓力。

Redis有哪2種持久化方式,分別的優(yōu)缺點

Redis 的讀寫操作都是在內(nèi)存中,所以 Redis 性能才會高,但是當(dāng) Redis 重啟后,內(nèi)存中的數(shù)據(jù)就會丟失,那為了保證內(nèi)存中的數(shù)據(jù)不會丟失,Redis 實現(xiàn)了數(shù)據(jù)持久化的機制,這個機制會把數(shù)據(jù)存儲到磁盤,這樣在 Redis 重啟就能夠從磁盤中恢復(fù)原有的數(shù)據(jù)。Redis 共有三種數(shù)據(jù)持久化的方式:

AOF 日志:每執(zhí)行一條寫操作命令,就把該命令以追加的方式寫入到一個文件里;

RDB 快照:將某一時刻的內(nèi)存數(shù)據(jù),以二進制的方式寫入磁盤;

AOF 日志是如何實現(xiàn)的?

Redis 在執(zhí)行完一條寫操作命令后,就會把該命令以追加的方式寫入到一個文件里,然后 Redis 重啟時,會讀取該文件記錄的命令,然后逐一執(zhí)行命令的方式來進行數(shù)據(jù)恢復(fù)。

我這里以「_set name xiaolin_」命令作為例子,Redis 執(zhí)行了這條命令后,記錄在 AOF 日志里的內(nèi)容如下圖:Redis 提供了 3 種寫回硬盤的策略, 在 Redis.conf 配置文件中的 appendfsync 配置項可以有以下 3 種參數(shù)可填:

Always,這個單詞的意思是「總是」,所以它的意思是每次寫操作命令執(zhí)行完后,同步將 AOF 日志數(shù)據(jù)寫回硬盤;

Everysec,這個單詞的意思是「每秒」,所以它的意思是每次寫操作命令執(zhí)行完后,先將命令寫入到 AOF 文件的內(nèi)核緩沖區(qū),然后每隔一秒將緩沖區(qū)里的內(nèi)容寫回到硬盤;

No,意味著不由 Redis 控制寫回硬盤的時機,轉(zhuǎn)交給操作系統(tǒng)控制寫回的時機,也就是每次寫操作命令執(zhí)行完后,先將命令寫入到 AOF 文件的內(nèi)核緩沖區(qū),再由操作系統(tǒng)決定何時將緩沖區(qū)內(nèi)容寫回硬盤。

我也把這 3 個寫回策略的優(yōu)缺點總結(jié)成了一張表格:

RDB 快照是如何實現(xiàn)的呢?

因為 AOF 日志記錄的是操作命令,不是實際的數(shù)據(jù),所以用 AOF 方法做故障恢復(fù)時,需要全量把日志都執(zhí)行一遍,一旦 AOF 日志非常多,勢必會造成 Redis 的恢復(fù)操作緩慢。為了解決這個問題,Redis 增加了 RDB 快照。

所謂的快照,就是記錄某一個瞬間東西,比如當(dāng)我們給風(fēng)景拍照時,那一個瞬間的畫面和信息就記錄到了一張照片。所以,RDB 快照就是記錄某一個瞬間的內(nèi)存數(shù)據(jù),記錄的是實際數(shù)據(jù),而 AOF 文件記錄的是命令操作的日志,而不是實際的數(shù)據(jù)。因此在 Redis 恢復(fù)數(shù)據(jù)時, RDB 恢復(fù)數(shù)據(jù)的效率會比 AOF 高些,因為直接將 RDB 文件讀入內(nèi)存就可以,不需要像 AOF 那樣還需要額外執(zhí)行操作命令的步驟才能恢復(fù)數(shù)據(jù)。

Redis 提供了兩個命令來生成 RDB 文件,分別是 save 和 bgsave,他們的區(qū)別就在于是否在「主線程」里執(zhí)行:

    • 執(zhí)行了 save 命令,就會在主線程生成 RDB 文件,由于和執(zhí)行操作命令在同一個線程,所以如果寫入 RDB 文件的時間太長,

會阻塞主線程

    • ;執(zhí)行了 bgsave 命令,會創(chuàng)建一個子進程來生成 RDB 文件,這樣可以

避免主線程的阻塞

 

優(yōu)缺點

AOF:

優(yōu)點:首先,AOF提供了更好的數(shù)據(jù)安全性,因為它默認每接收到一個寫命令就會追加到文件末尾。即使Redis服務(wù)器宕機,也只會丟失最后一次寫入前的數(shù)據(jù)。其次,AOF支持多種同步策略(如everysec、always等),可以根據(jù)需要調(diào)整數(shù)據(jù)安全性和性能之間的平衡。同時,AOF文件在Redis啟動時可以通過重寫機制優(yōu)化,減少文件體積,加快恢復(fù)速度。并且,即使文件發(fā)生損壞,AOF還提供了redis-check-aof工具來修復(fù)損壞的文件。

缺點:因為記錄了每一個寫操作,所以AOF文件通常比RDB文件更大,消耗更多的磁盤空間。并且,頻繁的磁盤IO操作(尤其是同步策略設(shè)置為always時)可能會對Redis的寫入性能造成一定影響。而且,當(dāng)問個文件體積過大時,AOF會進行重寫操作,AOF如果沒有開啟AOF重寫或者重寫頻率較低,恢復(fù)過程可能較慢,因為它需要重放所有的操作命令。

RDB:

優(yōu)點: RDB通過快照的形式保存某一時刻的數(shù)據(jù)狀態(tài),文件體積小,備份和恢復(fù)的速度非??臁2⑶?,RDB是在主線程之外通過fork子進程來進行的,不會阻塞服務(wù)器處理命令請求,對Redis服務(wù)的性能影響較小。最后,由于是定期快照,RDB文件通常比AOF文件小得多。

缺點: RDB方式在兩次快照之間,如果Redis服務(wù)器發(fā)生故障,這段時間的數(shù)據(jù)將會丟失。并且,如果在RDB創(chuàng)建快照到恢復(fù)期間有寫操作,恢復(fù)后的數(shù)據(jù)可能與故障前的數(shù)據(jù)不完全一致

Redis除了緩存,還有哪些應(yīng)用

Redis實現(xiàn)消息隊列

使用Pub/Sub模式:

    • Redis的Pub/Sub是一種基于發(fā)布/訂閱的消息模式,任何客戶端都可以訂閱一個或多個頻道,發(fā)布者可以向特定頻道發(fā)送消息,所有訂閱該頻道的客戶端都會收到此消息。該方式實現(xiàn)起來比較簡單,發(fā)布者和訂閱者完全解耦,支持模式匹配訂閱。但是這種方式不支持消息持久化,消息發(fā)布后若無訂閱者在線則會被丟棄;不保證消息的順序和可靠性傳輸。

使用List結(jié)構(gòu)

        使用List的方式通常是使用LPUSH命令將消息推入一個列表,消費者使用BLPOP或BRPOP阻塞地從列表中取出消息(先進先出FIFO)。這種方式可以實現(xiàn)簡單的任務(wù)隊列。這種方式可以結(jié)合Redis的過期時間特性實現(xiàn)消息的TTL;通過Redis事務(wù)可以保證操作的原子性。但是需要客戶端自己實現(xiàn)消息確認、重試等機制,相比專門的消息隊列系統(tǒng)功能較弱。

Redis實現(xiàn)分布式鎖

set nx方式:Redis提供了幾種方式來實現(xiàn)分布式鎖,最常用的是基于SET命令的爭搶鎖機制。客戶端可以使用SET resource_name lock_value NX PX milliseconds命令設(shè)置鎖,其中NX表示只有當(dāng)鍵不存在時才設(shè)置,PX指定鎖的有效時間(毫秒)。如果設(shè)置成功,則認為客戶端獲得鎖。客戶端完成操作后,解鎖的還需要先判斷鎖是不是自己,再進行刪除,這里涉及到 2 個操作,為了保證這兩個操作的原子性,可以用 lua 腳本來實現(xiàn)。

RedLock算法:

    為了提高分布式鎖的可靠性,Redis作者Antirez提出了RedLock算法,它基于多個獨立的Redis實例來實現(xiàn)一個更安全的分布式鎖。它的基本原理是客戶端嘗試在多數(shù)(大于半數(shù))Redis實例上同時加鎖,只有當(dāng)在大多數(shù)實例上加鎖成功時才認為獲取鎖成功。鎖的超時時間應(yīng)該遠小于單個實例的超時時間,以避免死鎖。該方式可以通過跨多個節(jié)點減少單點故障的影響,提高了鎖的可用性和安全性。

Redis分布式鎖的實現(xiàn),什么場景下用到分布式鎖

分布式鎖是用于分布式環(huán)境下并發(fā)控制的一種機制,用于控制某個資源在同一時刻只能被一個應(yīng)用所使用。如下圖所示:

Redis 本身可以被多個客戶端共享訪問,正好就是一個共享存儲系統(tǒng),可以用來保存分布式鎖,而且 Redis 的讀寫性能高,可以應(yīng)對高并發(fā)的鎖操作場景。Redis 的 SET 命令有個 NX 參數(shù)可以實現(xiàn)「key不存在才插入」,所以可以用它來實現(xiàn)分布式鎖:

    如果 key 不存在,則顯示插入成功,可以用來表示加鎖成功;如果 key 存在,則會顯示插入失敗,可以用來表示加鎖失敗。

基于 Redis 節(jié)點實現(xiàn)分布式鎖時,對于加鎖操作,我們需要滿足三個條件。

    加鎖包括了讀取鎖變量、檢查鎖變量值和設(shè)置鎖變量值三個操作,但需要以原子操作的方式完成,所以,我們使用 SET 命令帶上 NX 選項來實現(xiàn)加鎖;鎖變量需要設(shè)置過期時間,以免客戶端拿到鎖后發(fā)生異常,導(dǎo)致鎖一直無法釋放,所以,我們在 SET 命令執(zhí)行時加上 EX/PX 選項,設(shè)置其過期時間;鎖變量的值需要能區(qū)分來自不同客戶端的加鎖操作,以免在釋放鎖時,出現(xiàn)誤釋放操作,所以,我們使用 SET 命令設(shè)置鎖變量值時,每個客戶端設(shè)置的值是一個唯一值,用于標識客戶端;

滿足這三個條件的分布式命令如下:

SET?lock_key?unique_value?NX?PX?10000
    lock_key 就是 key 鍵;unique_value 是客戶端生成的唯一的標識,區(qū)分來自不同客戶端的鎖操作;NX 代表只在 lock_key 不存在時,才對 lock_key 進行設(shè)置操作;PX 10000 表示設(shè)置 lock_key 的過期時間為 10s,這是為了避免客戶端發(fā)生異常而無法釋放鎖。

而解鎖的過程就是將 lock_key 鍵刪除(del lock_key),但不能亂刪,要保證執(zhí)行操作的客戶端就是加鎖的客戶端。所以,解鎖的時候,我們要先判斷鎖的 unique_value 是否為加鎖客戶端,是的話,才將 lock_key 鍵刪除。

可以看到,解鎖是有兩個操作,這時就需要 Lua 腳本來保證解鎖的原子性,因為 Redis 在執(zhí)行 Lua 腳本時,可以以原子性的方式執(zhí)行,保證了鎖釋放操作的原子性。

//?釋放鎖時,先比較?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é)點上完成了分布式鎖的加鎖和解鎖。

Java

SpringBoot自動裝配原理是什么?

SpringBoot 的自動裝配原理是基于Spring Framework的條件化配置和@EnableAutoConfiguration注解實現(xiàn)的。這種機制允許開發(fā)者在項目中引入相關(guān)的依賴,SpringBoot 將根據(jù)這些依賴自動配置應(yīng)用程序的上下文和功能。

SpringBoot 定義了一套接口規(guī)范,這套規(guī)范規(guī)定:SpringBoot 在啟動時會掃描外部引用 jar 包中的META-INF/spring.factories文件,將文件中配置的類型信息加載到 Spring 容器(此處涉及到 JVM 類加載機制與 Spring 的容器知識),并執(zhí)行類中定義的各種操作。對于外部 jar 來說,只需要按照 SpringBoot 定義的標準,就能將自己的功能裝置進 SpringBoot。

通俗來講,自動裝配就是通過注解或一些簡單的配置就可以在SpringBoot的幫助下開啟和配置各種功能,比如數(shù)據(jù)庫訪問、Web開發(fā)。

SpringBoot自動裝配原理

首先點進@SpringBootApplication注解的內(nèi)部

接下來將逐個解釋這些注解的作用:

    @Target({ElementType.TYPE}): 該注解指定了這個注解可以用來標記在類上。在這個特定的例子中,這表示該注解用于標記配置類。@Retention(RetentionPolicy.RUNTIME): 這個注解指定了注解的生命周期,即在運行時保留。這是因為 Spring Boot 在運行時掃描類路徑上的注解來實現(xiàn)自動配置,所以這里使用了 RUNTIME 保留策略。@Documented: 該注解表示這個注解應(yīng)該被包含在 Java 文檔中。它是用于生成文檔的標記,使開發(fā)者能夠看到這個注解的相關(guān)信息。@Inherited: 這個注解指示一個被標注的類型是被繼承的。在這個例子中,它表明這個注解可以被繼承,如果一個類繼承了帶有這個注解的類,它也會繼承這個注解。@SpringBootConfiguration: 這個注解表明這是一個 Spring Boot 配置類。如果點進這個注解內(nèi)部會發(fā)現(xiàn)與標準的 @Configuration 沒啥區(qū)別,只是為了表明這是一個專門用于 SpringBoot 的配置。@EnableAutoConfiguration: 這個注解是 Spring Boot 自動裝配的核心。它告訴 Spring oot 啟用自動配置機制,根據(jù)項目的依賴和配置自動配置應(yīng)用程序的上下文。通過這個注解,SpringBoot 將嘗試根據(jù)類路徑上的依賴自動配置應(yīng)用程序。@ComponentScan: 這個注解用于配置組件掃描的規(guī)則。在這里,它告訴 SpringBoot 在指定的包及其子包中查找組件,這些組件包括被注解的類、@Component 注解的類等。其中的 excludeFilters 參數(shù)用于指定排除哪些組件,這里使用了兩個自定義的過濾器,分別是 TypeExcludeFilter 和 AutoConfigurationExcludeFilter。

@EnableAutoConfiguration

這個注解是實現(xiàn)自動裝配的核心注解

    @AutoConfigurationPackage,將項目src中main包下的所有組件注冊到容器中,例如標注了Component注解的類等@Import({AutoConfigurationImportSelector.class}),是自動裝配的核心,接下來分析一下這個注解

AutoConfigurationImportSelector

AutoConfigurationImportSelector 是 Spring Boot 中一個重要的類,它實現(xiàn)了 ImportSelector 接口,用于實現(xiàn)自動配置的選擇和導(dǎo)入。具體來說,它通過分析項目的類路徑和條件來決定應(yīng)該導(dǎo)入哪些自動配置類。代碼太多,選取部分主要功能的代碼

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?文件,獲取所有實現(xiàn)了?AutoConfiguration?接口的自動配置類
??List<String>?configurations?=?getCandidateConfigurations(annotationMetadata,?attributes);

??//?過濾掉不滿足條件的自動配置類,比如一些自動裝配類
??configurations?=?filter(configurations,?annotationMetadata,?attributes);

??//?排序自動配置類,根據(jù)?@AutoConfigureOrder?和?@AutoConfigureAfter/@AutoConfigureBefore?注解指定的順序
??sort(configurations,?annotationMetadata,?attributes);

??//?將滿足條件的自動配置類的類名數(shù)組返回,這些類將被導(dǎo)入到應(yīng)用程序上下文中
??return?StringUtils.toStringArray(configurations);
?}

?//?...?(其他方法)
?protected?List<String>?getCandidateConfigurations(AnnotationMetadata?metadata,?AnnotationAttributes?attributes)?{
??//?獲取自動配置類的候選列表,從?META-INF/spring.factories?文件中讀取
??//?通過類加載器加載所有候選類
??List<String>?configurations?=?SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
????getBeanClassLoader());

??//?過濾出實現(xiàn)了?AutoConfiguration?接口的自動配置類
??configurations?=?configurations.stream()
????.filter(this::isEnabled)
????.collect(Collectors.toList());

??//?對于?Spring?Boot?1.x?版本,還需要添加?spring-boot-autoconfigure?包中的自動配置類
??//?configurations.addAll(getAutoConfigEntry(getAutoConfigurationEntry(metadata)));
??return?configurations;
?}

?//?...?(其他方法)
?protected?List<String>?filter(List<String>?configurations,?AnnotationMetadata?metadata,
???AnnotationAttributes?attributes)?{
??//?使用條件判斷機制,過濾掉不滿足條件的自動配置類
??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ū)ψ詣优渲妙愡M行排序
??configurations.sort((o1,?o2)?->?{
???int?i1?=?getAutoConfigurationOrder(o1,?metadata,?attributes);
???int?i2?=?getAutoConfigurationOrder(o2,?metadata,?attributes);
???return?Integer.compare(i1,?i2);
??});
?}
??
???//?...?(其他方法)

}

梳理一下,以下是AutoConfigurationImportSelector的主要工作:

    • 掃描類路徑: 在應(yīng)用程序啟動時,

AutoConfigurationImportSelector

    • 會掃描類路徑上的

META-INF/spring.factories

    • 文件,這個文件中包含了各種 Spring 配置和擴展的定義。在這里,它會查找所有實現(xiàn)了

AutoConfiguration

    • 接口的類,具體的實現(xiàn)為

getCandidateConfigurations

    • 方法。條件判斷: 對于每一個發(fā)現(xiàn)的自動配置類,

AutoConfigurationImportSelector

    • 會使用條件判斷機制(通常是通過

@ConditionalOnXxx

    注解)來確定是否滿足導(dǎo)入條件。這些條件可以是配置屬性、類是否存在、Bean是否存在等等。根據(jù)條件導(dǎo)入自動配置類: 滿足條件的自動配置類將被導(dǎo)入到應(yīng)用程序的上下文中。這意味著它們會被實例化并應(yīng)用于應(yīng)用程序的配置。

說幾個啟動器(starter)?

spring-boot-starter-web:這是最常用的起步依賴之一,它包含了Spring MVC和Tomcat嵌入式服務(wù)器,用于快速構(gòu)建Web應(yīng)用程序。

spring-boot-starter-security:提供了Spring Security的基本配置,幫助開發(fā)者快速實現(xiàn)應(yīng)用的安全性,包括認證和授權(quán)功能。

mybatis-spring-boot-starter:這個Starter是由MyBatis團隊提供的,用于簡化在Spring Boot應(yīng)用中集成MyBatis的過程。它自動配置了MyBatis的相關(guān)組件,包括SqlSessionFactory、MapperScannerConfigurer等,使得開發(fā)者能夠快速地開始使用MyBatis進行數(shù)據(jù)庫操作。

spring-boot-starter-data-jpaspring-boot-starter-jdbc:如果使用的是Java Persistence API (JPA)進行數(shù)據(jù)庫操作,那么應(yīng)該使用spring-boot-starter-data-jpa。這個Starter包含了Hibernate等JPA實現(xiàn)以及數(shù)據(jù)庫連接池等必要的庫,可以讓你輕松地與MySQL數(shù)據(jù)庫進行交互。你需要在application.properties或application.yml中配置MySQL的連接信息。如果傾向于直接使用JDBC而不通過JPA,那么可以使用spring-boot-starter-jdbc,它提供了基本的JDBC支持。

spring-boot-starter-data-redis:用于集成Redis緩存和數(shù)據(jù)存儲服務(wù)。這個Starter包含了與Redis交互所需的客戶端(默認是Jedis客戶端,也可以配置為Lettuce客戶端),以及Spring Data Redis的支持,使得在Spring Boot應(yīng)用中使用Redis變得非常便捷。同樣地,需要在配置文件中設(shè)置Redis服務(wù)器的連接詳情。

spring-boot-starter-test:包含了單元測試和集成測試所需的庫,如JUnit, Spring Test, AssertJ等,便于進行測試驅(qū)動開發(fā)(TDD)。

怎么搭建SpringBoot項目的?

用 IntelliJ IDEA工具開發(fā)的SpringBoot項目

voliatle關(guān)鍵字有什么作用?

volatite作用有 2 個:

保證變量對所有線程的可見性。當(dāng)一個變量被聲明為volatile時,它會保證對這個變量的寫操作會立即刷新到主存中,而對這個變量的讀操作會直接從主存中讀取,從而確保了多線程環(huán)境下對該變量訪問的可見性。這意味著一個線程修改了volatile變量的值,其他線程能夠立刻看到這個修改,不會受到各自線程工作內(nèi)存的影響。

禁止指令重排序優(yōu)化。volatile關(guān)鍵字在Java中主要通過內(nèi)存屏障來禁止特定類型的指令重排序。

        • 1)

      寫-寫(Write-Write)屏障

      • :在對volatile變量執(zhí)行寫操作之前,會插入一個寫屏障。這確保了在該變量寫操作之前的所有普通寫操作都已完成,防止了這些寫操作被移到volatile寫操作之后。
        • 2)

      讀-寫(Read-Write)屏障

      • :在對volatile變量執(zhí)行讀操作之后,會插入一個讀屏障。它確保了對volatile變量的讀操作之后的所有普通讀操作都不會被提前到volatile讀之前執(zhí)行,保證了讀取到的數(shù)據(jù)是最新的。
        • 3)

      寫-讀(Write-Read)屏障

    • :這是最重要的一個屏障,它發(fā)生在volatile寫之后和volatile讀之前。這個屏障確保了volatile寫操作之前的所有內(nèi)存操作(包括寫操作)都不會被重排序到volatile讀之后,同時也確保了volatile讀操作之后的所有內(nèi)存操作(包括讀操作)都不會被重排序到volatile寫之前。

JVM內(nèi)存共享區(qū)域有哪些?

根據(jù) JVM8 規(guī)范,JVM 運行時內(nèi)存共分為虛擬機棧、堆、元空間、程序計數(shù)器、本地方法棧五個部分。還有一部分內(nèi)存叫直接內(nèi)存,屬于操作系統(tǒng)的本地內(nèi)存,也是可以直接操作的。

JVM的內(nèi)存結(jié)構(gòu)主要分為以下幾個部分:

元空間

    • :元空間并不是在堆上分配的,而是在堆外空間進行分配的,它的大小默認沒有上限,我們常說的方法區(qū),就在元空間中。元空間的本質(zhì)和永久代類似,都是對JVM規(guī)范中方法區(qū)的實現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機中,而是使用本地內(nèi)存。

Java 虛擬機棧

    • :每個線程有一個私有的棧,隨著線程的創(chuàng)建而創(chuàng)建。棧里面存著的是一種叫“棧幀”的東西,每個方法會創(chuàng)建一個棧幀,棧幀中存放了局部變量表(基本數(shù)據(jù)類型和對象引用)、操作數(shù)棧、方法出口等信息。棧的大小可以固定也可以動態(tài)擴展。

本地方法棧

    • :與虛擬機棧類似,區(qū)別是虛擬機棧執(zhí)行java方法,本地方法站執(zhí)行native方法。在虛擬機規(guī)范中對本地方法棧中方法使用的語言、使用方法與數(shù)據(jù)結(jié)構(gòu)沒有強制規(guī)定,因此虛擬機可以自由實現(xiàn)它。

程序計數(shù)器:

    • 程序計數(shù)器可以看成是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。在任何一個確定的時刻,一個處理器(對于多內(nèi)核來說是一個內(nèi)核)都只會執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要一個獨立的程序計數(shù)器,我們稱這類內(nèi)存區(qū)域為“線程私有”內(nèi)存。

堆內(nèi)存

    • :堆內(nèi)存是 JVM 所有線程共享的部分,在虛擬機啟動的時候就已經(jīng)創(chuàng)建。所有的對象和數(shù)組都在堆上進行分配。這部分空間可通過 GC 進行回收。當(dāng)申請不到空間時會拋出 OutOfMemoryError。堆是JVM內(nèi)存占用最大,管理最復(fù)雜的一個區(qū)域。其唯一的用途就是存放對象實例:所有的對象實例及數(shù)組都在對上進行分配。jdk1.8后,字符串常量池從永久代中剝離出來,存放在隊中。

直接內(nèi)存

    :直接內(nèi)存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是Java 虛擬機規(guī)范中農(nóng)定義的內(nèi)存區(qū)域。在JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O 方式,它可以使用native 函數(shù)庫直接分配堆外內(nèi)存,然后通脫一個存儲在Java堆中的DirectByteBuffer 對象作為這塊內(nèi)存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回復(fù)制數(shù)據(jù)。

方法區(qū)中的方法的執(zhí)行過程?

當(dāng)程序中通過對象或類直接調(diào)用某個方法時,主要包括以下幾個步驟:

解析方法調(diào)用:JVM會根據(jù)方法的符號引用找到實際的方法地址(如果之前沒有解析過的話)。

棧幀創(chuàng)建:在調(diào)用一個方法前,JVM會在當(dāng)前線程的Java虛擬機棧中為該方法分配一個新的棧幀,用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。

執(zhí)行方法:執(zhí)行方法內(nèi)的字節(jié)碼指令,涉及的操作可能包括局部變量的讀寫、操作數(shù)棧的操作、跳轉(zhuǎn)控制、對象創(chuàng)建、方法調(diào)用等。

返回處理:方法執(zhí)行完畢后,可能會返回一個結(jié)果給調(diào)用者,并清理當(dāng)前棧幀,恢復(fù)調(diào)用者的執(zhí)行環(huán)境。

方法區(qū)中還有哪些東西?

《深入理解Java虛擬機》書中對方法區(qū)(Method Area)存儲內(nèi)容描述如下:它用于存儲已被虛擬機加載的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等。

    類信息:包括類的結(jié)構(gòu)信息、類的訪問修飾符、父類與接口等信息。常量池:存儲類和接口中的常量,包括字面值常量、符號引用,以及運行時常量池。靜態(tài)變量:存儲類的靜態(tài)變量,這些變量在類初始化的時候被賦值。方法字節(jié)碼:存儲類的方法字節(jié)碼,即編譯后的代碼。符號引用:存儲類和方法的符號引用,是一種直接引用不同于直接引用的引用類型。運行時常量池:存儲著在類文件中的常量池數(shù)據(jù),在類加載后在方法區(qū)生成該運行時常量池。常量池緩存:用于提升類加載的效率,將常用的常量緩存起來方便使用。

類加載器有哪些?

啟動類加載器(Bootstrap Class Loader)

    • :這是最頂層的類加載器,負責(zé)加載Java的核心庫(如位于jre/lib/rt.jar中的類),它是用C++編寫的,是JVM的一部分。啟動類加載器無法被Java程序直接引用。

擴展類加載器(Extension Class Loader)

    • :它是Java語言實現(xiàn)的,繼承自ClassLoader類,負責(zé)加載Java擴展目錄(jre/lib/ext或由系統(tǒng)變量java.ext.dirs指定的目錄)下的jar包和類庫。擴展類加載器由啟動類加載器加載,并且父加載器就是啟動類加載器。

系統(tǒng)類加載器(System Class Loader)/ 應(yīng)用程序類加載器(Application Class Loader)

    • :這也是Java語言實現(xiàn)的,負責(zé)加載用戶類路徑(ClassPath)上的指定類庫,是我們平時編寫Java程序時默認使用的類加載器。系統(tǒng)類加載器的父加載器是擴展類加載器。它可以通過ClassLoader.getSystemClassLoader()方法獲取到。

自定義類加載器(Custom Class Loader)

    :開發(fā)者可以根據(jù)需求定制類的加載方式,比如從網(wǎng)絡(luò)加載class文件、數(shù)據(jù)庫、甚至是加密的文件中加載類等。自定義類加載器可以用來擴展Java應(yīng)用程序的靈活性和安全性,是Java動態(tài)性的一個重要體現(xiàn)。

這些類加載器之間的關(guān)系形成了雙親委派模型,其核心思想是當(dāng)一個類加載器收到類加載的請求時,首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中。

只有當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。

雙親委派模型的作用

保證類的唯一性:通過委托機制,確保了所有加載請求都會傳遞到啟動類加載器,避免了不同類加載器重復(fù)加載相同類的情況,保證了Java核心類庫的統(tǒng)一性,也防止了用戶自定義類覆蓋核心類庫的可能。

保證安全性:由于Java核心庫被啟動類加載器加載,而啟動類加載器只加載信任的類路徑中的類,這樣可以防止不可信的類假冒核心類,增強了系統(tǒng)的安全性。例如,惡意代碼無法自定義一個java.lang.System類并加載到JVM中,因為這個請求會被委托給啟動類加載器,而啟動類加載器只會加載標準的Java庫中的類。

支持隔離和層次劃分:雙親委派模型支持不同層次的類加載器服務(wù)于不同的類加載需求,如應(yīng)用程序類加載器加載用戶代碼,擴展類加載器加載擴展框架,啟動類加載器加載核心庫。這種層次化的劃分有助于實現(xiàn)沙箱安全機制,保證了各個層級類加載器的職責(zé)清晰,也便于維護和擴展。

簡化了加載流程:通過委派,大部分類能夠被正確的類加載器加載,減少了每個加載器需要處理的類的數(shù)量,簡化了類的加載過程,提高了加載效率。

講一下類加載過程?

類從被加載到虛擬機內(nèi)存開始,到卸載出內(nèi)存為止,它的整個生命周期包括以下 7 個階段:

加載:

    • 通過類的全限定名(包名 + 類名),獲取到該類的.class文件的二進制字節(jié)流,將二進制字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu),轉(zhuǎn)化為方法區(qū)運行時的數(shù)據(jù)結(jié)構(gòu),在內(nèi)存中生成一個代表該類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口

連接:

    • 驗證、準備、解析 3 個階段統(tǒng)稱為連接。

      • 驗證:確保class文件中的字節(jié)流包含的信息,符合當(dāng)前虛擬機的要求,保證這個被加載的class類的正確性,不會危害到虛擬機的安全。驗證階段大致會完成以下四個階段的檢驗動作:文件格式校驗、元數(shù)據(jù)驗證、字節(jié)碼驗證、符號引用驗證準備:為類中的靜態(tài)字段分配內(nèi)存,并設(shè)置默認的初始值,比如int類型初始值是0。被final修飾的static字段不會設(shè)置,因為final在編譯的時候就分配了

解析:解析階段是虛擬機將常量池的「符號引用」直接替換為「直接引用」的過程。符號引用是以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用的時候可以無歧義地定位到目標即可。直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄,直接引用是和虛擬機實現(xiàn)的內(nèi)存布局相關(guān)的。如果有了直接引用, 那引用的目標必定已經(jīng)存在在內(nèi)存中了。

初始化:初始化是整個類加載過程的最后一個階段,初始化階段簡單來說就是執(zhí)行類的構(gòu)造器方法(() ),要注意的是這里的構(gòu)造器方法()并不是開發(fā)者寫的,而是編譯器自動生成的。

使用:使用類或者創(chuàng)建對象

卸載:如果有下面的情況,類就會被卸載:1. 該類所有的實例都已經(jīng)被回收,也就是java堆中不存在該類的任何實例。2. 加載該類的ClassLoader已經(jīng)被回收。3. 類對應(yīng)的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。

判斷垃圾的方法有哪些?

在Java中,判斷對象是否為垃圾(即不再被使用,可以被垃圾回收器回收)主要依據(jù)兩種主流的垃圾回收算法來實現(xiàn):引用計數(shù)法和可達性分析算法。

引用計數(shù)法(Reference Counting)

原理:為每個對象分配一個引用計數(shù)器,每當(dāng)有一個地方引用它時,計數(shù)器加1;當(dāng)引用失效時,計數(shù)器減1。當(dāng)計數(shù)器為0時,表示對象不再被任何變量引用,可以被回收。

缺點:不能解決循環(huán)引用的問題,即兩個對象相互引用,但不再被其他任何對象引用,這時引用計數(shù)器不會為0,導(dǎo)致對象無法被回收。

可達性分析算法(Reachability Analysis)

Java虛擬機主要采用此算法來判斷對象是否為垃圾。

原理

    :從一組稱為GC Roots(垃圾收集根)的對象出發(fā),向下追溯它們引用的對象,以及這些對象引用的其他對象,以此類推。如果一個對象到GC Roots沒有任何引用鏈相連(即從GC Roots到這個對象不可達),那么這個對象就被認為是不可達的,可以被回收。GC Roots對象包括:虛擬機棧(棧幀中的本地變量表)中引用的對象、方法區(qū)中類靜態(tài)屬性引用的對象、本地方法棧中JNI(Java Native Interface)引用的對象、活躍線程的引用等。

垃圾回收算法有哪些?

標記-清除算法:標記-清除算法分為“標記”和“清除”兩個階段,首先通過可達性分析,標記出所有需要回收的對象,然后統(tǒng)一回收所有被標記的對象。標記-清除算法有兩個缺陷,一個是效率問題,標記和清除的過程效率都不高,另外一個就是,清除結(jié)束后會造成大量的碎片空間。有可能會造成在申請大塊內(nèi)存的時候因為沒有足夠的連續(xù)空間導(dǎo)致再次 GC。

復(fù)制算法:為了解決碎片空間的問題,出現(xiàn)了“復(fù)制算法”。復(fù)制算法的原理是,將內(nèi)存分成兩塊,每次申請內(nèi)存時都使用其中的一塊,當(dāng)內(nèi)存不夠時,將這一塊內(nèi)存中所有存活的復(fù)制到另一塊上。然后將然后再把已使用的內(nèi)存整個清理掉。復(fù)制算法解決了空間碎片的問題。但是也帶來了新的問題。因為每次在申請內(nèi)存時,都只能使用一半的內(nèi)存空間。內(nèi)存利用率嚴重不足。

標記-整理算法:復(fù)制算法在 GC 之后存活對象較少的情況下效率比較高,但如果存活對象比較多時,會執(zhí)行較多的復(fù)制操作,效率就會下降。而老年代的對象在 GC 之后的存活率就比較高,所以就有人提出了“標記-整理算法”。標記-整理算法的“標記”過程與“標記-清除算法”的標記過程一致,但標記之后不會直接清理。而是將所有存活對象都移動到內(nèi)存的一端。移動結(jié)束后直接清理掉剩余部分。

分代回收算法:分代收集是將內(nèi)存劃分成了新生代和老年代。分配的依據(jù)是對象的生存周期,或者說經(jīng)歷過的 GC 次數(shù)。對象創(chuàng)建時,一般在新生代申請內(nèi)存,當(dāng)經(jīng)歷一次 GC 之后如果對還存活,那么對象的年齡 +1。當(dāng)年齡超過一定值(默認是 15,可以通過參數(shù) -XX:MaxTenuringThreshold 來設(shè)定)后,如果對象還存活,那么該對象會進入老年代。

標記清除算法的缺點是什么?

主要缺點有兩個:

    一個是效率問題,標記和清除過程的效率都不高;另外一個是空間問題,標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致,當(dāng)程序在以后的運行過程中需要分配較大對象時無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。

堆分為哪幾部分呢?

Java堆(Heap)是Java虛擬機(JVM)中內(nèi)存管理的一個重要區(qū)域,主要用于存放對象實例和數(shù)組。隨著JVM的發(fā)展和不同垃圾收集器的實現(xiàn),堆的具體劃分可能會有所不同,但通常可以分為以下幾個部分:

新生代(Young Generation):新生代分為Eden Space和Survivor Space。在Eden Space中, 大多數(shù)新創(chuàng)建的對象首先存放在這里。Eden區(qū)相對較小,當(dāng)Eden區(qū)滿時,會觸發(fā)一次Minor GC(新生代垃圾回收)。在Survivor Spaces中,通常分為兩個相等大小的區(qū)域,稱為S0(Survivor 0)和S1(Survivor 1)。在每次Minor GC后,存活下來的對象會被移動到其中一個Survivor空間,以繼續(xù)它們的生命周期。這兩個區(qū)域輪流充當(dāng)對象的中轉(zhuǎn)站,幫助區(qū)分短暫存活的對象和長期存活的對象。

老年代(Old Generation/Tenured Generation):存放過一次或多次Minor GC仍存活的對象會被移動到老年代。老年代中的對象生命周期較長,因此Major GC(也稱為Full GC,涉及老年代的垃圾回收)發(fā)生的頻率相對較低,但其執(zhí)行時間通常比Minor GC長。老年代的空間通常比新生代大,以存儲更多的長期存活對象。

元空間(Metaspace):從Java 8開始,永久代(Permanent Generation)被元空間取代,用于存儲類的元數(shù)據(jù)信息,如類的結(jié)構(gòu)信息(如字段、方法信息等)。元空間并不在Java堆中,而是使用本地內(nèi)存,這解決了永久代容易出現(xiàn)的內(nèi)存溢出問題。

大對象區(qū)(Large Object Space / Humongous Objects):在某些JVM實現(xiàn)中(如G1垃圾收集器),為大對象分配了專門的區(qū)域,稱為大對象區(qū)或Humongous Objects區(qū)域。大對象是指需要大量連續(xù)內(nèi)存空間的對象,如大數(shù)組。這類對象直接分配在老年代,以避免因頻繁的年輕代晉升而導(dǎo)致的內(nèi)存碎片化問題。

推薦器件

更多器件
器件型號 數(shù)量 器件廠商 器件描述 數(shù)據(jù)手冊 ECAD模型 風(fēng)險等級 參考價格 更多信息
TLP291-4(V4GBTPE(T 1 Toshiba America Electronic Components Transistor Output Optocoupler, 4-Element, 2500V Isolation
$2.08 查看
LTC6994CS6-1#TRMPBF 1 Linear Technology LTC6994 - TimerBlox: Delay Block/ Debouncer; Package: SOT; Pins: 6; Temperature Range: 0&deg;C to 70&deg;C
$3.04 查看
CPC1718J 1 Littelfuse Inc Transistor Output SSR, 1-Channel, 2500V Isolation, ROHS COMPLIANT, ISOPLUS264, 4 PIN

ECAD模型

下載ECAD模型
$6.36 查看

相關(guān)推薦

電子產(chǎn)業(yè)圖譜