深刻理解Java虛擬機第三版,總結筆記【隨時更新】

最近一直在看《深刻理解Java虛擬機》第三版,無心中發現了第三版是最近才發行的,據說講解的JDK版本升級,新增了近50%的內容。java

這種神書,看懂了,看進去了,真的看的很快,並無想象中的晦澀難懂,畢竟是公認的經典,做者書面描述能力確定了得。雖然這種書,不會讓你的代碼能力立刻提高,可是真正的讓你知其然,還知其因此然。等遇到了這方面的問題,確定不會像無頭蒼蠅同樣,一頭霧水,起碼有必定的思路。更多Java、計算機方面的一些好書正在路上,今年必定要好好地提高一下內功。程序員

不過,好比第五章的內容,調優實戰,沒有充足的實戰經驗和一些大型項目經驗,雖然說一些地方能看懂做者在說什麼,可是沒有一個本身有過經驗的實際場景去代入,理解的仍是不夠充分。算法

固然看一次確定不能消化完整,雖然在看的時候就在有道筆記上作了一些筆記,可是仍是上傳到博客園吧,就當水一篇博客啦。編程

Java內存區域與內存溢出異常

2.2運行時數據區域

Java虛擬機所管理的內存包含如下幾個運行時數據區域:

  1.程序計數器:是當前線程所執行的字節碼的行號指示器。就是經過改變這個行號指示器的值來選取下一個須要執行的字節碼指令,從而能夠實現循環、跳轉、分支、異常處理等基礎功能。Java虛擬機的多線程是經過線程間的輪流切換、粉配處理器執行時間來實現的,因此爲了讓線程切換後恢復到正確的執行位置,每一個線程的計數器是獨立的,互不影響,包括主線程。若是線程執行的是Java代碼,計數器記錄的是字節碼的行號,若是執行的是本地方法,計數器爲空。Undefined。這個區域不會報內存溢出異常。數組

  2.Java虛擬機棧:其也是線程私有的,生命週期與線程相同,其描述的是Java執行的線程內存模型。每一個方法被執行時會建立一個棧幀(一種數據結構),用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。(其中動態連接是:在類加載機制中,解析步驟會把符號引用轉爲直接引用,還有一部分會在執行過程當中才變成直接引用,這就是動態連接)。棧幀的入棧到出棧就是一個方法完整的執行過程。重點是虛擬機棧中的局部變量表,其存放的是基本數據類型、對象引用、returnAddress類型(一條字節碼的地址)。當線程請求的棧的深度大於虛擬機容許的深度,會報StackOverflowError。當棧拓展時沒法申請到足夠的內存會報OutOfMemoryError。緩存

  3.本地方法棧:其與Java虛擬機棧做用類似,只是Java虛擬機棧爲Java方法服務,而本地方法棧爲本地方法服務。也有上面兩種異常。安全

  4.Java堆:虛擬機管理的內存中最大的一塊。Java堆是全部線程共享的,其惟一目的就是存放對象實例。一個對象的建立,其引用放在棧,實例放在堆。Java堆是垃圾收集器管理的內存區域,所以有的人稱他爲GC堆。GC相關內容後面再記。不管堆這麼劃分,其存儲的都只是對象的實例,細分的目的只是爲了更好的回收內存和分配內存。當Java堆沒法完成實例分配,堆也沒法拓展,會報OutOfMemoryError。服務器

  5.方法區:與Java堆同樣,是線程共享的內存區域。用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。若是方法區沒法知足內存分配需求,將會報OutOfMemoryError異常。數據結構

 

2.3HotSpot虛擬機對象探祕

對象建立的基本過程:

  1. 檢查new這個指令的參數是否能在常量池中找到一個符號引用,而且檢查該引用表明的類是否已經被加載、解析、初始化過。
  2. 爲新生對象分配內存。這個內存分配涉及到的東西不少,好比不一樣的垃圾收集器也會有不一樣的分配方法,若是是帶壓縮整理過程的收集器,其分配起來較爲簡單,由於其內存空閒地址是連續的。可是在併發狀況下,也是不安全的,這時候可能就要採用CAS。這裏只講大概過程,具體細節後面章節會講。
  3. 內存分配完成後,虛擬機將分配到的內存空間都初始化爲0值。
  4. 初始化後,虛擬機會進行一些必要的設置,好比這個對象是哪一個類的實例、在GC中分代年齡信息等,這些信息會被放到對象頭中。
  5. 上面四步,對於虛擬機來講,一個對象的建立已經完成,但對於程序的角度來講,還差一步,就是類的初始化方法,構造函數。程序員能夠按照本身的需求來寫這個初始化方法,到這裏,這個對象徹底被建立完成。

對象在內存當中能夠被分爲三個部分:對象頭、實例數據、對其填充

  對象頭部分包括兩類信息,第一類信息存儲對象自己的運行時的數據,如:哈希碼、GC分代年齡、線程持有的鎖、鎖狀態、偏向時間戳、偏向線程ID等。多線程

  另外一部分是類型指針,即經過這個指針肯定這是哪一個類的實例。若是是數組,還會存儲數組的長度。

  實例數據部分是對象真正存儲的有效信息,即咱們定義的各類類型的字段內容。

  對其填充沒什麼實際意義,HotSopt的內存管理要求任何對象的大小必須是8的整倍數,對象頭已經被設計爲8的整倍數,可是實例數據就不必定了,這時候就須要對其填充來補全。

  對象的訪問定位:

  Java堆是存放實例數據的,Java棧上的reference數據是存放這個實例的引用的。而這個引用主流有兩種實現方法:

  1.使用句柄,Java堆可能會劃分一塊內存空間來做爲句柄池,reference存儲的就是對象句柄的地址,這個句柄包含了對象的實例數據和具體地址信息。優勢:不用改變reference,若是GC讓對象地址改變,只改變句柄中的地址就好了。

  2.使用指針直接訪問,reference存儲的就是對象的地址,若是隻是訪問對象自己的話,就不須要像句柄那樣再次訪問一個地址了。優勢:不用二次指針定位。

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

  程序計數器、Java虛擬機棧、本地方法棧這三個區域都是隨着線程生而生、隨着線程滅而滅。這幾個區域不須要考慮太多垃圾回收的問題,方法執行完 了、線程結束了,內存天然就回收了。因此重點Java堆和方法區,他們是共享的區域,因此不會隨着線程生滅,也有着不少不肯定性。

3.2對象已死?

  垃圾收集器在對堆進行回收以前,確定要判斷對象實例是否還有沒有用,若是在Java程序中一個對象沒有任何做用,其天然就須要被回收。下面兩種就是目前主流的方法

  1.引用計數算法,在對象中添加一個引用計數器,每當一個對象被引用時,計數器加一,當引用失效時,計數器減一。計數器會0時,就是不可能再被使用了。這種算法不少應用都在使用,好比FlashPlayer、Python等。可是在Java中有個問題,引用計數算法卻不能解決,就是循環引用。好比兩個對象A和B互相引用,即便它們都爲null了,或者都沒用了,可是它們的計數器依舊不會0。可是Java虛擬機任然能夠回收它們,說明Java虛擬機不是使用的這種算法。

  2.可達性分析算法,經過一系列稱爲「GC Roots」的根對象做爲起始節點集,注意是集,不僅是一個節點。從這些節點出發,走過的路徑稱爲「引用鏈」,而在這條引用鏈上的對象,都是不須要回收的存活對象,而不在引用鏈上的就須要被回收。能看成GC Root的對象有不少種,主要有:全部已經被加載的類、線程當前棧幀的引用、同步鎖持有的對象等。具體還要哪些能夠看P70。當一個對象不在引用鏈上時,不表明必定就會被回收。一個對象被回收至少要被標記兩次,第一次被標記後,會進行一次篩選,判斷這個對象是否有必要執行finalize()方法,若是這個方法以前已經執行過一次,或者這個對象沒有重寫這個方法,那麼就表明須要被回收。若是這個對象重寫了finalize方法,且以前沒有被執行過,他就會被放到F_Queue隊列中,等待執行這個方法,這個方法是有時間限制的,防止執行太過緩慢對系統形成威脅。這時候,這個對象能夠在finalize方法裏完成自救,即把本身從新和引用鏈上的某個對象鏈接起來。

  3.2.5回收方法區

  Java虛擬機規範沒有強制要求這一區域的垃圾收集工做,主要緣由是性價比過低,即判斷條件高,回收空間少。方法區主要是回收常量和再也不使用的類型。對於常量,若是任何對象都沒有引用這個常量,那麼它就能夠回收了。對於一個類型是否須要回收,判斷起來就要複雜許多:1.該類的全部實例被回收。2.加載該類的類加載器被回收。3.沒有經過反射使用該類。

  3.3垃圾收集算法

  上一節說到了該如何判斷哪些對象須要被回收,這一節講講該如何回收這些對象。在這以前須要瞭解一下分代收集理論,不少垃圾收集器就是基於這個理論去設計的。分代即把Java堆劃分出幾個不一樣的區域,而後根據對象年齡分配到不一樣的區域(年齡是指熬過垃圾收集器的次數)。主要有兩個大區域,新生代和老年代。新生代區域的對象,會被頻繁回收,若是在新生代熬過必定回收次數後,就會被放到老年代。這樣劃分的好處就是能夠根據不一樣區域對象的消亡特徵設計不一樣的垃圾收集算法。這就有了後面要說到的一些算法。可是還存在一個隱性的問題,就是跨代引用,即新生代和老年代之間存在引用,那麼咱們以前說到的GC Roots就不得不包含一些老年代的對象了,加大了一些額外的開銷。有一個跨代引用的假說,若是兩個對象之間存在引用,它們應該是共存亡的,出現不一樣代的引用是極少數的,因此也不必去爲了那極少數,而加大一些額外開銷。

  具體的算法描述能夠看書,P77開始。

  1.標記-清除算法:經過可達性分析後,找出須要回收的對象,並標記上,對標記的對象進行回收。也能夠反過來,標記不須要回收的對象,對沒有標記的對象進行回收。主要有兩個缺點:一個是執行效率不穩定,其執行效率是隨着清楚對象的增長而下降的。一個是會產生空間碎片,由於回收的對象也許是零零散散的,致使空餘內存空間也是零散的。

  2.標記-複製算法:把內存空間分爲兩個相等部分,一次只用其中一個部分,每次把存活對象依次規整的放到另外一部分,而後再把已使用的那一部分總體回收。其最大的缺點就是將可用內存縮小了一半,空間浪費的太多了。後來IBM有項研究代表,新生代的對象98%熬不過第一次回收,因此針對這一特色,在標記-複製算法的基礎上,把新生代分爲一個Eden區、兩個Survivor區。其內存佔比爲8:1:1。每次分配內存只使用一個Eden區和一個Survivor區。把這兩個區的存活對象複製到另外一個Survivor區中,而後再清除Eden和已使用的Survivor。

  3.標記-整理算法:這個算法通常用在老年代,標記過程和標記-清除算法同樣,可是在清除階段會把存活對象移到內存一端,而後直接清除到邊界之外的內存。可是在移動過程當中會STW(即暫停用戶線程)。整理和不整理都有好處和壞處,因此側重點不一樣,就有了Parallel Scavenge收集器和CMS收集器。

經典垃圾收集器

  Serial(Serial Old)收集器:如同其名字同樣,他是個單線程收集器,意味着它在工做時,用戶線程必須中止,也就是常說的STW。在新生代中它採用的是標記-複製算法,在老年代中採用的是標記-整理算法。雖然這個線程是最基礎、歷史最悠久的收集器,可是相比較於其餘單線程的收集器,它依舊是很是優秀的,在HotSpot虛擬機客戶端模式下(Server啓動慢,編譯更徹底,編譯器是自適應編譯器,效率高,針對服務端應用優化,在服務器環境中最大化程序執行速度而設計;Client啓動快速,內存佔用少,編譯快,針對桌面應用程序優化,爲在客戶端環境中減小啓動時間而優化),新生代的默認收集器就是它。

  ParNew收集器:只在新生代中,其只是Serial的多線程版本,其餘的與Serial沒有太多區別。依舊須要STW,它在新生代採用的是標記-複製算法。是服務端虛擬機新生代的首選收集器。

  Parallel Scavenge(Parallel Old)收集器:Parallerl Scavenge是一款新生代收集器,採用的是標記-複製算法。它也是多線程的,可是與其餘收集器不一樣的是它更關注吞吐量(用戶代碼運行時間/用戶代碼運行時間+垃圾收集時間)。適合在後臺運算而不須要太多交互的任務。Parallel Old是老年代版本,採用的是標記-整理算法。也是注重吞吐量的多線程垃圾收集器。

  CMS收集器:老年代的收集器,注重的是減小STW的時間,基於標記-清除算法。其過程相比其餘收集器更爲複雜,大概分爲四步:1.初始標記,根據GC Roots找到直接關聯的對象。2.併發標記,根據初始標記階段的對象找到更爲完整的關聯對象。3.從新標記,因爲併發標記是和用戶線程併發的,在這個過程不免會出現一些新的可回收對象。4.併發清理,因爲採用的是標記-清除算法,不須要移動對象,因此能夠和用戶線程併發進行。雖然這是HotSpot追求低停頓時間的一次成功嘗試,可是也有一些缺點,好比:併發清除階段會產生新的垃圾、標記-清除算法產生的內存空間碎片、CMS默認的回收線程是(處理器核心數量+3)/4,對處理器敏感。

  G1收集器:是一款主要面向服務端應用的垃圾收集器,做用於整個Java堆,是具備里程碑式意義的。雖然G1依舊保留新生代和老年代的機率,但它們再也不是固定的,而是把Java堆劃分紅多個大小相等的Region區,每一個Region區能夠根據須要扮演新生代和老年代中的角色,總體來看,他採用的是標記-整理算法,可是在兩個Region之間,採用的是標記-複製算法。用戶能夠設定收集停頓模型,會優先回收價值收益最大的那些Region。其工做過程分爲初始標記、併發標記、最終標記、篩選回收。前三個階段與CMS相似,篩選回收階段會對各個Region的回收價值和成本排序,根據前面用戶的設定來制定回收計劃。除了併發標記階段,其他三個階段也是須要STW的。G1被稱爲里程碑式的設計一個重要緣由就是,設計者的思想從原來一次性把垃圾收集乾淨,到只是回收的速度比分配速度快就好了,這樣在知足需求的狀況下,性能也獲得了很大的提高。

  總結:從名字看,除了G1做用於整個Java堆,CMS做用於老年代,其他五款均可以根據名字判斷出做用於老年代仍是新生代。根據以前垃圾收集算法的特色,老年代多用標記-整理算法,新生代多用標記-複製,除了CMS,其做用於老年代是標記-清除算法。而做用在服務端仍是客戶端,因爲服務器多核心CPU較爲常見,因此多線程收集器用在服務端更好。

可達性分析

  爲了方便描述,首先定義一個三色標記,白色就是可達性分析中還未被標記的對象,若是從始至終都是白色那就是須要回收的對象。黑色就是已被標記,它的引用也被掃描過的對象。灰色就是已被標記,可是它的引用還未被掃描的對象。

在併發標記時,對象之間的引用可能會不停變更,當同時出現這兩種狀況時,原本在引用鏈上的對象會丟失:1.黑色對象增長了一個到對象A的引用。2.灰色對象刪除了到對象A的引用。若是一個對象同時出現這兩個狀況就會丟失。爲何要同時出現呢?由於已被標記的對象不會回頭去檢查,而正在被標記的對象若是引用發生變更會立刻生效。因此針對這兩種狀況,只要解決其中一條就不會出現對象丟失的問題。1.增量更新,破壞的就是第一條,黑色對象新增一個引用就會被記錄下來,等併發標記結束後,再以記錄的對象爲根從新掃描一邊。2.原始快照:破壞第二條。灰色對象刪除一條引用就將這個灰色對象記錄下來,併發標記結束後再以記錄的對象爲根,掃描一邊。

低延遲垃圾收集器

  垃圾收集器三個重要的指標:內存佔用、吞吐量和延遲。內存佔用和吞吐量隨着硬件性能的提高,幫助了軟件很多,不須要那麼關注這兩點,隨着硬件的提高這兩項指標也會隨着提高。可是延遲不同,延遲也就是STW的時間,隨着內存條的容量愈來愈大,Java堆可用的內存也愈來愈大,意味着須要回收的空間也愈來愈大,那麼STW也就越久。

  Shenandoah收集器:是一款非官方的垃圾收集器,是由RedHat公司開發的項目,受到來自Sun公司的排斥,因此在正式商用版的JDK中是不支持這個收集器的,只有在OpenJDK纔有。雖然沒有擁有正統血脈,可是在代碼上它相較於ZGC更像是G1的繼承者,在不少階段與G1高度一致,甚至共用了一部分源碼,但相較於G1又有一些改進。最主要有三個改進:

  1.支持併發標記-整理算法。

  2.默認不適用分代收集,Shennandoah和G1同樣使用Region分區,可是在Shennandoah中並無Region會去扮演新生代或者老年代。

  3.G1中存儲引用關係的記憶集佔用了大量的內存空間,在Shennandoah改用爲鏈接矩陣,具體能夠看P107。

  Shennandoah收集工做過程大概能夠分爲9個步驟

  1.初識標記:與G1同樣,標記處與GC Roots直接關聯的對象,STW。

  2.併發標記:與G1相同,根據上一步的對象,完整標記出可達對象。

  3.最終標記:也與G1同樣,利用原始快照的方法標記出上個階段變更的對象,還會在這個階段統計出回收價值最高的Region,組成一個回收集。

  4.併發清理:這個階段會清理整個Region區一個存活對象都沒有的區域,因此能夠併發進行。

  5.併發回收:將回收集中存活的對象複製一份到其餘未被使用的Region區中。

  6.初始引用更新:併發回收階段複製後,還需修正到複製後的新地址,但這個階段並未作什麼具體操做,只是至關於一個集合點,確保併發回收階段全部線程都完成了本身的複製工做。

  7.併發引用更新:這個階段纔是真正修正引用的階段。

  8.最終引用更新:上一步只是修正了Java堆中對象的引用,還要修正存在於GC Roots的引用,最後一次短暫的暫停,只與GC Roots數量有關。

  9.併發清理:通過了併發回收的複製和引用修正,會收集中的Region就能夠徹底清理了。

  再說說Shennandoah的一個特色,也就是前面說到的併發標記-整理算法。整理階段能夠細分爲5,6,7,8四個步驟。其最大的一個問題就是,在複製或者在修正引用的時候用戶線程可能正在使用這個對象。原來有個解決相似問題的方案,就是保護陷阱,大概過程就是當用戶線程訪問到對象就地址後,會進入一個異常處理器中,由該處理器轉發到新的地址。而在Shennandoah中用的是一種相對更好的方案:轉發指針,就是在每一個對象前面加個新的引用字段,當不處於併發移動的狀況下,該引用指向本身,併發移動了的話就指向新地址。

  ZGC收集器:ZGC的目標和Shennandoah類似,都但願在不影響吞吐量的狀況下,將停頓時間限制在10毫秒之內。ZGC也是基於Region佈局的,還並未支持分代收集,但其Region有大中小三個類型

  1.小型Region容量固定爲2MB,用於放置小於256KB的小對象。

  2.中型Region固定容量爲32MB,用於放置大於等於256KB,小於4MB的對象。

  3.大型Region容量不固定,但必定是2的整倍數,用於存放大於4MB的對象。

  ZGC在實現併發整理時用到了染色指針,以前的的收集器若是想在對象中額外存儲一些信息,大多會在對象頭裏存儲,好比轉發指針。再就是以前說到的可達性分析中的三色標記,其只是表達了對象引用的狀況,跟對象自己的數據沒任何關係,因此染色指針就是把這些標記信息記錄在引用對象的指針上。指針爲何還能存儲信息呢?這就要說到系統架構了,具體看P114,染色指針只支持64位系統,而AMD64架構中只支持到了52位,而各大操做系統又有本身的限制,染色指針在Linux支持的46位指針寬度中,拿出4位存儲這些標記信息,因此使用了ZGC進一步壓縮了本來46位的地址空間,從而致使了ZGC能管理的內存不能超過4TB,在今天看來,4TB的內存依舊很是充足。

  染色指針的三大優點:

  1.一旦某個Region的存活對象被移走後,這個Region當即就能被回收從新利用,而Shennandoah須要一個初始引用更新,等待全部線程複製完畢。

  2.染色指針能夠大幅度減小在垃圾收集過程當中內存屏障的使用數量(後面過程當中的第五步提到),一部分功能就是由於染色指針把信息存儲在指針上了,還有一部分緣由就是ZGC還並未支持分代收集,因此也不存在跨代引用。

  3.染色指針在將來能夠拓展,記錄更多信息,前面說到在64位系統中,Linux只用到了46位,還要18位未被開發。還有一個問題就是染色指針從新定義指針中的幾位,操做系統 是否支持,虛擬機也只是一個進程而已,這裏就用到了虛擬內存映射,具體看P116。

  ZGC工做過程大概能夠分爲如下幾步:

  1.初始標記:與以前幾個收集器同樣,找到GC Roots的直接關聯對象。

  2.併發標記:標記出完整的可達對象,與G1和Shennandoah不一樣的是,它是在指針上作更新而不是對象頭。

  3.最終標記:和Shennandoah同樣。

  4.併發預備重分配:這個階段須要根據特定的查詢條件統計出本次收集過程要清理哪些Region。這裏的分配集不是像G1那樣按收益優先的回收集,分配集只是決定了裏面的對象會被複制到新的Region,這裏的Region要被釋放了。

  5.併發重分配:這個過程要把分配集中的對象複製到新的Region中,併爲分配集中的每一個Region維護一個轉發表,得益於染色指針的幫助,能夠僅從引用上就能夠得知某個對象是否在分配集上,若是在複製時,一個用戶線程訪問了分配集中的對象就會被內存屏障截獲,而後根據轉發表將訪問轉發到新的對象上,並修正這個線程訪問該對象的引用,這個過程稱爲指針的自愈。

  6.併發重映射:這個階段要修正整個堆中指向重分配集中舊對象的全部引用。這個階段比較特殊,由於它不是迫切須要去執行的,上個階段的自愈過程就是針對某一對象的引用修正,因此即便沒有這一步也不會出現問題,只是第一次自愈有個轉發過程會稍慢一點,後面也都正常了。正由於這種不迫切,ZGC巧妙的把這步工做合併到了併發標記過程中,由於併發標記也須要遍歷全部對象,這一步也須要修正全部舊對象的引用。

  ZGC的一大問題就是其暫時尚未分代收集,這限制了它能承受的對象分配速率不會過高。若是長時間的回收速率比不上分配速率,產生的浮動垃圾愈來愈多,可分配的空間也愈來愈小了。因此要從根本上解決這個問題仍是要引入分代收集,讓新生代專門去存儲這些頻繁回收建立的對象。

 

虛擬機性能監控、故障處理

  1. jps:虛擬機進程情況工具,能夠列出正在運行的虛擬機進程。選擇參數:-l:進程主類全名;-v:虛擬機進程啓動時的參數。-m:進程啓動時傳給main函數的參數;
  2. jstat:監視虛擬機各類運行狀態信息的命令行工具。能夠顯示本地或者遠程虛擬機進程中的類加載、內存、垃圾收集、即時編譯等運行時數據。選擇參數:-class:監視類加載、卸載數量、總空間等。-gc:監視Java堆情況。更多選項看P142
  3. jinfo:實時查看和調整虛擬機的各項參數,上面說到的jps -v能夠看虛擬機啓動時顯式指定的參數,虛擬機默認的參數能夠用jinfo -flag查看。
  4. jmap:用於生成堆轉儲快照(是一個Java進程在某個時間點上的內存快照。Heap Dump是有着多種類型的。不過整體上heap dump在觸發快照的時候都保存了java對象和類的信息。一般在寫heap dump文件前會觸發一次FullGC,因此heap dump文件中保存的是FullGC後留下的對象信息)。還能夠查詢finalize執行隊列、Java堆和方法區的詳細信息。選項參數看P144。常見如:-dump:生成轉儲快照;-finalizerinfo:查看finalize執行隊列;-heap:顯示Java堆詳細信息。
  5. jhat:這個命令與jmap搭配使用,用於分析jmap生成的堆轉儲快照。
  6. jstack:用於生成虛擬機當前時刻的線程快照(線程快照是虛擬機內每一條線程正在執行的方法堆棧的集合)。生成線程快照的目的一般是由於線程長時間停頓,如線程間死鎖、死循環、請求外部資源致使的長時間掛起等。選項:-F:當正常的輸出請求不被響應時,強制輸出線程堆棧。-l:顯示關於鎖的附加信息;-m:若是調用了本地方法,能夠線程C/C++的堆棧。

 

虛擬機類加載機制

7.2類加載的時機

  一個類從被加載到卸載出內存,一共要通過七個階段:加載-鏈接(包括:驗證-準備-解析)-初始化-使用-卸載。

一個類何時進行加載並無強制約束,可是初始化有且只有六種狀況下才能進行,如使用new關鍵字實例化對象時、讀取一個靜態字段時、經過反射調用一個類時、子類要初始化時父類必須也初始化等,詳細的見P264.總結來講,就是對類型的主動引用,纔會去進行初始化。用到的時候纔去初始化,這也符合咱們的正常思惟。固然初始化前確定要進行前面幾步,可是何時加載是沒有限制的。

7.3類加載的過程

  1.加載:這個加載是整個流程的第一步,與標題的類加載不是同一個意思。這一步主要作三件事:1.1獲取此類的二進制字節流。1.2將字節流中表明靜態存儲結構轉化爲方法區的運行時數據結構。1.3生成一個表明這個類的Class對象,做爲方法區這個類各個數據的訪問入口。Java虛擬機規範並無對這三件事作很嚴格的要求,好比獲取二進制字節流,並無要求必定要從Class文件中獲取,因此有了如今的jar包、war包等從壓縮文件中讀取。也能夠從其餘文件裏讀取,好比jsp文件。

  2.驗證:這個階段很是重要,工做量也在整個流程當中佔至關大一部分。這一階段要確保字節流中的信息符合規範要求,不存在危害虛擬機的代碼。若是僅在Java代碼層面,是很難作出不合規範的操做,好比訪問數組邊界外的數據等等,編譯器都會拋出異常,拒絕編譯。整個階段主要有如下四個檢驗動做:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。文件格式驗證:驗證字節流是否符合Class文件格式的規範,好比是否以魔數0xCAFEBABE開頭、代碼版本號是否在當前虛擬機接受範圍內等。元數據驗證:主要是對類的元數據信息進行語義驗證,保證要符合Java語言規範。好比這個類是否有父類,除了Object,每一個類都應該有父類等。字節碼驗證:對方法體進行校驗,保證方法在運行時不會危害虛擬機,如保證在任什麼時候候都不會跳到方法體之外的字節碼指令上等。符號引用驗證:這個驗證主要是確保解析行爲能正常執行,這個驗證會發生在解析階段,即符號引用轉爲直接引用,如檢查符號引用中經過全限定名是否能找到對應的類等。

  3.準備:這個階段是爲類變量分配內存(方法區)和設置初始值。注意這裏說的是類變量,即被static修飾的,而不包括其餘的變量,其餘的變量會在這個類實例化時隨着這個類的對象一塊兒分配。還有一點就是,初始值是零值,也就是一些基本數據類型的默認值,好比int是0,即便語句以下:static int value=123;初始化是0,而不是123.除非是常量,如static final int value=123,那麼它的初始值就是123.

  4.解析:這個階段是將常量池內的符號引用替換爲直接引用。符號引用:用一組符號來描述所引用的目標,能夠是任何形式的字面量,只要能定位到目標便可。直接引用:能夠是直接指向目標的指針,或者一個句柄,總之就是能直接定位到目標,並且只要有了直接引用,那麼虛擬機內存中必定就有該引用目標。

  5.初始化:是類加載的最後一步,在準備階段時已經爲變量設置了初始零值,這個階段會根據程序代碼初始化變量和其餘資源。這個階段才從咱們編碼角度進行真正的初始化。初始化階段其實就是執行類構造器<clinit>()方法的過程。這個方法並非由程序員去編寫的,而是Javac編譯器自動生成的。是由編譯器自動收集類中全部類變量的賦值動做和靜態語句塊中的語句合併生成的,收集順序就是代碼的編寫順序。所以<clinit>()方法也不是類和接口必須的,若是一個類中沒有類變量的賦值語句和靜態語句塊,也就不會有這個方法了。若是一個類生成了這個方法,即便是多個線程同時調用了,執行<clinit>()也只能有一個線程,其餘線程會被阻塞。

7.4類加載器

  Java虛擬機的設計團隊有意把類加載階段中獲取一個類的二進制字節流這個動做放到Java虛擬機外部去實現。而實現這個動做的代碼被稱爲「類加載器」。在Java虛擬機的角度看,只有兩種兩種不一樣的類加載器:1.啓動類加載器,使用C++實現,是Java虛擬機的一部分。2.其餘全部類加載器,這個類加載器由Java語言實現,獨立存在Java虛擬機以外,而且要所有繼承啓動類加載器。可是在Java開發人員的角度看,類加載器應該被分的更細一點,自JDK1.2以來Java一直保持這三層類加載器、雙親委派的類加載架構。下面說到的是JDK8及以前版本的三層類加載器和雙親委派模型。

  啓動類加載器:這個類加載器負責加載存放在JAVA_HOME\lib目錄、或者被-Xbootclasspath參數指定的路徑的類庫。而且是Java虛擬機可以識別類庫,即按名稱識別,若是名稱不符合要求,即便在這個目錄中也不會被加載。

  擴展類加載器:這個類加載器是Java代碼實現的。負責加載<JAVA_HOME>\lib\ext目錄中、或被java.ext.dirs系統變量指定的路徑中全部的類庫。就如其名,用戶能夠將一些通用的類庫放到ext目錄中,以拓展JAVA SE的功能。

  應用程序類加載器:負責加載ClassPath即用戶路徑下全部的類庫,若是沒有自定義的類加載器,通常狀況下,這將是默認的類加載器。

 加上用戶自定義的類加載器,各個類加載的協做關係一般如P283的圖所示,即從自定義類加載器——>應用程序類加載器——>擴展類加載——>啓動類加載。層層遞進。而這種關係,被稱爲類的雙親委派模型。

  雙親委派模型的工做過程以下:一個類加載器收到加載一個類的請求後,本身不會去加載,而是請求委派給父類加載器,如上箭頭同樣,層層遞進,直到父類沒法完成這個加載請求時(它的執行目錄下沒找到該類)本身才會去加載。

這麼作的好處就是,若是用戶本身編寫一個與Java類庫中重名的Java類,好比Object類,而各加載器都各自加載,那系統中就會出現多個不一樣的Object類,後果確定是混亂的。有了雙親委派模型後,Object類會一直被委派到啓動類加載器中去執行,若是這個時候用戶再寫一個Object類,最後也到達啓動類加載器時,會先根據類名判斷這個類是否被加載過,若是被加載過就再也不加載。能夠看P284雙親委派模型的實現源碼,第一句就是根據name判斷是否加載過,很好的杜絕了上面的那個問題。

 

 

Java內存模型與線程

 12.3內存模型

  首先要明白一點,這裏所說的Java內存模型與前面說到的Java堆、Java棧等不是一個層次的對內存的劃分。Java堆等區域是Java虛擬機所管理的內存中運行時的數據區域。其實這全部的劃分在物理機角度上是不存在的,只是邏輯上的劃分,是Java虛擬機爲了方便管理內存而設計的,就像Java堆裏還分爲老年代和新生代。看完前面的章節知道了運行時的幾個數據區域的做用很是多也很是重要,而Java內存模型的主要目的是爲了定義程序中各類變量的訪問規則。不一樣的硬件和操做系統它們對內存的訪問規則均可能有所不一樣,爲了屏蔽這種差別,就有了Java內存模型。

  上面說到的對變量的訪問規則,這裏的變量並非指代碼裏面的全部變量,而是包括成員變量、類變量和構成數組對象的元素。不包括局部變量、方法參數。由於後者是線程私有的,不存在競爭問題。Java內存模型主要分爲主內存和工做內存,上面規定的變量都存在主內存中,每條線程有本身私有的工做內存。工做內存中保存着該線程當前操做的變量在主內存中的副本。線程操做變量時,會從主內存複製一份到本身的工做內存,修改完後再把新值賦值到主內存中的變量。

  內存間的交互Java內存模型定義了8種操做來完成,這8種操做都是原子性的。

  1.lock:做用在主內存,標識一個變量爲線程獨佔。2.unlock:做用於主內存,把線程獨佔的變量釋放出來。

  3.read:做用於主內存,把一個變量的值傳輸到線程的工做內存。4.load:做用於工做內存,把read到的值放入到副本中。

  5.use:做用於工做內存,把工做內存中變量的值傳給執行引擎。咱們在獲取一個變量的值時就是這個操做。

  6.assign:把執行引擎傳來的值賦值給工做內存中的變量。咱們給一個變量賦值時就是這個操做。

  7.store:把工做內存中變量的值傳給主內存。8.write:把store傳來的值放入到主內存對應的變量中。

  除了這8種操做,還有一些對這8種操做的規定。如lock標識的變量其餘線程不能使用。上面的原子操做每每是須要兩個一塊兒配合才能完成一個完整的步驟的,因此還有些規則規定這些原子操做間的配合不能不合邏輯,有衝突。如read後不load,assign後就無論了等。詳情看P443。

  針對volatile修飾的變量的特殊規則:

  volatile有兩個做用:1.volatile變量對全部線程是當即可見的,即volatile變量的全部操做都能當即反映到其餘線程之中。這是普通變量不具有的,普通變量被一個線程修改後,必需要被該線程傳回主內存,而其餘線程必須讀取主內存中這個變量後才知道這個變量改變了。2.禁止指令重排序優化,指令重排序即處理器會把多條指令分發給不一樣的電路單元進行處理,有時候這種處理順序不必定是程序上的順序,但不會打亂有先後關聯的兩個指令。好比一個變量A,第一條指令是A+10,第二條指令是A*2,第三條指令是B-3,顯然第一條指令和第二條指令不能打亂順序,而第三條指令跟它們沒有任何關聯,因此是放在它們前面仍是後面都沒有影響。

  因此這些特殊規則也都是爲了知足上述的volatile的兩個做用。好比線程use一個變量前必須load,這就是爲何volatile變量是當即可見的;線程執行了assign,才能執行store,這是爲了保證每次修改都能同步到主內存中,才能保證其餘線程能當即看到改變。詳情見P449.

 

  最後一點內容與線程有關,但提到的並非不少。關於線程的筆記會在《Java併發編程的藝術》中再記。該篇筆記總計一萬兩千字左右,在看完整本書後,做爲理論部分的複習筆記也是不錯的。若是之後對Java虛擬機有更深入或者其餘的理解,也會隨時更新到這個筆記中。

相關文章
相關標籤/搜索