深入理解jvm內存模型以及gc原理

2019-10-10     IT技術分享

整體架構

Jvm = 類加載器 + 執行引擎 + 運行時數據區域

類加載器

作用

類加載器是將編譯好的class文件加載到內存中,並進行驗證、初始化等步驟,形成能被jvm直接使用的類型。

加載過程

可分解為5個步驟:加載–>連接–>初始化 –> 使用 –> 卸載。


  • 加載:把class文件以二進位位元組流的形式存儲到方法區中,並在堆中創建對應的class對象。
  • 連接:連接過程又分為3步,驗證、準備、解析。

驗證:驗證文件格式、元數據、位元組碼是否符合規範。

準備:為成員變量分配內存並初始化值。

解析: 解析是虛擬機將常量池的符號引用替換為直接引用的過程。

  • 初始化:初始化過程主要包括執行構造方法,初始化靜態變量、靜態塊。

執行引擎

其作用是將class位元組碼轉變成機器能識別的碼,然後在jvm中創建方法棧去執行方法。

運行時數據區

運行時數據區包含方法區、堆、虛擬機棧、本地方法棧、程序計數器 。如下圖:


方法區

方 法區同堆一樣,是所有線程共享的內存區域,為了區分堆,又被稱為非堆。 用於存儲已被虛擬機加載的類信息、常量、靜態變量,如static修飾的變量加載類的時候就被加載到方法區 中。

簡單的說就是對象的存儲區,它是被所有線程共享的一塊區域 堆是java虛擬機管理內存最大的一塊內存區域,因為堆存放的對象是線程共享的,所以多線程的時候也需要同步機制。堆回收算法使用的複製算法效率高沒有碎片利用率低分為三個區 eden 、 S0、 S1 按照8:1:1的默認值。

程序計數器

我們知道對於一個處理器,在一個確定的時刻都會執行一條線程中的指令,一條線程中有多個指令,為了線程切換可以恢復到正確執行位置,每個線程都需有獨 立的一個程序計數器,不同線程之間的程序計數器互不影響,獨立存儲。

虛擬機 棧

虛擬機棧指的是java方 法執行的內存概念模型,每個方法執行時都會在棧內存裡面創建一個棧幀,棧幀用來存儲局 部變量表、操作數棧、動態連結、方法出口等信息。 每個方法從調用到執行完成,都會對應一個棧幀在虛擬機中入棧到出棧的過程。

本地方法棧

本地方法棧與虛擬機棧功能相似,是為虛擬機使用的Native方法服務。有的虛擬機可能會把這兩個棧合二為一。

程序計數器

我們知道對於一個處理器,在一個確定的時刻都會執行一條線程中的指令,一條線程中有多個指令,為了線程切換可以恢復到正確執行位置,每個線程都需有獨 立的一個程序計數器,不同線程之間的程序計數器互不影響,獨立存儲。

目前比較常用的gc回收是年代回收法,JVM將堆分成了二個大區新生代 、老年代 和持久代。新生代和老年代的內存區域是在堆上也是gc回收的主要區域,默認情況新生代與老年代比例為1:2,該值可以通過參數–XX:NewRatio 設定。 持久代是在方法區,持久代存放一些一般不需要被回收的對象,持久代一般情況不會觸發GC。

新生代

新生代又分為Eden和Survivor區,而Survivor由S0 和 S1 組成。,新生代默認分配是 eden:S0:S1為8:1:1, 該值可以通過參數 –XX:SurvivorRatio 來設定。 新生代採用的是複製回收算法,當第一次產生對象是在eden區分配空間,當eden區空間滿了時候,會在S0區域分配空間,當S0空間滿了時候會觸發Minor GC , 這時候會把eden區和S0區存活對象複製出來放在S1區,然後直接清空eden區和S0區,新生代就是這麼反覆的進行垃圾回收。

老年代

老年代用於存放經過多次Minor GC之後依然存活的對象 ,在年代回收法對象有個年齡的概念,在新生代每進行一次 Minor GC仍然存活的對象年齡都會加1,當對象年齡達到一定的值就會進入老年代區域, 默認的值是15 ,可以通過參數-XX:MaxTenuringThreshold 來設定 。 還有一種情況是當對象特別大時候不需要達到設定值會直接進入老年代。老年代由於對象比較穩定所以老年代採用標記整理算法進行Full GC,此算法會減少內存碎片帶來的效率損耗,下面會重點介紹一下本算法。

垃圾收集算法

. (標記-清除)算法

這是最基礎的算法,標記-清除算法分為「標記」和「清除」兩個階段:首先標記出所有需要回收的對象,標記完成後統一回收所有被標記的對象。 這種算法的缺點是會產生內存碎片而且效率也不高。下圖是此算法的執行過程。


. (複製)算法

為了解決 (標記-清除) 算法的缺陷,複製算法就被提了出來。它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象複製到另外一塊上面,這種算法雖然實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。很顯然,複製算法的效率跟存活對象的數目多少有很大的關係,如果存活對象很多,那麼複製 算法的效率將會大大降低。我們的新生代GC算法採用的是這種算法 。複製算法執行過程如下圖:


. (標記-整理)算法

因為複製算法效率低,清除算法會產生內存碎片,所以又產生了 了(標記-整理)算法。該算法標記階段和(標記-清除) 一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後回收被標記的對象,此算法的好處是效率高,同時不會產生內存碎片。 標記-整理 算法執行過程如下圖:


內存泄漏

在我們平時寫代碼時候很容易發生gc沒有及時回收的情況,這時就會發生內存泄漏情況,下面介紹一個內存泄漏的例子。

Public class test{
Public static Map map = new HashMap();
Public void insert(){
for (int i=1;i<100; i++){
Object o=new Object()
map.put(i,o);
}
}
}

在這個例子中, 由於map是靜態的,所以gc不會回收,當執行insert方法時候,進入for循環,聲明o對象,然後放進map裡面,執行完此方法o對象原本可以被gc回收,但是由於map是靜態的所以不會被回收,這樣就會導致內存泄漏,所以我們在寫代碼時候一定要謹慎使用常量和靜態變量這類型的變量,可能在不經意間造成內存泄漏情況。

jvm以及gc都是我們寫代碼和設計程序時經常能涉及到的知識,深入的學習一下jvm和gc能提高對jvm調優的能力,也可以讓自己寫出更優雅的代碼。提高自己的java水平。

文章來源: https://twgreatdaily.com/zh-tw/gGQTtm0BMH2_cNUg4Txf.html