高頻面試知識點總結,看看你能答對多少
開篇:題目答案總結並非標準,僅供參考,如果有錯誤或者更好的見解,歡迎留言討論,往期公眾號整理的一些面試題看這裡:Java面試題內容聚合
事務
1、什麼是事務?事務的特性(ACID)
什麼是事務:事務是程式中一系列嚴密的操作,所有操作執行必須成功完成,否則在每個操作所做的更改將會被撤銷,這也是事務的原子性(要麼成功,要麼失敗)。
事務特性分為四個:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持續性(Durability)簡稱ACID。
1、原子性:事務是資料庫的邏輯工作單位,事務中包含的各操作要麼都做,要麼都不做。
2、一致性:事務執行的結果必須是使資料庫從一個一致性狀態變到另一個一致性狀態。因此當資料庫只包含成功事務提交的結果時,就說資料庫處於一致性狀態。如果資料庫系統執行中發生故障,有些事務尚未完成就被迫中斷,這些未完成事務對資料庫所做的修改有一部分已寫入物理資料庫,這時資料庫就處於一種不正確的狀態,或者說是不一致的狀態。
3、隔離性:一個事務的執行不能其它事務干擾。即一個事務內部的操作及使用的資料對其它併發事務是隔離的,併發執行的各個事務之間不能互相干擾。
4、永續性:也稱永久性,指一個事務一旦提交,它對資料庫中的資料的改變就應該是永久性的。接下來的其它操作或故障不應該對其執行結果有任何影響。
2、事務的隔離級別有幾種,最常用的隔離級別是哪兩種?
併發過程中會出現的問題:
-
丟失更新:是不可重複讀的特殊情況。如果兩個事物都讀取同一行,然後兩個都進行寫操作,並提交,第一個事物所做的改變就會丟失。
-
髒讀:一個事務讀取到另一個事務未提交的更新資料。
-
幻讀也叫虛讀:一個事務執行兩次查詢,第二次結果集包含第一次中沒有或某些行已經被刪除的資料,造成兩次結果不一致,只是另一個事務在這兩次查詢中間插入或刪除了資料造成的。
-
不可重複讀:一個事務兩次讀取同一行的資料,結果得到不同狀態的結果,中間正好另一個事務更新了該資料,兩次結果相異,不可被信任。
事務的隔離級別有4種:
1、未提交讀(Read uncommitted)
-
定義:就是一個事務讀取到其他事務未提交的資料,是級別最低的隔離機制。
-
缺點:會產生髒讀、不可重複讀、幻讀。
2、提交讀(Read committed)
-
定義:就是一個事務讀取到其他事務提交後的資料。Oracle預設隔離級別。
-
缺點:會產生不可重複讀、幻讀。
3、可重複讀(Repeatable read)
-
定義:就是一個事務對同一份資料讀取到的相同,不在乎其他事務對資料的修改。MySQL預設的隔離級別。
-
缺點:會產生幻讀。
4、序列化(Serializable)
-
定義:事務序列化執行,隔離級別最高,犧牲了系統的併發性。
-
缺點:可以解決併發事務的所有問題。但是效率地下,消耗資料庫效能,一般不使用。
快取
3、分散式快取的典型應用場景?
-
頁面快取,用來快取Web頁面的內容片段,包括HTML、CSS 和圖片等,多應用於社交網站等。
-
應用物件快取,快取系統作為ORM框架的二級快取對外提供服務,目的是減輕資料庫的負載壓力,加速應用訪問。
-
狀態快取,快取包括Session會話狀態及應用橫向擴充套件時的狀態資料等,這類資料一般是難以恢復的,對可用性要求較高,多應用於高可用叢集。
-
並行處理,通常涉及大量中間計算結果需要共享。
-
事件處理,分散式快取提供了針對事件流的連續查詢(continuous query)處理技術,滿足實時性需求。
-
極限事務處理,分散式快取為事務型應用提供高吞吐率、低延時的解決方案,支援高併發事務請求處理,多應用於鐵路、金融服務和電信等領域。
資料庫
4、MongoDB與Mysql的區別?
兩種資料庫的區別:
-
傳統的關係型資料庫,資料是以表單為媒介進行儲存的。
-
相比較Mysql,Mongodb以一種直觀文件的方式來完成資料的儲存。
Mongodb的鮮明特徵:
-
自帶GirdFS的分散式檔案系統,這也為Mongodb的部署提供了很大便利。
-
Mongodb內自建了對map-reduce運算框架的支援,雖然這種支援從功能上看還算是比較簡單的,相當於MySQL裡GroupBy功能的擴充套件版,不過也為資料的統計帶來了方便。
-
Mongodb在啟動後將資料庫中得資料以檔案對映的方式載入到記憶體中,如果記憶體資源相當豐富的話,這將極大的提高資料庫的查詢速度。
Mongodb的優勢:
-
Mongodb適合那些對資料庫具體格式不明確或者資料庫資料格式經常變化的需求模型,而且對開發者十分友好。
-
Mongodb官方就自帶一個分散式檔案系統,Mongodb官方就自帶一個分散式檔案系統,可以很方便的部署到伺服器機群上。
Mongodb的缺陷:
-
事務關係支援薄弱。這也是所有NoSQL資料庫共同的缺陷,不過NoSQL並不是為了事務關係而設計的,具體應用還是很需求。
-
穩定性有些欠缺
-
方便開發者的同時,對運維人員提出了更高的要求。
Mongodb的應用場景:
-
表結構不明確且資料不斷變大:MongoDB是非結構化文件資料庫,擴充套件欄位很容易且不會影響原有資料。內容管理或者部落格平臺等,例如圈子系統,儲存使用者評論之類的。
-
更高的寫入負載:MongoDB側重高資料寫入的效能,而非事務安全,適合業務系統中有大量“低價值”資料的場景。本身存的就是json格式資料。例如做日誌系統。
-
資料量很大或者將來會變得很大:Mysql單表資料量達到5-10G時會出現明細的效能降級,需要做資料的水平和垂直拆分、庫的拆分完成擴充套件,MongoDB內建了sharding、很多資料分片的特性,容易水平擴充套件,比較好的適應大資料量增長的需求。
-
高可用性:自帶高可用,自動主從切換(副本集):
不適用的場景:
-
MongoDB不支援事務操作,需要用到事務的應用建議不用MongoDB。
-
MongoDB目前不支援join操作,需要複雜查詢的應用也不建議使用MongoDB。
-
在帶“_id”插入資料的時候,MongoDB的插入效率其實並不高。如果想充分利用MongoDB效能的話,推薦採取不帶“_id”的插入方式,然後對相關欄位作索引來查詢。
關係型資料庫和非關係型資料庫的應用場景對比:
關係型資料庫適合儲存結構化資料,如使用者的帳號、地址:
-
這些資料通常需要做結構化查詢,比如join,這時候,關係型資料庫就要勝出一籌。
-
這些資料的規模、增長的速度通常是可以預期的。
-
事務性、一致性。
NoSQL適合儲存非結構化資料,如文章、評論:
-
這些資料通常用於模糊處理,如全文搜尋、機器學習。
-
這些資料是海量的,而且增長的速度是難以預期的。
-
根據資料的特點,NoSQL資料庫通常具有無限(至少接近)伸縮性。
-
按key獲取資料效率很高,但是對join或其他結構化查詢的支援就比較差。
5、Mysql索引相關問題。
1)什麼是索引?
-
索引其實是一種資料結構,能夠幫助我們快速的檢索資料庫中的資料。
2)索引具體採用的哪種資料結構呢?
-
常見的MySQL主要有兩種結構:Hash索引和B+ Tree索引,通常使用的是InnoDB引擎,預設的是B+樹。
3)InnoDb記憶體使用機制?
Innodb體系結構如圖所示:
Innodb關於查詢效率有影響的兩個比較重要的引數分別是innodb_buffer_pool_size,innodb_read_ahead_threshold:
-
innodb_buffer_pool_size指的是Innodb緩衝池的大小,該引數的大小可通過命令指定innodb_buffer_pool_size 20G。緩衝池使用改進的LRU演算法進行管理,維護一個LRU列表、一個FREE列表,FREE列表存放空閒頁,資料庫啟動時LRU列表是空的,當需要從緩衝池分頁時,首先從FREE列表查詢空閒頁,有則放入LRU列表,否則LRU執行淘汰,淘汰尾部的頁分配給新頁。
-
innodb_read_ahead_threshold相對應的是資料預載入機制,innodb_read_ahead_threshold 30表示的是如果一個extent中的被順序讀取的page超過或者等於該引數變數的,Innodb將會非同步的將下一個extent讀取到buffer pool中,比如該引數的值為30,那麼當該extent中有30個pages被sequentially的讀取,則會觸發innodb linear預讀,將下一個extent讀到記憶體中;在沒有該變數之前,當訪問到extent的最後一個page的時候,Innodb會決定是否將下一個extent放入到buffer pool中;可以在Mysql服務端通過show innodb status中的Pages read ahead和evicted without access兩個值來觀察預讀的情況:Innodb_buffer_pool_read_ahead:表示通過預讀請求到buffer pool的pages;Innodb_buffer_pool_read_ahead_evicted:表示由於請求到buffer pool中沒有被訪問,而驅逐出記憶體的頁數。
可以看出來,Mysql的緩衝池機制是能充分利用記憶體且有預載入機制,在某些條件下目標資料完全在記憶體中,也能夠具備非常好的查詢效能。
4)B+ Tree索引和Hash索引區別?
-
雜湊索引適合等值查詢,但是無法進行範圍查詢。
-
雜湊索引沒辦法利用索引完成排序。
-
雜湊索引不支援多列聯合索引的最左匹配規則。
-
如果有大量重複鍵值的情況下,雜湊索引的效率會很低,因為存在雜湊碰撞問題。
5)B+ Tree的葉子節點都可以存哪些東西嗎?
-
InnoDB的B+ Tree可能儲存的是整行資料,也有可能是主鍵的值。
6)這兩者有什麼區別嗎?
-
在 InnoDB 裡,索引B+ Tree的葉子節點儲存了整行資料的是主鍵索引,也被稱之為聚簇索引。而索引B+ Tree的葉子節點儲存了主鍵的值的是非主鍵索引,也被稱之為非聚簇索引。
7)聚簇索引和非聚簇索引,在查詢資料的時候有區別嗎?
-
聚簇索引查詢會更快,因為主鍵索引樹的葉子節點直接就是我們要查詢的整行資料了。而非主鍵索引的葉子節點是主鍵的值,查到主鍵的值以後,還需要再通過主鍵的值再進行一次查詢。
8)主鍵索引查詢只會查一次,而非主鍵索引需要回表查詢多次(這個過程叫做回表)。是所有情況都是這樣的嗎?非主鍵索引一定會查詢多次嗎?
覆蓋索引(covering index)指一個查詢語句的執行只用從索引中就能夠取得,不必從資料表中讀取。也可以稱之為實現了索引覆蓋。當一條查詢語句符合覆蓋索引條件時,MySQL只需要通過索引就可以返回查詢所需要的資料,這樣避免了查到索引後再返回表操作,減少I/O提高效率。
如,表covering_index_sample中有一個普通索引 idx_key1_key2(key1,key2)。當我們通過SQL語句:select key2 from covering_index_sample where key1 = 'keytest';的時候,就可以通過覆蓋索引查詢,無需回表。
9)在建立索引的時候都會考慮哪些因素呢?
一般對於查詢概率比較高,經常作為where條件的欄位設定索引。
10)在建立聯合索引的時候,需要做聯合索引多個欄位之間順序,這是如何選擇的呢?
在建立多列索引時,我們根據業務需求,where子句中使用最頻繁的一列放在最左邊,因為MySQL索引查詢會遵循最左字首匹配的原則,即最左優先,在檢索資料時從聯合索引的最左邊開始匹配。
所以當我們建立一個聯合索引的時候,如(key1,key2,key3),相當於建立了(key1)、(key1,key2)和(key1,key2,key3)三個索引,這就是最左匹配原則。
11)你知道在MySQL 5.6中,對索引做了哪些優化嗎?
-
索引條件下推:“索引條件下推”,稱為 Index Condition Pushdown (ICP),這是MySQL提供的用某一個索引對一個特定的表從表中獲取元組”,注意我們這裡特意強調了“一個”,這是因為這樣的索引優化不是用於多表連線而是用於單表掃描,確切地說,是單表利用索引進行掃描以獲取資料的一種方式。
-
例如有索引(key1,key2),SQL語句中
where key1 = 'XXX' and key2 like '%XXX%'
: -
如果沒有使用索引下推技術,MySQL會通過key1 = 'XXX'從儲存引擎返回對應的資料至MySQL服務端,服務端再基於key2 like 判斷是否符合條件。
-
如果使用了索引下推技術,MySQL首先返回key1='XXX'的索引,再根據key2 like 判斷索引是否符合條件,如果符合則通過索引定位資料,如果不符合則直接reject掉。有了索引下推優化,可以在有like條件查詢的情況下,減少回表次數。
12)如何知道索引是否生效?
explain顯示了MySQL如何使用索引來處理select語句以及連線表。可以幫助選擇更好的索引和寫出更優化的查詢語句。使用方法,在select語句前加上explain就可以了。
13)那什麼情況下會發生明明建立了索引,但是執行的時候並沒有通過索引呢?
在一條單表查詢語句真正執行之前,MySQL的查詢優化器會找出執行該語句所有可能使用的方案,對比之後找出成本最低的方案。這個成本最低的方案就是所謂的執行計劃。優化過程大致如下:
-
根據搜尋條件,找出所有可能使用的索引。
-
計算全表掃描的代價。
-
計算使用不同索引執行查詢的代價。
-
對比各種執行方案的代價,找出成本最低的那一個。
14)為什麼索引結構預設使用B+Tree,而不是Hash,二叉樹,紅黑樹?
-
B+tree是一種多路平衡查詢樹,節點是天然有序的,非葉子節點包含多個元素,不儲存資料,只用來索引,葉子節點包含完整資料和帶有指向下一個節點的指標,形成一個有序連結串列,有助於範圍和順序查詢。因為非葉子節點不儲存資料,所以同樣大小的磁碟頁可以容納更多的元素,同樣能資料量的情況下,B+tree相比B-tree高度更低,因此查詢時IO會更少。
-
B-tree不管葉子節點還是非葉子節點,都會儲存資料,這樣導致在非葉子節點中能儲存的指標數量變少(有些資料也稱為扇出),指標少的情況下要儲存大量資料,只能增加樹的高度,導致IO操作變多,查詢效能變低;
-
Hash索引底層是基於雜湊表,就是以key-value儲存資料的結構,多個數據在儲存關係上是沒有任何順序關係的。只適合等值查詢,不適合範圍查詢,而且也無法利用索引完成排序,不支援聯合索引的最左匹配原則,如果有大量重複鍵值的情況下,雜湊索引效率會很低,因為存在雜湊碰撞。
-
二叉樹:樹的高度不均勻,不能自平衡,查詢效率跟資料有關(樹的高度),並且IO代價高。
-
紅黑樹:樹的高度隨著資料量增加而增加,IO代價高。
6、如何優化MySQL?
MySQL優化大致可以分為三部分:索引的優化、SQL語句優化和表的優化
索引優化可以遵循以下幾個原則:
-
聯合索引最左字首匹配原則
-
儘量把欄位長度小的列放在聯合索引的最左側(因為欄位越小,一頁儲存的資料量越大,IO效能也就越好)
-
order by 有多個列排序的,應該建立聯合索引
-
對於頻繁的查詢優先考慮使用覆蓋索引
-
前導模糊查詢不會使用索引,比如說Like '%aaa%'這種
-
負向條件不會使用索引,如!=,<>,not like,not in,not exists
-
索引應該建立在區分度比較高的欄位上 一般區分度在80%以上的時候就可以建立索引,區分度可以使用 count(distinct(列名))/count(*)
-
對於where子句中經常使用的列,最好設定索引
SQL語句優化,可以通過explain檢視SQL的執行計劃,優化語句原則可以有:
-
在where和order by涉及的列上建立合適的索引,避免全表掃描
-
任何查詢都不要使用select * ,而是用具體的欄位列表代替
-
多表連線時,儘量小表驅動大表,即小表join大表
-
用exists代替in
-
儘量避免在where字句中對欄位進行函式操作
資料庫表優化
-
表字段儘可能用not null
-
欄位長度固定表查詢會更快
-
將資料庫大表按照時間或者一些標誌拆分成小表
-
水平拆分:將記錄雜湊到不同的表中,每次從分表查詢
-
垂直拆分:將表中的大欄位單獨拆分到另一張表,形成一對一的關係
7、為什麼任何查詢都不要使用SELECT *?
-
多出一些不用的列,這些列可能正好不在索引的範圍之內(索引的好處不多說)select * 杜絕了索引覆蓋的可能性,而索引覆蓋又是速度極快,效率極高,業界極為推薦的查詢方式。(索引覆蓋)
-
資料庫需要知道 * 等於什麼 = 查資料字典會增大開銷(記錄資料庫和應用程式元資料的目錄)。
-
不需要的欄位會增加資料傳輸的時間,即使 mysql 伺服器和客戶端是在同一臺機器上,使用的協議還是 tcp,通訊也是需要額外的時間。
-
大欄位,例如很長的 varchar,blob,text。準確來說,長度超過 728 位元組的時候,會把超出的資料放到另外一個地方,因此讀取這條記錄會增加一次 io 操作。(mysql innodb)
-
影響資料庫自動重寫優化SQL(類似 Java 中編譯 class 時的編譯器自動優化) 。(Oracle)
-
select * 資料庫需要解析更多的 物件,欄位,許可權,屬性相關,在 SQL 語句複雜,硬解析較多的情況下,會對資料庫造成沉重的負擔。
-
額外的 io,記憶體和 cpu 的消耗,因為多取了不必要的列。
-
用 SELECT * 需謹慎,因為一旦列的個數或順序更改,就有可能程式執行失敗。
多執行緒
Java實現多執行緒有幾種方式?
有三種方式:
-
繼承Thread類,並重寫run方法。
-
實現Runnable介面,並重寫run方法。
-
實現Callable介面,並重寫run方法,並使用FutureTask包裝器。
執行緒的生命週期
1、新建狀態(New):新建立了一個執行緒物件。
2、就緒狀態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,變得可執行,等待獲取CPU的使用權。
3、執行狀態(Running):就緒狀態的執行緒獲取了CPU,執行程式程式碼。
4、阻塞狀態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。阻塞的情況分三種:
-
等待阻塞:執行的執行緒執行wait()方法,JVM會把該執行緒放入等待池中。(wait會釋放持有的鎖)
-
同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池中。
-
其他阻塞:執行的執行緒執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。(注意,sleep是不會釋放持有的鎖)
5、死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。
start()方法和run()方法的區別?
-
start()方法會使得該執行緒開始執行,java虛擬機器會去呼叫該執行緒的run()方法。
-
通過呼叫Thread類的 start()方法來啟動一個執行緒,這時此執行緒處於就緒(可執行)狀態,並沒有執行,一旦得到cpu時間片,就開始執行run()方法,這裡方法 run()稱為執行緒體,它包含了要執行的這個執行緒的內容,run方法執行結束,此執行緒隨即終止。
-
run()方法只是類的一個普通方法而已,如果直接呼叫run方法,程式中依然只有主執行緒這一個執行緒,其程式執行路徑還是隻有一條,還是要順序執行,還是要等待run方法體執行完畢後才可繼續執行下面的程式碼,這樣就沒有達到多執行緒的目的。
Runnable介面和Callable介面的區別?
-
Runnable介面中的run()方法的返回值是void,它做的事情只是純粹地去執行run()方法中的程式碼而已。
-
Callable介面中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合可以用來獲取非同步執行的結果。
-
這其實是很有用的一個特性,因為多執行緒相比單執行緒更難、更復雜的一個重要原因就是因為多執行緒充滿著未知性,某條執行緒是否執行了?某條執行緒執行了多久?某條執行緒執行的時候我們期望的資料是否已經賦值完畢?無法得知,我們能做的只是等待這條多執行緒的任務執行完畢而已。而Callable + Future/FutureTask卻可以獲取多執行緒執行的結果,可以在等待時間太長沒獲取到需要的資料的情況下取消該執行緒的任務,真的是非常有用。
volatile關鍵字
volatile基本介紹:volatile可以看成是synchronized的一種輕量級的實現,但volatile並不能完全代替synchronized,volatile有synchronized可見性的特性,但沒有synchronized原子性的特性。可見性即用volatile關鍵字修飾的成員變量表明該變數不存在工作執行緒的副本,執行緒每次直接都從主記憶體中讀取,每次讀取的都是最新的值,這也就保證了變數對其他執行緒的可見性。另外,使用volatile還能確保變數不能被重排序,保證了有序性。
當一個變數定義為volatile之後,它將具備兩種特性:
-
①保證此變數對所有執行緒的可見性:當一條執行緒修改了這個變數的值,新值對於其他執行緒可以說是可以立即得知的。Java記憶體模型規定了所有的變數都儲存在主記憶體,每條執行緒還有自己的工作記憶體,執行緒的工作記憶體儲存了該執行緒使用到的變數在主記憶體的副本拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀取主記憶體中的變數。
-
②禁止指令重排序優化:
volatile boolean isOK = false; //假設以下程式碼線上程A執行 A.init(); isOK=true; //假設以下程式碼線上程B執行 while(!isOK){ sleep(); } B.init();
A執行緒在初始化的時候,B執行緒處於睡眠狀態,等待A執行緒完成初始化的時候才能夠進行自己的初始化。這裡的先後關係依賴於isOK這個變數。如果沒有volatile修飾isOK這個變數,那麼isOK的賦值就可能出現在A.init()之前(指令重排序,Java虛擬機器的一種優化措施),此時A沒有初始化,而B的初始化就破壞了它們之前形成的那種依賴關係,可能就會出錯。
volatile使用場景:
如果正確使用volatile的話,必須依賴下以下種條件:
-
對變數的寫操作不依賴當前變數的值。
-
該變數沒有包含在其他變數的不變式中。
在以下兩種情況下都必須使用volatile:
-
狀態的改變。
-
讀多寫少的情況。
什麼是執行緒安全?
如果你的程式碼在多執行緒下執行和在單執行緒下執行永遠都能獲得一樣的結果,那麼你的程式碼就是執行緒安全的。
執行緒安全的級別:
-
1)不可變:像String、Integer、Long這些,都是final型別的類,任何一個執行緒都改變不了它們的值,要改變除非新建立一個,因此這些不可變物件不需要任何同步手段就可以直接在多執行緒環境下使用。
-
2)絕對執行緒安全:不管執行時環境如何,呼叫者都不需要額外的同步措施。要做到這一點通常需要付出許多額外的代價,Java中標註自己是執行緒安全的類,實際上絕大多數都不是執行緒安全的,不過絕對執行緒安全的類,Java中也有,比方說CopyOnWriteArrayList、CopyOnWriteArraySet。
-
3)相對執行緒安全:相對執行緒安全也就是我們通常意義上所說的執行緒安全,像Vector這種,add、remove方法都是原子操作,不會被打斷,但也僅限於此,如果有個執行緒在遍歷某個Vector、有個執行緒同時在add這個Vector,99%的情況下都會出現ConcurrentModificationException,也就是fail-fast機制。
-
4)執行緒非安全:ArrayList、LinkedList、HashMap等都是執行緒非安全的類。
sleep方法和wait方法有什麼區別?
-
原理不同:sleep()方法是Thread類的靜態方法,是執行緒用來控制自身流程的,它會使此執行緒暫停執行一段時間,而把執行機會讓給其他執行緒,等到計時時間一到,此執行緒會自動甦醒。而wait()方法是Object類的方法,用於執行緒間的通訊,這個方法會使當前擁有該物件鎖的程序等待,直到其他執行緒用呼叫notify()或notifyAll()時才甦醒過來,開發人員也可以給它指定一個時間使其自動醒來。
-
對鎖的處理機制不同:由於sleep()方法的主要作用是讓執行緒暫停一段時間,時間一到則自動恢復,不涉及執行緒間的通訊,因此呼叫sleep()方法並不會釋放鎖。而wait()方法則不同,當呼叫wait()方法後,執行緒會釋放掉它所佔用的鎖,從而使執行緒所在物件中的其他synchronized資料可被別的執行緒使用。
-
使用區域不同:wait()方法必須放在同步控制方法或者同步語句塊中使用,而sleep方法則可以放在任何地方使用。
-
sleep()方法必須捕獲異常,而wait()、notify()、notifyAll()不需要捕獲異常。在sleep的過程中,有可能被其他物件呼叫它的interrupt(),產生InterruptedException異常。
-
由於sleep不會釋放鎖標誌,容易導致死鎖問題的發生,一般情況下,不推薦使用sleep()方法,而推薦使用wait()方法。
寫一個會導致死鎖的程式。
public class MyThread{ private static Object lock1 = new Object(); private static Object lock2 = new Object(); public static void main(String[] args) { new Thread(()->{ synchronized (lock1){ System.out.println("thread1 get lock1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2){ System.out.println("thread1 get lock2"); } System.out.println("thread1 end"); } }).start(); new Thread(()->{ synchronized (lock2){ System.out.println("thread2 get lock2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1){ System.out.println("thread2 get lock1"); } System.out.println("thread2 end"); } }).start(); } }
類載入過程
1、類載入過程:載入->連結(驗證+準備+解析)->初始化(使用前的準備)->使用->解除安裝
具體過程如下:
1)載入:首先通過一個類的全限定名來獲取此類的二進位制位元組流;其次將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;最後在java堆中生成一個代表這個類的Class物件,作為方法區這些資料的訪問入口。總的來說就是查詢並載入類的二進位制資料。
2)連結:
驗證:確保被載入類的正確性。
準備:為類的靜態變數分配記憶體,並將其初始化為預設值。
解析:把類中的符號引用轉換為直接引用。
-
符號引用即用字串符號的形式來表示引用,其實被引用的類、方法或者變數還沒有被載入到記憶體中。
-
直接引用則是有具體引用地址的指標,被引用的類、方法或者變數已經被載入到記憶體中。
直接引用可以是:
-
直接指向目標的指標。(個人理解為:指向物件,類變數和類方法的指標)
-
相對偏移量。(指向例項的變數,方法的指標)
-
一個間接定位到物件的控制代碼。
為什麼要使用符號引用?
符號引用要轉換成直接引用才有效,這也說明直接引用的效率要比符號引用高。那為什麼要用符號引用呢?這是因為類載入之前,javac會將原始碼編譯成.class檔案,這個時候javac是不知道被編譯的類中所引用的類、方法或者變數他們的引用地址在哪裡,所以只能用符號引用來表示,當然,符號引用是要遵循java虛擬機器規範的。
還有一種情況需要用符號引用,就例如前文舉得變數的符號引用的例子,是為了邏輯清晰和程式碼的可讀性。
3)為類的靜態變數賦予正確的初始值。
2、類的初始化
1)類什麼時候才被初始化:
-
建立類的例項,也就是new一個物件。
-
訪問某個類或介面的靜態變數,或者對該靜態變數賦值。
-
呼叫類的靜態方法。
-
反射(Class.forName(“com.lyj.load”))。
-
初始化一個類的子類(會首先初始化子類的父類)。
-
JVM啟動時標明的啟動類,即檔名和類名相同的那個類。
2)類的初始化順序
-
如果這個類還沒有被載入和連結,那先進行載入和連結
-
假如這個類存在直接父類,並且這個類還沒有被初始化(注意:在一個類載入器中,類只能初始化一次),那就初始化直接的父類(不適用於介面)
-
加入類中存在初始化語句(如static變數和static塊),那就依次執行這些初始化語句。
-
總的來說,初始化順序依次是:
(靜態變數、靜態初始化塊)–>(變數、初始化塊)–> 構造器;
如果有父類,則順序是:父類的靜態變數 –> 父類的靜態程式碼塊 –> 子類的靜態變數 –> 子類的靜態程式碼塊 –> 父類的非靜態變數 –> 父類的非靜態程式碼塊 –> 父類的構造方法 –> 子類的非靜態變數 –> 子類的非靜態程式碼塊 –> 子類的構造方法。
3、類的載入
類的載入指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個這個類的java.lang.Class物件,用來封裝類在方法區類的物件。如:
類的載入的最終產品是位於堆區中的Class物件。Class物件封裝了類在方法區內的資料結構,並且向Java程式設計師提供了訪問方法區內的資料結構的介面。載入類的方式有以下幾種:
-
從本地系統直接載入。
-
通過網路下載.class檔案。
-
從zip,jar等歸檔檔案中載入.class檔案。
-
從專有資料庫中提取.class檔案。
-
將Java原始檔動態編譯為.class檔案(伺服器)。
4、載入器
JVM的類載入是通過ClassLoader及其子類來完成的,類的層次關係和載入順序可以由下圖來描述:
載入器介紹:
1)BootstrapClassLoader(啟動類載入器):
負責載入JAVA_HOME中jre/lib/rt.jar裡所有的class,載入System.getProperty(“sun.boot.class.path”)所指定的路徑或jar。
2)ExtensionClassLoader(標準擴充套件類載入器):
負責載入java平臺中擴充套件功能的一些jar包,包括JAVAHOME中jre/lib/rt.jar裡所有的class,載入System.getProperty(“sun.boot.class.path”)所指定的路徑或jar。2)ExtensionClassLoader(標準擴充套件類載入器):負責載入java平臺中擴充套件功能的一些jar包,包括JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包。載System.getProperty(“java.ext.dirs”)所指定的路徑或jar。
3)AppClassLoader(系統類載入器):
負責載入classpath中指定的jar包及目錄中class。
4)CustomClassLoader(自定義載入器):
屬於應用程式根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現。
類載入器的順序
-
載入過程中會先檢查類是否被已載入,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已載入就視為已載入此類,保證此類只所有ClassLoader載入一次。而載入的順序是自頂向下,也就是由上層來逐層嘗試載入此類。
-
在載入類時,每個類載入器會將載入任務上交給其父,如果其父找不到,再由自己去載入。
-
Bootstrap Loader(啟動類載入器)是最頂級的類載入器了,其父載入器為null。
5、類載入器之雙親委派模型
-
所謂的雙親委派模型指除了啟動類載入器以外,其餘的載入器都有自己的父類載入器,而在工作的時候,如果一個類載入器收到載入請求,他不會馬上載入類,而是將這個請求向上傳遞給他的父載入器,看父載入器能不能載入這個類,載入的原則就是優先父載入器載入,如果父載入器載入不了,自己才能載入。
-
因為有了雙親委派模型的存在,類似Object類重複多次的問題就不會存在了,因為經過層層傳遞,載入請求最終都會被Bootstrap ClassLoader所響應。載入的Object物件也會只有一個。並且面對同一JVM程序多版本共存的問題,只要自定義一個不向上傳遞載入請求的載入器就好啦。
垃圾回收機制
Java記憶體區域劃分
我們先來看看Java的記憶體區域劃分情況,如下圖所示:
私有記憶體區的區域名和相應的特性如下表所示:
虛擬機器棧中的區域性變量表裡面存放了三個資訊:
-
各種基本資料型別(boolean、byte、char、short、int、float、long、double)。
-
物件引用(reference)。
-
returnAddress地址。
這個returnAddress和程式計數器有什麼區別?前者是指示JVM的指令執行到了哪一行,後者是指你的程式碼執行到哪一行。
共享記憶體區(接下來主要講jdk1.7)的區域名和相應的特性如下表所示:
哪些記憶體需要回收?
私有記憶體區伴隨著執行緒的產生而產生,一旦執行緒中止,私有記憶體區也會自動消除,因此我們在本文中討論的記憶體回收主要是針對共享記憶體區。
Java堆
新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java物件大都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC (但非絕對,在Parallel Scavenge收集器的收集策略裡就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。
新生代:剛剛新建的物件在Eden中,經歷一次Minor GC, Eden中的存活物件就被移動到第一塊survivor space S0,Eden被清空;等Eden區再滿了,就再觸發一次Minor GC, Eden和S0中的存活物件會被複制送入第二塊survivor space S1。S0和Eden被清空,然後下一輪S0與S1交換角色,如此迴圈往復。如果物件的複製次數達到16次,該物件就被送到老年代中。
為什麼新生代記憶體需要有兩個Sruvivor區:
先不去想為什麼有兩個Survivor區,第一個問題是,設定Survivor區的意義在哪裡?
如果沒有Survivor,Eden區每進行一次Minor GC,存活的物件就會被送到老年代。老年代很快被填滿,觸發Major GC(因為Major GC一般伴隨著Minor GC,也可以看做觸發了Full GC)。老年代的記憶體空間遠大於新生代,進行一次Full GC消耗的時間比Minor GC長得多。你也許會問,執行時間長有什麼壞處?頻發的Full GC消耗的時間是非常可觀的,這一點會影響大型程式的執行和響應速度,更不要說某些連線會因為超時發生連線錯誤了。那我們來想想在沒有Survivor的情況下,有沒有什麼解決辦法,可以避免上述情況:
顯而易見,沒有Survivor的話,上述兩種解決方案都不能從根本上解決問題。我們可以得到第一條結論:Survivor的存在意義,就是減少被送到老年代的物件,進而減少Full GC的發生,Survivor的預篩選保證,只有經歷16次Minor GC還能在新生代中存活的物件,才會被送到老年代。
設定兩個Survivor區最大的好處就是解決了碎片化,下面我們來分析一下。為什麼一個Survivor區不行?
第一部分中,我們知道了必須設定Survivor區。假設現在只有一個survivor區,我們來模擬一下流程:
剛剛新建的物件在Eden中,一旦Eden滿了,觸發一次Minor GC,Eden中的存活物件就會被移動到Survivor區。這樣繼續迴圈下去,下一次Eden滿了的時候,問題來了,此時進行Minor GC,Eden和Survivor各有一些存活物件,如果此時把Eden區的存活物件硬放到Survivor區,很明顯這兩部分物件所佔有的記憶體是不連續的,也就導致了記憶體碎片化。
那麼,順理成章的,應該建立兩塊Survivor區,剛剛新建的物件在Eden中,經歷一次Minor GC,Eden中的存活物件就會被移動到第一塊survivor space S0,Eden被清空;等Eden區再滿了,就再觸發一次Minor GC,Eden和S0中的存活物件又會被複制送入第二塊survivor space S1(這個過程非常重要,因為這種複製演算法保證了S1中來自S0和Eden兩部分的存活物件佔用連續的記憶體空間,避免了碎片化的發生)。S0和Eden被清空,然後下一輪S0與S1交換角色,如此迴圈往復。如果物件的複製次數達到16次,該物件就會被送到老年代中。
參考文章:https://blog.csdn.net/antony9118/article/details/51425581
老年代:如果某個物件經歷了幾次垃圾回收之後還存活,就會被存放到老年代中。老年代的空間一般比新生代大。
這個流程如下圖所示:
什麼時候回收?
Java並沒有給我們提供明確的程式碼來標註一塊記憶體並將其回收。或許你會說,我們可以將相關物件設為null或者用System.gc()。然而,後者將會嚴重影響程式碼的效能,因為每一次顯示呼叫system.gc()都會停止所有響應,去檢查記憶體中是否有可回收的物件,這會對程式的正常執行造成極大威脅。
另外,呼叫該方法並不能保障JVM立即進行垃圾回收,僅僅是通知JVM要進行垃圾回收了,具體回收與否完全由JVM決定。
生存還是死亡
可達性演算法:這個演算法的基本思路是通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。
二次標記:在可達性分析演算法中被判斷是物件不可達時不一定會被垃圾回收機制回收,因為要真正宣告一個物件的死亡,必須經歷兩次標記的過程。
如果發現物件不可達時,將會進行第一次標記,此時如果該物件呼叫了finalize()方法,那麼這個物件會被放置在一個叫F-Queue的佇列之中,如果在此佇列中該物件沒有成功拯救自己(拯救自己的方法是該物件有沒有被重新引用),
那麼GC就會對F-Queue佇列中的物件進行小規模的第二次標記,一旦被第二次標記的物件,將會被移除佇列並等待被GC回收,所以finalize()方法是物件逃脫死亡命運的最後一次機會。
在Java語言中,可作為GC Roots的物件包括下面幾種:
-
虛擬機器棧(棧幀中的本地變量表)中引用的物件。
-
方法區中靜態屬性引用的物件。
-
方法區中常量引用的物件。
-
本地方法棧中JNI(即一般說的Native方法)引用的物件。
GC的演算法
引用計數法(Reference Counting):
給物件新增一個引用計數器,每過一個引用計數器值就+1,少一個引用就-1。當它的引用變為0時,該物件就不能再被使用。它的實現簡單,但是不能解決互相迴圈引用的問題。
優點:
-
及時回收無效記憶體,實時性高。
-
垃圾回收過程中無需掛起。
-
沒有全域性掃描,效能高。
缺點:
-
物件建立時需要更新引用計數器,耗費一部分時間。
-
浪費CPU資源,計數器統計需要實時進行。
-
無法解決迴圈引用問題,即使物件無效仍不會被回收。
標記-清除(Mark-Sweep)演算法:
分為兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件(後續的垃圾回收演算法都是基於此演算法進行改進的)。
缺點:效率問題,標記和清除兩個過程的效率都不高;空間問題,會產生很多碎片。
複製演算法:
將可用記憶體按容量劃分為大小相等的兩塊,每次只用其中一塊。當這一塊用完了,就將還存活的物件複製到另外一塊上面,然後把原始空間全部回收。高效、簡單。
缺點:將記憶體縮小為原來的一半。
標記-整理(Mark-Compat)演算法
標記過程與標記-清除演算法過程一樣,但後面不是簡單的清除,而是讓所有存活的物件都向一端移動,然後直接清除掉端邊界以外的記憶體。
分代收集(Generational Collection)演算法
新生代中,每次垃圾收集時都有大批物件死去,只有少量存活,就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。
老年代中,其存活率較高、沒有額外空間對它進行分配擔保,就應該使用“標記-整理”或“標記-清除”演算法進行回收。
增量回收GC和並行回收GC這裡就不做具體介紹了,有興趣的朋友可以自行了解一下。
垃圾收集器
Serial收集器:單執行緒收集器,表示在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束。"Stop The World"。
ParNew收集器:實際就是Serial收集器的多執行緒版本。
-
併發(Parallel):指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍然處於等待狀態。
-
並行(Concurrent):指使用者執行緒與垃圾收集執行緒同時執行,使用者程式在繼續執行,而垃圾收集程式運行於另一個CPU上。
Parallel Scavenge收集器:該收集器比較關注吞吐量(Throughout)(CPU用於使用者程式碼的時間與CPU總消耗時間的比值),保證吞吐量在一個可控的範圍內。
CMS(Concurrent Mark Sweep)收集器:CMS收集器是一種以獲取最短回收停頓時間為目標的垃圾收集器,是基於“標記——清除”演算法實現的。
其回收過程主要分為四個步驟:
-
初始標記:標記一下GC Roots能直接關聯到的物件,速度很快。
-
併發標記:進行GC Roots Tracing的過程,也就是標記不可達的物件,相對耗時。
-
重新標記:修正併發標記期間因使用者程式繼續運作導致的標記變動,速度比較快。
-
併發清除:對標記的物件進行統一回收處理,比較耗時。
由於初始標記和重新標記速度比較快,其它工作執行緒停頓的時間幾乎可以忽略不計,所以CMS的記憶體回收過程是與使用者執行緒一起併發執行的。初始標記和重新標記兩個步驟需要Stop the world;併發標記和併發清除兩個步驟可與使用者執行緒併發執行。“Stop the world”意思是垃圾收集器在進行垃圾回收時,會暫停其它所有工作執行緒,直到垃圾收集結束為止。
CMS的缺點:
-
對CPU資源非常敏感;也就是說當CMS開啟垃圾收集執行緒進行垃圾回收時,會佔用部分使用者執行緒,如果在CPU資源緊張的情況下,會導致使用者程式的工作效率下降。
-
無法處理浮動垃圾導致又一次FULL GC的產生;由於CMS併發回收垃圾時使用者執行緒同時也在執行,伴隨使用者執行緒的執行自然會有新的垃圾產生,這部分垃圾出現在標記過程之後,CMS無法在當次收集過程中進行回收,只能在下一次GC時在進行清除。所以在CMS執行期間要確保記憶體中有足夠的預留空間用來存放使用者執行緒的產生的浮動垃圾,不允許像其它收集器一樣等到老年代區完全填滿了之後再進行收集;那麼當記憶體預留的空間不足時就會產生又一次的FULL GC來釋放記憶體空間,由於是通過Serial Old收集器進行老年代的垃圾收集,所以導致停頓的時間變長了(系統有一個閾值來觸發CMS收集器的啟動,這個閾值不允許太高,太高反而導致效能降低)。
-
標記——清除演算法會產生記憶體碎片;如果產生過多的記憶體碎片時,當系統虛擬機器想要再分配大物件時,會找不到一塊足夠大的連續記憶體空間進行儲存,不得不又一次觸發FULL GC。
G1(Garbage First)收集器:G1收集器是一款成熟的商用的垃圾收集器,是基於“標記——整理”演算法實現的。
其回收過程主要分為四個步驟:
-
初始標記:標記一下GC Roots能直接關聯到的物件,速度很快。
-
併發標記:進行GC Roots Tracing的過程,也就是標記不可達的物件,相對耗時。
-
最終標記:修正併發標記期間因使用者程式繼續運作導致的標記變動,速度比較快。
-
篩選回收:首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃。
G1收集器的特點:
-
併發與並行:機型垃圾收集時可以與使用者執行緒併發執行。
-
分代收集:能根據物件的存活時間採取不同的收集演算法進行垃圾回收。
-
不會產生記憶體碎片:基於標記——整理演算法和複製演算法保證不會產生記憶體空間碎片。
-
可預測的停頓:G1除了追求低停頓時間外,還能建立可預測的停頓時間模型,便於使用者的實時監控。
CMS收集器與G1收集器的區別:
-
CMS採用標記——清除演算法會產生空間碎片,G1採用標記——整理演算法不會產生空間碎片。
-
G1可以建立可預測的停頓時間模型,而CMS則不能。
JDK 1.8 JVM的變化
1、為什麼取消方法區
-
它在啟動時固定大小,很難進行調優,並且FullGC時會移動類元資訊。
-
類及方法的資訊等比較難確定大小,因此對永久代的大小指定比較困難。
-
在某些場景下,如果動態載入類過多,容易造成Perm區的OOM。
-
字串存在方法區中,容易出現效能問題和記憶體溢位。
-
永久代GC垃圾回收效率偏低。
2、JDK 1.8裡Perm區中的所有內容中字串常量移至堆記憶體,其他內容如類元資訊、欄位、靜態屬性、方法、常量等都移動到元空間內。
3、元空間
元空間(MetaSpace)不在堆記憶體上,而是直接佔用的本地記憶體。因此元空間的大小僅受本地記憶體限制
也可通過引數來設定元空間的大小:
-
-XX:MetaSpaceSize 初始元空間大小
-
-XX:MaxMetaSpaceSize 最大元空間大小
除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性:
-XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少為分配空間所導致的垃圾收集。
-XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少為釋放空間所導致的垃圾收集。
元空間的特點:
-
每個載入器有專門的儲存空間。
-
不會單獨回收某個類。
-
元空間裡的物件的位置是固定的。
-
如果發現某個載入器不再存貨了,會把相關的空間整個回收。
效能優化:
-
減少new物件。每次new物件之後,都要開闢新的記憶體空間。這些物件不被引用之後,還要回收掉。因此,如果最大限度地合理重用物件,或者使用基本資料型別替代物件,都有助於節省記憶體。
-
多使用區域性變數,減少使用靜態變數。區域性變數被建立在棧中,存取速度快。靜態變數則是儲存在堆記憶體中。
-
避免使用finalize,該方法會給GC增添很大的負擔。
-
如果是單執行緒,儘量使用非多執行緒安全的,因為執行緒安全來自於同步機制,同步機制會降低效能。例如,單執行緒程式,能使用HashMap,就不要使用HashTabl。同理,儘量減少使用synchronized。
-
用移位符號替代乘除號。比如:a*8應該寫作a<<3。
-
對於經常反覆使用的物件使用快取。
-
儘量使用基本型別而不是包裝型別,儘量使用一維陣列而不是二維陣列。
-
儘量使用final修飾符,final表示不可修改,訪問效率高。
-
單執行緒下(或者是針對於區域性變數),字串儘量使用StringBuilder,比StringBuffer要快。
-
儘量使用StringBuffer來連線字串。這裡需要注意的是,StringBuffer的預設快取容量是16個字元,如果超過16,append方法呼叫私有的expandCapacity()方法,來保證足夠的快取容量。因此,如果可以預設StringBuffer的容量,避免append再去擴充套件容量。
java自動裝箱拆箱總結
當基本型別包裝類與基本型別值進行==運算時,包裝類會自動拆箱。即比較的是基本型別值。
具體實現上,是呼叫了Integer.intValue()方法實現拆箱。
int a = 1; Integer b = 1; Integer c = new Integer(1); System.out.println(a == b); //true System.out.println(a == c); //true System.out.println(c == b); //false Integer a = 1; 會呼叫這個 Integer a = Integer.valueOf(1); Integer已經預設建立了數值【-128到127】的Integer常量池 Integer a = -128; Integer b = -128; System.out.println(a == b); //true Integer a = 128; Integer b = 128; System.out.println(a == b); //false Java的數學計算是在記憶體棧裡操作的 c1 + c2 會進行拆箱,比較還是基本型別 int a = 0; Integer b1 = 1000; Integer c1 = new Integer(1000); Integer b2 = 0; Integer c2 = new Integer(0); System.out.println(b1 == b1 + b2); //true System.out.println(c1 == c1 + c2); //true System.out.println(b1 == b1 + a); //true System.out.println(c1 == c1 + a); //true
以上這些,答案總結並非標準,僅供參考,如果有錯誤或者更好的見解,歡迎留言討論,往期公眾號整理的一些面試題看這裡:Java面試題內容聚合
&n