深入理解Java虛擬機---自動內存管理機制

寫在前面
本文介紹的Java虛擬機(JVM)的自動內存管理機制主要是參照《深入理解Java虛擬機》(第2版)一書中的內容,主要分為兩個部分:Java內存區(qū)域和內存溢出異常、垃圾回收和內存分配策略。因此我也會分為兩個部分來講解,但這并不代表這兩個部分在JVM中是分割的。反之,其實這兩個部分關聯性很強。只不過為了便于介紹,所以我才分開來講。在介紹它們詳細內容之前,我首先會給出兩幅思維導圖以便讀者可以了解一下里面所包含的內容,然后我會根據思維導圖中的知識點一一為大家進行介紹。

第一部分 Java內存區(qū)域和內存溢出異常

Java內存區(qū)域與內存溢出異常

下面我將對圖中所涉及到的部分進行介紹

運行時數據區(qū)域

由于直接內存(Direct Memory)并不是虛擬機運行時數據區(qū)的一部分,也不是Java虛擬機規(guī)范中定義的內存區(qū)域。但是這部分內存也被頻繁地使用,而且也可能導致內存溢出異常(OutOfMemoryError)出現,所以也放到這部分進行介紹。

Java虛擬機在執(zhí)行Java程序的過程中會把它所管理的內存劃分為若干個不同的數據區(qū)域。這些區(qū)域都有各自的用途以及創(chuàng)建和銷毀的時間。有的區(qū)域(線程共享的數據區(qū)域)隨著虛擬機的啟動而存在,有的區(qū)域(線程隔離的數據區(qū)域)則要依賴用戶線程的啟動和結束來創(chuàng)建或者是銷毀。

程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器。學過《計算機組成原理》這門課之后我們知道----在計算機中,其實程序計數器就是一個寄存器,依據不同計算機細節(jié)的差異,它可以存放當前正在被執(zhí)行的指令,也可以存放下一個要被執(zhí)行的指令。由此,我們可以對“當前線程所執(zhí)行的字節(jié)碼的行號指示器”有更好的理解。
在虛擬機的概念模型中,字節(jié)碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。由于Java虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內核)都只會執(zhí)行一條線程中的指令。因此為了線程切換之后能夠恢復到正確的執(zhí)行位置,每條線程都需要擁有一個獨立的程序計數器,各條線程之間計數器互補影響,獨立存儲。所以程序計數器是線程私有的內存(線程隔離)。
如果線程正在執(zhí)行的是一個Java方法,這個計數器記錄的就是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;如果正在執(zhí)行的是Native方法,那么這個計數器的值就為空(Undefined)。此內存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。

Java虛擬機棧

和程序計數器一樣,Java虛擬機棧(Java Virtual Machine Stack)也是線程私有的,即它的生命周期和線程的相同。虛擬機棧描述的是Java方法執(zhí)行的內存模型:每個方法在執(zhí)行時都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態(tài)鏈接、方法出口等信息。每一個方法從調用直至執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
我們常常說的棧內存其實就是現在講的虛擬機棧,或者說是虛擬機棧中局部變量表部分。
局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同于對象本身,可能是指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。
其中64位長度的long和double類型的數據會占用2個局部變量空間(Slot),其余數據類型只占用1個。局部變量表所需要的內存空間在編譯時期完成分配。當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別就是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。其實虛擬機規(guī)范中對本地方發(fā)棧中方法所使用的語言、使用方式以及數據結構都沒有強制規(guī)定,因此具體的虛擬機可以自由地實現它。甚至在有的虛擬機(如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二為一。與虛擬機棧一樣,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemory異常。

Java堆

對于大多數應用來說,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊數據區(qū)域,在虛擬機啟動時創(chuàng)建。此內存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。但是隨著JIT編譯器的發(fā)展與逃逸分析技術逐漸成熟,棧上分配、標量替換優(yōu)化技術將會導致一些微妙的變化發(fā)生,所有的對象都分配在堆上也逐漸變得不是那么“絕對”。
Java堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱為“GC堆”。Java堆還可以細分為新生代和老年代等等。這一部分在講垃圾回收算法的時候還會繼續(xù)介紹。
根據Java虛擬機規(guī)范規(guī)定,Java堆可以處于物理上不連續(xù)的內存空間中,即只要邏輯上是連續(xù)的即可,就像我們磁盤空間一樣。在實現時,可以固定大小,也可是可拓展的,主流的虛擬機都是按照可拓展來實現的(通過-Xmx和-Xms來控制)。如果在堆中沒有內存完成實例分配,并且堆也無法繼續(xù)拓展時,將會拋出OutOfMemortError異常。

方法區(qū)

方法區(qū)(Method Area)與Java堆一樣,是各個線程共享的內存區(qū)域,它用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數據。雖然Java虛擬機將其描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆)。目的是與Java堆區(qū)分開來。(以前很多人把方法區(qū)稱為永久代,現在JDK1.8中已經用元數據區(qū)域取代了永久代)。

運行時常量池

運行時常量池是方法區(qū)(Runtime Constant Pool)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池,用于存放編譯時期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區(qū)的運行時常量池中存放。Java虛擬機對于運行時常量池沒有做任何細節(jié)的要求。
運行時常量池具備動態(tài)性,Java語言并不要求常量一定只有編譯期才能產生,也就是并非預置入Class文件中常量池的內容才能進入方法區(qū)運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的便是String類的intern()方法。

直接內存

由于直接內存(Direct Memory)并不是虛擬機運行時數據區(qū)的一部分,也不是Java虛擬機規(guī)范中定義的內存區(qū)域。但是這部分內存也被頻繁地使用,而且也可能導致內存溢出異常(OutOfMemoryError)出現,所以也放到這部分進行介紹。
顯然,本機直接內存的分配不會受到Java堆大小的限制。但是肯定還是會受到本機總內存大小以及處理器尋址空間的限制。管理員在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,但經常忽略直接內存,使得各個內存區(qū)域總和大于物理內存限制(包括物理的和操作系統(tǒng)級的限制),從而導致動態(tài)拓展時出現OutOfMemoryError異常。

對象的創(chuàng)建方式

在Java程序當中每時每刻都有對象被創(chuàng)建出來。在語言層面上,創(chuàng)建對象通常僅僅是使用一個new關鍵字而已,而在虛擬機中,對象(僅限于普通Java對象)的創(chuàng)建又是怎樣一個過程呢?

虛擬機遇到一條new指令時,首先將去檢查這個指令的參數能否在常量池中定位到一個類的符號引用。并且檢查這個符號引用代表的類是否已經被加載、解析和初始化過。如果沒有,那就先執(zhí)行類加載的過程(關于類加載過程在后面的博客中會進行介紹)。

在類加載檢查通過后,接下來虛擬機將為新生對象分配內存。對象所需內存的大小在類加載完成之后便可完全確定(在對象的內存布局部分會介紹)。

為對象分配空間的任務等同于把一塊確定大小的內存從Java堆中劃分出來。有兩種方式:

  • 指針碰撞:假設Java堆中內存是規(guī)整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放著一個指針作為分界點的指示器,那分配內存就是將指針往空間空間挪動一段與對象大小相等的距離,這種分配內存的方式就被稱為指針碰撞;
  • 空閑列表:如果Java堆中的內存并不是規(guī)整的,已經使用的內存和空閑內存相互交錯,那就沒有辦法簡單地使用指針碰撞的方法進行內存分配了。虛擬機此時必須維護一個列表用來記錄哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間為分配給對象實例,并且更新列表上的記錄,這種分配方式就被稱為空閑列表。

選擇哪一種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
除了如何劃分可用空間之外,還要考慮的一個問題就是對象創(chuàng)建在虛擬機中是非常頻繁的行為,即使是僅僅修改一個指針的位置,在并發(fā)的情況之下也并不是線程安全的----可能出現正在給對象A分配內存,指針還沒來得及修改,對象B同時使用了原來的指針來分配內存的情況。解決方案也有兩種:

  • 一種是對分配內存空間的動作進行同步處理----實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性;
  • 另一種是把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱為本地線程緩沖分配(Thread Local Allocation Buffer,TLAB)。哪個線程需要分派內存,就在哪個線程的TLAB上分配,只有TLAB用完并分配新的TLAB時,才需要同步鎖定。虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。

內存分配完成之后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭),如果使用TLAB,則此工作可以提前至TLAB分配時進行。這一步操作保證了對象的實例字段在Java代碼中可以不賦初值就可以直接使用,程序能訪問到這些字段的數據類型所對應的零值。
接下來,虛擬機要對對象進行一些必要的設置,比如這個對象是哪個類的實例、如何才能找到類的元數據、對象的哈希碼、對象的GC分代年齡等信息。

在上面的工作完成之后,從虛擬機的角度來看,一個新的對象已經產生了。但從Java程序的角度來看,對象創(chuàng)建才剛剛開始----<init>方法還沒執(zhí)行,所有的字段都還為零。一般來說(由字節(jié)碼中是否跟隨invokespecial指令所決定),執(zhí)行new指令之后會接著執(zhí)行<init>方法,把對象按照程序員的意愿進行初始化,這樣一個真正的對象才算創(chuàng)建完成。

對象的內存布局

對象頭

  • 第一部分:用于存儲自身的運行時數據,包括哈希碼、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程ID、偏向時間戳等。
  • 第二部分:類型指針,即對象指向它的元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。不過并不是所有的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據信息并不一定要經過對象本身。另外,如果對象是一個Java數組,那在對象頭中還必須有一塊用于記錄數組長度的數據,因為虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是從數組的元數據中卻無法確定數組的大小。

實例數據
實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。這部分的存儲順序會受到虛擬機分配策略參數和字段在Java源碼中定義順序的影響。
對齊填充
對齊填充并不是必然存在的,也沒有特殊的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動內存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數倍,換句話說,就是對象的大小必須是8字節(jié)的整數倍。而對象頭部分正好是8字節(jié)的倍數(一倍或者兩倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

對象的訪問定位

建立對象是為了使用對象,我們的Java程序需要通過棧上的reference數據來操作堆上的具體對象。由于reference類型在Java虛擬機規(guī)范中只規(guī)定了一個指向對象的引用,并沒有定義這個引用應該通過何種方式去定位、訪問堆中的對象的具體位置,所以對象訪問方法也是取決于虛擬機的實現而決定的。目前主流的訪問方式有使用句柄和直接指針兩種。

通過句柄訪問對象

通過句柄訪問對象

優(yōu)點:reference存儲的是穩(wěn)定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要改變;
缺點:增加了一次指針定位的時間開銷。

通過直接指針訪問對象

通過直接指針訪問對象

優(yōu)點:節(jié)省了一次指針定位的開銷
缺點:在對象被移動時reference本身需要被修改。

常見的內存溢出異常

Java堆溢出
Java堆用于存儲對象實例,只要不停地創(chuàng)建對象,并且保證GC Roots到對象之間有可達路徑類避免垃圾回收機制清除這些對象對象,那么在對象數量達到最大堆的容量限制后就會產生內存溢出異常。
虛擬機棧和本地方法棧溢出
關于虛擬機棧和本地方法棧,在Java虛擬機規(guī)范中描述了兩種異常:

  • 如果線程請求的棧深度大于虛擬機所允許的最大深度,將拋出StackOverflowError異常;
  • 如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。

這里把異常分為兩種情況,看似較為嚴謹,但卻存在著一些互相重疊的地方:當??臻g無法繼續(xù)分配時,到底是已使用的??臻g太大,還是內存太小,其本質上都只是對同一件事情的兩種描述而已。
方法區(qū)和運行時常量池溢出
本機直接內存溢出

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

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

其實當我們在討論垃圾回收的時候,我們常常要思考垃圾收集(Garbage Collection)需要完成的三件事情:

  • 哪些內存需要回收?(What?)
  • 什么時候回收?(When?)
  • 如何回收?(How?)

那么對于Java虛擬機來說,垃圾收集主要是發(fā)生在哪些區(qū)域呢?
由于程序計數器、虛擬機棧、本地方法棧這三個區(qū)域是隨線程而生,隨線程而亡的;棧中的棧幀隨著方法的進入和退出有條不紊地執(zhí)行著入棧和出棧操作,每一個棧幀中分配多少內存基本上都是在類結構確定下來時就已知的。因此這幾個區(qū)域的內存分配和回收策略都具備確定性,在這幾個區(qū)域就不需要過多考慮回收的問題。因為方法結束或者線程結束之后。這部分內存自然也就隨著回收了。
但是Java堆和方法區(qū)則不一樣,因為一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存可能也不一樣,我們只有在程序運行期間才能知道到底會創(chuàng)建哪些對象,這部分內存的分配是動態(tài)的,是不確定的。所以我們要針對這兩塊區(qū)域制訂合適的垃圾收集策略。因此,在后面我們提到的對內存進行垃圾回收,說的主要也是針對Java堆和方法區(qū)這兩塊區(qū)域。

對象已死嗎?

在Java堆中存放著Java世界中幾乎所有的對象實例。垃圾收集器在進行垃圾收集行為之前,需要對這些對象進行判斷,看看哪些對象已經“死”了,哪些對象依然“存活”著。

引用計數法

很多書上用來判斷對象是否存活的方法是這樣的:給對象添加一個引用計數器,如果有一個地方引用它的時候,這個計數器就加一;當引用失效時,計數器就減一;任何時刻計數器為零的對象意味著它已經不能再被使用了。
引用計數法看起來很簡單,也很容易理解。但是主流的Java虛擬機中沒有選用引用計數法來對內存進行管理。很大一部分原因就是因為此算法不能解決兩個對象相互引用的問題。如果不相信的話,下面可以用程序驗證一下:

引用計數法的缺陷
運行結果

我們可以看到,如果虛擬機中采用的是引用計數法的話,那么objA和objB引用計數器的值都應不為零,故不應該發(fā)生垃圾回收。但是從運行結果來看,此時確實發(fā)生了垃圾回收行為。這也就驗證了在這里,Java虛擬機并不是采用引用計數法來管理內存。

可達性分析

可達性分析算法的基本思想是通過一些列被稱為“GC Roots”的對象作為起始點,然后從這些節(jié)點向下開始搜索,搜索走過的路徑被稱為引用鏈(Reference Chain),當某個對象(節(jié)點)到“GC Roots”之間不存在引用鏈的話,則證明此對象不可用。其實了解二叉樹的話這里就很好理解了:從根節(jié)點出發(fā),如果不能遍歷到某個對象,則此對象就不可用。
在Java語言中,可以作為GC Roots的對象有:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象;
  • 方法區(qū)中類靜態(tài)屬性引用的對象;
  • 方法區(qū)中常量引用的對象;
  • 本地方法棧中JNI(即常說的Native方法)引用的對象。

再談引用

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象的引用鏈是否可達,我們可以知道判定對象是否存活都與引用有關。
在JDK1.2之前,Java中的引用的定義很簡單粗暴:如果reference類型的數據中存儲的數值代表的是另一塊內存的起始地址,就稱這塊內存代表著一個引用。這種定義很簡單,但是太過于狹隘-----一個對象在這種定義之下就只有兩種狀態(tài):引用或者沒有引用。對于描述一些“食之無味,棄之可惜”的對象就顯得無能為力。
所以在JDK1.2之后,Java對引用的概念進行了擴充,將引用分為:

  • 強引用(Strong Reference)
    強引用就是在內存中普遍存在的。類似于“Object obj = new Object()”這樣的引用。只要強引用還存在,垃圾收集器就不會將被引用的對象回收。
  • 軟引用(Soft Reference)
    軟引用就是用來描述一些還有用但并非必需的對象。對于軟引用關聯著的對象,在系統(tǒng)將要發(fā)生內存溢出之前,將會把這些對象列入回收范圍中進行二次回收。如果這次回收還沒有得到足夠的內存,才會拋出內存溢出異常。SoftReference類可以實現軟引用。
  • 弱引用(Weak Reference)
    弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些。被弱引用關聯的對象只能存活到下一次垃圾回收發(fā)生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。WeakReference類可以實現弱引用
  • 虛引用(Phantom Reference)
    虛引用又被稱為幽靈引用或者是幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知。PhantomReference類可以實現虛引用。

生存 or 死亡

即使是在可達性分析算法中不可達的對象,其實也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,必須要經歷兩次過程:

  • 如果對象在進行可達性分析之后發(fā)現沒有與GC Roots相連接的引用鏈,那么它會被第一次標記并進行一次篩選。篩選的條件是此方法是否有必要執(zhí)行finalize()方法。當對象沒有覆蓋finalize()方法或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執(zhí)行”;
  • 如果這個對象被視為有必要執(zhí)行finalize()方法,那么這個對象將會放置在一個F-Queue的隊列之中,并在稍后由一個虛擬機自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行它。這里的“執(zhí)行”是指虛擬機會觸發(fā)這個方法,但并不會承諾等待它運行結束。這樣做的目的是防止一個對象在finalize()方法中執(zhí)行緩慢或者是發(fā)生了死循環(huán)從而導致F-Queue隊列中其他對象永久處于等待狀態(tài),甚至導致程序崩潰。

回收方法分區(qū)

許多人認為在方法區(qū)中不會發(fā)生垃圾回收行為。Java虛擬機規(guī)范中也說過可以不要求虛擬機在方法區(qū)實現垃圾回收。但是其實在方法區(qū)也是存在垃圾回收的,主要是針對兩部分:

  • 廢棄常量
  • 無用的類

判斷一個常量是否為廢棄常量是一件比較簡單的事情,而要判定一個類是否是“無用的類”的條件相對苛刻。類要同時滿足下面3個條件才能算是“無用的類”:

  • 該類所有的實例都已被回收,即Java堆中不存在此類的任何實例;
  • 加載該類的ClassLoader已被回收;
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

垃圾收集算法

這里主要介紹幾種算法的思想,不深究其實現過程

標記 - 清除算法

“標記 - 清除”(Mark-Sweep)算法是最基礎的算法。此算法共分為兩個階段:標記階段和清除階段。其實很簡單,就是首先標記出所有需要被回收的對象,然后在標記完成之后統(tǒng)一回收所有被標記的對象。
不足:

  • 效率問題,標記和清除兩個過程的效率都不高;
  • 空間問題,標記清除之后會產生大量不連續(xù)的內存碎片,空間碎片太多可能會導致以后再程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內存而不得不提前觸發(fā)另一次垃圾收集動作。
復制算法

為了解決效率問題,復制算法就出現了,它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完了,就將還存活著的對象復制到另一塊上,然后再把已使用過的內存空間清理掉。這樣就使得每次都是對整個半區(qū)進行內存回收,在進行內存分配的時候也無需考慮內存碎片等復雜問題,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小為原來的一半。

標記 - 整理算法

復制算法在對象存活率較高時就要進行較多的復制操作,效率會降低。更為關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以老年代一般不能直接選用這種算法。
根據老年代的特點,有人提出了另一種“標記 - 整理”算法,標記過程仍與“標記 - 清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都往一端移動,然后直接清理端邊界以外的內存。

分代收集算法

當前商業(yè)虛擬機都采用“分代收集”(Generational Collection)算法,這種算法就是根據對象存活周期的不同將內存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據年代的不同來選擇最合適的垃圾收集算法。

  • 在新生代中,每次垃圾收集時都有大批的對象死去,只有少量對象存活。那就選用復制算法。這樣依賴只需付出少量存貨對象的復制成本即可完成垃圾收集。
  • 老年代中對象存活率較高、沒有額外空間進行分配擔保,所以必須使用“標記 - 清除”或者“標記 - 整理”算法來進行回收。

HotSpot算法實現(待完善)

垃圾收集器(待完善)

內存分配與回收策略

Java的自動內存管理歸根結底其實就是解決了兩個問題:給對象分配內存以及回收分配給對象的內存空間。我們前面已經講了非常多有關于內存回收的知識,下面將開始介紹有關于內存分配的只是。
對象的內存分配,在宏觀上來看,其實就是在堆上分配(也可能經過JIT編譯后被拆散為標量類型并間接地棧上分配),對象主要分配在新生代的Eden區(qū)上,如果啟動了本地線程分配緩沖,將按線程優(yōu)先在TLAB上分配。少數情況下也可能會直接分配在老年代。其實分配的規(guī)則并不是固定的,其細節(jié)還取決于當前使用的是哪一種垃圾收集器組合,還有虛擬機中能夠與內存相關的參數設置。

對象優(yōu)先在Eden分配

大多數情況下,對象在新生代Eden區(qū)中分配。當Eden區(qū)沒有足夠空間進行分配時,虛擬機將發(fā)起一次Minor GC。

大對象直接進入老年代

所謂大對象是指需要大量連續(xù)內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。大對象對虛擬機的內存分配來說就是一個壞消息,經常出現大對象容易導致內存還有不少空間時就提前觸發(fā)垃圾收集以獲取足夠多的連續(xù)空間來“安置”它們。

長期存活的對象將進入老年代

既然虛擬機采用了分代收集的思想來管理內存,那么內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代。為了做到這點,虛擬機給每個對象定義了一個對象年齡(Age)計數器。如果對象在Eden出生并經過一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動到Survivor空間中,并且對象年齡設為1.對象在Survivor區(qū)每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲),就會晉升到老年代中。對象晉升老年代的年齡閾值,可以通過-XX:MaxTenuringThreshold設置。

動態(tài)對象年齡判定

為了更好地適應不同程度的內存情況,虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Servivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

空間分配擔保(待完善)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容