JVM内存模型及CMS、G1和ZGC垃圾回收器详解
1. JVM 內(nèi)存模型
JVM 內(nèi)存模型主要指運(yùn)行時(shí)的數(shù)據(jù)區(qū),包括 5 個(gè)部分,如下圖所示。
-
棧也叫方法棧,是線(xiàn)程私有的,線(xiàn)程在執(zhí)行每個(gè)方法時(shí)都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀,用來(lái)存儲(chǔ)局部變量表、操作棧、動(dòng)態(tài)鏈接、方法出口等信息。調(diào)用方法時(shí)執(zhí)行入棧,方法返回時(shí)執(zhí)行出棧。
-
本地方法棧與棧類(lèi)似,也是用來(lái)保存線(xiàn)程執(zhí)行方法時(shí)的信息,不同的是,執(zhí)行 Java 方法使用棧,而執(zhí)行 native 方法使用本地方法棧。
-
程序計(jì)數(shù)器保存著當(dāng)前線(xiàn)程所執(zhí)行的字節(jié)碼位置,每個(gè)線(xiàn)程工作時(shí)都有一個(gè)獨(dú)立的計(jì)數(shù)器。程序計(jì)數(shù)器為執(zhí)行 Java 方法服務(wù),執(zhí)行 native 方法時(shí),程序計(jì)數(shù)器為空。
棧、本地方法棧、程序計(jì)數(shù)器這三個(gè)部分都是線(xiàn)程獨(dú)占的。 -
堆是 JVM 管理的內(nèi)存中最大的一塊,堆被所有線(xiàn)程共享,目的是為了存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配。當(dāng)堆內(nèi)存沒(méi)有可用的空間時(shí),會(huì)拋出 OOM 異常。根據(jù)對(duì)象存活的周期不同,JVM 把堆內(nèi)存進(jìn)行分代管理,由垃圾回收器來(lái)進(jìn)行對(duì)象的回收管理。
-
方法區(qū)也是各個(gè)線(xiàn)程共享的內(nèi)存區(qū)域,又叫非堆區(qū)。用于存儲(chǔ)已被虛擬機(jī)加載的類(lèi)信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù),JDK 1.7 中的永久代和 JDK 1.8 中的 Metaspace 都是方法區(qū)的一種實(shí)現(xiàn)。
2. JMM 內(nèi)存可見(jiàn)性
JMM 是 Java 內(nèi)存模型,與剛才講到的 JVM 內(nèi)存模型是兩回事,JMM 的主要目標(biāo)是定義程序中變量的訪問(wèn)規(guī)則,如下圖所示,所有的共享變量都存儲(chǔ)在主內(nèi)存中共享。每個(gè)線(xiàn)程有自己的工作內(nèi)存,工作內(nèi)存中保存的是主內(nèi)存中變量的副本,線(xiàn)程對(duì)變量的讀寫(xiě)等操作必須在自己的工作內(nèi)存中進(jìn)行,而不能直接讀寫(xiě)主內(nèi)存中的變量。
在多線(xiàn)程進(jìn)行數(shù)據(jù)交互時(shí),例如線(xiàn)程 A 給一個(gè)共享變量賦值后,由線(xiàn)程 B 來(lái)讀取這個(gè)值,A 修改完變量是修改在自己的工作區(qū)內(nèi)存中,B 是不可見(jiàn)的,只有從 A 的工作區(qū)寫(xiě)回主內(nèi)存,B 再?gòu)闹鲀?nèi)存讀取自己的工作區(qū)才能進(jìn)行進(jìn)一步的操作。由于指令重排序的存在,這個(gè)寫(xiě)—讀的順序有可能被打亂。因此 JMM 需要提供原子性、可見(jiàn)性、有序性的保證。
3. JMM 如何保證原子性、可見(jiàn)性,有序性
-
原子性
JMM 保證對(duì)除 long 和 double 外的基礎(chǔ)數(shù)據(jù)類(lèi)型的讀寫(xiě)操作是原子性的。另外關(guān)鍵字 synchronized 也可以提供原子性保證。synchronized 的原子性是通過(guò) Java 的兩個(gè)高級(jí)的字節(jié)碼指令 monitorenter 和 monitorexit 來(lái)保證的。 -
可見(jiàn)性
JMM 可見(jiàn)性的保證,一個(gè)是通過(guò) synchronized,另外一個(gè)就是 volatile。volatile 強(qiáng)制變量的賦值會(huì)同步刷新回主內(nèi)存,強(qiáng)制變量的讀取會(huì)從主內(nèi)存重新加載,保證不同的線(xiàn)程總是能夠看到該變量的最新值。 -
有序性
對(duì)有序性的保證,主要通過(guò) volatile 和一系列 happens-before 原則。volatile 的另一個(gè)作用就是阻止指令重排序,這樣就可以保證變量讀寫(xiě)的有序性。
happens-before 原則包括一系列規(guī)則,如:
程序順序原則,即一個(gè)線(xiàn)程內(nèi)必須保證語(yǔ)義串行性;
鎖規(guī)則,即對(duì)同一個(gè)鎖的解鎖一定發(fā)生在再次加鎖之前;
happens-before 原則的傳遞性、線(xiàn)程啟動(dòng)、中斷、終止規(guī)則等。
4. 類(lèi)加載機(jī)制
類(lèi)的加載指將編譯好的 Class 類(lèi)文件中的字節(jié)碼讀入內(nèi)存中,將其放在方法區(qū)內(nèi)并創(chuàng)建對(duì)應(yīng)的 Class 對(duì)象。類(lèi)的加載分為加載、鏈接、初始化,其中鏈接又包括驗(yàn)證、準(zhǔn)備、解析三步。如下圖所示。
- 加載是文件到內(nèi)存的過(guò)程。通過(guò)類(lèi)的完全限定名查找此類(lèi)字節(jié)碼文件,并利用字節(jié)碼文件創(chuàng)建一個(gè) Class 對(duì)象。
- 驗(yàn)證是對(duì)類(lèi)文件內(nèi)容驗(yàn)證。目的在于確保 Class 文件符合當(dāng)前虛擬機(jī)要求,不會(huì)危害虛擬機(jī)自身安全。主要包括四種:文件格式驗(yàn)證,元數(shù)據(jù)驗(yàn)證,字節(jié)碼驗(yàn)證,符號(hào)引用驗(yàn)證。
- 準(zhǔn)備階段是進(jìn)行內(nèi)存分配。為類(lèi)變量也就是類(lèi)中由 static 修飾的變量分配內(nèi)存,并且設(shè)置初始值。這里要注意,初始值是 0 或者 null,而不是代碼中設(shè)置的具體值,代碼中設(shè)置的值是在初始化階段完成的。另外這里也不包含用 final 修飾的靜態(tài)變量,因?yàn)?final 在編譯的時(shí)候就會(huì)分配。
- 解析主要是解析字段、接口、方法。主要是將常量池中的符號(hào)引用替換為直接引用的過(guò)程。直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量等。
- 初始化,主要完成靜態(tài)塊執(zhí)行與靜態(tài)變量的賦值。這是類(lèi)加載最后階段,若被加載類(lèi)的父類(lèi)沒(méi)有初始化,則先對(duì)父類(lèi)進(jìn)行初始化。
只有對(duì)類(lèi)主動(dòng)使用時(shí),才會(huì)進(jìn)行初始化,初始化的觸發(fā)條件包括在創(chuàng)建類(lèi)的實(shí)例時(shí)、訪問(wèn)類(lèi)的靜態(tài)方法或者靜態(tài)變量時(shí)、Class.forName() 反射類(lèi)時(shí)、或者某個(gè)子類(lèi)被初始化時(shí)。
如上圖所示,淺綠的兩個(gè)部分表示類(lèi)的生命周期,就是從類(lèi)的加載到類(lèi)實(shí)例的創(chuàng)建與使用,再到類(lèi)對(duì)象不再被使用時(shí)可以被 GC 卸載回收。這里要注意一點(diǎn),由 Java 虛擬機(jī)自帶的三種類(lèi)加載器加載的類(lèi)在虛擬機(jī)的整個(gè)生命周期中是不會(huì)被卸載的,只有用戶(hù)自定義的類(lèi)加載器所加載的類(lèi)才可以被卸載。
5. 類(lèi)加載器
如上圖所示,Java 自帶的三種類(lèi)加載器分別是:BootStrap 啟動(dòng)類(lèi)加載器、擴(kuò)展類(lèi)加載器和應(yīng)用加載器(也叫系統(tǒng)加載器)。圖右邊的桔黃色文字表示各類(lèi)加載器對(duì)應(yīng)的加載目錄。啟動(dòng)類(lèi)加載器加載 java home 中 lib 目錄下的類(lèi),擴(kuò)展加載器負(fù)責(zé)加載 ext 目錄下的類(lèi),應(yīng)用加載器加載 classpath 指定目錄下的類(lèi)。除此之外,可以自定義類(lèi)加載器。
Java 的類(lèi)加載使用雙親委派模式,即一個(gè)類(lèi)加載器在加載類(lèi)時(shí),先把這個(gè)請(qǐng)求委托給自己的父類(lèi)加載器去執(zhí)行,如果父類(lèi)加載器還存在父類(lèi)加載器,就繼續(xù)向上委托,直到頂層的啟動(dòng)類(lèi)加載器,如上圖中藍(lán)色向上的箭頭。如果父類(lèi)加載器能夠完成類(lèi)加載,就成功返回,如果父類(lèi)加載器無(wú)法完成加載,那么子加載器才會(huì)嘗試自己去加載。如圖中的桔黃色向下的箭頭。
這種雙親委派模式的好處,可以避免類(lèi)的重復(fù)加載,另外也避免了 Java 的核心 API 被篡改。
6. 垃圾分代回收
Java 的堆內(nèi)存被分代管理,為什么要分代管理呢?分代管理主要是為了方便垃圾回收,這樣做基于2個(gè)事實(shí),第一,大部分對(duì)象很快就不再使用;第二,還有一部分不會(huì)立即無(wú)用,但也不會(huì)持續(xù)很長(zhǎng)時(shí)間。
虛擬機(jī)劃分為年輕代、老年代、和永久代,如下圖所示。
年輕代主要用來(lái)存放新創(chuàng)建的對(duì)象,年輕代分為 Eden 區(qū)和兩個(gè) Survivor 區(qū)。大部分對(duì)象在 Eden 區(qū)中生成。當(dāng) Eden 區(qū)滿(mǎn)時(shí),還存活的對(duì)象會(huì)在兩個(gè) Survivor 區(qū)交替保存,達(dá)到一定次數(shù)的對(duì)象會(huì)晉升到老年代。
老年代用來(lái)存放從年輕代晉升而來(lái)的,存活時(shí)間較長(zhǎng)的對(duì)象。
永久代,主要保存類(lèi)信息等內(nèi)容,這里的永久代是指對(duì)象劃分方式,不是專(zhuān)指 1.7 的 PermGen,或者 1.8 之后的 Metaspace。
根據(jù)年輕代與老年代的特點(diǎn),JVM 提供了不同的垃圾回收算法。垃圾回收算法按類(lèi)型可以分為引用計(jì)數(shù)法、復(fù)制法和標(biāo)記清除法。
引用計(jì)數(shù)法是通過(guò)對(duì)象被引用的次數(shù)來(lái)確定對(duì)象是否被使用,缺點(diǎn)是無(wú)法解決循環(huán)引用的問(wèn)題。
復(fù)制算法需要 from 和 to 兩塊相同大小的內(nèi)存空間,對(duì)象分配時(shí)只在 from 塊中進(jìn)行,回收時(shí)把存活對(duì)象復(fù)制到 to 塊中,并清空 from 塊,然后交換兩塊的分工,即把 from 塊作為 to 塊,把 to 塊作為 from 塊。缺點(diǎn)是內(nèi)存使用率較低。
標(biāo)記清除算法分為標(biāo)記對(duì)象和清除不在使用的對(duì)象兩個(gè)階段,標(biāo)記清除算法的缺點(diǎn)是會(huì)產(chǎn)生內(nèi)存碎片。
JVM 中提供的年輕代回收算法 Serial、ParNew、Parallel Scavenge 都是復(fù)制算法,而 CMS、G1、ZGC 都屬于標(biāo)記清除算法。
6. CMS 算法
基于分代回收理論,詳細(xì)介紹幾個(gè)典型的垃圾回收算法,先來(lái)看 CMS 回收算法。CMS 在 JDK1.7 之前可以說(shuō)是最主流的垃圾回收算法。CMS 使用標(biāo)記清除算法,優(yōu)點(diǎn)是并發(fā)收集,停頓小。CMS 算法如下圖所示。
第一個(gè)階段是初始標(biāo)記,這個(gè)階段會(huì) stop the world,標(biāo)記的對(duì)象只是從 root 集最直接可達(dá)的對(duì)象;
第二個(gè)階段是并發(fā)標(biāo)記,這時(shí) GC 線(xiàn)程和應(yīng)用線(xiàn)程并發(fā)執(zhí)行。主要是標(biāo)記可達(dá)的對(duì)象;
第三個(gè)階段是重新標(biāo)記階段,這個(gè)階段是第二個(gè) stop the world 的階段,停頓時(shí)間比并發(fā)標(biāo)記要小很多,但比初始標(biāo)記稍長(zhǎng),主要對(duì)對(duì)象進(jìn)行重新掃描并標(biāo)記;
第四個(gè)階段是并發(fā)清理階段,進(jìn)行并發(fā)的垃圾清理;
最后一個(gè)階段是并發(fā)重置階段,為下一次 GC 重置相關(guān)數(shù)據(jù)結(jié)構(gòu)。
7. G1 算法
G1 在 1.9 版本后成為 JVM 的默認(rèn)垃圾回收算法,G1 的特點(diǎn)是保持高回收率的同時(shí),減少停頓。
G1 算法取消了堆中年輕代與老年代的物理劃分,但它仍然屬于分代收集器。G1 算法將堆劃分為若干個(gè)區(qū)域,稱(chēng)作 Region,如下圖中的小方格所示。一部分區(qū)域用作年輕代,一部分用作老年代,另外還有一種專(zhuān)門(mén)用來(lái)存儲(chǔ)巨型對(duì)象的分區(qū)。
G1 也和 CMS 一樣會(huì)遍歷全部的對(duì)象,然后標(biāo)記對(duì)象引用情況,在清除對(duì)象后會(huì)對(duì)區(qū)域進(jìn)行復(fù)制移動(dòng)整合碎片空間。G1 回收過(guò)程如下。
G1 的年輕代回收,采用復(fù)制算法,并行進(jìn)行收集,收集過(guò)程會(huì) STW。
G1 的老年代回收時(shí)也同時(shí)會(huì)對(duì)年輕代進(jìn)行回收。主要分為四個(gè)階段:
依然是初始標(biāo)記階段完成對(duì)根對(duì)象的標(biāo)記,這個(gè)過(guò)程是STW的;
并發(fā)標(biāo)記階段,這個(gè)階段是和用戶(hù)線(xiàn)程并行執(zhí)行的;
最終標(biāo)記階段,完成三色標(biāo)記周期;
復(fù)制/清除階段,這個(gè)階段會(huì)優(yōu)先對(duì)可回收空間較大的 Region 進(jìn)行回收,即 garbage first,這也是 G1 名稱(chēng)的由來(lái)。
G1 采用每次只清理一部分而不是全部的 Region 的增量式清理,由此來(lái)保證每次 GC 停頓時(shí)間不會(huì)過(guò)長(zhǎng)。
總結(jié)如下,G1 是邏輯分代不是物理劃分,需要知道回收的過(guò)程和停頓的階段。此外還需要知道,G1 算法允許通過(guò) JVM 參數(shù)設(shè)置 Region 的大小,范圍是 1~32MB,可以設(shè)置期望的最大 GC 停頓時(shí)間等。有興趣讀者也可以對(duì) CMS 和 G1 使用的三色標(biāo)記算法做簡(jiǎn)單了解。
7. ZGC垃圾回收器
7.1 ZGC 特點(diǎn)
ZGC 是最新的 JDK1.11 版本中提供的高效垃圾回收算法,ZGC 針對(duì)大堆內(nèi)存設(shè)計(jì)可以支持 TB 級(jí)別的堆,ZGC 非常高效,能夠做到 10ms 以下的回收停頓時(shí)間。這么快的響應(yīng),ZGC 是如何做到的呢?這是由于 ZGC 具有以下特點(diǎn)。
ZGC 使用了著色指針技術(shù),我們知道 64 位平臺(tái)上,一個(gè)指針的可用位是 64 位,ZGC 限制最大支持 4TB 的堆,這樣尋址只需要使用 42 位,那么剩下 22 位就可以用來(lái)保存額外的信息,著色指針技術(shù)就是利用指針的額外信息位,在指針上對(duì)對(duì)象做著色標(biāo)記。
第二個(gè)特點(diǎn)是使用讀屏障,ZGC 使用讀屏障來(lái)解決 GC 線(xiàn)程和應(yīng)用線(xiàn)程可能并發(fā)修改對(duì)象狀態(tài)的問(wèn)題,而不是簡(jiǎn)單粗暴的通過(guò) STW 來(lái)進(jìn)行全局的鎖定。使用讀屏障只會(huì)在單個(gè)對(duì)象的處理上有概率被減速。
由于讀屏障的作用,進(jìn)行垃圾回收的大部分時(shí)候都是不需要 STW 的,因此 ZGC 的大部分時(shí)間都是并發(fā)處理,也就是 ZGC 的第三個(gè)特點(diǎn)。
第四個(gè)特點(diǎn)是基于 Region,這與 G1 算法一樣,不過(guò)雖然也分了 Region,但是并沒(méi)有進(jìn)行分代。ZGC 的 Region 不像 G1 那樣是固定大小,而是動(dòng)態(tài)地決定 Region 的大小,Region 可以動(dòng)態(tài)創(chuàng)建和銷(xiāo)毀。這樣可以更好的對(duì)大對(duì)象進(jìn)行分配管理。
第五個(gè)特點(diǎn)是壓縮整理。CMS 算法清理對(duì)象時(shí)原地回收,會(huì)存在內(nèi)存碎片問(wèn)題。ZGC 和 G1 一樣,也會(huì)在回收后對(duì) Region 中的對(duì)象進(jìn)行移動(dòng)合并,解決了碎片問(wèn)題。
雖然 ZGC 的大部分時(shí)間是并發(fā)進(jìn)行的,但是還會(huì)有短暫的停頓。來(lái)看一下 ZGC 的回收過(guò)程。
7.2 ZGC 回收過(guò)程
如下圖所示,使用 ZGC 算法進(jìn)行回收,從上往下看。初始狀態(tài)時(shí),整個(gè)堆空間被劃分為大小不等的許多 Region,即圖中綠色的方塊。
開(kāi)始進(jìn)行回收時(shí),ZGC 首先會(huì)進(jìn)行一個(gè)短暫的 STW,來(lái)進(jìn)行 roots 標(biāo)記。這個(gè)步驟非常短,因?yàn)?roots 的總數(shù)通常比較小。
然后就開(kāi)始進(jìn)行并發(fā)標(biāo)記,如上圖所示,通過(guò)對(duì)對(duì)象指針進(jìn)行著色來(lái)進(jìn)行標(biāo)記,結(jié)合讀屏障解決單個(gè)對(duì)象的并發(fā)問(wèn)題。其實(shí),這個(gè)階段在最后還是會(huì)有一個(gè)非常短的 STW 停頓,用來(lái)處理一些邊緣情況,這個(gè)階段絕大部分時(shí)間是并發(fā)進(jìn)行的,所以沒(méi)有明顯標(biāo)出這個(gè)停頓。
下一個(gè)是清理階段,這個(gè)階段會(huì)把標(biāo)記為不在使用的對(duì)象進(jìn)行回收,如上圖所示,把橘色的不在使用的對(duì)象進(jìn)行了回收。
最后一個(gè)階段是重定位,重定位就是對(duì) GC 后存活的對(duì)象進(jìn)行移動(dòng),來(lái)釋放大塊的內(nèi)存空間,解決碎片問(wèn)題。
重定位最開(kāi)始會(huì)有一個(gè)短暫的 STW,用來(lái)重定位集合中的 root 對(duì)象。暫停時(shí)間取決于 root 的數(shù)量、重定位集與對(duì)象的總活動(dòng)集的比率。
最后是并發(fā)重定位,這個(gè)過(guò)程也是通過(guò)讀屏障,與應(yīng)用線(xiàn)程并發(fā)進(jìn)行的。
總結(jié)
以上是生活随笔為你收集整理的JVM内存模型及CMS、G1和ZGC垃圾回收器详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 关于Ext.grid.EditorGri
- 下一篇: 怎么在html页面添加qq临时会话