<<深入理解JVM>>筆記與一些體悟


這段時間這本書的第二遍已經看完了,但是很多地方模糊不清楚,總的來說,這邊書收獲很大,作為Java開發,這本書很有幫助,本來準備進軍多線程的,因此而耽擱一段時間,總之感覺還是把這本書一些章節吃透,再去多線程

第一章 走進Java(歷史與展望)

展望Java技術的未來:從目前國內形勢來看,go-lang在逐漸走上臺面,php也同時占有一席之地
PYPL排行榜也是一個關于編程語言流行度的參考指標,其榜單數據的排名均是根據榜單對象在 Google 上相關的搜索頻率進行統計排名,原始數據來自 Google Trends,也就是說某項語言或者某款 IDE 在 Google 上搜索頻率越高,表示它越受歡迎。上面這份排行是基于google搜索次數決定的



TIOBE編程社區索引是編程語言流行程度的一個指標。索引每月更新一次。評級是基于全球熟練工程師、課程和第三方供應商的數量。流行的搜索引擎,如谷歌,必應,雅虎!,維基百科,亞馬遜,YouTube和百度被用來計算收視率。需要注意的是,TIOBE索引并不是關于最好的編程語言,也不是大多數代碼都是用哪種語言編寫的。索引可用于檢查您的編程技能是否仍然是最新的,或者在開始構建新的軟件系統時,對應采用何種編程語言作出戰略決策

Java從當初的一次編寫到處運行 ,到未來的期望無語言傾向,感覺面臨著巨大的挑戰,畢竟現在還是天下第一
Java的優勢:
1.龐大的用戶群體。
2.穩定的語言,使得項目更正規,更容易形成大型體系的工程。
缺點:
1.泛型那里很不好用,Java采用了類型擦除方式。使得使用泛型會造成大量的自動拆箱、裝箱,使得泛型速度變慢。
2.啟動Java虛擬機的時候,還是太長了,不如一些動態類型語言來的開發效率高,對編程人員,用戶也友好。

正式進入本書:
一、無語言傾向
2018年4月,Oracle Labs新公開了一項黑科技:Graal VM,這是一個在Hotspot虛擬機基礎上增強而成的跨語言全棧虛擬機,可以作為“任何語言”的運行平臺,既包括Java、Scala、Groovy等基于Java虛擬機的語言,還包括C、C++、Rust等基于LLVM的語言,同時也支持,Javascript、Python和R語言等。Graal VM 可以無額外開銷地混合使用這些編程語言,支持不同語言中混用對方的接口和對象,也能夠支持這些語言使用已經編寫好地本地庫文件。它的基本工作原理:將這些語言的源代碼或者源代碼編譯后的中間格式(例如LLVM字節碼)通過解釋器轉換為能被Graal VM接受的中間表示,例如設計一個解釋器專門對LLVM輸出的字節碼進行轉換來支持C語言。(Truffle快速構建面向一種新語言的解釋器。)Graal VM才是真正意義上的與物理計算機相對應的高級語言虛擬機,理由是它與物理硬件的指令集一樣,做到了只與機器特性相關而不與某種高級語言特性相關。
Graal VM相比于Hotspot 主要差異在于即時編譯器,相比較起來互有勝負,但是Oracle Labs和美國大學里所做的最新即時編譯技術的研究全部都遷移到基于Graal VM之上進行了,令人期待。

二、新一代即時編譯器
Hotspot 虛擬機中含有兩個即時編譯器,分別是編譯耗時短但是輸出代碼優化程度較低的客戶端編譯器(C1)以及編譯耗時長但輸出代碼優化質量也更高的服務端編譯器(C2),通常他們會在分層編譯機制下與解釋器互相配合來共同構成Hotspot虛擬機的執行子系統。
Graal 編譯器(作為C2編譯器替代者),C2時間太長了,其作者都因為太復雜而不愿意維護,且用C++編寫而成。Graal編譯器本身就是Java語言寫成,且C2編譯器代碼可以輕松移植到Graal編譯器上,不成熟,需要通過-XX:+UnlockExperimentalVMOptions -XX:UseJVMCICompiler參數來開啟。

三、向native邁進
Java自身存在缺點,主要是近幾年在從大型單體應用架構向小型微服務架構發展的技術潮流之下,Java表現的不適應。(沒看過微服務所以詳細寫一下)在微服務架構視角下,應用拆分后,單個微服務不在需要面隊數十、數百GB乃至TB的內存,有了高可用的服務集群,也無需追求單個服務7*24小時運行,隨時中斷和更新;但是Java的啟動時間較長,需要時間才能到達最高性能,就和這些場景有點矛盾。在無服務架構下,矛盾會更大。
AppCDS 允許把加載解析的類型信息緩存起來,從而提升下次啟動速度。提前編譯能帶來的最大好處是Java虛擬機加載這些預編譯成二進制庫之后能直接調用,無需等待即時編譯器在運行是將其編譯成二進制機器碼,理論上,提前編譯可以減少即時編譯帶來的預熱時間,減少Java長期給人帶來的第一次運行慢的不良體驗。但是壞處也很明顯,必須為不同的硬件、操作系統去編譯對應的發行包;降低Java連接過程的動態性,必須要求加載的代碼在編譯期全部已知,而不能在運行期才確定否則只能舍棄以及編譯好的,退回即時編譯狀態。
SubStrate VM出現,一個極小的運行時環境,包括了獨立的異常處理、同步調度、線程管理、內存管理和JNI訪問,目標是代替Hotspot用來支持提前編譯后的程序運行,無需重復開啟Java虛擬機初始化過程,不能動態加載其他編譯器不可知的代碼和類庫。好處就是顯著降低內存占用和啟動時間,運行在Substrate VM上的小規模應用,其內存占用和啟動時間比Hotspot下降5-50倍。

四、Java語法糖持續變多,給編程人員提供良好的體驗
結束~
第一部分 自動內存管理 援引作者一句話Java與C++之間由內存動態分配和垃圾收集技術所圍成的高墻,墻外面的人想進去,墻里面的人卻想出來

第二章 Java內存區域與內存溢出異常

1. 運行時數據區域
共包含:方法區(線程公有)、Java堆(線程公有)、Java虛擬機棧(線程私有)、本地方法棧(線程私有)、程序計數器(線程私有)、運行時常量池(方法區一部分)、直接內存(不是虛擬機運行時數據區的一部分,也不是《Java虛擬機規范》中定義的區域。
1.1 程序計數器
1.字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條執行的字節碼指令,是程序控制流的指示器,線程恢復也需要依賴程序計數器。
2.Java虛擬機多線程通過線程切換、分配處理器執行時間的方式實現,一個確定的時刻一個處理器只會執行一條線程中的指令,因此每個線程各自擁有自己的程序計數器
3.如果執行Java方法則計數器記錄的是正在執行的虛擬機字節碼指令的地址,如果執行的是native方法,則計數器值為空
1.2 虛擬機棧
1.其線程私有、它的生命周期與線程相同;每個方法被執行時,Java虛擬機都會創建一個棧幀用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完畢,就對應一個棧幀在虛擬機棧中從入棧到出棧的過程。
2.Java內存區域,程序員最關心兩個區域堆(heap)和棧(stack),棧指的是虛擬機棧或者虛擬機棧中的局部變量表部分。
3.局部變量表存放了基本數據類型(boolean、byte、char、short、int、float、long、double)、引用類型(reference)和returnAddress類型。
4.局部變量表的存儲空間以局部變量槽slot表示,long 和 double都占用2個slot,其余占用一個,局部變量表所需的內存空間在編譯期就完成,在方法的運行時期不會改變局部變量表大小
5.如果線程請求的棧深度大于虛擬機所允許的深度,拋出StackOverFlow異常;如果Java虛擬機棧容量可以動態擴展、當棧擴展時無法申請到足夠的內存會拋出OutOfMemory異常。
1.3 本地方法棧
1.和虛擬機棧發揮的作用類似,區別就是虛擬機棧為執行Java方法服務,本地方法棧則為虛擬機棧用到的Native方法服務。
2.異常類同虛擬機棧
1.4 Java堆
1.堆中只存儲對象實例。
2.Java是垃圾收集器管理的內存區域,從分配內存的角度看,所有線程共享的Java堆可以劃分出多個線程私有的分配緩沖區,用來提升對象分配效率。Java堆的劃分只有一個目的就是,更好的分配內存,更好的回收內存
3.堆里面不要求物理上連續存儲,但邏輯上是連續的,對于大對象(典型的數組對象)很可能要求連續的內存空間。
4.如果在Java堆中沒有內存完成實例分配,并且堆無法再擴展時,拋出OutOfMemoryError(OOM)。
1.5 方法區
1.用于存儲已經被虛擬機加載類型信息、常量、靜態變量、即時編譯器編譯后的代碼緩存等數據。
2.JDK8完全廢除永久代,采用本地內存來實現方法區,使用元空間
3.這個區域內存回收的目標是:常量池的回收和對類型的卸載。(比較難實現)
4.如果方法區無法滿足新的內存分配需求時,拋出OOM。
1.5 運行時常量池
1.作為方法區的一部分,常量池表,用于存放編譯期生成的各種字面量與符號引用,這部分內容將在類加載后放在運行時常量池中。可能也會存儲直接引用
2.具備動態性,Java并不要求常量一定要編譯期才可以產生例如String類的intern()方法。
3.當常量池無法申請到內存時拋出OOM
1.6 直接內存
1.不是虛擬機運行時數據區的一部分,也不是《Java虛擬機規范》中定義的區域。
2.JDK1.4 新加入的NIO,引入了一種基于通道channel與緩沖區BUffer的I/O方式,使用Native函數直接分配堆外內存,通過存儲在Java堆里面的DirectByteBUffer對象作為這塊區域的引用進行操作,避免了Java堆和Native堆中來回復制數據。
3.本機直接內存分配不受到Java堆大小的限制,但收到總內存限制,一般服務器管理員配置虛擬機參數時,忽略直接內存,使得各內存區域總和大于物理內存限制,導致OOM異常。
2. 對象的一生
Hotspot虛擬機在Java堆中對象分配、布局和訪問的全過程。
2.1 對象的創建
1.當虛擬機遇到一條字節碼new指令時,首先去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,并且檢查到這個符號引用代表的類是否已經被加載、解析和初始化過,如果沒有先執行相應的類加載。
2.類加載檢查通過后,給對象分配內存:
(1)指針碰撞:假設Java堆中內存是絕對規整的,所有被用過的內存放一邊,空閑的內存放一邊,中間放一個指針作為分界點的指示器,那所分配內存就是僅僅把那個指針向空閑空間方向挪動一段與對象大小相等的距離。
(2)空閑列表:如果不規整,就必須維護一個空閑列表,記錄哪塊內存可以用,分配的時候從列表中找到足夠大的一塊空間分給對象實例,并更新記錄。
選擇哪種方式由Java堆是否規整來決定,而Java堆是否規整,又由所采用的垃圾收集器是否帶有空間壓縮整理決定。因此,使用Serial、Parnew等帶壓縮整理的收集器,采用指針碰撞;采用CMS基于清楚算法的收集器時,理論上采用空閑列表實現。
還需要考慮一個問題,對象創建是很頻繁的行為,僅僅修改一個指針所指向的位置,并發情況下并不是線程安全的,可能出現給A分配內存、指針沒修改,對象B又使用原來指針分配內存,解決方法:
(1)對分配內存空間進行同步處理——實際上虛擬機采用CAS配上失敗重試的方式保證操作的原子性
CAS看這里CAS
(2)另外一種是把內存分配的動作按照線程劃分在不同空間中進行,即每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩沖(TLAB),哪個線程要分配內存,就在哪個線程的本地緩沖區分配,只有本地緩沖區用完了,分配新的緩沖區才需要同步鎖定。
3.內存分配完成后,虛擬機將分配到的內存空間(但不包括對象頭)都初始化為0,這步操作保證了對象的實例字段在Java代碼中不賦初值就可以使用,使程序能訪問到這些字段的數據類型所對應的零值。
4.接下來,對對象進行必要的設置,例如這個對象是哪個類的實例,、如何才能找到類的元數據信息、對象的哈希碼、對象的GC年齡分帶,存儲在對象頭之中
5.構造函數,即CLass中的<init>()方法,按照程序員自己的意愿進行初始化,這樣一個對象才被完整構建出來
2.2 對象的內存布局
1.對象在堆內存中可以劃分為三個部分:對象頭、實例數據、對齊填充
2.對象頭:MarkWord 如:哈希碼、GC年齡分帶、鎖狀態標志、線程持有的鎖、偏向線程ID等,另外一部分就是類型指針,即對象指向它的類型數據的指針,虛擬機需要通過這個指針來確定該對象是哪個類的實例。如果對象是Java數組,還要存儲一塊這個對象多大,記錄長度。數組大小不確定,虛擬機無法通過元數據確定數組大小。
3.實例數據:是對象真正存儲的有效信息
4.對齊填充:因為Hotspot要求對象起始地址必須是8字節的整數倍,換句話說就是任何對象大小必須是8字節的整數倍,所以需要對齊填充。
2.3 對象的訪問定位
1.主流方式使用句柄和直接指針兩種
(1)使用句柄的化,Java堆中可能劃分一塊內存作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型實例數據各自具體的地址信息。
優勢:reference中存儲的是穩定的句柄地址,在對象移動(垃圾收集)的時候,只會改變句柄中實例數據指針,而reference本身不要修改。
(2)使用直接指針的化,Java堆中對象的內存布局就必須要考慮如何放置訪問類型數據的相關信息,reference中存儲的直接就是對象地址,如果訪問對象本身的化,就不需多一次間接訪問的開銷。
優勢:速度快,節省一次指針定位的時間開銷,(Hotspot)中就用直接指針。

3. 實戰:OutOfMemoryError異常

第三章 垃圾收集器與內存分配策略

1)垃圾回收出現的原因:前面一章,分析了,JVM哪些地方會出現OOM異常,這章介紹Java垃圾收集器為了避免內存溢出異常都做了哪些努力。
2)為什么學習垃圾回收?當排查各種內存溢出、內存泄漏問題時,當垃圾收集成為系統達到更高并發量的瓶頸時,我們就必須對這些“自動化技術”的技術實施必要的監控和調節。
3)哪些內存需要回收?程序計數器、虛擬機棧、本地方法棧隨線程而生,隨線程消滅,棧中的棧幀隨著方法的進入和退出有條不紊的執行著入棧和出棧操作。并且每一個棧幀分配多少內存是確定下來的,大體上是編譯器已知的,因此這幾個區域回收都具有確定性,當方法結束或者線程結束就不需要考慮太多問題。所以回收主要面向Java堆方法區

  1. 對象已死?
    1)引用計數法:對象中添加一個引用計數器,如果引用+1,引用失效-1,任何時刻引用為零的對象就是不可能再被使用的。但是有些問題無法解決,例如循環引用問題。
    2)可達性分析算法:用GC Roots作為根對象,根據引用關系向下搜索,對象不可達,則對象不在使用。固定作為GC Roots的對象包括以下幾種,
    1.在虛擬機棧(棧幀中的本地變量表)中引用的對象,比如各個線程被調用的方法堆棧中用到的參數、局部變量、臨時變量。
    2.在方法區中類靜態屬性引用的對象,比如Java類的引用類型靜態變量。
    3.方法區中常量引用的對象。
    4.本地方法棧中JNI引用的對象。
    5.基本數據類型對應的Class對象,系統類加載器。
    6.所有被同步鎖(synchronized關鍵字)持有的對象。
    7.反應Java虛擬機內部情況地JMXBean、JVMTI中注冊的回調、本地代碼緩存等。
    備注:可能會有臨時性加入,做局部回收的時候,某個區域內的對象完全有可能被位于堆中的其他區域引用,這時就需要將這些關聯區域對象一并加入GC Roots集合中去,才能保證可達性分析的正確。
    3)四種引用關系的出現
    出現原因:當內存空間還足夠時,能保留在內存中,如果內存空間再進行垃圾收集后仍然非常緊張,那就可以拋棄這些對象——系統緩存
    1.強引用:Object obj = new Object() 這種引用關系。只要強引用在,垃圾收集器就永遠不會回收掉被引用對象。
    2.軟引用:軟引用用來描述一些還有用、非必須的對象,只要被軟引用關聯著的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收范圍進行第二次回收,SoftReference
    3.弱引用:用來描述那些非必須的對象,但是它強度比軟引用更弱一點,被弱引用關聯的對象只能存活到下一次垃圾收集之前,垃圾收集器開始工作,都會回收,WeakReference
    4.虛引用:一個對象是否有虛引用,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例,唯一目的就是:為了能在這個對象被收集器回收時收到一個系統通知,PhantomReference。
    4)回收方法區
    回收內容:廢棄的常量和不再使用的類型
    判讀類型是否不再被使用(同時滿足以下條件):
    1.該類中所有實例都被回收
    2.加載該類的類加載器已經被回收,除非是精心設計過的可替換的類加載器,如OSGI,JSP等,否則很難達成
    3.該類對應的對象沒有在任何地方被引用過,無法在任何地方通過反射訪問該類
    備注:在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及OSGI這類頻繁自定義類加載器的場景中,通常都需要具備類型卸載的能力,以保證不會造成過大的內存壓力。
  2. 垃圾收集算法
    首先分為:引用計數式垃圾收集和追蹤式垃圾收集也被稱為直接垃圾收集和簡接垃圾收集,Java主要用追蹤式垃圾收集。
    1)分代收集理論
    弱分代假說:絕大多數對象都是朝生夕滅的。
    強分代假說:熬過越多次垃圾收集過程的對象就越難以消亡。
    跨代引用假說:跨代引用相對于同代引用來說僅占極少數。
    設計原則:收集器應該將Java堆劃分出不同的區域,然后回收對象依據其年齡(年齡即對象熬過垃圾收集過程的次數)分配到不同的區域之中存儲。顯而易見,如果一個區域中大多數對象都是朝生夕滅,難以熬過垃圾收集過程那么把它們集中放在儀器,每次回收只關注如何保留少量存活等。Java堆分區之后,才有了Minor GC、Major GC、Full GC這樣的回收類型的劃分才會有按照存亡特征相匹配的算法,“標記-復制、標記-清除、標記-整理算法。但是有很大的困難,例如跨代引用。針對跨代引用,完全掃描不合適,在新生代建立一個記憶集,Remembered Set,這個結構把老年代劃分成若干小塊,引用的小塊里的對象才會被加入到GC Roots進行掃描,雖然賦值時會增加開銷,但是劃算的。
    2)標記清除算法(三種算法比較了解,不做過多贅述)
    3)標記復制算法
    4)標記整理算法(老年代)
    備注:是否移動回收后的存活對象是一項優缺點并存的風險:如果移動對象,老年代這種大量對象存活,移動就是一種比較復雜的過程,移動對象時必須暫停用戶線程,stop the world,但是如果按照標記清除那樣子考慮,就會產生空間碎片問題,所以移動對象與否都會有問題,移動內存回收會更復雜,不移動則內存分配時更復雜,從停頓時間來看,不移動停頓時間更短,但從吞吐量上來看,移動會劃算,因為內存分配和訪問垃圾收集頻率比要高的多,這部分耗時增加,總吞吐量下降。如果關注吞吐量,Parallel Scavenge收集器基于整理算法,關注延遲的則基于清除算法CMS。
  3. HotSpot的算法細節實現
    1)根節點枚舉
    1.查找能作為GC Roots的引用,迄今為止所有收集器在根節點枚舉這一步都必須暫停用戶線程,根結點枚舉始終必須在一個能保障一致性的快照中才得以進行,一致性就是在某個時間點停下來,原因是不暫停程序,根節點集合的對象引用關系還在不斷變化。
    2.算法產生原因:當用戶線程暫停下來之后,其實并不需要一個不漏的檢查上下文和全局引用的位置,虛擬機通過OopMap的數據結構來知道直接哪些地方存著對象引用。
    3.當虛擬機加載完成時,即時編譯過程中,也會在特定的位置記錄下棧里和寄存器哪些位置是引用,所以直接掃描。
    2)安全點
    1.原因:導致OopMap內容變化的指令非常多,如果為每一條指令都生成對應的OopMap,那將需要大量的額外存儲空間。
    2.在特定位置記錄了這些信息,這些位置被稱為安全點,也就是決定了用戶程序執行時并非在代碼指令流的任意位置都能停頓下來開始垃圾收集,而強制要求到達安全點開始收集。
    3.選定安全點的標準是,是否具有讓程序長時間執行的特征。最明顯的特征是指令序列的復用,例如方法調用、循環跳轉、異常跳轉等。所以只有這些功能的指令才會產生安全點。
    問題:如何讓垃圾收集器發生時讓所有線程(不包括JNI——native)都跑到最近的安全點,然后停頓下來。
    方案:搶先式中斷和主動式中斷
    搶先式中斷:不需要線程的執行代碼主動去配合,在垃圾收集發生時,系統首先把所有用戶線程全部中斷,如果發現有用戶線程中斷的地方不在安全點上,就恢復這條線程執行,讓它跑到安全點上再中斷。
    主動式中斷:當垃圾收集器需要中斷線程時,不直接對線程操作,僅僅簡單在未來設置一個標志位,各個線程執行過程中會不停主動去輪詢(輪詢(Polling)是一種CPU決策如何提供周邊設備服務的方式。輪詢法的概念是:由CPU定時發出詢問,依序詢問每一個周邊設備是否需要其服務,有即給予服務,服務結束后再問下一個周邊,接著不斷周而復始。)這個標志,一旦發現中斷標志為真就自己在最近的安全點上主動中斷掛起。(Hotspot實現)
    3)安全區域
    1.原因:當程序不執行的時候,不執行就是沒有分配處理器時間,典型的場景便是用戶線程處于Sleep和Blocked狀態,這時用戶線程就無法響應虛擬機的中斷請求,所以引入“安全區域”。
    2.安全區域是指能夠確保在某一段代碼片段中,引用關系不會發生變化,因此,在這個區域中任意地方開始垃圾收集都是安全的。當用戶線程執行到安全區域里面的代碼時,首先標識自己進入安全區域,當這段時間里虛擬機要發起垃圾收集時就不必去管這些已經聲明自己在安全區域內的線程了。當離開安全區域時,它要檢查虛擬機是否已經完成了根節點枚舉(或者其他需要暫停用戶線程的行為),如果完成了,那線程就當沒事發生過,繼續執行否則它就必須一直等待,直到收到可以離開安全區域的信號為止。
    4)記憶集與卡表
    1.原因:解決對象跨代引用所帶來的問題。
    2.垃圾收集器在新生代建立記憶集,以避免把整個老年代加入GC Roots掃描范圍
    3.記憶集是一種用于記錄從非收集區域指向收集區域的指針集合的抽象數據結構。
    4.列舉一些可供選擇的記錄精度:
    字長精度:每個記錄精確到一個機器字長(就是處理器的尋址位數,如32位/64位,這個精度決定了機器訪問物理內存地址的指針長度),該字包含跨代指針。
    對象精度:每個記錄精確到一個對象,該對象里有字段含有跨代指針。
    卡精度:每個記錄精確到一塊內存區域,該區域內有對象含有跨代指針。
    第三種“卡精度”所指的就是用一種稱為“卡表”的方式去實現記憶集。它定義了記憶集的記錄精度、與堆內存的映射關系等。卡表最簡單的形式可以只是一個字節數組,字節數組的每一個元素都對應著其標識的內存區域中的一塊特定大小的內存塊,這個內存塊被稱作“卡頁”。卡頁的大小都是以2的N次冪的字節數,例如Hotspot中卡頁是2的9次冪,512字節
    一個卡頁的內存中通常包含不止一個對象,只要卡頁內有一個或者更多的對象字段存在跨代指針,那就將對應卡表的數組元素的值標識為1,稱為元素變臟,沒有則標識為0。在垃圾收集發生時,只要篩選出卡表中變臟的元素就能輕易得出哪些卡頁內存塊包含跨代指針,把他們加入GC Roots中一并掃描。
    5)寫屏障/偽共享
    1.原因:卡表元素如何維護問題,例如他們何時變臟、誰來把他們變臟。
    2.有其他分代區域中對象引用了本區域對象時,其對應的卡表元素就應該變臟,時間點應該發生在引用類型字段賦值的那一刻。
    3.Hotspot虛擬機通過寫屏障技術維護卡表狀態,寫屏障可以看作在虛擬機層面對“引用類型字段賦值”這個動作的AOP切面,寫后屏障和寫前屏障。
    偽共享:高并發場景下產生,偽共享是處理并發底層細節時,一種經常要考慮的問題,現代中央處理器的緩存系統中是以緩存行(Cache Line)為單位存儲的,當多線程修改互相獨立的變量時,如果這些變量恰好共享同一個緩存行,就會彼此影響(寫回、無效化或者同步)而導致性能降低,這就是偽共享問題。
    解決方法:不采用無條件的寫屏障,而是先檢查卡表標記,只有當該卡表元素未被標記過時才將其標記為變臟,不過會增加額外的開銷,但能夠避免偽共享問題,兩者各有性能損耗。
    6)并發可達性分析/增量更新/原始快照
    1.垃圾收集器基本上都是依靠可達性分析算法來判定對象是否存活,可達性分析算法理論上全過程都基于一個能保障一致性的快照才能分析,這意味著必須全程凍結用戶線程的運行。在根節點枚舉這個步驟中,由于GC Roots相比起整個Java堆中全部對象畢竟還是少數,且在優化技巧(OopMap)的加持下,非常短暫了。但是從GC Roots 再繼續往下遍歷對象圖,這一步驟的停頓時間就必然和Java堆容量直接成正比例關系了。
    2.標記階段是所有追蹤式垃圾收集算法的共同特征,如果這個階段會隨著堆變大而等比例增加停頓時間,必須削減這部分停頓時間。先搞清楚為什么必須在一個能保障一致性的快照上才能進行對象圖的遍歷?
    白色:對象尚未被垃圾收集器訪問過,顯然在剛開始的階段,所有對象都是白色的,若分析結束的階段,仍然是白色的對象,即代表不可達。
    黑色:表示對象已經被垃圾收集器訪問過,且這個對象的所有引用都已經掃描過。黑色的對象代表已經掃描過,它是安全存活的,如果有其他對象引用指向了黑色對象,無須重新掃描一遍。
    灰色:表示對象已經被垃圾收集器訪問過,但這個對象上至少存在一個引用還沒被掃描過。
    如果不暫停用戶線程,可能會導致收集器在對象圖上標記顏色,同時用戶線程在修改引用關系——即修改對象圖結構,可能出現兩種后果:一種是把原本消亡的對象錯誤標記為存活,只不過產生了一點逃過本次收集的浮動垃圾而已。第二種是把原本存活的對象錯誤標記為已消亡,這是致命的后果。
    Wilson于1994年在理論上證明了,當且僅當以下兩個條件同時滿足時,會產生對象消失問題,即原本應該是黑色的對象被誤標為白色:
    1.賦值器插入了一條或多條從黑色對象到白色對象的新引用;
    2.賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用
    只需要破壞一個條件,就可以避免對象消失,產生兩種解決方案:增量更新和原始快照
    增量更新:當黑色對象插入新的白色對象的引用關系時,就將這個新插入的引用記錄下來,等并發掃描結束之后,再將這些記錄過的引用關系中的黑色對象為根,重新掃描一次。
    原始快照:當灰色對象要刪除指向白色對象的引用關系時,就將這個要刪除的引用記錄下來,等并發掃描結束之后,再將這些記錄過的引用關系中的灰色對象為根,重新掃描一次。
    備注:無論是對引用關系的插入還是刪除,虛擬機的記錄操作都是通過寫屏障實現的。CMS是基于增量更新來做并發標記的、G1和Shenandoah則是用原始快照來實現。
  4. 幾種經典的垃圾收集器
    吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 運行垃圾收集時間)
    1)Serial
    1.一個單線程收集器,啟用時必須 stop the world,采取標記-復制算法實現。
    2.迄今為止,它仍然是Hotspot虛擬機運行再客戶端模式下的默認新生代收集器,他是所有收集器里額外內存消耗最小的,且沒有線程交互的開銷,專心做垃圾收集。
    3.在桌面場景以及近年來流行的部分微服務應用中,分配給虛擬機管理的內存一般不會特別大,收集幾十兆甚至一百兆的新生代,垃圾收集的停頓時間完全可以控制在十幾、幾十毫秒,最多100毫秒以內,所以Serial對于運行在客戶端模式下的虛擬機來說是一個很好的選擇。
    2)ParNew
    1.實質上是Serial的多線程并行版本,新生代采用復制算法,多線程并行,暫停用戶線程。
    2.只能和CMS搭配使用,在服務端常用,無法與Parallel Scavenge搭配使用,因為一個面向低延遲一個面向高吞吐量,除此之外就是,Parallel沒有分代框架,而CMS又是基于這種強分代框架下。
    3)CMS
    1.CMS收集器是一種以獲取最短回收停頓時間為目標的收集器,基于標記-清除算法,其中包含幾個階段:
    初始標記:需要stop the world 。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快;
    并發標記:就是從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,雖然耗時較長但是不需要停頓用戶線程;
    重新標記:需要stop the world ,則是為了修正并發標記期間,因用戶線程線程繼續運轉而導致的標記變動的記錄;
    并發清除:清理掉標記階段已經死亡的對象,由于不需要移動存活對象,所以也是并發清除。
    2.缺點:
    ①:CMS對處理器資源非常敏感,因占用一部分線程而導致程序變慢,降低總吞吐量。
    ②:CMS收集器無法處理浮動垃圾,有可能導致Concurrent Mode Failure失敗進而導致另一次完全的Stop the world 的Full GC產生。由于由于垃圾收集階段用戶線程還在持續運行,那還需要預留足夠的內存空間提供給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全填滿了再進行收集,必須預留一部分空間供并發收集時的程序運行,JDK6時,CMS收集器啟動閾值已經默認提升至92%,但又會面臨另外一種風險:要是CMS運行期間預留的內存無法滿足程序分配新對象的需要,就會出現一次并發失敗,這時虛擬機不得不啟動后備預案:凍結用戶線程,臨時啟動Serial Old 收集器來重新進行老年代的垃圾收集。
    ③:產生大量碎片空間,空間碎片過多時,將會給大對象分配帶來麻煩,往往會出現老年代還有很多剩余空間,但就是無法找到足夠大的連續空間來分配當前對象,而不得不提前觸發一次Full GC時開啟內存碎片的合并整理過程,且必須移動存活對象,是無法并發的。
    4)G1
    1.G1是里程碑式的收集器,弱化分代概念,開創了面向局部收集和基于Region的內存布局形式,主要面向服務端應用。
    2.設計者們希望能建立起“停頓時間模型”的收集器,停頓時間模型的意思是能夠支持指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間大概率不超過N毫秒這樣的目標。
    3.G1面向堆內存任何部分來組成回收集,進行回收,衡量標準不再是它屬于哪一個分代,而是哪塊內存存放的垃圾數量最多,回收收益最大,這就是G1的Mixed GC模式。
    4.G1把連續的Java堆劃分成多個大小相等的獨立區域(Region),每一個Region根據需要扮演新生代Eden空間、Survivor空間或者老年代空間。收集器能夠對扮演不同角色的Region采用不同的策略去處理,這樣就能達到很好的收集效果。
    5.Region中還有一類特殊的Humongous區域,專門用來存儲大對象,G1認為超過了一個Region大小的一半即可判定為大對象,每個Region取值范圍是1~32MB,為2的N次冪。對于超過了整個Region容量的超大對象,將會被存放在N個連續的Humongous Region中,G1的大多數行為都把Humongous Region作為老年代的一部分開看待。
    6.每次垃圾收集根據用戶設定的允許的收集停頓時間,優先處理回收價值最大的那部分Region。
    7.G1收集器面臨的問題:
    ①:多個Region的跨代引用問題,每個Region都會維護自己的記憶集,這些記憶集會記錄下別的Region指向自己的指針,并且標記這些指針分別在哪個范圍之內,本質是G1記憶集是一個hash表,Key是別的Region的起始地址,Value是一個集合,里面存儲的元素是卡表的索引號。這種雙向卡表結構(卡表是我指向誰,這個結構還有誰指向我),所以維護起來,就有著更高的內存占用負擔,G1至少要消耗大約相當于Java堆容量10%到20%的額外內存來維持收集器工作。
    ②:并發標記階段如何保證收集線程與用戶線程互不干擾的運行。CMS采用增量更新的算法實現,而G1采用原始快照算法(SATB)實現。此外垃圾收集對用戶線程的影響還體現在回收過程中新創建對象的內存分配上,程序要繼續運行就肯定會持續由新對象被創建,G1為每一個Region設計了兩個名為TAMS的指針,把Region中的一部分空間劃分出來用于并發回收過程中的新對象分配,并發回收時新分配的對象地址都必須要在這兩個指針位置以上。G1收集器默認在這個地址以上的對象是被隱式標記過的,即默認它們是存活的。如果內存回收速度趕不上內存分配速度,G1收集器也要被迫凍結用戶線程,導致Stop the world。Full GC。
    ③:如何建立起可靠的停頓預測模型?用戶通過參數指定的停頓時間只意味著垃圾收集發生之前的期望值,G1收集器的停頓時間預測模型是以衰減均值為理論基礎來實現的,在垃圾收集的過程中,G1收集器會記錄每個Region的回收耗時、每個Region記憶集里臟卡數量等各個可測量的步驟花費成本,并分析得出平均值、標準偏差、置信度等統計信息。這里強調的“衰減平均值”是指它會比普通的平均值更容易受到新數據的影響,平均值代表整體平均狀態,但衰減平均值更準確代表“最近的”平均狀態。換句話說,Region的統計狀態越新越能決定回收的價值。然后通過這些信息預測現在開始回收的話,由哪些Region組成回收集才可以在不超過期望停頓時間的約束下獲得最高的收益。
    8.運作步驟:
    初始標記:僅僅只是標記一下GC Root能直接關聯到的對象,并且修改TAMS指針的值,讓下一階段用戶線程并發運行時,能正確地在可用的Region中分配新對象,需要停頓線程。
    并發標記:從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆里地對象圖,當掃描完成以后,還需要重新處理SATB記錄下地在并發時有引用變動的對象。
    最終標記:對用戶線程做另外一個短暫的暫停,用于處理并發階段結束后遺留下來的最后那少量的SATB記錄。
    篩選回收:負責更新Region統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃;可以自由選擇任意個Region構成回收集,然后把決定回收的那一部分Region的存活對象復制到空的Region中,再清理掉整個舊的Region的全部空間,這里的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程并行完成的。
    由此可看到,G1除了并發標記之外,其余階段也是要暫停用戶線程的。并非純粹追求低延遲,官方的設計目標是在延遲可控的情況下,盡可能獲得高的吞吐量。
    9.用戶指定期望的停頓時間是G1很強大的一個功能。通常設定100~300ms。
    10.從G1開始最先進的垃圾收集器的設計導向都不約而同地變為能夠應付應用地內存分配速率,而不追求一次把整個Java堆全部清理干凈。這樣,應用在分配,同時收集器在收集,只要收集器的速度能跟得上對象分配的速度,那一切就能運作得很完美
    11.CMS與G1收集器的對比
    G1優點:指定最大停頓時間、分region的內存布局、按收益動態確定回收集。G1從整體上基于標記整理算法,局部(兩個Region之間)看又是基于標記復制算法,不會產生內存空間碎片,收集完成后能提供規整的可用內存。
    G1相比于CMS缺點:在用戶程序運行過程中,G1無論是為了垃圾收集產生的內存占用還是程序運行時的額外執行負載都比CMS高。G1的記憶集可能會占整個堆容量的20%乃至更多的內存空間;從執行負載的角度來看,CMS用寫后屏障更新維護卡表,而G1除了使用寫后屏障來維護卡表之外,為了實現原始快照算法(SATB),還要使用寫前屏障來跟蹤并發時的指針變化情況。(相比較增量更新算法,原始快照算法能減少并發標記和重新標記階段的消耗,避免CMS在最終標記階段停頓時間過長的缺點,但會產生額外的負擔。CMS寫屏障是同步操作,而G1就不得不將其實現為類似消息隊列的結構,把寫前屏障和寫后屏障要做的事情放在消息隊列里,然后再異步處理。
    結論:小內存上CMS好一點,大內存G1上大多能發揮其優勢,6GB到8GB之間。
  5. low_delay垃圾收集器
    1)ZGC
    1.ZGC收集器是一款基于Region內存布局的,(暫時)不設分代的,使用了讀屏障、染色指針和內存多重映射等技術來實現的標記-整理算法,以低延遲為首要目標的一款垃圾收集器。
    2.ZGC的Region具有動態性——動態創建和銷毀,以及動態的區域容量大小:
    小型Region:容量固定2MB,用于放置小于256KB的小對象。
    中型Region:容量固定為32MB,用于放置大于等于256KB但小于4MB的對象。
    大型Region:容量不固定,可以動態變化,但必須為2MB的整數倍,用于放置4MB以上的大對象。每個大型的Region中只會存放一個大對象,雖然名字是大型Region但最小容量可低至4MB。大型Region在ZGC的實現中是不會被重分配,因為復制一個大對象的代價極高。
    3.ZGC的核心問題——并發整理算法
    ZGC標志性設計——染色指針技術:如果我們要在對象上存儲一些額外的、只供收集器、或者虛擬機本身使用的數據,通常會在對象頭中增加額外的存儲字段,如對象的哈希碼、分代年齡、鎖記錄等就是這樣存儲的。這種記錄方式在有對象訪問的場景下是很自然流暢的,不會有什么額外的負擔。但如果對象存在被移動過的可能性,即不能保證對象訪問能夠成功呢?能不能從指針或者與對象內存無關的地方得到這些信息——追蹤式收集算法的標記階段就可能存在只跟指針打交道而不必涉及指針所引用的對象本身的場景。例如對象標記階段過程需要給對象打上三色標記,這些標記本質上就只和對象的引用有關,而與對象本身無關。
  6. 實戰:內存分配與回收策略

第二部分 虛擬機執行子系統 代碼編譯的結果從本地機器碼轉變為字節碼,是存儲格式發展的一小步,卻是編程語言發展的一大步

第六章 類文件結構

這里就不做整理了,第一次看書時就打開了一個編譯后的.class文件查看了,具體位置等,傳一張照片。



  1. Class類文件結構
  2. 字節碼指令簡介
  3. 公有設計,私有實現

第七章 虛擬機類加載機制(鑒于字數過多,從第七章開始分小文章整理)

代碼編譯的結果從本地機器碼轉變為字節碼,是存儲格式發展的一小步,卻是編程語言發展的一大步。
Java虛擬機把描述類的數據從Class文件加載到內存,并對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這個過程被稱作虛擬機的類加載機制。
與那些在編譯時需要連接的語言不同,在Java語言里面,類型的加載、連接和初始化過程都是在程序運行期間完成的,這種策略讓Java語言進行提前編譯會面臨額外的困難,也會讓類加載時稍微增加一點額外的開銷,但卻為Java應用提供了極高的擴展性和靈活性,Java天生可以動態擴展的語言特性就是依賴運行期間動態加載和動態鏈接實現的。

  1. 類加載的時機
  2. 類加載的過程
  3. 類加載器
  4. Java模塊化系統

第八章 虛擬機字節碼執行引擎

  1. 運行時棧幀結構
  2. 方法調用
  3. 動態類型語言支持
  4. 基于棧的字節碼解釋執行引擎

第九章 類加載及執行子系統的案例與實戰

  1. TomCat
  2. OSGI
  3. 字節碼生成技術與動態代理技術
  4. Backport工具:Java的時光

前后端編譯 這里就只看了,泛型 自動拆箱裝箱技術

第十章 前端編譯與優化

1.泛型、自動拆箱、裝箱與foreach循環

第十一章 后端編譯與優化

  1. 即時編譯器
  2. 提前編譯器
  3. 編譯器優化技術
  4. 深入理解Graal編譯器
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容