Java虛擬機學習記錄(內存劃分、垃圾回收、類加載等機制)

一直以來以爲虛擬機是Java最難的一部分,涉及最底層的原理,學起來難度很大,並且工做中基本上用不到這些原理,因此對這部分「敬而遠之」。現現在工做五年了,從Java基礎到算法、數據結構、網絡、數據庫、設計模式都有涉獵,虛擬機部分在腦海裏仍是空空蕩蕩,連常常被談起的垃圾回收機制都不瞭解,實在是慚愧。瞭解虛擬機通往高級Java程序員的必由之路,同時學好虛擬機也能提升咱們代碼的質量,知道對象是怎麼建立的,放在哪裏,怎麼執行,怎麼回收的?明白這些問題讓咱們在程序的世界裏當一個「明白人」。java

1、Java內存區域

學習java時都知道Java內存分爲兩大快堆和棧,堆存放對象實例和數組對象,棧存放基本數據類型和對象的引用,這樣有點籠統,實際這裏說的堆指的是圖中左邊的Java堆,棧指的是本地方法棧,更具體的應該是棧裏面棧幀的局部變量表。程序員

內存區域總共分兩大塊:左邊的堆內存區域和右邊的棧內存加計數器,左邊的堆內存是線程共享的,只有一份;右邊部分每一個線程獨立一份,隨線程而生,隨線程而滅,是線程運行的內存區域。算法

  1. Java堆:是程序中內存管理最大的一部分,主要存放Java中的對象的實例、數組,堆裏面爲了內存回收方便化分了老年代和新生代區域。數據庫

  2. 方法區:方法區也能夠理解爲常說的永久代,和堆相似,只是邏輯上存放的數據不一樣,主要存放被虛擬機加載的類信息、常量、靜態變量、緩存的常量池等。既然是永久代,通常方法去的內存不多被回收,相對來講最穩定。設計模式

  3. 虛擬機棧:存放線程運行時的上下文信息,棧內部包括棧幀,每一個棧幀表明一個方法調用,方法的調用體如今棧幀的入棧和出棧,每一個棧幀內部都存在一個局部變量表,用於存放方法內的變量,包括基本數據類型和引用數據類型,引用數據類型時這裏只存放引用,地址指向的是堆中的一塊內存區域。數組

  4. 本地方法棧:與虛擬機棧相似,不一樣爲這裏存放的是本地方法調用的運行數據,在java中聲明的native方法。緩存

  5. 程序計數器:用於記錄當前線程執行到那個位置,線程內執行流程的控制依賴程序計算器來完成。安全

2、垃圾回收與內存分配

虛擬機從加載程序到運行程序都要進行內存分配,分配的時候也伴隨的內存的回收,當對象「已死」(無引用)的時候進行回收。網絡

一、對象可回收的兩種判斷算法數據結構

如何判斷對象已死呢,通常有兩種方式:

  • 引用計數算法:經過創建對象引用的計算器,每增長一個引用引用數+1;引用失效時-1;引用數爲0表明這個時候這個對象已經沒有被用到了,能夠回收。

  • 可達性分析算法:經過路徑查找的方式判斷對象是否能夠到達,經過維護一個「GC Roots」集合表明頂層對象,在此頂層對象的「引用鏈」以外的對象,說明是一個不能到達的對象,能夠放心回收了。

引用計數算法和可達性分析算法各有利弊,引用計數算法實現起來簡單,可是須要維護一個引用計數,更新的次數太頻繁,並且引用計數表也須要佔必定內存;可達性分析是相對更廣泛的一種實現方法,在回收時再進行一次檢查,不用每次引用發生變化時發生更新,缺點是實現起來更復雜,維護「GC Roots」的算法比較複雜。

二、垃圾收集算法

通常虛擬機實現都採用了分代的方法,把內存劃分了老年代和新生代,老年代存放的是相對穩定的對象;新生代存放的是活躍的對象,短時間須要回收的。針對這兩類的特色分別做出不一樣的策略,提升回收的效率。

  • 標記-清除算法:最基礎的一個算法,第一步先標記出須要回收的對象,而後統一清除。標記清除有兩個缺點:第一,執行效率不穩定,若是大部分都是須要回收的對象,標記清除效率較低;第二,清除後會形成內存的不連續,大量的碎片,若是建立一個大對象沒有連續的內存又須要執行垃圾回收。

  • 標記-複製算法:標記複製算法是爲了不標記清除算法對於大部分對象須要回收執行效率率低的問題,把內存區域劃分了兩部分,把須要回收的一部分複製到另一邊,而後執行整塊區域的回收,兩塊區域交替的使用。這種算法缺點是浪費了一半內存空間,因此有一個優化的方案,把內存區域拆分紅三快,一塊Eden兩塊Survivor,HotSpot的二者比例是8:1,Eden存放新分配的對象,每次回收時把存放的對象複製到其中一塊空閒的Survivor,清除Eden另一塊Survivor空間,交替的使用、清除Survivor空間;這種狀況下存放數據的區域有90%,只有10%的空間浪費,空間利用很好,可是須要考慮當存活的對象大於10%時,這種狀況就須要借用老年代,把它分配到老年代。

  • 標記-整理算法:整理算法是在標記清除和標記複製之間折中的一種算法,使用標記清除,可是按期整理,把不連續的內存整理到一塊去,解決了內存的碎片和空間上的浪費。缺點是每次整理是一個很負重的操做,會形成用戶程序的暫停。

這三種算法中,標記清除和標記整理適合老年代,須要回收的對象佔少部分的狀況;標記複製算法適合新生代,每次絕大部分對象須要回收,只須要把小量存活的挪到另外一塊位置。

三、內存分配的幾條策略

  • 大多數狀況下對象在堆中的新生代Eden空間分配,當Eden沒有空間時會觸發一次GC。

  • 當Eden空間不夠或一個大的對象(例如大的數組)建立將分配到老年代。

  • 長期存活的新生代對象會轉移到老年代,在新生代的對象每熬過一次GC,年齡加1,默認15歲時將會移動到老年代。

3、類加載的過程

程序經過new、靜態方法、靜態字段引用、子父類的引用、反射調用等方式會觸發類的加載,把類的字節碼加載到虛擬機。加載流程:

  1. 加載:類的字節碼加載到虛擬機,經過類加載器加載到虛擬機,默認經過Java的引導類加載(Bootstrap),也能夠經過自定義的類加載器加載,加載的不必定必須是一個本地文件,只要是符合要求的二進制字節碼便可,能夠來源於網絡或數據庫。

  2. 驗證:驗證字節碼的正確性,是不是一個合格的字節碼文件,保證虛擬機的運行安全。

  3. 準備:分配內存和初始化零值。

  4. 解析:符號引用替換成直接引用,符號引用是字面量的形式,前面已經分配了內存,這裏替換成指向的內存地址。

  5. 初始化:類加載的最後一步,執行程序代碼裏的初始化,包括靜態代碼塊,構造方法,默認字段值。

4、Java內存模型

Java內存模型是定義了程序中變量的訪問規則。

每一個線程都有一個工做內存,工做內存經過讀寫操做和主內存交互,達到變量的共享。

交互操做:

  • lock和unclock: 對主內存的變量進行加鎖和解鎖,鎖定後其餘線程將不可操做。

  • read和load: read從主內存讀取一個變量到工做內存,load放入讀取的變量放到工做內存中。

  • store和write: store把一個工做內存的變量傳遞到主內存中,write把傳遞過來的變量寫入主內存。

  • use: 把一個工做內存中的變量傳遞給執行引擎使用。

  • assign: 把從執行引擎接收到的賦值給工做內存的變量。

相關文章
相關標籤/搜索