轉:Java面試題解析(事務+緩存+數據庫+多線程+JVM)

20000字Java面試題解析(事務+緩存+數據庫+多線程+JVM)

前言

題目答案總結並不是標準,僅供參考,若是有錯誤或者更好的看法,歡迎留言討論!java

事務

一、什麼是事務?事務的特性(ACID)mysql

什麼是事務:事務是程序中一系列嚴密的操做,全部操做執行必須成功完成,不然在每一個操做所作的更改將會被撤銷,這也是事務的原子性(要麼成功,要麼失敗)。程序員

事務特性分爲四個:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持續性(Durability)簡稱ACID。面試

一、原子性:事務是數據庫的邏輯工做單位,事務中包含的各操做要麼都作,要麼都不作。算法

二、一致性:事務執行的結果必須是使數據庫從一個一致性狀態變到另外一個一致性狀態。所以當數據庫只包含成功事務提交的結果時,就說數據庫處於一致性狀態。若是數據庫系統運行中發生故障,有些事務還沒有完成就被迫中斷,這些未完成事務對數據庫所作的修改有一部分已寫入物理數據庫,這時數據庫就處於一種不正確的狀態,或者說是不一致的狀態。sql

三、隔離性:一個事務的執行不能其它事務干擾。即一個事務內部的操做及使用的數據對其它併發事務是隔離的,併發執行的各個事務之間不能互相干擾。數據庫

四、持久性:也稱永久性,指一個事務一旦提交,它對數據庫中的數據的改變就應該是永久性的。接下來的其它操做或故障不該該對其執行結果有任何影響。json


二、事務的隔離級別有幾種,最經常使用的隔離級別是哪兩種?數組

併發過程當中會出現的問題:緩存

  • 丟失更新:是不可重複讀的特殊狀況。若是兩個事物都讀取同一行,而後兩個都進行寫操做,並提交,第一個事物所作的改變就會丟失。
  • 髒讀:一個事務讀取到另外一個事務未提交的更新數據。
  • 幻讀也叫虛讀:一個事務執行兩次查詢,第二次結果集包含第一次中沒有或某些行已經被刪除的數據,形成兩次結果不一致,只是另外一個事務在這兩次查詢中間插入或刪除了數據形成的。
  • 不可重複讀:一個事務兩次讀取同一行的數據,結果獲得不一樣狀態的結果,中間正好另外一個事務更新了該數據,兩次結果相異,不可被信任。

事務的隔離級別有4種:

一、未提交讀(Read uncommitted)

  • 定義:就是一個事務讀取到其餘事務未提交的數據,是級別最低的隔離機制。
  • 缺點:會產生髒讀、不可重複讀、幻讀。

二、提交讀(Read committed)

  • 定義:就是一個事務讀取到其餘事務提交後的數據。Oracle默認隔離級別。
  • 缺點:會產生不可重複讀、幻讀。

三、可重複讀(Repeatable read)

  • 定義:就是一個事務對同一份數據讀取到的相同,不在意其餘事務對數據的修改。MySQL默認的隔離級別。
  • 缺點:會產生幻讀。

四、串行化(Serializable)

  • 定義:事務串行化執行,隔離級別最高,犧牲了系統的併發性。
  • 缺點:能夠解決併發事務的全部問題。可是效率地下,消耗數據庫性能,通常不使用。

緩存

三、分佈式緩存的典型應用場景?

  • 頁面緩存,用來緩存Web頁面的內容片斷,包括HTML、CSS 和圖片等,多應用於社交網站等。
  • 應用對象緩存,緩存系統做爲ORM框架的二級緩存對外提供服務,目的是減輕數據庫的負載壓力,加速應用訪問。
  • 狀態緩存,緩存包括Session會話狀態及應用橫向擴展時的狀態數據等,這類數據通常是難以恢復的,對可用性要求較高,多應用於高可用集羣。
  • 並行處理,一般涉及大量中間計算結果須要共享。
  • 事件處理,分佈式緩存提供了針對事件流的連續查詢(continuous query)處理技術,知足實時性需求。
  • 極限事務處理,分佈式緩存爲事務型應用提供高吞吐率、低延時的解決方案,支持高併發事務請求處理,多應用於鐵路、金融服務和電信等領域。

數據庫

四、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或其餘結構化查詢的支持就比較差。

五、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代價高。

六、如何優化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
  • 字段長度固定表查詢會更快
  • 將數據庫大表按照時間或者一些標誌拆分紅小表
  • 水平拆分:將記錄散列到不一樣的表中,每次從分表查詢
  • 垂直拆分:將表中的大字段單獨拆分到另外一張表,造成一對一的關係

七、爲何任何查詢都不要使用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包裝器。

線程的生命週期

一、新建狀態(New):新建立了一個線程對象。

二、就緒狀態(Runnable):線程對象建立後,其餘線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。

三、運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。

四、阻塞狀態(Blocked):阻塞狀態是線程由於某種緣由放棄CPU使用權,暫時中止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的狀況分三種:

  • 等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。(wait會釋放持有的鎖)
  • 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。
  • 其餘阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程從新轉入就緒狀態。(注意,sleep是不會釋放持有的鎖)

五、死亡狀態(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)加載:首先經過一個類的全限定名來獲取此類的二進制字節流;其次將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;最後在java堆中生成一個表明這個類的Class對象,做爲方法區這些數據的訪問入口。總的來講就是查找並加載類的二進制數據。

2)連接:

驗證:確保被加載類的正確性。

準備:爲類的靜態變量分配內存,並將其初始化爲默認值。

解析:把類中的符號引用轉換爲直接引用。

  • 符號引用即用字符串符號的形式來表示引用,其實被引用的類、方法或者變量尚未被加載到內存中。
  • 直接引用則是有具體引用地址的指針,被引用的類、方法或者變量已經被加載到內存中。

直接引用能夠是:

  • 直接指向目標的指針。(我的理解爲:指向對象,類變量和類方法的指針)
  • 相對偏移量。(指向實例的變量,方法的指針)
  • 一個間接定位到對象的句柄。

爲何要使用符號引用?

符號引用要轉換成直接引用纔有效,這也說明直接引用的效率要比符號引用高。那爲何要用符號引用呢?這是由於類加載以前,javac會將源代碼編譯成.class文件,這個時候javac是不知道被編譯的類中所引用的類、方法或者變量他們的引用地址在哪裏,因此只能用符號引用來表示,固然,符號引用是要遵循java虛擬機規範的。

還有一種狀況須要用符號引用,就例如前文舉得變量的符號引用的例子,是爲了邏輯清晰和代碼的可讀性。

3)爲類的靜態變量賦予正確的初始值。

二、類的初始化

1)類何時才被初始化:

  • 建立類的實例,也就是new一個對象。
  • 訪問某個類或接口的靜態變量,或者對該靜態變量賦值。
  • 調用類的靜態方法。
  • 反射(Class.forName(「com.lyj.load」))。
  • 初始化一個類的子類(會首先初始化子類的父類)。
  • JVM啓動時標明的啓動類,即文件名和類名相同的那個類。

2)類的初始化順序

  • 若是這個類尚未被加載和連接,那先進行加載和連接
  • 假如這個類存在直接父類,而且這個類尚未被初始化(注意:在一個類加載器中,類只能初始化一次),那就初始化直接的父類(不適用於接口)
  • 加入類中存在初始化語句(如static變量和static塊),那就依次執行這些初始化語句。
  • 總的來講,初始化順序依次是:
  • (靜態變量、靜態初始化塊)–>(變量、初始化塊)–> 構造器;
  • 若是有父類,則順序是:父類的靜態變量 –> 父類的靜態代碼塊 –> 子類的靜態變量 –> 子類的靜態代碼塊 –> 父類的非靜態變量 –> 父類的非靜態代碼塊 –> 父類的構造方法 –> 子類的非靜態變量 –> 子類的非靜態代碼塊 –> 子類的構造方法。

三、類的加載

類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,而後在堆區建立一個這個類的java.lang.Class對象,用來封裝類在方法區類的對象。如:

類的加載的最終產品是位於堆區中的Class對象。Class對象封裝了類在方法區內的數據結構,而且向Java程序員提供了訪問方法區內的數據結構的接口。加載類的方式有如下幾種:

  • 從本地系統直接加載。
  • 經過網絡下載.class文件。
  • 從zip,jar等歸檔文件中加載.class文件。
  • 從專有數據庫中提取.class文件。
  • 將Java源文件動態編譯爲.class文件(服務器)。

四、加載器

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。

五、類加載器之雙親委派模型

  • 所謂的雙親委派模型指除了啓動類加載器之外,其他的加載器都有本身的父類加載器,而在工做的時候,若是一個類加載器收到加載請求,他不會立刻加載類,而是將這個請求向上傳遞給他的父加載器,看父加載器能不能加載這個類,加載的原則就是優先父加載器加載,若是父加載器加載不了,本身才能加載。
  • 由於有了雙親委派模型的存在,相似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次,該對象就會被送到老年代中。

老年代:若是某個對象經歷了幾回垃圾回收以後還存活,就會被存放到老年代中。老年代的空間通常比新生代大。

這個流程以下圖所示:

何時回收?

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的變化

一、爲何取消方法區

  • 它在啓動時固定大小,很難進行調優,而且FullGC時會移動類元信息。
  • 類及方法的信息等比較難肯定大小,所以對永久代的大小指定比較困難。
  • 在某些場景下,若是動態加載類過多,容易形成Perm區的OOM。
  • 字符串存在方法區中,容易出現性能問題和內存溢出。
  • 永久代GC垃圾回收效率偏低。

二、JDK 1.8裏Perm區中的全部內容中字符串常量移至堆內存,其餘內容如類元信息、字段、靜態屬性、方法、常量等都移動到元空間內。

三、元空間

元空間(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

複製代碼
相關文章
相關標籤/搜索