最全的JAVA知識彙總(附講解和思惟導圖)

微信公衆號:內核小王子 關注可瞭解更多關於數據庫,JVM內核相關的知識; 若是你有任何疑問也能夠加我pigpdong[^1]java

jvm 一行代碼是怎麼運行的

首先,java代碼會被編譯成字節碼,字節碼就是java虛擬機定義的一種編碼格式,須要java虛擬機纔可以解析,java虛擬機須要將字節碼轉換成機器碼才能在cpu上執行。 咱們能夠用硬件實現虛擬機,這樣雖然能夠提升效率可是就沒有了一次編譯處處運行的特性了,因此通常在各個平臺上用軟件來實現,目前的虛擬機還提供了一套運行環境來進行垃圾回收,數組越界檢查,權限校驗等。虛擬機通常將一行字節碼解釋成機器碼而後執行,稱爲解釋執行,也能夠將一個方法內的全部字節碼解釋成機器碼以後在執行,前者執行效率低,後者會致使啓動時間慢,通常根據二八法則,將百分之20的熱點代碼進行即時編譯。JIT編譯的機器碼存放在一個叫codecache的地方,這塊內存屬於堆外內存,若是這塊內存不夠了,那麼JIT編譯器將再也不進行即時編譯,可能致使程序運行變慢。linux

jvm如何加載一個類

第一步:加載,雙親委派:啓動類加載器(jre/lib),系統擴展類加載器(ext/lib),應用類加載器(classpath),前者爲c++編寫,因此係統加載器的parent爲空,後面兩個類加載器都是經過啓動類加載器加載完成後才能使用。加載的過程就是查找字節流,能夠經過網絡,也能夠本身在代碼生成,也能夠來源一個jar包。另外,同一個類,被不一樣的類加載器加載,那麼他們將不是同一個類,java中經過類加載器和類的名稱來界定惟一,因此咱們能夠在一個應用成存在多個同名的類的不一樣實現。c++

第二步:連接:(驗證,準備,解析) 驗證主要是校驗字節碼是否符合約束條件,通常在字節碼注入的時候關注的比較多。準備:給靜態字段分配內存,可是不會初始化,解析主要是爲了將符號引用轉換爲實際引用,可能會觸發方法中引用的類的加載。算法

第三步:初始化,若是賦值的靜態變量是基礎類型或者字符串而且是final的話,該字段將被標記爲常量池字段,另外靜態變量的賦值和靜態代碼塊,將被放在一個叫cinit的方法內被執行,爲了保證cinit方法只會被執行一次,這個方法會加鎖,咱們通常實現單例模式的時候爲保證線程安全,會利用類的初始化上的鎖。 初始化只有在特定條件下才會被觸發,例如new 一個對象,反射被調用,靜態方法被調用等。數據庫

java對象的內存佈局

java中每個非基本類型的對象,都會有一個對象頭,對象頭中有64位做爲標記字段,存儲對象的哈希碼,gc信息,鎖信息,另外64位存儲class對象的引用指針,若是開啓指針壓縮的話,該指針只須要佔用32位字節。設計模式

Java對象中的字段,會進行重排序,主要爲了保證內存對齊,使其佔用的空間正好是8的倍數,不足8的倍數會進行填充,因此想知道一個屬性相對對象其始地址的偏移量須要經過unsafe裏的fieldOffset方法,內存對齊也爲了不讓一個屬性存放在兩個緩存行中,disruptor中爲了保證一個緩存行只能被一個屬性佔用,也會用空對象進行填充,由於若是和其餘對象公用一個緩存行,其餘對象的失效會將整個緩存行失效,影響性能開銷,jdk8中引入了contended註解來讓一個屬性獨佔一個緩存行,內部也是進行填充,用空間換取時間,如何計算一個對象佔用多少內存,若是不精確的話就進行遍歷而後加上對象頭,這種狀況沒辦法考慮重排序和填充,若是精確的話只能經過javaagent的instrument工具。數組

反射的原理

反射真的慢麼?緩存

首先class.forname和class.getmethod 第一個是一個native方法,第二個會遍歷本身和父類中的方法,並返回方法的一個拷貝,因此這兩個方法性能都很差,建議在應用層進行緩存。 而反射的具體調用有兩種方式,一種是調用本地native方法,一種是經過動態字節碼生成一個類來調用,默認採用第一種,當被調用15次以後,採用第二種動態字節碼方式,由於生成字節碼也耗時,若是隻調用幾回不必,而第一種方式因爲須要在java和c++之間切換,native 方法自己性能消耗嚴重,因此對於熱點代碼頻繁調用反射的話,性能並不會不好。安全

屬性的反射,採用unsafe類中setvalue來實現,須要傳入該屬性相對於對象其始地址的偏移量,也就是直接操做內存。其實就是根據這個屬性在內存中的起始地址和類型來讀取一個字段的值,在LockSupport類中,park和unpark方法,設置誰將線程掛起的時候也有用到這種方式。微信

動態代理

java自己的動態代理也是經過字節碼實現的

Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

工具類中須要提供 類加載器,須要實現的接口,攔截器的實現,也就是須要在InvocationHandler中調用原方法並作加強處理。而且這個實現,必定會被放到新生成的動態代理類裏。

生成動態代理類的步驟:先經過聲明的接口生成一個byte數組,這個數組就是字節流,經過傳入的類加載進行加載生成一個class對象,這個class 裏面有個構造方法接收一個參數,這個參數就是InvocationHandler,經過這個構造方法的反射獲取一個實例類,在這個class裏面,接口的實現中會調用InvocationHandler,而這個class對象爲了防止生成太多又沒有被回收,因此是一個弱引用對象。

jvm的內存模型

併發問題的根源:可見性,原子性,亂序執行

java內存模型定義了一些規則來禁止cpu緩存和編譯器優化,happen-before用來描述兩個操做的內存的可見性,有如下6條

  • 1.程序的順序執行,前一個語句對後一個語句可見 (當兩個語句沒有依賴的狀況下仍是能夠亂序執行)
  • 2.volatile變量的寫對另外一個線程的讀可見
  • 3.happen-before 具備傳遞性
  • 4.一個線程對鎖的釋放對另一個線程的獲取鎖可見 (也就是一個線程在釋放鎖以前對共享變量的操做,另一個線程獲取鎖後會看的到)
  • 5.線程a調用了線程b的start()方法,那麼線程a在調用start方法以前的操做,對線程b內的run()方法可見
  • 6.線程a調用了線程b的join方法,那麼線程b裏的全部操做,將對線程a調用join以後的操做可見。

jvm的垃圾回收

兩種實現:引用計數和可達性分析,引用計數會出現循環引用的問題,目前通常採用可達性分析。

爲了保證程序運行線程和垃圾回收線程不會發生併發影響,jvm採用安全點機制來實現stop the world,也就是當垃圾收集線程發起stop the world請求後,工做線程開始進行安全點檢測,只有當全部線程都進入安全點以後,垃圾收集線程纔開始工做,在垃圾收集線程工做過程當中,工做線程每執行一行代碼都會進行安全點檢測,若是這行代碼安全就繼續執行,若是這行代碼不安全就將該線程掛起,這樣能夠保證垃圾收集線程運行過程當中,工做線程也能夠繼續執行。

安全點:例如阻塞線程確定是安全點,運行的jni線程若是不訪問java對象也是安全的,若是線程正在編譯生成機器碼那他也是安全的,Java虛擬機在有垃圾回收線程執行期間,每執行一個字節碼都會進行安全檢測。

基礎垃圾收集算法:清除算法會形成垃圾碎片,清除後整理壓縮浪費cpu耗時,複製算法浪費內存。

基礎假設:大部分的java對象只存活了一小段時間,只有少部分java對象存活好久。新建的對象放到新生代,當通過屢次垃圾回收還存在的,就把它移動到老年代。針對不一樣的區域採用不一樣的算法。由於新生代的對象存活週期很短,常常須要垃圾回收,因此須要採用速度最快的算法,也就是複製,因此新生代會分紅兩塊。一塊eden區,兩塊大小相同的survivor區。

新的對象默認在eden區進行分配,因爲堆空間是共享的,因此分配內存須要加鎖同步,否則會出現兩個對象指向同一塊內存,爲了不頻繁的加鎖,一個線程能夠申請一塊連續內存,後續內存的分配就在這裏進行,這個方案稱爲tlab。tlab裏面維護兩個指針,一個是當前空餘內存起始位置,另一個tail指向尾巴申請的內存結束位置,分配內存的時候只須要進行指針加法並判斷是否大於tail,若是超過則須要從新申請tlab。

若是eden區滿了則會進行一次minorGc ,將eden區的存活對象和from區的對象移動到to區,而後交換from和to的指針。

垃圾收集器的分類:針對的區域,老年代仍是新生代,串行仍是並行,採用的算法分類複製仍是標記整理

g1 基於可控的停頓時間,增長吞吐量,取代cms g1將內存分爲多個塊,每一個塊均可能是 eden survivor old 三種之一 首先清除全是垃圾的快 這樣能夠快速釋放內存。

若是發現JVM常常進行full gc 怎麼排查?

不停的進行full gc表示可能老年代對象佔有大小超過閾值,而且通過屢次full gc仍是沒有降到閾值如下,因此猜想可能老年代裏有大量的數據存活了好久,多是出現了內存泄露,也多是緩存了大量的數據一直沒有釋放,咱們能夠用jmap將gc日誌dump下來,分析下哪些對象的實例個數不少,以及哪些對象佔用空間最多,而後結合代碼進行分析。

併發和鎖

線程的狀態機

線程池參數:核心線程數,最大線程數,線程工廠,線程空閒時間,任務隊列,拒絕策略 先建立核心線程,以後放入任務隊列,任務隊列滿了建立線程直到最大線程數,在超過最大線程數就會拒絕,線程空閒後超過核心線程數的會釋放,核心線程也能夠經過配置來釋放,針對那些一天只跑一個任務的狀況。newCachedThreadPool線程池會致使建立大量的線程,由於用了同步隊列。

synchronized

同步塊會有一個monitorenter和多個monitorexist ,重量級鎖是經過linux內核pthread裏的互斥鎖實現的,包含一個waitset和一個阻塞隊列。 自旋鎖,會不停嘗試獲取鎖,他會致使其餘阻塞的線程沒辦法獲取到鎖,因此他是不公平鎖,而輕量級鎖和偏向鎖,均是在當前對象的對象頭裏作標記,用cas方法設置該標記,主要用於多線程在不一樣時間點獲取鎖,以及單線程獲取鎖的狀況,從而避免重量級鎖的開銷,鎖的升級和降級也須要在安全點進行。

  • reentrantlock相對synchronized的優點:能夠控制公平仍是非公平,帶超時,響應中斷。
  • CyclicBarrier 多個線程相互等待,只有全部線程所有完成後才通知一塊兒繼續 (調用await 直到全部線程都調用await才一塊兒恢復繼續執行)
  • countdownlatch 一個線程等待,其餘線程執行完後它才能繼續。(調用await後被阻塞,直到其餘地方調用countdown()將state減到1 這個地方的其餘能夠是其餘多個線程也能夠其餘單個任務)
  • semaphore 同一個時刻只運行n個線程,限制同時工做的線程數目。
  • 阻塞隊列通常用兩個鎖,以及對應的條件鎖來實現,默認爲INTEGER.MAX爲容量,而同步隊列沒有容量,優先級隊列內部用紅黑樹來實現。

若是要頻繁讀取和插入建議用concurrenthashmap 若是頻繁修改建議用 concurrentskiplistmap,copyonwrite適合讀多寫少,寫的時候進行拷貝,並加鎖。讀不加鎖,可能讀取到正在修改的舊值。concurrent系列實際上都是弱一致性,而其餘的都是fail-fast,拋出ConcurrentModificationException,而弱一致性容許修改的時候還能夠遍歷。例如concurrent類的size方法可能不是百分百準確。

AQS 的設計,用一個state來表示狀態,一個先進先出的隊列,來維護正在等待的線程,提供了acquire和release來獲取和釋放鎖,鎖,條件,信號量,其餘併發工具都是基於aqs實現。

字符串

字符串能夠經過intern()方法緩存起來,放到永久代,通常一個字符串申明的時候會檢查常量區是否存在,若是存在直接返回其地址,字符串是final的,他的hashcode算法採用31進制相加,字符串的拼接須要建立一個新的字符串,通常使用stringbuilder。String s1 = "abc"; String s2 = "abc"; String s1 = new String("abc"); s1和s2多是相等的,由於都指向常量池。

集合

  • vector 線程安全,arraylist 實現 randomaccess 經過數組實現支持隨機訪問,linkedlist 雙向鏈表能夠支持快速的插入和刪除。
  • treeset 依賴於 treemap 採用紅黑樹實現,能夠支持順序訪問,可是插入和刪除複雜度爲 log(n)
  • hashset 依賴於 hashmap 採用哈希算法實現,能夠支持常數級別的訪問,可是不能保證有序
  • linkedhashset 在hashset的節點上加了一個雙向鏈表,支持按照訪問和插入順序進行訪問
  • hashtable早版本實現,線程安全 不支持空鍵。
  • hashmap:根據key的hashcode的低位進行位運算,由於高位衝突機率較高,根據數組長度計算某個key對應數組位置,相似求餘算法,在put的時候會進行初始化或者擴容,當元素個數超過 數組的長度乘以負載因子的時候進行擴容,當鏈表長度超過8會進行樹化,數組的長度是2的多少次方,主要方便位運算,另外一個好處是擴容的時候遷移數據只須要遷移一半。當要放 15個元素的時候,通常數組初始化的長度爲 15/0.75= 20 而後對應的2的多少次方,那麼數組初始化長度爲 32.
  • ConcurrentHashMap 內部維護了一個segment數組,這個segment繼承自reentrantlock,他自己是一個hashmap,segment數組的長度也就是併發度,通常爲16. hashentry內部的value字段爲volatile來保證可見性.size()方法須要獲取全部的segment的鎖,而jdk8的size()方法用一個數組存儲每一個segment對應的長度。

io

輸入輸出流的數據源有 文件流,字節數組流,對象流 ,管道。帶緩存的輸入流,須要執行flush,reader和writer是字符流,須要根據字節流封裝。

bytebuffer裏面有position,capcity,limit 能夠經過flip重置換,通常先寫入以後flip後在從頭開始讀。

文件拷貝 若是用一個輸入流和一個輸出流效率過低,能夠用transfer方法,這種模式不用到用戶空間,直接在內核進行拷貝。

一個線程一個鏈接針對阻塞模式來講效率很高,可是吞吐量起不來,由於沒辦法開那麼多線程,並且線程切換也有開銷,通常用多路複用,基於事件驅動,一個線程去掃描監聽的鏈接中是否有就緒的事件,有的話交給工做線程進行讀寫。通常用這種方式實現C10K問題。

堆外內存(direct) 通常適合io頻繁而且長期佔用的內存,通常建議重複使用,只能經過Native Memory Tracking(NMT)來診斷,MappedByteBuffer能夠經過FileChannel.map來建立,能夠在讀文件的時候少一次內核的拷貝,直接將磁盤的地址映射到用戶空間,使用戶感受像操做本地內存同樣,只有當發生缺頁異常的時候纔會觸發去磁盤加載,一次只會加載要讀取的數據頁,例如rocketmq裏一次映射1g的文件,並經過在每一個數據頁寫1b的數據進行預熱,將整個1G的文件都加載到內存。

設計模式

  • 建立對象:工廠 構建 單例
  • 結構型: 門面 裝飾 適配器 代理
  • 行爲型:責任鏈 觀察者 模版
  • 封裝(隱藏內部實現) 繼承(代碼複用) 多態(方法的重寫和重載)
  • 設計原則:單一指責,開關原則,里氏替換,接口分離,依賴反轉

微信公衆號:內核小王子 關注可瞭解更多關於數據庫,JVM內核相關的知識; 若是你有任何疑問也能夠加我pigpdong[^1]

歷史文章:

JAVA和操做系統交互細節

經過MySQL存儲原理來分析排序和鎖

網絡內核之TCP是如何發送和接收消息的

相關文章
相關標籤/搜索