JVM 知識點匯總
首先看看 JVM 的知識點匯總篱瞎。
如上圖所示,JVM 知識點有 6 個大方向痒芝,其中俐筋,內(nèi)存模型、類加載機制严衬、GC 垃圾回收是比較重點的內(nèi)容澄者。性能調(diào)優(yōu)部分偏重實際應(yīng)用,重點突出實踐能力请琳。編譯器優(yōu)化和執(zhí)行模式部分偏重理論基礎(chǔ)粱挡,主要掌握知識點。
各個部分需要了解的知識點如下俄精。
- 內(nèi)存模型:程序計數(shù)器询筏、方法區(qū)、堆嘀倒、棧屈留、本地方法棧的作用,保存哪些數(shù)據(jù)测蘑。
- 類加載:雙親委派的加載機制灌危,以及常用類加載器分別加載哪種類型的類。
- GC:分代回收的思想和依據(jù)碳胳,以及不同垃圾回收算法實現(xiàn)的思路勇蝙、適合的場景。
- 性能調(diào)優(yōu):常用的 JVM 優(yōu)化參數(shù)的作用挨约,參數(shù)調(diào)優(yōu)的依據(jù)味混,常用的 JVM 分析工具能分析哪類問題,以及使用方法诫惭。
- 執(zhí)行模式:解釋翁锡、編譯、混合模式的優(yōu)缺點夕土,Java7 提供的分層編譯技術(shù)馆衔。需要知道 JIT 即時編譯技術(shù)和 OSR(棧上替換)瘟判,知道 C1、C2 編譯器針對的場景角溃,其中 C2 針對 Server 模式拷获,優(yōu)化更激進(jìn)。在新技術(shù)方面可以了解 Java10 提供的由 Java 實現(xiàn)的 Graal 編譯器减细。
- 編譯優(yōu)化:前端編譯器 javac 的編譯過程匆瓜、AST 抽象語法樹、編譯期優(yōu)化和運行期優(yōu)化未蝌。編譯優(yōu)化的常用技術(shù)包括公共子表達(dá)式的消除驮吱、方法內(nèi)聯(lián)、逃逸分析萧吠、棧上分配糠馆、同步消除等。明白了這些才能寫出對編譯器友好的代碼怎憋。
詳解 JVM 內(nèi)存模型
JVM 內(nèi)存模型主要指運行時的數(shù)據(jù)區(qū)又碌,包括 5 個部分,如下圖所示绊袋。
-
棧也叫方法棧毕匀,是線程私有的,線程在執(zhí)行每個方法時都會同時創(chuàng)建一個棧幀癌别,用來存儲局部變量表皂岔、操作棧、動態(tài)鏈接展姐、方法出口等信息躁垛。調(diào)用方法時執(zhí)行入棧,方法返回時執(zhí)行出棧圾笨。
本地方法棧與棧類似教馆,也是用來保存線程執(zhí)行方法時的信息,不同的是擂达,執(zhí)行 Java 方法使用棧土铺,而執(zhí)行 native 方法使用本地方法棧。
程序計數(shù)器保存著當(dāng)前線程所執(zhí)行的字節(jié)碼位置板鬓,各線程之間獨立存儲悲敷,互不影響。程序計數(shù)器是一塊很小的內(nèi)存空間俭令,主要用來記錄各個線程執(zhí)行的字節(jié)碼的地址后德,例如,分支抄腔、循環(huán)瓢湃、跳轉(zhuǎn)窟赏、異常、線程恢復(fù)等都依賴于計數(shù)器箱季。
由于 Java 是多線程語言,當(dāng)執(zhí)行的線程數(shù)量超過 CPU 核數(shù)時棍掐,線程之間會根據(jù)時間片輪詢爭奪 CPU 資源藏雏。如果一個線程的時間片用完了,或者是其它原因?qū)е逻@個線程的 CPU 資源被提前搶奪作煌,那么這個退出的線程就需要單獨的一個程序計數(shù)器掘殴,來記錄下一條運行的指令。程序計數(shù)器也是JVM中唯一不會OOM(OutOfMemory)的內(nèi)存區(qū)域
程序計數(shù)器為執(zhí)行 Java 方法服務(wù)奏寨,執(zhí)行 native 方法時,程序計數(shù)器為空鹰服。
棧套菜、本地方法棧、程序計數(shù)器這三個部分都是線程獨占的戏溺。
堆是 JVM 管理的內(nèi)存中最大的一塊,堆被所有線程共享屠尊,目的是為了存放對象實例旷祸,幾乎所有的對象實例都在這里分配。當(dāng)堆內(nèi)存沒有可用的空間時讼昆,會拋出 OOM 異常肋僧。根據(jù)對象存活的周期不同,JVM 把堆內(nèi)存進(jìn)行分代管理控淡,由垃圾回收器來進(jìn)行對象的回收管理嫌吠。
方法區(qū)也是各個線程共享的內(nèi)存區(qū)域,又叫非堆區(qū)掺炭。用于存放已被虛擬機加載的類相關(guān)信息辫诅,包括類信息、靜態(tài)變量涧狮、常量炕矮、運行時常量池么夫、字符串常量池。JDK 1.7 中的永久代和 JDK 1.8 中的 元空間(Metaspace )都是方法區(qū)的一種實現(xiàn)肤视,并且元空間的存儲位置是本地档痪。
兩個要點:一個是各部分的功能,另一個是哪些線程共享邢滑,哪些獨占腐螟。
- 運行時常量池(Run-Time Constant Pool)。這是方法區(qū)的一部分困后。如果仔細(xì)分析過反編譯的類文件結(jié)構(gòu)乐纸,你能看到版本號、字段摇予、方法汽绢、超類、接口等各種信息侧戴,還有一項信息就是常量池宁昭。Java 的常量池可以存放各種常量信息,不管是編譯器生成的各種字面量酗宋,還是需要在運行時決定的符號引用久窟,所以它比一般語言的符號表存儲的信息更加寬泛。
接下來本缠,我們來看看什么是 OOM 問題斥扛,它可能在哪些內(nèi)存區(qū)域發(fā)生?
除了程序計數(shù)器丹锹,其它區(qū)域都有可能會因為可能的空間不足發(fā)生OutOfMemoryError稀颁,簡單總結(jié)如下:
- 堆內(nèi)存不足時最常見的 OOM 原因之一,拋出的錯誤信息是“java.lang.OutOfMemoryError:Java heap space"楣黍,原因可能千奇百怪匾灶,例如,可能存在內(nèi)存泄漏問題租漂;也有可能就是堆的大小不合理阶女,比如我們要處理比較可觀的數(shù)據(jù)量,但是沒有顯示指定 JVM 堆大小或者指定數(shù)值偏辛ㄖ巍秃踩;或者出現(xiàn) JVM 處理引用不及時,導(dǎo)致堆積起來业筏,內(nèi)存無法釋放等憔杨。
- 對于 Java 虛擬機棧和本地方法棧,如果我們寫一段程序不斷的進(jìn)行遞歸調(diào)用蒜胖,而且沒有退出條件消别,就會導(dǎo)致不斷的壓棧抛蚤。類似這種情況,JVM 實際會拋出 StackOverFlowError寻狂;如果 JVM 試圖去擴展椝昃空間的的時候失敗,則會拋出 OutOfMemoryError蛇券。
參考:
詳解 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ù)交互時,例如線程 A 給一個共享變量賦值后拥娄,由線程 B 來讀取這個值蚊锹,A 修改完變量是修改在自己的工作區(qū)內(nèi)存中,B 是不可見的稚瘾,只有從 A 的工作區(qū)寫回主內(nèi)存牡昆,B 再從主內(nèi)存讀取自己的工作區(qū)才能進(jìn)行進(jìn)一步的操作。由于指令重排序的存在摊欠,這個寫—讀的順序有可能被打亂丢烘。因此 JMM 需要提供原子性、可見性些椒、有序性的保證播瞳。
詳解 JMM 保證
如下圖所示,來看 JMM 如何保證原子性免糕、可見性赢乓,有序性。
原子性
JMM 保證對除 long 和 double 外的基礎(chǔ)數(shù)據(jù)類型的讀寫操作是原子性的石窑。另外關(guān)鍵字 synchronized 也可以提供原子性保證牌芋。synchronized 的原子性是通過 Java 的兩個高級的字節(jié)碼指令 monitorenter 和 monitorexit 來保證的。
可見性
JMM 可見性的保證松逊,一個是通過 synchronized姜贡,另外一個就是 volatile。volatile 強制變量的賦值會同步刷新回主內(nèi)存棺棵,強制變量的讀取會從主內(nèi)存重新加載楼咳,保證不同的線程總是能夠看到該變量的最新值熄捍。
有序性
對有序性的保證,主要通過 volatile 和一系列 happens-before 原則母怜。volatile 的另一個作用就是阻止指令重排序余耽,這樣就可以保證變量讀寫的有序性。
happens-before 原則包括一系列規(guī)則苹熏,如:
- 程序順序原則碟贾,即一個線程內(nèi)必須保證語義串行性;
- 鎖規(guī)則轨域,即對同一個鎖的解鎖一定發(fā)生在再次加鎖之前袱耽;
- happens-before 原則的傳遞性、線程啟動干发、中斷朱巨、終止規(guī)則等。
詳解類加載機制
類的加載指將編譯好的 Class 類文件中的字節(jié)碼讀入內(nèi)存中枉长,將其放在方法區(qū)內(nèi)并創(chuàng)建對應(yīng)的 Class 對象冀续。類的加載分為加載、鏈接必峰、初始化洪唐,其中鏈接又包括驗證、準(zhǔn)備吼蚁、解析三步凭需。如下圖所示。
- 加載是文件到內(nèi)存的過程肝匆。通過類的完全限定名查找此類字節(jié)碼文件功炮,并利用字節(jié)碼文件創(chuàng)建一個 Class 對象。
- 驗證是對類文件內(nèi)容驗證术唬。目的在于確保 Class 文件符合當(dāng)前虛擬機要求薪伏,不會危害虛擬機自身安全。主要包括四種:文件格式驗證粗仓,元數(shù)據(jù)驗證嫁怀,字節(jié)碼驗證,符號引用驗證借浊。
- 準(zhǔn)備階段是進(jìn)行內(nèi)存分配塘淑。為類變量也就是類中由 static 修飾的變量分配內(nèi)存,并且設(shè)置初始值蚂斤。這里要注意存捺,初始值是 0 或者 null,而不是代碼中設(shè)置的具體值,代碼中設(shè)置的值是在初始化階段完成的捌治。另外這里也不包含用 final 修飾的靜態(tài)變量岗钩,因為 final 在編譯的時候就會分配。
- 解析主要是解析字段肖油、接口兼吓、方法。主要是將常量池中的符號引用替換為直接引用的過程森枪。直接引用就是直接指向目標(biāo)的指針视搏、相對偏移量等。
- 初始化县袱,主要完成靜態(tài)塊執(zhí)行與靜態(tài)變量的賦值浑娜。這是類加載最后階段,若被加載類的父類沒有初始化式散,則先對父類進(jìn)行初始化筋遭。
只有對類主動使用時,才會進(jìn)行初始化杂数,初始化的觸發(fā)條件包括在創(chuàng)建類的實例時宛畦、訪問類的靜態(tài)方法或者靜態(tài)變量時瘸洛、Class.forName() 反射類時揍移、或者某個子類被初始化時。
如上圖所示反肋,淺綠的兩個部分表示類的生命周期那伐,就是從類的加載到類實例的創(chuàng)建與使用,再到類對象不再被使用時可以被 GC 卸載回收石蔗。這里要注意一點罕邀,由 Java 虛擬機自帶的三種類加載器加載的類在虛擬機的整個生命周期中是不會被卸載的,只有用戶自定義的類加載器所加載的類才可以被卸載养距。
詳解類加載器
如上圖所示诉探,Java 自帶的三種類加載器分別是:BootStrap 啟動類加載器、擴展類加載器和應(yīng)用加載器(也叫系統(tǒng)加載器)棍厌。圖右邊的桔黃色文字表示各類加載器對應(yīng)的加載目錄肾胯。啟動類加載器加載 java home 中 lib 目錄下的類,擴展加載器負(fù)責(zé)加載 ext 目錄下的類耘纱,應(yīng)用加載器加載 classpath 指定目錄下的類敬肚。除此之外,可以自定義類加載器束析。
Java 的類加載使用雙親委派模式艳馒,即一個類加載器在加載類時,先把這個請求委托給自己的父類加載器去執(zhí)行员寇,如果父類加載器還存在父類加載器弄慰,就繼續(xù)向上委托第美,直到頂層的啟動類加載器,如上圖中藍(lán)色向上的箭頭曹动。如果父類加載器能夠完成類加載斋日,就成功返回,如果父類加載器無法完成加載墓陈,那么子加載器才會嘗試自己去加載恶守。如圖中的桔黃色向下的箭頭。
這種雙親委派模式的好處贡必,可以避免類的重復(fù)加載兔港,另外也避免了 Java 的核心 API 被篡改。
詳解分代回收
Java 的堆內(nèi)存被分代管理仔拟,為什么要分代管理呢衫樊?分代管理主要是為了方便垃圾回收,這樣做基于2個事實利花,第一科侈,大部分對象很快就不再使用;第二炒事,還有一部分不會立即無用臀栈,但也不會持續(xù)很長時間。
虛擬機劃分為年輕代挠乳、老年代权薯、和永久代,如下圖所示睡扬。
- 年輕代主要用來存放新創(chuàng)建的對象盟蚣,年輕代分為 Eden 區(qū)和兩個 Survivor 區(qū)。大部分對象在 Eden 區(qū)中生成卖怜。當(dāng) Eden 區(qū)滿時屎开,還存活的對象會在兩個 Survivor 區(qū)交替保存,達(dá)到一定次數(shù)的對象會晉升到老年代马靠。
- 老年代用來存放從年輕代晉升而來的奄抽,存活時間較長的對象。
- 永久代虑粥,主要保存類信息等內(nèi)容如孝,這里的永久代是指對象劃分方式,不是專指 1.7 的 PermGen娩贷,或者 1.8 之后的 Metaspace第晰。
根據(jù)年輕代與老年代的特點,JVM 提供了不同的垃圾回收算法。垃圾回收算法按類型可以分為引用計數(shù)法茁瘦、復(fù)制法和標(biāo)記清除法品抽。
- 引用計數(shù)法是通過對象被引用的次數(shù)來確定對象是否被使用,缺點是無法解決循環(huán)引用的問題甜熔。
- 復(fù)制算法需要 from 和 to 兩塊相同大小的內(nèi)存空間圆恤,對象分配時只在 from 塊中進(jìn)行,回收時把存活對象復(fù)制到 to 塊中腔稀,并清空 from 塊盆昙,然后交換兩塊的分工,即把 from 塊作為 to 塊焊虏,把 to 塊作為 from 塊淡喜。缺點是內(nèi)存使用率較低。
- 標(biāo)記清除算法分為標(biāo)記對象和清除不在使用的對象兩個階段诵闭,標(biāo)記清除算法的缺點是會產(chǎn)生內(nèi)存碎片炼团。
JVM 中提供的年輕代回收算法 Serial、ParNew疏尿、Parallel Scavenge 都是復(fù)制算法瘟芝,而 CMS、G1褥琐、ZGC 都屬于標(biāo)記清除算法锌俱。
詳解 CMS 算法
基于分代回收理論,詳細(xì)介紹幾個典型的垃圾回收算法踩衩,先來看 CMS 回收算法嚼鹉。CMS 在 JDK1.7 之前可以說是最主流的垃圾回收算法贩汉。CMS 使用標(biāo)記清除算法驱富,優(yōu)點是并發(fā)收集,停頓小匹舞。
CMS 算法如下圖所示褐鸥。
第一個階段是初始標(biāo)記,這個階段會 stop the world赐稽,標(biāo)記的對象只是從 root 集最直接可達(dá)的對象叫榕;
第二個階段是并發(fā)標(biāo)記,這時 GC 線程和應(yīng)用線程并發(fā)執(zhí)行姊舵。主要是標(biāo)記可達(dá)的對象晰绎;
第三個階段是重新標(biāo)記階段,這個階段是第二個 stop the world 的階段括丁,停頓時間比并發(fā)標(biāo)記要小很多荞下,但比初始標(biāo)記稍長,主要對對象進(jìn)行重新掃描并標(biāo)記;
第四個階段是并發(fā)清理階段尖昏,進(jìn)行并發(fā)的垃圾清理仰税;
最后一個階段是并發(fā)重置階段,為下一次 GC 重置相關(guān)數(shù)據(jù)結(jié)構(gòu)抽诉。
詳解 G1 算法
G1 在 1.9 版本后成為 JVM 的默認(rèn)垃圾回收算法陨簇,G1 的特點是保持高回收率的同時,減少停頓迹淌。
G1 算法取消了堆中年輕代與老年代的物理劃分河绽,但它仍然屬于分代收集器。G1 算法將堆劃分為若干個區(qū)域唉窃,稱作 Region葵姥,如下圖中的小方格所示。一部分區(qū)域用作年輕代句携,一部分用作老年代榔幸,另外還有一種專門用來存儲巨型對象的分區(qū)。
G1 也和 CMS 一樣會遍歷全部的對象矮嫉,然后標(biāo)記對象引用情況削咆,在清除對象后會對區(qū)域進(jìn)行復(fù)制移動整合碎片空間。
G1 回收過程如下蠢笋。
- G1 的年輕代回收拨齐,采用復(fù)制算法,并行進(jìn)行收集昨寞,收集過程會 STW瞻惋。
- G1 的老年代回收時也同時會對年輕代進(jìn)行回收。主要分為四個階段:
a. 依然是初始標(biāo)記階段完成對根對象的標(biāo)記援岩,這個過程是STW的歼狼;
b. 并發(fā)標(biāo)記階段,這個階段是和用戶線程并行執(zhí)行的享怀;
c. 最終標(biāo)記階段羽峰,完成三色標(biāo)記周期;
d. 復(fù)制/清除階段添瓷,這個階段會優(yōu)先對可回收空間較大的 Region 進(jìn)行回收梅屉,即 garbage first,這也是 G1 名稱的由來鳞贷。
G1 采用每次只清理一部分而不是全部的 Region 的增量式清理坯汤,由此來保證每次 GC 停頓時間不會過長。
總結(jié)如下搀愧,G1 是邏輯分代不是物理劃分惰聂,需要知道回收的過程和停頓的階段凿滤。此外還需要知道,G1 算法允許通過 JVM 參數(shù)設(shè)置 Region 的大小庶近,范圍是 1~32MB翁脆,可以設(shè)置期望的最大 GC 停頓時間等。有興趣讀者也可以對 CMS 和 G1 使用的三色標(biāo)記算法做簡單了解鼻种。
詳解 ZGC
ZGC 特點
ZGC 是最新的 JDK1.11 版本中提供的高效垃圾回收算法反番,ZGC 針對大堆內(nèi)存設(shè)計可以支持 TB 級別的堆,ZGC 非常高效叉钥,能夠做到 10ms 以下的回收停頓時間罢缸。
這么快的響應(yīng),ZGC 是如何做到的呢投队?這是由于 ZGC 具有以下特點枫疆。
- ZGC 使用了著色指針技術(shù),我們知道 64 位平臺上敷鸦,一個指針的可用位是 64 位息楔,ZGC 限制最大支持 4TB 的堆,這樣尋址只需要使用 42 位扒披,那么剩下 22 位就可以用來保存額外的信息值依,著色指針技術(shù)就是利用指針的額外信息位,在指針上對對象做著色標(biāo)記碟案。
- 第二個特點是使用讀屏障愿险,ZGC 使用讀屏障來解決 GC 線程和應(yīng)用線程可能并發(fā)修改對象狀態(tài)的問題,而不是簡單粗暴的通過 STW 來進(jìn)行全局的鎖定价说。使用讀屏障只會在單個對象的處理上有概率被減速辆亏。
- 由于讀屏障的作用,進(jìn)行垃圾回收的大部分時候都是不需要 STW 的鳖目,因此 ZGC 的大部分時間都是并發(fā)處理扮叨,也就是 ZGC 的第三個特點。
- 第四個特點是基于 Region疑苔,這與 G1 算法一樣甫匹,不過雖然也分了 Region甸鸟,但是并沒有進(jìn)行分代惦费。ZGC 的 Region 不像 G1 那樣是固定大小,而是動態(tài)地決定 Region 的大小抢韭,Region 可以動態(tài)創(chuàng)建和銷毀薪贫。這樣可以更好的對大對象進(jìn)行分配管理。
- 第五個特點是壓縮整理刻恭。CMS 算法清理對象時原地回收瞧省,會存在內(nèi)存碎片問題扯夭。ZGC 和 G1 一樣,也會在回收后對 Region 中的對象進(jìn)行移動合并鞍匾,解決了碎片問題交洗。
雖然 ZGC 的大部分時間是并發(fā)進(jìn)行的,但是還會有短暫的停頓橡淑。來看一下 ZGC 的回收過程构拳。
ZGC 回收過程
如下圖所示,使用 ZGC 算法進(jìn)行回收梁棠,從上往下看置森。初始狀態(tài)時,整個堆空間被劃分為大小不等的許多 Region,即圖中綠色的方塊。
開始進(jìn)行回收時泻帮,ZGC 首先會進(jìn)行一個短暫的 STW炎辨,來進(jìn)行 roots 標(biāo)記。這個步驟非常短茸塞,因為 roots 的總數(shù)通常比較小。
然后就開始進(jìn)行并發(fā)標(biāo)記,如上圖所示瓮顽,通過對對象指針進(jìn)行著色來進(jìn)行標(biāo)記,結(jié)合讀屏障解決單個對象的并發(fā)問題围橡。其實暖混,這個階段在最后還是會有一個非常短的 STW 停頓,用來處理一些邊緣情況翁授,這個階段絕大部分時間是并發(fā)進(jìn)行的拣播,所以沒有明顯標(biāo)出這個停頓。
下一個是清理階段收擦,這個階段會把標(biāo)記為不在使用的對象進(jìn)行回收贮配,如上圖所示,把橘色的不在使用的對象進(jìn)行了回收塞赂。
最后一個階段是重定位泪勒,重定位就是對 GC 后存活的對象進(jìn)行移動,來釋放大塊的內(nèi)存空間宴猾,解決碎片問題圆存。
重定位最開始會有一個短暫的 STW,用來重定位集合中的 root 對象仇哆。暫停時間取決于 root 的數(shù)量沦辙、重定位集與對象的總活動集的比率。
最后是并發(fā)重定位讹剔,這個過程也是通過讀屏障油讯,與應(yīng)用線程并發(fā)進(jìn)行的详民。