JVM內存分配及GC詳述

  在闡述JVM的內存區域之前,先來看下計算機的存儲單位。從小到大依次爲Bit,Byte,KB,MB,GB,TB。相鄰的單位相差2的10次方。

  計算機運行中的存儲元件主要分爲寄存器(位於CPU)和內存,寄存器和內存之間通過地址總線連接。地址總線的寬度影響了物理地址的索引範圍,因爲總線寬度決定了處理器一次可以從寄存器或內存中獲取多少個Bit,同時也決定了處理器最大可以尋址的地址空間。

  這些地址空間被劃分爲了內核空間和用戶空間,程序只能使用用戶空間的內存。內核空間主要是指操作系統運行時所使用的用於程序調度、虛擬內存的使用或者鏈接硬件資源的程序邏輯。區分內核空間和用戶空間的目的主要是從系統的穩定性的角度考慮的。Windows 32操作系統默認內核空間和用戶空間的比例是1:1,即2G內核空間、2G內存空間,32位Linux系統中默認比例則是1:3,即1G內核空間,3G內存空間。

  字長CPU的主要技術指標之一,指的是CPU一次能並行處理二進制的位數(Bit)。通常稱處理字長爲8位數據的CPU爲8位CPU,32位CPU就是在同一時間內處理字長爲32位的二進制數據。不過目前雖然CPU大多是64位的,但還是以32位字長運行。

  JVM(Java Virtual Machine,Java虛擬機)定義了程序在運行時需要使用到的內存區域,大致分爲5個部分:  

    1.Method Area(Non-Heap)(方法區) ——線程共享
    2.Heap(堆) ——線程共享
    3.Program Counter Register(程序計數器) ——非線程共享
    4.VM Stack(虛擬機棧)——非線程共享
    5.Native Method Stack ( 本地方法棧 )——非線程共享

   JVM運行的時候會分配好Method Area(方法區)和Heap(堆);JVM 每遇到一個線程,就爲其分配一個Program Counter Register(程序計數器), VM Stack(虛擬機棧)和Native Method Stack (本地方法棧), 當線程終止時,三者(虛擬機棧,本地方法棧和程序計數器)所佔用的內存空間也會被釋放掉。
  非線程共享的那三個區域的生命週期與所屬線程相同,而線程共享的區域與Java程序運行生命週期相同,這也是GC只發生在線程共享的區域(大部分發生在Heap上)的原因。

  

  另外,JVM在運行時爲數據分配區域,還會對不適用的數據予以垃圾回收(GC),從而確保程序運行中對內存的正常需求。那麼,JVM在進行垃圾回收時,哪些內存會被回收呢?主要是針對不再被任何場景使用的對象。JVM在進行垃圾回收時主要在堆區以及方法區內進行,其中尤以回收堆區中不適用的對象爲多。

  一.線程共享的內存區域

  1.Method Area(方法區)

  (1)《Java虛擬機規範》只是規定了有方法區這麼個概念和它的作用,但是並沒有規定如何去實現它。一般來說,在Java8以前,方法區通過永久代(PermGen)實現;從Java8開始,Java廢棄了永久代(方法區的實現),並替換爲Metaspace(元空間,位於本地內存中)。方法區內很少發生垃圾回收,在這裏進行的GC主要是方法區裏的常量池和類型的卸載。    

元空間是方法區在HotSpot jvm 中的實現,方法區主要用於存儲類的信息、常量池、方法數據、方法代碼等。方法區邏輯上屬於堆的一部分,但是爲了與堆進行區分,通常又叫“非堆”。
元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存,它在理論上取決於32位/64位系統可虛擬的內存大小。

     (2)方法區主要用來存儲已被虛擬機加載的類信息、常量、靜態變量和即時編譯後的代碼等數據。
    (3)方法區裏有一個運行時常量池(Runtime Constant Pool),用於存放靜態編譯產生的字面量和符號引用。運行時生成的常量也會存在這個常量池中。比如String類的intern()方法。

  GC在方法區內主要回收廢棄常量和無用的類。廢棄常量是指沒有再被符號引用的常量;無用的類要同時滿足三個條件:該類所有的實例都被回收;加載該類的ClassLoader已被回收;該類對應的java.lang.Class對象未在任何地方被引用,無法在任何地方通過反射訪問該類。

  2.Heap(堆)

  堆空間在虛擬機啓動時創建,幾乎所有的對象實例都在這裏創建,因此該區域經常發生垃圾回收操作。
  堆空間分爲新生代(有時也稱爲“年輕代”)和老年代。剛創建的對象存放在新生代,而老年代中存放生命週期長久的實例對象。新生代中又被分爲Eden區(聖經中的伊甸園)和兩個Survivor區(From Space和To Space)。新的對象分配是首先放在Eden區,Survivor區作爲Eden區和Old區的緩衝,在Survivor區的對象經歷若干次收集仍然存活的,就會被轉移到老年代。一般來說,位於老年代的對象不易被回收,這是因爲該區域內的對象存活率極高。

  在JDK1.2之前,Java中引用的定義很傳統:如果引用類型的數據中存儲的數值代表的是另一塊內存的起始地址,就稱這塊內存代表着一個引用。這種定義很純粹,也過於狹隘,一個對象只有被引用或者沒被引用兩種狀態。

  在JDK1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用4種,這4種引用強度依次減弱。
  ①強引用
  代碼中普遍存在的類似"Object obj = new Object()"這類的引用,只要強引用還存在,GC    垃圾收集器永遠不會回收掉被引用的對象。
  ②軟引用
  描述有些還有用但並非必需的對象。在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍進行二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。Java中的類SoftReference表示軟引用。
  ③弱引用
  描述非必需對象。被弱引用關聯的對象只能生存到下一次垃圾回收之前,垃圾收集器工作之後,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。Java中的類WeakReference表示弱引用。
  ④虛引用
  這個引用存在的唯一目的就是在這個對象被收集器回收時收到一個系統通知,被虛引用關聯的對象,和其生存時間完全沒關係。Java中的類PhantomReference表示虛引用。

  GC(Garbage Collection,垃圾收集)在堆中進行垃圾回收時,當一個對象大於eden區而小於old區(老年代)時,GC會將其直接扔到old區。當對象大於old區時,會直接拋出OutOfMemoryError(OOM)。

  一般來說,GC回收的是已經沒有引用的對象,那怎麼判斷一個對象沒有引用?這裏需要簡單介紹2種方法:引用計數法和可達性分析算法;

    這裏簡單說一下引用計數法:對象中添加一個引用計數器,每當有一個地方引用,計數器就增加1,引用失效就減少1,計數器爲0就不可用;缺點在於無法處理對象直接相互引用的問題,因爲相互引用以後無法使計數器爲0,所以無法回收。

    可達性分析算法,也就是我們常說的GC Root,,當一個對象沒有與任何引用鏈相連的時候,就可以對該對象進行回收,下面是Java中GC Root對象使用的幾個地方:

  有三種場景會觸發GC:第一種是當年輕代或者老年代滿了,Java虛擬機無法再爲新的對象分配內存空間了,那麼Java虛擬機就會觸發一次GC去回收掉那些已經不會再被使用到的對象;第二種是手動調用System.gc()方法,通常這樣會觸發一次Full GC以及至少一次的Minor GC;第三種是程序運行的時候有一條低優先級的GC線程,它是一條守護線程,當這條線程處於運行狀態的時候,自然就觸發了一次GC。

(1)新生代GC(Minor GC)
    指發生在新生代的垃圾收集動作,因爲大多數Java對象存活率都不高,所以Minor GC非常頻繁,一般回收速度也比較快。
(2)老年代GC(Major GC/Full GC)
    指發生在老年代的垃圾收集動作,出現了Major GC,經常會伴隨至少一次的Minor GC(但並不是絕對的)。Major GC的速度一般要比Minor GC慢上10倍以上。 

  二.線程私有的內存區域

  1.VM Stack(虛擬機棧)

  虛擬機棧也就是我們平常所稱的棧內存,它爲java方法服務,每個方法在執行的時候都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接和方法出口等信息。       

棧幀是一個內存區塊,是一個數據集,是一個有關方法(Method)和運行期數據的數據集,當一個方法 A 被調用時就產生了一個棧幀 F1,並被壓入到棧中,A 方法又調用了 B 方法,於是產生棧幀 F2 也被壓入棧,執行完畢後,先彈出 F2棧幀,再彈出 F1 棧幀,遵循“先進後出”原則。
棧幀保存了創建棧幀的方法的返回地址和局部變量。每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接。動態鏈接就是將常量池中的符號引用在運行期轉化爲直接引用。

   棧內存的生命期是跟隨線程的生命週期的,線程結束棧內存也就釋放,對於棧來說不存在垃圾回收問題,只要線程一結束,該棧就 Over,所以不存在垃圾回收。

  局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象的引用(reference類型,不等同於對象本身,根據不同的虛擬機實現,可能是一個指向對象起始地址的引用指針,也可能是一個代表對象的句柄或者其他與對象相關的位置)和 returnAdress類型(指向下一條字節碼指令的地址)。局部變量表所需的內存空間在編譯期間完成分配,在方法運行之前,該局部變量表所需要的內存空間是固定的,運行期間也不會改變。

  2.Native Method Stack(本地方法棧)

  本地方法棧和虛擬機棧類似,只不過本地方法棧爲Native方法服務。

  3.Program Counter Register(程序計數器)

  代表着當前線程所執行字節碼的行號指示器。分支、循環、跳轉、異常處理和線程恢復等功能都需要依賴這個計數器完成。程序計數器是唯一一個java虛擬機規範沒有規定任何OOM(Out Of Memory)情況的區域。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章