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

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

一、Java內存區域

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

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

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

  1. Java堆:是程序中內存管理最大的一部分,主要存放Java中的對象的實例、數組,堆裡面為了內存回收方便化分了老年代和新生代區域。
  2. 方法區:方法區也可以理解為常說的永久代,和堆類似,只是邏輯上存放的數據不同,主要存放被虛擬機加載的類信息、常量、靜態變量、緩存的常量池等。既然是永久代,一般方法去的內存很少被回收,相對來說最穩定。
  3. 虛擬機棧:存放線程運行時的上下文信息,棧內部包括棧幀,每個棧幀代表一個方法調用,方法的調用體現在棧幀的入棧和出棧,每個棧幀內部都存在一個局部變量表,用於存放方法內的變量,包括基本數據類型和引用數據類型,引用數據類型時這裡只存放引用,地址指向的是堆中的一塊內存區域。
  4. 本地方法棧:與虛擬機棧類似,不同為這裡存放的是本地方法調用的運行數據,在java中聲明的native方法。
  5. 程序計數器:用於記錄當前線程執行到那個位置,線程內執行流程的控制依賴程序計算器來完成。

二、垃圾回收與內存分配

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

1、對象可回收的兩種判斷算法

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

  • 引用計數算法:通過建立對象引用的計算器,每增加一個引用引用數+1;引用失效時-1;引用數為0代表這個時候這個對象已經沒有被用到了,可以回收。
  • 可達性分析算法:通過路徑查找的方式判斷對象是否可以到達,通過維護一個“GC Roots”集合代表頂層對象,在此頂層對象的“引用鏈”之外的對象,說明是一個不能到達的對象,可以放心回收了。

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

2、垃圾收集算法

一般虛擬機實現都採用了分代的方法,把內存劃分了老年代和新生代,老年代存放的是相對穩定的對象;新生代存放的是活躍的對象,短期需要回收的。針對這兩類的特點分別作出不同的策略,提高回收的效率。

  • 標記-清除算法:最基礎的一個算法,第一步先標記出需要回收的對象,然後統一清除。標記清除有兩個缺點:第一,執行效率不穩定,如果大部分都是需要回收的對象,標記清除效率較低;第二,清除後會造成內存的不連續,大量的碎片,如果創建一個大對象沒有連續的內存又需要執行垃圾回收。
  • 標記-複製算法:標記複製算法是為了避免標記清除算法對於大部分對象需要回收執行效率率低的問題,把內存區域劃分了兩部分,把需要回收的一部分複製到另外一邊,然後執行整塊區域的回收,兩塊區域交替的使用。這種算法缺點是浪費了一半內存空間,所以有一個優化的方案,把內存區域拆分成三塊,一塊Eden兩塊Survivor,HotSpot的兩者比例是8:1,Eden存放新分配的對象,每次回收時把存放的對象複製到其中一塊空閒的Survivor,清除Eden另外一塊Survivor空間,交替的使用、清除Survivor空間;這種情況下存放數據的區域有90%,只有10%的空間浪費,空間利用很好,但是需要考慮當存活的對象大於10%時,這種情況就需要借用老年代,把它分配到老年代。
  • 標記-整理算法:整理算法是在標記清除和標記複製之間折中的一種算法,使用標記清除,但是定期整理,把不連續的內存整理到一塊去,解決了內存的碎片和空間上的浪費。缺點是每次整理是一個很負重的操作,會造成用戶程序的暫停。

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

3、內存分配的幾條策略

  • 大多數情況下對象在堆中的新生代Eden空間分配,當Eden沒有空間時會觸發一次GC。
  • 當Eden空間不夠或一個大的對象(例如大的數組)創建將分配到老年代。
  • 長期存活的新生代對象會轉移到老年代,在新生代的對象每熬過一次GC,年齡加1,默認15歲時將會移動到老年代。

三、類加載的過程

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

  1. 加載:類的字節碼加載到虛擬機,通過類加載器加載到虛擬機,默認通過Java的引導類加載(Bootstrap),也可以通過自定義的類加載器加載,加載的不一定必須是一個本地文件,只要是符合要求的二進制字節碼即可,可以來源於網絡或數據庫。
  2. 驗證:驗證字節碼的正確性,是否是一個合格的字節碼文件,保證虛擬機的運行安全。
  3. 準備:分配內存和初始化零值。
  4. 解析:符號引用替換成直接引用,符號引用是字面量的形式,前面已經分配了內存,這裡替換成指向的內存地址。
  5. 初始化:類加載的最後一步,執行程序代碼裡的初始化,包括靜態代碼塊,構造方法,默認字段值。

四、Java內存模型

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

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

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

交互操作:

  • lock和unclock: 對主內存的變量進行加鎖和解鎖,鎖定後其他線程將不可操作。
  • read和load: read從主內存讀取一個變量到工作內存,load放入讀取的變量放到工作內存中。
  • store和write: store把一個工作內存的變量傳遞到主內存中,write把傳遞過來的變量寫入主內存。
  • use: 把一個工作內存中的變量傳遞給執行引擎使用。
  • assign: 把從執行引擎接收到的賦值給工作內存的變量。


分享到:


相關文章: