JVM
簡(jiǎn)述
JVM將java等編程語(yǔ)言的class文件通過解釋器或者JIT生成字節(jié)碼用于和硬件設(shè)備交互断凶。java程序通過生成在JVM虛擬機(jī)運(yùn)行的字節(jié)碼坝茎,JVM虛擬機(jī)通過字節(jié)碼去和硬件進(jìn)行交互姨蝴,屏蔽了很多的操作系統(tǒng)平臺(tái)相關(guān)信息,保證了java的跨平臺(tái)運(yùn)行
Java內(nèi)存區(qū)域
Java 內(nèi)存區(qū)域和內(nèi)存模型是不一樣的東西柴我,內(nèi)存區(qū)域是指 JVM 運(yùn)行時(shí)將數(shù)據(jù)分區(qū)域存儲(chǔ)媒区,強(qiáng)調(diào)對(duì)內(nèi)存空間的劃分;內(nèi)存模型(Java Memory Model已脓,簡(jiǎn)稱 JMM )是定義了線程和主內(nèi)存之間的抽象關(guān)系珊楼,即 JMM 定義了 JVM 在計(jì)算機(jī)內(nèi)存(RAM)中的工作方式
-
程序計(jì)數(shù)器
線程私有
線程是占用CPU執(zhí)行的基本單位,多線程的實(shí)現(xiàn)是CPU通過時(shí)間片輪轉(zhuǎn)方式來讓線程輪詢占用度液,當(dāng)前線程的時(shí)間片使用完之后就需要讓出CPU厕宗,等下次輪到自己再執(zhí)行。程序計(jì)數(shù)器就是用來記錄線程讓出CPU時(shí)的執(zhí)行地址的(線程私有的原因)
如果執(zhí)行的是Java方法堕担,則程序計(jì)數(shù)器記錄的是下一條指令的地址已慢;如果執(zhí)行Native方法,記錄的是undefined地址
-
虛擬機(jī)棧
線程私有
存放線程的局部變量霹购、調(diào)用棧幀佑惠。每個(gè)方法執(zhí)行時(shí)會(huì)在虛擬機(jī)棧生成一個(gè)棧幀,一個(gè)方法就是一個(gè)棧幀從入棧到出棧的過程
-
本地方法棧
線程私有
與虛擬機(jī)棧類似齐疙,本地方法棧對(duì)應(yīng)Native方法膜楷,虛擬機(jī)棧對(duì)應(yīng)java方法
-
堆
線程共享
存放對(duì)象實(shí)例
-
方法區(qū)
線程共享
JDK7之前:使用永久代實(shí)現(xiàn);存放JVM加載的類型信息贞奋、字符串常量池和靜態(tài)變量
JDK7:字符串常量池和靜態(tài)變量移至Java堆
JDK8:廢棄永久代概念赌厅,改用直接內(nèi)存中實(shí)現(xiàn)元空間(Meta-space),將剩余部分(主要是類型信息)移到元空間
-
永久代和元空間是方法區(qū)的兩種實(shí)現(xiàn)方式
永久代:存儲(chǔ)包括類信息忆矛、常量察蹲、字符串常量请垛、類靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)洽议∽谑眨可以通過
-XX:PermSize
和-XX:MaxPermSize
來進(jìn)行調(diào)節(jié)。當(dāng)內(nèi)存不足時(shí)亚兄,會(huì)導(dǎo)致 OutOfMemoryError 異常混稽。JDK8 徹底將永久代移除出 HotSpot JVM,將其原有的數(shù)據(jù)遷移至 Java Heap 或 Native Heap(Metaspace)审胚,取代它的是另一個(gè)內(nèi)存區(qū)域被稱為元空間(Metaspace)元空間(Metaspace):元空間是方法區(qū)的在 HotSpot JVM 中的實(shí)現(xiàn)匈勋,方法區(qū)主要用于存儲(chǔ)類信息、常量池膳叨、方法數(shù)據(jù)洽洁、方法代碼、符號(hào)引用等菲嘴。元空間的本質(zhì)和永久代類似饿自,都是對(duì) JVM 規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中龄坪,而是使用本地內(nèi)存昭雌。理論上取決于32位/64位系統(tǒng)內(nèi)存大小,可以通過
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
配置內(nèi)存大小健田。
類加載過程
-
loading
類加載器烛卧。雙親委派模型:查找負(fù)責(zé)加載的類時(shí)從下至上查找,加載類時(shí)從上至下詢問(bootstracp -> extension -> application -> 自定義)
好處:1.保護(hù)java核心類不被篡改妓局;2.防止重復(fù)加載
-
linking
Verification:校驗(yàn)類是否符合JVM規(guī)定
Preparation:靜態(tài)變量成員賦默認(rèn)值
Resolution:將類总放、方法、屬性等符號(hào)引用解析為直接引用跟磨;常量池中的各種符號(hào)引用解析為指針间聊、偏移量等內(nèi)存地址的直接引用
-
Initializing
調(diào)用類初始化代碼,給對(duì)象賦初始值
對(duì)象創(chuàng)建過程
-
具體過程
類加載檢查抵拘,沒有執(zhí)行類加載則執(zhí)行類加載過程
給對(duì)象分配內(nèi)存
給對(duì)象賦默認(rèn)值
設(shè)置對(duì)象頭,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例型豁,如何能找到類的元數(shù)據(jù)信息僵蛛,GC年齡,是否啟用偏向鎖等(哈希碼要在執(zhí)行了Object::hashCode()方法才生成)
執(zhí)行<Init>方法迎变,給對(duì)象賦初始值
-
內(nèi)存分配原則
-
分配方式
- 指針碰撞:假設(shè)Java堆中內(nèi)存絕對(duì)規(guī)整充尉,被使用的內(nèi)存和沒有被使用的內(nèi)存被放在兩側(cè),中間用指針作為分界器衣形,每次分配內(nèi)存就只需要將指針向右移動(dòng)對(duì)象內(nèi)存大小相等的距離即可
空閑列表:假如Java堆內(nèi)存不規(guī)整驼侠,就需要維護(hù)一個(gè)記錄未使用內(nèi)存區(qū)域的列表姿鸿,每次分配內(nèi)存時(shí)從列表中找出一塊足夠大小的空間分配給對(duì)象
-
取決于內(nèi)存空間是否規(guī)整,也就是取決于使用的垃圾回收器
-
解決分配導(dǎo)致的并發(fā)問題
分配內(nèi)存是個(gè)高頻操作倒源,會(huì)有線程安全的問題苛预,并且頻繁請(qǐng)求堆來獲取內(nèi)存分配太耗時(shí)也太耗資源
TLAB(Thread Local Allocation Buffer 本地線程分配緩沖),JVM會(huì)先從java堆中分配一定的內(nèi)存空間給每個(gè)線程笋熬,線程分配內(nèi)存時(shí)先從TLAB中分配热某,如果TLAB不夠再?gòu)亩焉先シ峙?/p>
CAS+重試機(jī)制
對(duì)象結(jié)構(gòu)
-
對(duì)象頭
Mark Word:8字節(jié)(1位=8字節(jié)),存放hashcode胳螟,偏向鎖標(biāo)識(shí)昔馋,鎖類型標(biāo)識(shí),GC分代年齡等
Classpointer:類指針糖耸,指向類的元數(shù)據(jù)信息秘遏,表明對(duì)象所屬類。默認(rèn)壓縮4字節(jié)嘉竟,不壓縮8字節(jié)
數(shù)據(jù)長(zhǎng)度:4字節(jié)邦危,數(shù)組對(duì)象才有(因?yàn)橥ㄟ^元數(shù)據(jù)信息是知道對(duì)象長(zhǎng)度,但是不能知道數(shù)組長(zhǎng)度)
-
實(shí)例數(shù)據(jù)
String/引用數(shù)據(jù)/Oops(ordinary object pointer 普通對(duì)象指針周拐,對(duì)象引用的句柄) 4位铡俐,int 8位
-
對(duì)齊填充
對(duì)齊Padding(保證對(duì)象位數(shù)是8的倍數(shù))
對(duì)象定位
從棧中的引用定位到堆中具體數(shù)據(jù)
句柄定位:棧存句柄的地址,堆規(guī)劃一個(gè)句柄池區(qū)域妥粟,句柄池存對(duì)象數(shù)據(jù)指針和對(duì)象類型指針
棧中存對(duì)象數(shù)據(jù)的地址审丘,堆的對(duì)象數(shù)據(jù)里面劃分一個(gè)區(qū)域來存指向方法區(qū)里面的對(duì)象類型的地址,定位快速(HotSpot實(shí)現(xiàn))
垃圾的定義
JVM中如果沒有任何引用指向某個(gè)對(duì)象勾给,這個(gè)對(duì)象就被視為垃圾滩报,會(huì)被JVM內(nèi)存回收機(jī)制回收
垃圾對(duì)象判定方式
-
根可達(dá)算法
從GC Roots對(duì)象出發(fā)開始查找引用,沒有在引用鏈上的對(duì)象則被判定為垃圾對(duì)象播急。
-
GC Roots
虛擬機(jī)棧脓钾,本地方法棧,運(yùn)行時(shí)常量池(屬于方法區(qū))桩警,方法區(qū)里面的Reference對(duì)象(常量池對(duì)象可训,線程池對(duì)象,JNI)
-
HotSpot實(shí)現(xiàn)
實(shí)際上不會(huì)將所有的GCRoots作為起源去遍歷引用鏈捶枢,因?yàn)橄奶笪战兀琀otSpot使用oopMap的數(shù)據(jù)結(jié)構(gòu)來直接得到哪些地方存放有對(duì)象引用,在類加載完成后烂叔,就會(huì)記錄下對(duì)象內(nèi)什么偏移量是什么類型的數(shù)據(jù)計(jì)算出來谨胞,而不需要一個(gè)不漏從GC Roots開始查找
對(duì)象從分配到被回收的過程
常見的垃圾回收算法
標(biāo)記清除
復(fù)制
-
標(biāo)記整理
常用的回收算法都是分代回收,即針對(duì)年輕代和老年代對(duì)象的不同采用不同的回收算法蒜鸡,年輕代劃分一個(gè)Eden區(qū)和兩個(gè)Survivor區(qū)
安全點(diǎn)胯努,安全區(qū)域
安全點(diǎn):HotSpot只有在安全點(diǎn)的地方才會(huì)生成oopMap牢裳,程序只有在運(yùn)行到安全點(diǎn)的時(shí)候才能進(jìn)行垃圾回收,采用主動(dòng)式中斷讓線程自己去輪詢標(biāo)志叶沛,當(dāng)標(biāo)記為真就自己在最近的安全點(diǎn)上主動(dòng)中斷掛起蒲讯。
安全區(qū)域:由于有些程序處于sleep或者blocked狀態(tài),沒法中斷掛起自己恬汁。安全區(qū)域能夠確保在某一段代碼片段中伶椿,引用關(guān)系不發(fā)生變化,相當(dāng)于擴(kuò)展的安全點(diǎn)氓侧。
記憶集脊另、卡表
-
解決什么問題
GC時(shí),由于有些新生代對(duì)象會(huì)被老年代對(duì)象引用(跨代引用约巷,主要問題發(fā)生在老年代對(duì)象引用新生代對(duì)象)偎痛,那么當(dāng)發(fā)生YGC時(shí),就需要把這些老年代對(duì)象加入到GC Roots独郎,那么如果不清楚新生代對(duì)象被哪些老年代對(duì)象引用踩麦,就需要將整個(gè)老年代加入GC Roots掃描范圍 ,根節(jié)點(diǎn)引用鏈時(shí)間消耗過長(zhǎng)氓癌,會(huì)導(dǎo)致GC效率低谓谦。
-
原理簡(jiǎn)述
JVM使用了記憶集來記錄那些從非收集區(qū)域指向收集區(qū)域的指針集合的抽象數(shù)據(jù)結(jié)構(gòu),保證每次回收時(shí)直接把記憶集中的對(duì)象加入GC Roots即可贪婉。記憶集的實(shí)現(xiàn)有很多的維度反粥,卡表是記憶集的一種實(shí)現(xiàn)∑S兀基于卡表會(huì)把堆空間劃分為一系列的卡頁(yè)組成才顿,一個(gè)卡表對(duì)應(yīng)一個(gè)卡頁(yè),HotSpot JVM的卡頁(yè)大小為512字節(jié)尤蒿,卡表被實(shí)現(xiàn)成一個(gè)簡(jiǎn)單數(shù)組郑气。當(dāng)發(fā)生跨代引用時(shí),引用對(duì)象所在卡表會(huì)被變"臟"(dirty)腰池,每次只需要將這些卡表里面的對(duì)象加入GC Roots即可
-
實(shí)現(xiàn)細(xì)節(jié)
-
關(guān)于用卡表實(shí)現(xiàn)記憶集顆粒度的問題
首先要回歸我們解決問題的本質(zhì)是為了防止GC時(shí)遍歷整個(gè)非手機(jī)區(qū)域的對(duì)象來尋找跨代引用對(duì)象尾组,如果我們精確到一個(gè)類,一個(gè)對(duì)象來說示弓,其實(shí)維護(hù)成本又會(huì)很大演怎,而卡表精確到一塊內(nèi)存區(qū)域的方式保證了快速GC,也防止記憶集維護(hù)成本過高
-
卡表在什么時(shí)候變“臟”避乏?以什么方式?
發(fā)生在其他分代區(qū)域的對(duì)象引用本區(qū)域?qū)ο髸r(shí)甘桑,時(shí)間點(diǎn)是引用類型字段賦值的時(shí)候
通過寫屏障維護(hù)拍皮,類似于引用類型字段賦值這個(gè)操作的AOP切面歹叮,G1之前的垃圾回收器都是用的寫后屏障
G1的記憶集比較特殊,每個(gè)Region分別維護(hù)著自己的記憶集铆帽,記錄下別的Region指向自己的指針咆耿,并且標(biāo)記這些指針分別在哪些卡頁(yè)的范圍內(nèi)(傳統(tǒng)的垃圾回收器的記憶集就是只需要維護(hù)一塊一塊的卡表即可,不需要和G1一樣每個(gè)Region分開維護(hù)爹橱,所以G1的內(nèi)存占用比其他傳統(tǒng)的垃圾回收高)萨螺。實(shí)際是一個(gè)哈希表結(jié)構(gòu),key是別的Region的起始地址愧驱,value是一個(gè)集合慰技,存儲(chǔ)著卡表的索引號(hào)。
-
常見的垃圾回收器
serial+serial old 單線程
po+ps:parallel old组砚、parallel scanvenge 多線程吻商,不允許并行,JDK7糟红,8默認(rèn)
CMS(老年代)+pn(parallel new 就為了配合cms) 收集過程艾帐;出現(xiàn)的問題兩種:浮動(dòng)垃圾(有一個(gè)內(nèi)存閾值的設(shè)置,目前默認(rèn)92%盆偿,就是總內(nèi)存(?)達(dá)到92%觸發(fā)FGC柒爸,因?yàn)閷?duì)象基本上屬于經(jīng)常被回收,“朝不保夕”事扭,所以92%也沒啥子問題捎稚。但是要是預(yù)留給浮動(dòng)垃圾的空間不夠了,直接報(bào)”并發(fā)回收失敗“句旱,然后GC就會(huì)把并發(fā)標(biāo)記給關(guān)了阳藻,然后啟動(dòng)CMS備用方案——serial old,直接單線程來回收谈撒,嚴(yán)重影響效率了)腥泥,內(nèi)存碎片(因?yàn)槭菢?biāo)記清除算法嘛,有設(shè)置啃匿,多久進(jìn)行一次標(biāo)記整理)
G1:初始標(biāo)記(STW)蛔外、并發(fā)標(biāo)記、最終標(biāo)記(STW)溯乒、篩選回收(復(fù)制)
-
并發(fā)標(biāo)記階段的算法
并發(fā)標(biāo)記消耗回收80%的時(shí)間夹厌,決定了GC快慢的因素。并發(fā)標(biāo)記由于引用會(huì)變化裆悄,可能會(huì)產(chǎn)生漏標(biāo)的情況矛纹,如果是該被回收的被標(biāo)記成存活的問題不大,下次回收就行光稼;但是如果是本來該存活的對(duì)象被標(biāo)記成死亡就有問題了或南。
會(huì)產(chǎn)生“對(duì)象消失”的情況需要滿足兩個(gè)條件:
1.重新加入了一條從黑色對(duì)象指向白色對(duì)象的新引用
2.所有灰色對(duì)象到該白色對(duì)象的直接或間接引用被刪除了
這樣本身該白色對(duì)象之前被標(biāo)記為死亡孩等,在加入黑色對(duì)象引用之后,就會(huì)被標(biāo)記為存活采够,但是只會(huì)標(biāo)記一次肄方,所以這個(gè)對(duì)象就會(huì)被回收。
解決方法:增強(qiáng)更新和原始快照蹬癌,兩種方式都是通過寫屏障實(shí)現(xiàn)的
-
增量更新(CMS)
破壞第一個(gè)條件权她,當(dāng)黑色對(duì)象插入新的指向白色對(duì)象的引用關(guān)系時(shí),記錄這些插入引用逝薪,等并發(fā)掃描結(jié)束后隅要,再講這些引用關(guān)系中的黑色對(duì)象為根,重新掃描一次翼闽∈搬悖可以理解為黑色對(duì)象一旦新插入了指向白色對(duì)象的引用,這個(gè)白色對(duì)象就變成灰色對(duì)象感局,所以叫增量更新尼啡。
-
原始快照(G1、Shenandoah)
破壞第二個(gè)條件询微,當(dāng)灰色對(duì)象要?jiǎng)h除指向白色對(duì)象的引用時(shí)崖瞭,記錄這些引用,并發(fā)掃描結(jié)束后撑毛,將這些引用關(guān)系中的灰色對(duì)象為根书聚,重新掃描一次≡宕疲可以理解為發(fā)生刪除引用關(guān)系的時(shí)候雌续,都按照剛開始掃描的那一刻的對(duì)象圖快照來進(jìn)行搜索,所以叫原始快照胯杭。
ZGC:colorpointer+寫屏障有時(shí)間再了解
-
對(duì)象進(jìn)入老年代的方式
對(duì)象太大驯杜,找不到足夠的內(nèi)存區(qū)域進(jìn)行分配
分代年齡達(dá)到晉級(jí)限制,除了CMS是6,其他默認(rèn)15
動(dòng)態(tài)年齡判定:進(jìn)survivor區(qū)的某個(gè)年齡的對(duì)象中大于survivor區(qū)域內(nèi)存大小的一半做个,那么大于等于這個(gè)年齡的對(duì)象都直接進(jìn)入老年代
空間分配擔(dān)保:年輕代GC采用復(fù)制算法進(jìn)行GC回收時(shí)鸽心,當(dāng)其中一個(gè)Survivor區(qū)不足以容納存活的對(duì)象,就需要老年代提供內(nèi)存空間來存放Survivor區(qū)無法容納的多出來的對(duì)象居暖。這就是擔(dān)保的概念顽频。 這時(shí)候有一個(gè)問題:我們無法在對(duì)象回收前得知有多少對(duì)象會(huì)存活下來剧辐。如果每次我們?yōu)榱舜_保老年代空間能完全容納新生代GC后幸存的對(duì)象归形,就需要每次都進(jìn)行Full GC,但是每次都進(jìn)行Full GC耗時(shí)過長(zhǎng)而導(dǎo)致停頓時(shí)間增長(zhǎng)殉农,用戶體驗(yàn)很不好。 針對(duì)這種情況莺奸,Hotspot采用的方法是:在垃圾回收之前丑孩,取之前每一次回收晉升到老年代的對(duì)象容量的平均大小作為參考,老年代剩余空間大于這個(gè)值或者大于新生代對(duì)象總大小則進(jìn)行Minor GC灭贷,小于這個(gè)值則進(jìn)行Full GC。雖然這樣仍然會(huì)出現(xiàn)“擔(dān)保失敗”的情況略贮,但是避免了過于頻繁的Full GC甚疟。
JVM的內(nèi)存模型(JMM,Java Memory Model)
每個(gè)java線程有自己的工作內(nèi)存逃延,線程只能操作自己的工作內(nèi)存览妖,只能通過save和load操作,對(duì)主內(nèi)存的數(shù)據(jù)更新來實(shí)現(xiàn)不同線程之間的數(shù)據(jù)同步
JVM調(diào)優(yōu)
調(diào)優(yōu)包括:預(yù)調(diào)優(yōu)(事前估計(jì)修改配置)揽祥、JVM運(yùn)行環(huán)境問題(卡頓問題)讽膏、運(yùn)行時(shí)出現(xiàn)的問題(OOM這些)
top
top HP -線程pid
jmap histo 這個(gè)命令用來看占用
你有沒有遇到過OutOfMemory問題?你是怎么來處理這個(gè)問題的?處理過程中有哪些收獲?
遇到過,導(dǎo)出大量Excel數(shù)據(jù)導(dǎo)致直接掛了拄丰,先是運(yùn)維報(bào)警府树,運(yùn)行緩慢,然后就OOM料按,把OOM的那個(gè)服務(wù)器剝離出來(因?yàn)樽隽烁呖捎醚傧溃圆挥绊?,利用jmap histo查看對(duì)象占用载矿,發(fā)現(xiàn)大多數(shù)數(shù)據(jù)是行列對(duì)象垄潮。解決方式:修改sql,改成多線程模式查詢闷盔,導(dǎo)出時(shí)用poi的Hssfbook弯洗,有一個(gè)內(nèi)存窗格的說法,保證內(nèi)存中存在的對(duì)象是固定的逢勾。