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