摘要:看來(lái)還是功力不夠,索性拆成了六篇文章,分別從自動(dòng)內(nèi)存管理機(jī)制類文件結(jié)構(gòu)類加載機(jī)制字節(jié)碼執(zhí)行引擎程序編譯與代碼優(yōu)化高效并發(fā)六個(gè)方面來(lái)做更加細(xì)致的介紹。本文先說(shuō)說(shuō)虛擬機(jī)的自動(dòng)內(nèi)存管理機(jī)制。在類加載檢查通過(guò)后,虛擬機(jī)將為新生對(duì)象分配內(nèi)存。
歡迎關(guān)注微信公眾號(hào):BaronTalk,獲取更多精彩好文!
書(shū)籍真的是常讀常新,古人說(shuō)「書(shū)讀百遍其義自見(jiàn)」還是蠻有道理的。周志明老師的這本《深入理解 Java 虛擬機(jī)》我細(xì)讀了不下三遍,每一次閱讀都有新的收獲,每一次閱讀對(duì) Java 虛擬機(jī)的理解就更進(jìn)一步。因而萌生了將讀書(shū)筆記整理成文的想法,一是想檢驗(yàn)下自己的學(xué)習(xí)成果,對(duì)學(xué)習(xí)內(nèi)容進(jìn)行一次系統(tǒng)性的復(fù)盤(pán);二是給還沒(méi)接觸過(guò)這部好作品的同學(xué)推薦下,在閱讀這部佳作之前能通過(guò)我的文章一窺書(shū)中的精華。
原想著一篇文章就夠了,但寫(xiě)著寫(xiě)著就發(fā)現(xiàn)篇幅大大超出了預(yù)期。看來(lái)還是功力不夠,索性拆成了六篇文章,分別從自動(dòng)內(nèi)存管理機(jī)制、類文件結(jié)構(gòu)、類加載機(jī)制、字節(jié)碼執(zhí)行引擎、程序編譯與代碼優(yōu)化、高效并發(fā)六個(gè)方面來(lái)做更加細(xì)致的介紹。本文先說(shuō)說(shuō) Java 虛擬機(jī)的自動(dòng)內(nèi)存管理機(jī)制。
一. 運(yùn)行時(shí)數(shù)據(jù)區(qū)Java 虛擬機(jī)在執(zhí)行 Java 程序的過(guò)程中會(huì)把它所管理的內(nèi)存區(qū)域劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。這些區(qū)域都有各自的用途,以及創(chuàng)建和銷毀的時(shí)間,有些區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而存在,有些區(qū)域則是依賴線程的啟動(dòng)和結(jié)束而建立和銷毀。Java 虛擬機(jī)所管理的內(nèi)存被劃分為如下幾個(gè)區(qū)域:
程序計(jì)數(shù)器程序計(jì)數(shù)器是一塊較小的內(nèi)存區(qū)域,可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。在虛擬機(jī)的概念模型里,字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來(lái)完成?!笇儆诰€程私有的內(nèi)存區(qū)域」
Java 虛擬機(jī)棧就是我們平時(shí)所說(shuō)的棧,每個(gè)方法被執(zhí)行時(shí),都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作棧、動(dòng)態(tài)鏈接、方法出口等信息。每個(gè)方法從被調(diào)用到執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從出棧到入棧的過(guò)程。「屬于線程私有的內(nèi)存區(qū)域」
局部變量表:局部變量表是 Java 虛擬機(jī)棧的一部分,存放了編譯器可知的基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(reference 類型,不等同于對(duì)象本身,根據(jù)不同的虛擬機(jī)實(shí)現(xiàn),它可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能是指向一個(gè)代表對(duì)象的句柄或者其他與此對(duì)象相關(guān)的位置)和 returnAddress 類型(指向了一條字節(jié)碼指令的地址)。本地方法棧
和虛擬機(jī)棧類似,只不過(guò)虛擬機(jī)棧為虛擬機(jī)執(zhí)行的 Java 方法服務(wù),本地方法棧為虛擬機(jī)使用的 Native 方法服務(wù)。「屬于線程私有的內(nèi)存區(qū)域」
Java 堆對(duì)大多數(shù)應(yīng)用而言,Java 堆是虛擬機(jī)所管理的內(nèi)存中最大的一塊,是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一作用就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都是在這里分配的(不絕對(duì),在虛擬機(jī)的優(yōu)化策略下,也會(huì)存在棧上分配、標(biāo)量替換的情況,后面的章節(jié)會(huì)詳細(xì)介紹)。Java 堆是 GC 回收的主要區(qū)域,因此很多時(shí)候也被稱為 GC 堆。從內(nèi)存回收的角度看,Java 堆還可以被細(xì)分為新生代和老年代;再細(xì)一點(diǎn)新生代還可以被劃分為 Eden Space、From Survivor Space、To Survivor Space。從內(nèi)存回收的角度看,線程共享的 Java 堆可能劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)。「屬于線程共享的內(nèi)存區(qū)域」
方法區(qū)用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)?!笇儆诰€程共享的內(nèi)存區(qū)域」
運(yùn)行時(shí)常量池: 運(yùn)行時(shí)常量池是方法區(qū)的一部分,Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息就是常量池(Constant Pool Table),用于存放在編譯期生成的各種字面量和符號(hào)引用。二. 對(duì)象的創(chuàng)建、內(nèi)存布局及訪問(wèn)定位直接內(nèi)存:直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是 Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。Java 中的 NIO 可以使用 Native 函數(shù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在 Java 堆中的 DiectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景顯著提高性能,因?yàn)楸苊饬嗽?Java 堆和 Native 堆中來(lái)回復(fù)制數(shù)據(jù)。直接內(nèi)存不受 Java 堆大小的限制。
前面介紹了 Java 虛擬機(jī)的運(yùn)行時(shí)數(shù)據(jù)區(qū),了解了虛擬機(jī)內(nèi)存的情況。接下來(lái)我們看看對(duì)象是如何創(chuàng)建的、對(duì)象的內(nèi)存布局是怎樣的以及對(duì)象在內(nèi)存中是如何定位的。
2.1 對(duì)象的創(chuàng)建要?jiǎng)?chuàng)建一個(gè)對(duì)象首先得在 Java 堆中(不絕對(duì),后面介紹虛擬機(jī)優(yōu)化策略的時(shí)候會(huì)做詳細(xì)介紹)為這個(gè)要?jiǎng)?chuàng)建的對(duì)象分配內(nèi)存,分配內(nèi)存的過(guò)程要保證并發(fā)安全,最后再對(duì)內(nèi)存進(jìn)行相應(yīng)的初始化,這一系列的過(guò)程完成后,一個(gè)真正的對(duì)象就被創(chuàng)建了。
內(nèi)存分配先說(shuō)說(shuō)內(nèi)存分配,當(dāng)虛擬機(jī)遇到一條 new 指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能夠在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載、解析和初始化。如果沒(méi)有,那必須先執(zhí)行相應(yīng)的類加載過(guò)程。在類加載檢查通過(guò)后,虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需的內(nèi)存大小在類加載完成后便可完全確定,為對(duì)象分配內(nèi)存空間的任務(wù)等同于把一塊確定大小的內(nèi)存從 Java 堆中劃分出來(lái)。
在 Java 堆中劃分內(nèi)存涉及到兩個(gè)概念:指針碰撞(Bump the Pointer)、空閑列表(Free List)。
如果 Java 堆中的內(nèi)存絕對(duì)規(guī)整,所有用過(guò)的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配的內(nèi)存就緊緊是把指針往空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱為「指針碰撞」。
如果 Java 堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒(méi)辦法簡(jiǎn)單的進(jìn)行指針碰撞了。虛擬機(jī)必須維護(hù)一個(gè)列表來(lái)記錄哪些內(nèi)存是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱為「空閑列表」。
選擇哪種分配方式是由 Java 堆是否規(guī)整來(lái)決定的,而 Java 堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
保證并發(fā)安全對(duì)象的創(chuàng)建在虛擬機(jī)中是一個(gè)非常頻繁的行為,哪怕只是修改一個(gè)指針?biāo)赶虻奈恢茫诓l(fā)情況下也是不安全的,可能出現(xiàn)正在給對(duì)象 A 分配內(nèi)存,指針還沒(méi)來(lái)得及修改,對(duì)象 B 又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存的情況。解決這個(gè)問(wèn)題有兩種方案:
對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理(采用 CAS + 失敗重試來(lái)保障更新操作的原子性);
把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行,即每個(gè)線程在 Java 堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer, TLAB)。哪個(gè)線程要分配內(nèi)存,就在哪個(gè)線程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 時(shí),才需要同步鎖。
初始化內(nèi)存分配完后,虛擬機(jī)要將分配到的內(nèi)存空間初始化為零值(不包括對(duì)象頭),如果使用了 TLAB,這一步會(huì)提前到 TLAB 分配時(shí)進(jìn)行。這一步保證了對(duì)象的實(shí)例字段在 Java 代碼中可以不賦初始值就直接使用。
接下來(lái)設(shè)置對(duì)象頭(Object Header)信息,包括對(duì)象是哪個(gè)類的實(shí)例、如何找到類的元數(shù)據(jù)、對(duì)象的 Hash、對(duì)象的 GC 分代年齡等。
這一系列動(dòng)作完成之后,緊接著會(huì)執(zhí)行
JVM 中對(duì)象的創(chuàng)建過(guò)程大致如下圖:
2.2 對(duì)象的內(nèi)存布局在 HotSpot 虛擬機(jī)中,對(duì)象在內(nèi)存中的布局可以分為 3 塊:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)。
對(duì)象頭對(duì)象頭包含兩部分信息,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),比如哈希碼(HashCode)、GC 分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程 ID、偏向時(shí)間戳等。這部分?jǐn)?shù)據(jù)稱之為 Mark Word。對(duì)象頭的另一部分是類型指針,即對(duì)象指向它的類元數(shù)據(jù)指針,虛擬機(jī)通過(guò)它來(lái)確定對(duì)象是哪個(gè)類的實(shí)例;如果是數(shù)組,對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)。(并不是所有所有虛擬機(jī)的實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類型指針,在下一小節(jié)介紹「對(duì)象的訪問(wèn)定位」的時(shí)候再做詳細(xì)說(shuō)明)。
實(shí)例數(shù)據(jù)對(duì)象真正存儲(chǔ)的有效數(shù)據(jù),也是在程序代碼中所定義的各種字段內(nèi)容。
對(duì)齊填充無(wú)特殊含義,不是必須存在的,僅作為占位符。
2.3 對(duì)象的訪問(wèn)定位Java 程序需要通過(guò)棧上的 reference 信息來(lái)操作堆上的具體對(duì)象。根據(jù)不同的虛擬機(jī)實(shí)現(xiàn),主流的訪問(wèn)對(duì)象的方式主要有句柄訪問(wèn)和直接指針兩種。
句柄訪問(wèn)Java 堆中劃分出一塊內(nèi)存來(lái)作為句柄池,reference 中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息。
使用句柄訪問(wèn)的好處就是 reference 中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)時(shí)只需要改變句柄中實(shí)例數(shù)據(jù)的指針,而 reference 本身不需要修改。
直接指針在對(duì)象頭中存儲(chǔ)類型數(shù)據(jù)相關(guān)信息,reference 中存儲(chǔ)的對(duì)象地址。
使用直接指針訪問(wèn)的好處是速度更快,它節(jié)省了一次指針定位的開(kāi)銷。由于對(duì)象訪問(wèn)在 Java 中非常頻繁,因此這類開(kāi)銷積少成多也是一項(xiàng)非??捎^的執(zhí)行成本。HotSpot 中采用的就是這種方式。
三. 垃圾回收器與內(nèi)存分配策略在前面我們介紹 JVM 運(yùn)行時(shí)數(shù)據(jù)區(qū)的時(shí)候說(shuō)過(guò),程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧 3 個(gè)區(qū)域隨線程而生,隨線程而死;棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊的執(zhí)行著入棧和出棧的操作。每一個(gè)棧幀中分配多少內(nèi)存基本上在數(shù)據(jù)結(jié)構(gòu)確定下來(lái)的時(shí)候就已經(jīng)知道了,因此這幾個(gè)區(qū)域內(nèi)存的分配和回收是具有確定性的,所以不用過(guò)度考慮內(nèi)存回收的問(wèn)題,因?yàn)樵诜椒ńY(jié)束或者線程結(jié)束時(shí),內(nèi)存就跟著回收了。
而 Java 堆和方法區(qū)則不一樣,一個(gè)接口中的多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能不一樣,一個(gè)方法中的多個(gè)分支需要的內(nèi)存也可能不一樣,我們只有在程序運(yùn)行期才能知道會(huì)創(chuàng)建哪些對(duì)象,這部分內(nèi)存的分配和回收是動(dòng)態(tài)的,垃圾收集器要關(guān)注的就是這部分內(nèi)存。
3.1 對(duì)象回收的判定規(guī)則垃圾收集器在做垃圾回收的時(shí)候,首先需要判定的就是哪些內(nèi)存是需要被回收的,哪些對(duì)象是「存活」的,是不可以被回收的;哪些對(duì)象已經(jīng)「死掉」了,需要被回收。
引用計(jì)數(shù)法判斷對(duì)象存活與否的一種方式是「引用計(jì)數(shù)」,即對(duì)象被引用一次,計(jì)數(shù)器就加 1,如果計(jì)數(shù)器為 0 則判斷這個(gè)對(duì)象可以被回收。但是引用計(jì)數(shù)法有一個(gè)很致命的缺陷就是它無(wú)法解決循環(huán)依賴的問(wèn)題,因此現(xiàn)在主流的虛擬機(jī)基本不會(huì)采用這種方式。
可達(dá)性分析算法可達(dá)性分析算法又叫根搜索算法,該算法的基本思想就是通過(guò)一系列稱為「GC Roots」的對(duì)象作為起始點(diǎn),從這些起始點(diǎn)開(kāi)始往下搜索,搜索所走過(guò)的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到 GC Roots 對(duì)象之間沒(méi)有任何引用鏈的時(shí)候(不可達(dá)),證明該對(duì)象是不可用的,于是就會(huì)被判定為可回收對(duì)象。
在 Java 中可作為 GC Roots 的對(duì)象包含以下幾種:
虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象;
方法區(qū)中類靜態(tài)屬性引用的對(duì)象;
方法區(qū)中常量引用的對(duì)象;
本地方法棧中 JNI(Native 方法)引用的對(duì)象。
Java 中是四種引用類型無(wú)論是通過(guò)引用計(jì)數(shù)器還是通過(guò)可達(dá)性分析來(lái)判斷對(duì)象是否可以被回收都設(shè)計(jì)到「引用」的概念。在 Java 中,根據(jù)引用關(guān)系的強(qiáng)弱不一樣,將引用類型劃為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)。
強(qiáng)引用:Object obj = new Object()這種方式就是強(qiáng)引用,只要這種強(qiáng)引用存在,垃圾收集器就永遠(yuǎn)不會(huì)回收被引用的對(duì)象。
軟引用:用來(lái)描述一些有用但非必須的對(duì)象。在 OOM 之前垃圾收集器會(huì)把這些被軟引用的對(duì)象列入回收范圍進(jìn)行二次回收。如果本次回收之后還是內(nèi)存不足才會(huì)觸發(fā) OOM。在 Java 中使用 SoftReference 類來(lái)實(shí)現(xiàn)軟引用。
弱引用:同軟引用一樣也是用來(lái)描述非必須對(duì)象的,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時(shí),無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。在 Java 中使用 WeakReference 類來(lái)實(shí)現(xiàn)。
虛引用:是最弱的一種引用關(guān)系,一個(gè)對(duì)象是否有虛引用的存在完全不影響對(duì)象的生存時(shí)間,也無(wú)法通過(guò)虛引用來(lái)獲取一個(gè)對(duì)象的實(shí)例。一個(gè)對(duì)象使用虛引用的唯一目的是為了在被垃圾收集器回收時(shí)收到一個(gè)系統(tǒng)通知。在 Java 中使用 PhantomReference 類來(lái)實(shí)現(xiàn)。
生存還是死亡,這是一個(gè)問(wèn)題在可達(dá)性分析中判定為不可達(dá)的對(duì)象,也不一定就是「非死不可的」。這時(shí)它們處于「緩刑」階段,真正要宣告一個(gè)對(duì)象死亡,至少需要經(jīng)歷兩次標(biāo)記過(guò)程:
第一次標(biāo)記:如果對(duì)象在進(jìn)行可達(dá)性分析后被判定為不可達(dá)對(duì)象,那么它將被第一次標(biāo)記并且進(jìn)行一次篩選。篩選的條件是此對(duì)象是否有必要執(zhí)行 finalize() 方法。對(duì)象沒(méi)有覆蓋 finalize() 方法或者該對(duì)象的 finalize() 方法曾經(jīng)被虛擬機(jī)調(diào)用過(guò),則判定為沒(méi)必要執(zhí)行。
第二次標(biāo)記:如果被判定為有必要執(zhí)行 finalize() 方法,那么這個(gè)對(duì)象會(huì)被放置到一個(gè) F-Queue 隊(duì)列中,并在稍后由虛擬機(jī)自動(dòng)創(chuàng)建的、低優(yōu)先級(jí)的 Finalizer 線程去執(zhí)行該對(duì)象的 finalize() 方法。但是虛擬機(jī)并不承諾會(huì)等待該方法結(jié)束,這樣做是因?yàn)?,如果一個(gè)對(duì)象的 finalize() 方法比較耗時(shí)或者發(fā)生了死循環(huán),就可能導(dǎo)致 F-Queue 隊(duì)列中的其他對(duì)象永遠(yuǎn)處于等待狀態(tài),甚至導(dǎo)致整個(gè)內(nèi)存回收系統(tǒng)崩潰。finalize() 方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì),如果對(duì)象要在 finalize() 中挽救自己,只要重新與 GC Roots 引用鏈關(guān)聯(lián)上就可以了。這樣在第二次標(biāo)記時(shí)它將被移除「即將回收」的集合,如果對(duì)象在這個(gè)時(shí)候還沒(méi)有逃脫,那么它基本上就真的被回收了。
方法區(qū)回收前面介紹過(guò),方法區(qū)在 HotSpot 虛擬機(jī)中被劃分為永久代。在 Java 虛擬機(jī)規(guī)范中沒(méi)有要求方法區(qū)實(shí)現(xiàn)垃圾收集,而且方法區(qū)垃圾收集的性價(jià)比也很低。
方法區(qū)(永久代)的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無(wú)用的類。
廢棄常量的回收和 Java 堆中對(duì)象的回收非常類似,這里就不做過(guò)多的解釋了。
類的回收條件就比較苛刻了。要判定一個(gè)類是否可以被回收,要滿足以下三個(gè)條件:
該類的所有實(shí)例已經(jīng)被回收;
加載該類的 ClassLoader 已經(jīng)被回收;
該類的 Class 對(duì)象沒(méi)有被引用,無(wú)法再任何地方通過(guò)反射訪問(wèn)該類的方法。
3.2 垃圾回收算法 標(biāo)記-清除算法正如標(biāo)記-清除的算法名一樣,該算法分為「標(biāo)記」和「清除」兩個(gè)階段:
首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后回收所有被標(biāo)記的對(duì)象。標(biāo)記-清除算法是一種最基礎(chǔ)的算法,后續(xù)其它算法都是在它的基礎(chǔ)上基于不足之處改進(jìn)而來(lái)的。它的不足體現(xiàn)在兩方面:一是效率問(wèn)題,標(biāo)記和清除的效率都不高;二是空間問(wèn)題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后程序的運(yùn)行過(guò)程中又要分配較大對(duì)象是,無(wú)法找打足夠的連續(xù)內(nèi)存而不得不提前出發(fā)下一次 GC。
復(fù)制算法為了解決效率問(wèn)題,于是就有了復(fù)制算法,它將內(nèi)存一分為二劃分為大小相等的兩塊內(nèi)存區(qū)域。每次只使用其中的一塊。當(dāng)這一塊用完時(shí),就將還存活的對(duì)象復(fù)制到另一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。這樣做的好處是不用考慮內(nèi)存碎片問(wèn)題了,簡(jiǎn)單高效。只不過(guò)這種算法代價(jià)也很高,內(nèi)存因此縮小了一半。
現(xiàn)在的商業(yè)虛擬機(jī)都采用這種算法來(lái)回收新生代,在 IBM 的研究中新生代中的對(duì)象 98% 都是「朝生夕死」,所以并不需要按照 1:1 的比例來(lái)劃分空間,而是將內(nèi)存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。當(dāng)回收時(shí),將 Eden 和 Survivor 中還存活的對(duì)象一次性復(fù)制到另一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過(guò)的 Survivor 空間。 HotSpot 默認(rèn) Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用的內(nèi)存為整個(gè)新生代容量的 90%(80%+10%),只有 10% 會(huì)被浪費(fèi)。當(dāng)然,98% 的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù),我們沒(méi)辦法保證每次回收后都只有不多于 10% 的對(duì)象存活,當(dāng) Survivor 空間不夠用時(shí),需要依賴其它內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保。如果另外一塊 Survivor 空間沒(méi)有足夠空間存放上一次新生代收集下來(lái)存活的對(duì)象時(shí),這些對(duì)象將直接通過(guò)分配擔(dān)保機(jī)制進(jìn)入老年代。
標(biāo)記-整理算法通過(guò)前面對(duì)復(fù)制-收集算法的介紹我們知道,其對(duì)老年代這種對(duì)象存活時(shí)間長(zhǎng)的內(nèi)存區(qū)域就不適用了,而標(biāo)記整理的算法就比較適用這一場(chǎng)景。
標(biāo)記-整理算法的標(biāo)記過(guò)程與「標(biāo)記-清除」算法一樣,但是后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。
分代回收算法當(dāng)前商業(yè)虛擬機(jī)的垃圾搜集都采用「分代回收」算法,這種算法并沒(méi)有什么新的思想,只是根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。一般是將 Java 堆分為新生代和老年代,這樣可以根據(jù)各個(gè)年代的特點(diǎn)采用最合適的搜集算法。
在新生代中,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。
而老年代中因?yàn)閷?duì)象存活率高、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用「標(biāo)記-清除」或者「標(biāo)記-整理」算法來(lái)進(jìn)行回收。
3.3 內(nèi)存分配與回收策略所謂自動(dòng)內(nèi)存管理,最終要解決的也就是內(nèi)存分配和內(nèi)存回收兩個(gè)問(wèn)題。前面我們介紹了內(nèi)存回收,這里我們?cè)賮?lái)聊聊內(nèi)存分配。
對(duì)象的內(nèi)存分配通常是在 Java 堆上分配(隨著虛擬機(jī)優(yōu)化技術(shù)的誕生,某些場(chǎng)景下也會(huì)在棧上分配,后面會(huì)詳細(xì)介紹),對(duì)象主要分配在新生代的 Eden 區(qū),如果啟動(dòng)了本地線程緩沖,將按照線程優(yōu)先在 TLAB 上分配。少數(shù)情況下也會(huì)直接在老年代上分配??偟膩?lái)說(shuō)分配規(guī)則不是百分百固定的,其細(xì)節(jié)取決于哪一種垃圾收集器組合以及虛擬機(jī)相關(guān)參數(shù)有關(guān),但是虛擬機(jī)對(duì)于內(nèi)存的分配還是會(huì)遵循以下幾種「普世」規(guī)則:
對(duì)象優(yōu)先在 Eden 區(qū)分配多數(shù)情況,對(duì)象都在新生代 Eden 區(qū)分配。當(dāng) Eden 區(qū)分配沒(méi)有足夠的空間進(jìn)行分配時(shí),虛擬機(jī)將會(huì)發(fā)起一次 Minor GC。如果本次 GC 后還是沒(méi)有足夠的空間,則將啟用分配擔(dān)保機(jī)制在老年代中分配內(nèi)存。
這里我們提到 Minor GC,如果你仔細(xì)觀察過(guò) GC 日常,通常我們還能從日志中發(fā)現(xiàn) Major GC/Full GC。
Minor GC 是指發(fā)生在新生代的 GC,因?yàn)?Java 對(duì)象大多都是朝生夕死,所有 Minor GC 非常頻繁,一般回收速度也非??欤?/p>
Major GC/Full GC 是指發(fā)生在老年代的 GC,出現(xiàn)了 Major GC 通常會(huì)伴隨至少一次 Minor GC。Major GC 的速度通常會(huì)比 Minor GC 慢 10 倍以上。
大對(duì)象直接進(jìn)入老年代所謂大對(duì)象是指需要大量連續(xù)內(nèi)存空間的對(duì)象,頻繁出現(xiàn)大對(duì)象是致命的,會(huì)導(dǎo)致在內(nèi)存還有不少空間的情況下提前觸發(fā) GC 以獲取足夠的連續(xù)空間來(lái)安置新對(duì)象。
前面我們介紹過(guò)新生代使用的是標(biāo)記-清除算法來(lái)處理垃圾回收的,如果大對(duì)象直接在新生代分配就會(huì)導(dǎo)致 Eden 區(qū)和兩個(gè) Survivor 區(qū)之間發(fā)生大量的內(nèi)存復(fù)制。因此對(duì)于大對(duì)象都會(huì)直接在老年代進(jìn)行分配。
長(zhǎng)期存活對(duì)象將進(jìn)入老年代虛擬機(jī)采用分代收集的思想來(lái)管理內(nèi)存,那么內(nèi)存回收時(shí)就必須判斷哪些對(duì)象應(yīng)該放在新生代,哪些對(duì)象應(yīng)該放在老年代。因此虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡的計(jì)數(shù)器,如果對(duì)象在 Eden 區(qū)出生,并且能夠被 Survivor 容納,將被移動(dòng)到 Survivor 空間中,這時(shí)設(shè)置對(duì)象年齡為 1。對(duì)象在 Survivor 區(qū)中每「熬過(guò)」一次 Minor GC 年齡就加 1,當(dāng)年齡達(dá)到一定程度(默認(rèn) 15) 就會(huì)被晉升到老年代。
動(dòng)態(tài)對(duì)象年齡判斷為了更好的適應(yīng)不同程序的內(nèi)存情況,虛擬機(jī)并不是永遠(yuǎn)要求對(duì)象的年齡必需達(dá)到某個(gè)固定的值(比如前面說(shuō)的 15)才會(huì)被晉升到老年代,而是會(huì)去動(dòng)態(tài)的判斷對(duì)象年齡。如果在 Survivor 區(qū)中相同年齡所有對(duì)象大小的總和大于 Survivor 空間的一半,年齡大于等于該年齡的對(duì)象就可以直接進(jìn)入老年代。
空間分配擔(dān)保在新生代觸發(fā) Minor GC 后,如果 Survivor 中任然有大量的對(duì)象存活就需要老年隊(duì)來(lái)進(jìn)行分配擔(dān)保,讓 Survivor 區(qū)中無(wú)法容納的對(duì)象直接進(jìn)入到老年代。
寫(xiě)在最后對(duì)于我們 Java 程序員來(lái)說(shuō),虛擬機(jī)的自動(dòng)內(nèi)存管理機(jī)制為我們?cè)诰幋a過(guò)程中帶來(lái)了極大的便利,不用像 C/C++ 等語(yǔ)言的開(kāi)發(fā)者一樣小心翼翼的去管理每一個(gè)對(duì)象的生命周期。但同時(shí)我們也喪失了內(nèi)存控制的管理權(quán)限,一旦發(fā)生內(nèi)存泄漏如果不了解虛擬機(jī)的內(nèi)存管理原理,就很排查問(wèn)題。希望這篇文章能對(duì)大家理解 Java 虛擬機(jī)的內(nèi)存管理機(jī)制有所幫助。如果想對(duì) Java 虛擬機(jī)有更進(jìn)一步的了解,推薦大家去讀周志明老師的《深入理解 Java 虛擬機(jī):JVM 高級(jí)特性與最佳實(shí)踐》這本書(shū)。
好了,關(guān)于 Java 虛擬機(jī)的自動(dòng)內(nèi)存管理機(jī)制就介紹到這里,下一篇我們來(lái)聊聊「類文件結(jié)構(gòu)」。
參考資料:
《深入理解 Java 虛擬機(jī):JVM 高級(jí)特性與最佳實(shí)踐(第 2 版)》
如果你喜歡我的文章,就關(guān)注下我的公眾號(hào) BaronTalk 、 知乎專欄 或者在 GitHub 上添個(gè) Star 吧!
微信公眾號(hào):BaronTalk
知乎專欄:https://zhuanlan.zhihu.com/baron
GitHub:https://github.com/BaronZ88
個(gè)人博客:http://baronzhang.com
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/74963.html
摘要:運(yùn)行時(shí)數(shù)據(jù)區(qū)域虛擬機(jī)在執(zhí)行程序的過(guò)程中會(huì)把它管理的內(nèi)存劃分成若干個(gè)不同的數(shù)據(jù)區(qū)域。堆虛擬機(jī)所管理的內(nèi)存中最大的一塊,堆是所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。 《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐(第二版》讀書(shū)筆記 1 概述 對(duì)于Java程序員來(lái)說(shuō),在虛擬機(jī)自動(dòng)內(nèi)存管理機(jī)制下,不再需要像C/C++程序開(kāi)發(fā)程序員這樣為內(nèi)一個(gè)new 操作去寫(xiě)對(duì)應(yīng)的delete/...
摘要:深入理解虛擬機(jī)高級(jí)特性與最佳實(shí)踐第二版讀書(shū)筆記與常見(jiàn)面試題總結(jié)本節(jié)常見(jiàn)面試題介紹下內(nèi)存區(qū)域運(yùn)行時(shí)數(shù)據(jù)區(qū)。運(yùn)行時(shí)數(shù)據(jù)區(qū)域虛擬機(jī)在執(zhí)行程序的過(guò)程中會(huì)把它管理的內(nèi)存劃分成若干個(gè)不同的數(shù)據(jù)區(qū)域。 《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐(第二版》讀書(shū)筆記與常見(jiàn)面試題總結(jié) 本節(jié)常見(jiàn)面試題: 介紹下Java內(nèi)存區(qū)域(運(yùn)行時(shí)數(shù)據(jù)區(qū))。 對(duì)象的訪問(wèn)定位的兩種方式。 1 概述 對(duì)于Java...
摘要:所以我們提到的內(nèi)存回收大都是指堆內(nèi)存的回收。根據(jù)堆內(nèi)存對(duì)對(duì)象的代的劃分我們對(duì)堆內(nèi)存有這樣劃分各版本和種類的垃圾回收器各有其用武之地,配合使用它們得到最好的效果十分重要。 這篇文章的素材來(lái)自周志明的《深入理解Java虛擬機(jī)》。作為Java開(kāi)發(fā)人員,一定程度了解JVM虛擬機(jī)的的運(yùn)作方式非常重要,本文就一些簡(jiǎn)單的虛擬機(jī)的相關(guān)概念和運(yùn)作機(jī)制展開(kāi)我自己的學(xué)習(xí)過(guò)程。 虛擬機(jī)內(nèi)存分區(qū) java虛擬機(jī)...
摘要:運(yùn)行時(shí)數(shù)據(jù)區(qū)域的學(xué)習(xí),是學(xué)習(xí)以及機(jī)制的基礎(chǔ),也是深入理解對(duì)象創(chuàng)建及運(yùn)行過(guò)程的前提。了解內(nèi)存區(qū)域劃分,是學(xué)習(xí)概念的前提。 Java 運(yùn)行時(shí)數(shù)據(jù)區(qū)域的學(xué)習(xí),是學(xué)習(xí) jvm 以及 GC 機(jī)制的基礎(chǔ),也是深入理解 java 對(duì)象創(chuàng)建及運(yùn)行過(guò)程的前提。廢話不多說(shuō),直接進(jìn)入正題: 一張圖總結(jié) showImg(https://segmentfault.com/img/bVOMAn?w=685&h=5...
閱讀 2345·2021-11-23 09:51
閱讀 1154·2021-11-22 15:35
閱讀 5430·2021-11-22 09:34
閱讀 1745·2021-10-08 10:13
閱讀 3083·2021-07-22 17:35
閱讀 2721·2019-08-30 15:56
閱讀 3158·2019-08-29 18:44
閱讀 3192·2019-08-29 15:32