Jvm學習總結
- 序
- jvm之運行時記憶體
- jvm之物件的一生
- jvm之天道的發展
- jvm之大并發時代
- 結
序
學習jvm已有半月,為了防止自己學完就忘記,寫此博客.
jvm之運行時記憶體
jvm的運行時記憶體,是學習jvm一個不錯的切入點,在此一一列出:
1.虛擬機堆疊: 一千個人眼中有一千個哈姆雷特,一千個執行緒有一千個虛擬機堆疊,在作業系統層面看的話,用戶級執行緒便是分著不同的堆疊去執行的,既然作業系統老大哥都這樣,jvm的執行緒肯定也是一個執行緒一個堆疊了.一個虛擬機堆疊中又有什么呢,看看老大哥的堆疊中,是一個一個的堆疊幀,jvm自然也是堆疊幀了(堆疊幀即方法).除了堆疊幀,jvm還有一個小的可以忽略的程式計數器(程式計數器記錄每個執行緒運行的位置,方便執行緒的切換),作業系統擁有著tcb(ThreadControllerTable),可以記錄自己運行到哪兒了,所以不需要程式計數器.因此就沒有這個概念了吧.那么堆疊幀里面又是什么呢,這里面jvm就分的很細致了,運算元堆疊,區域變數表,動態鏈接,回傳地址.
運算元堆疊是個啥呢?作業系統中,根據指令,將需要操作的資料放入暫存器組中,之后運算出來,運算中途的資料存入暫存器里面暫存,最后要是出了結果需要保存,那就把它保存到堆疊幀中(記憶體).那jvm的運算元堆疊是什么呢,筆者覺得這只是個抽象的概念,靠jvm具體的底層發揮和撰寫,或許它是一級快取又或者它就是暫存器,只要你讀取速度夠快,可以滿足jvm要求,應該就差不多了. 咳咳,后來看了下<<深入理解java虛擬機>>,找到了答案,原來是兩種不同的指令集結構,java用的是基于堆疊的指令集結構,而作業系統使用的是基于暫存器的指令集結構,他們的區別在于,基于堆疊的指令集結構因為不需要關注暫存器,使其可移植性好,但是速度較慢,因為需要頻繁的出入堆疊操作.而基于暫存器的指令集結構是主流cpu都支持的.書中也提到了jvm將常用的操作映射到了暫存器中,同時也使用了堆疊頂快取去加快指令運算速度,看來我之前的想法也并非全是錯誤的.(<<深入理解java虛擬機>>牛批!!!)
區域變數表是個啥呢?這個沒啥好說的了,從入門java開始,區域變數表一直是被我當作堆疊的存在,什么值傳遞,地址傳遞搞得我暈暈乎乎,貌似c語言可以自定義值傳遞還是地址傳遞啊(可能不是),但是java就寫死了,基本資料型別是值傳遞,其他的物件就是地址傳遞,這些值啊,地址啊是方法私有的,那么當然是存在每個堆疊幀的區域變數表里面的了.物件就把地址放到區域變數表中
動態鏈接的話,就比較遠了,可以扯很久,c也是有動態鏈接的,都是為了解決某些需要呼叫的時候才能確定的地址,你總不能直接寫地址吧,那以后改了點代碼,所有的地址是不是都要改一下呢,再者你也不知道加載到記憶體以后你的地址在哪兒了.java中的動態鏈接是將符號參考轉化為直接參考的程序,為什么叫動態的,是因為它在運行時這個符號參考的直接參考才確定下來的,那什么時候會發生動態鏈接呢?在java的重寫時,具有多型性,發生重寫的程序便是去檢查實際型別(A繼承B,A a = new B() 這時B為a的實際型別)的該方法,如果有,自然直接去呼叫,如果沒有就要去找他的父類有沒有,沒有就是父類的父類去找,這時便是會發生動態鏈接的,因為發生在運行時,找到了符合的方法才把符號參考替換掉為直接參考(重寫感覺和自帶的類加載器雙親委派機制反著來的)
回傳地址字面意思,方法執行完以后,堆疊幀出堆疊,那么你要回歸上一個堆疊幀去執行了,這個就是記錄你上個堆疊幀執行到哪兒了
2.堆:java運行時記憶體里面的大佬,堪稱一霸.為什么一霸,因為它一般情況下都是最大的那一塊.堆中又有方法區(1.8里面的hotspot為元空間),老年代,新生代這三為巨頭
方法區一個很穩定的巨頭,他的子民有些從混沌初開(jar包剛運行的時候)之時就基本誕生并且安居在此,有些也會在運行時誕生一些出來(類加載進來的類),已然擺脫六道輪回(YonugGc),但是卻難逃那天地重塑(FullGc),就算是fullgc也不是那么容易回收他們,真正的天難滅,地難葬.里面的居民都是些什么角色呢? 運行時常量池(Runtime Constant Pool) 欄位和方法資料、建構式和普通方法的位元組碼內容、還包括一些在類、實體、介面初始化時用到的特殊方法.另外方法區只是一個規范,具體的實作根據jdk的版本和廠商不同是各不相同的.
老年代是一些古董們的聚集地,他們通過后天的努力基本上也是擺脫了六道輪回(YonugGc)之苦,卻也是逃不過天地重塑(FullGc)的.另:有些實力天生雄厚的家伙(大物件)可以很容易混入老年代,而他們可能就是天地重塑的禍根之一
新生代烽煙四起之地,所有人飽受這六道輪回(YonugGc)之苦,想要活下去必須上頭有人(根可達),新生代分為Eden,From和To三個區域.只有歷經輪回(默認經歷16次的YoungGc)才能熬到老年代,不必日日擔憂
3.本地方法堆疊:本地方法堆疊的話,從作業系統上對比的話是這樣子的.C語言無法直接使用硬體,所以作業系統提供系統呼叫的api,讓C語言使用,Jvm作為模仿作業系統的小弟,他也沒法系統呼叫啊,所以直接封裝了本地方法堆疊(C語言寫的),提供給java去呼叫,來實作對硬體的控制.
4.直接記憶體:直接記憶體,已經脫離了jvm的管控了,一般是unsafe類的或者是nio里面的各種Buffer會使用的,在筆者看來,倒像是直接開始搞c了,自己回收記憶體,自己管理,大佬們的利器,小白(筆者)慎用!
jvm之物件的一生
當某個new命令被執行時,一個物件就要被創建了,讓我們看看他的一生吧.
1.物件結構:java的物件結構分為:物件頭,實體資料和填充部分
物件頭又可以分為markWord,物件指標和陣列長度,物件指標就是指向該物件是哪個類的,陣列長度是該物件如果是陣列,那么便記錄該陣列的長度,不是陣列物件則無該部分,markWord較為復雜了,其中有鎖資訊,物件的hash值,物件的分代年齡等
實體資料顧名思義,你的實體的資訊
填充部分 java物件的大小是有規則的,是8位元組的倍數,為什么有這樣的規則呢,是為了更輕松的管理記憶體(C也有這樣的對齊規矩)
2.物件的誕生:一個java物件是怎么出現的呢,new自然是一個很簡單的方法,其中反射的newInstance(Class的和Constructor的)也會創建物件,這兩種都是使用的類的構造方法.還有使用clone方法和反序列化也是會新建物件,并且不會呼叫構造方法
一個物件想要降生,那必須有登記在冊的類,所以第一件事情就是看看你是哪個類的呢?這個程序就是檢查驗證了,看看在方法區的常量池,拿著new的物件的字面量去找你這個類有沒有被加載,決議,初始化過,如果沒有則需要進行類的加載.
倘若類能夠被正確加載過來并完成了決議和初始化的話,那么這個類就會呼叫構造方法在堆中創建一個新的物件啦~~
你不會以為很簡單的就去創建了吧,在堆中創建一個物件可不是一件容易的事情,首先呢,你這個堆中的新物件要放在哪里呢,聰明的你會說是在堆的eden區里面.很棒!但是eden區的哪里呢?你會不會覺得我是只杠精,但是在實際的jvm分配記憶體去創建物件的話,這難道不是一個需要考慮的問題嗎.jvm中分配記憶體有兩種方法:第一種是指標碰撞,如果堆記憶體中沒有記憶體碎片,使用的記憶體和未使用的記憶體分別在eden區的兩端,這個時候,將中間分隔得指標向后移動物件大小,這種分配就叫指標碰撞.要是有記憶體碎片呢?那自然出現了第二種分配方法了:空閑串列:維護一個串列(比如位圖)去標記哪些記憶體使用了,然后選出合適的連續的沒有被使用的空間給堆去使用,這就是空閑串列了.(CMS垃圾收集器會出現記憶體碎片,PS和serial是不會產生記憶體碎片的)
可是,堆是執行緒共享的一個空間啊,那么要是這一塊空間被兩個執行緒同時得到了,這樣不就有問題了嗎?jvm在分配記憶體時會有cas操作去保證分配記憶體時不會出現吧并發問題,當然如果是小物件的話,會有TLAB(ThreadLocalAlloactionBuffer,每個執行緒會在堆中分配一小段空間屬于執行緒私有去創建物件的,來避免并發問題).之后設定物件的物件頭的資訊,最后執行物件的初始操作,這樣一個物件就這樣子誕生了!
3.物件的訪問:一個物件終于就此誕生了,對于堆疊中的單身狗來說,怎么聯系這個物件呢?jvm的規范中并沒有明確的規定該怎么訪問物件.現在主流的物件訪問分為兩種:Hotspot用的是直接指標, 而除了直接指標以外還有使用句柄池訪問的(詳見百度).
4.物件的回收:世上誰人能不死?任你風華絕代,艷冠天下,到頭來也是紅粉骷髏;任你一代天驕,坐擁萬里江山,到頭來也終將化成一抔黃土,物件也總有被回收的那一天.六道輪回(YoungGC),天地動蕩(FullGC).
絕境中求生:想要不死,不能單單靠自己,堆中想長久,必須要有一位老祖(gc root)坐鎮庇護,才能在一次又一次劫難中艱難求生.那什么樣的境界才能變成這樣的老祖呢?
老祖們是這些家伙:堆疊中的單身狗的物件(強參考),靜態屬性參考的物件,方法區中類靜態屬性參考的物件,Native中參考的物件.這些物件不僅自身不滅,只要是其一脈(有關聯的物件)也是可保周全的.有真的老祖也有偽神,號稱能庇護一方,實際是騙子!他們就是堆疊中的軟參考,弱參考和虛參考,軟應用的物件不怕youngGC但是fullGC便會殺死他,弱參考和虛參考卻連fullGC都抵抗不了,十足的外強中干(軟參考和弱參考常用于快取的框架,虛參考貌似沒什么大用).
jvm之天道的發展
道可道,非常道;名可名,非常名,無名,天地之始,有名,萬物之母,jvm的堆中的天道便是垃圾回收器
三千堆世界,每個堆世界都有這么一些天道,他們以萬物為芻狗,發起滅世之亂,動則六道輪回(YoungGC),甚至天地重塑(FullGC).天道無情,卻有跡可循,讓我們看看有哪些天道.
初代目:Serial/Serial Old串行收集組合,最古老的天道(垃圾回收器).單執行緒的收集方式以及有限的空間管理大小,令人發指"Stop the world"的時間,初代天道的力量終究是有點拿不出手哈.開啟引數-XX:+UseSerialGC
二代目:parNew/Serial Old初代目天道苦學東瀛影分身之術,終于能夠多執行緒的去回收垃圾了,他其實就是初代目Serial的多執行緒版本,在使用CMS作為垃圾回收器時會默認使用他收集新生代.因為時多執行緒收集,所以收集效率在cpu多核下比初代目好.開啟引數:-XX:UseParNewGC或者開啟CMS時也會默認使用parNew收集新生代,當CMS的空間碎片太多會啟動Serial Old回收老年代

三代目:PS組合,ParallelScavenge(處理新生代)與ParallelOld(處理老年代),在jdk1.8中默認使用的垃圾回收器,并行垃圾回收器的巔峰,吞吐量優先的回識訓制,GC自適應的調節策略(會根據設定的引數,動態設定新生代的大小來達到設定引數).ParallelScavenge提供了精確控制暫停時間和吞吐量的引數-XX:MaxGCPauseMillis,-XX:GCTimeRatio .不會jvm調優怎么辦PS組合自適應的調節策略帶你飛.怎么設定ps?都說了默認開啟,不配置就行~~
四代目:CMS(Concurrent Mark Sweep),并行的時代終將結束.CMS是天道中劃時代的存在,他的誕生開啟了垃圾回收器的大并發時代!(并發:指的是不用stop the world 就可以進行回收了),PS組合已經將吞吐量做到了極致,CMS想要出頭,必須另辟蹊徑,既然吞吐量出不了頭,那就去搞并發吧,CMS成功了,但是也失敗了,他開啟了一個時代,但是他并沒有完善很多缺陷,最后不得不求助parNew/Serial Old幫他處理爛攤子.開啟引數-XX:+UseConcurMarkSweep

五代目:Garbage-First(G1),繼承了CMS的精神,貫徹落實CMS的并發思想,他成功了!開啟引數:-XX:UseG1GC

jvm之大并發時代
對于垃圾回收器的并發收集,是需要更加深入理解的.特開一節,在這里我們聊一聊CMS和G1垃圾回收器的實作并發收集的細節以及一些坑與解決方法
標記清掃演算法它是用可達性分析演算法(可達性分析演算法是jvm中垃圾回收器使用的判斷物件是否可回收的一個演算法)分辨出哪個物件是可回收的,哪個是不可回收的,但是因為標記清掃演算法它每次回收完是首先將所有物件的標志位變為0,然后標記好不可回收的是1,可回收的為0.等回收完了以后,會把剩余物件的標志位1變成0方便下次標記清除.因為這個演算法它在不同的時期標志位的0和1是有不同意思的,如果是并發的收集模式的話,新產生的物件標記位到底是0還是1呢?如果是0的話,此時如果結束了標記狀態開始回收,是必然將新物件回收掉了,但是如果是1的話,萬一沒有被置為0,下次這個對像本來是要回收的.但是因為初始是1,那必然不會被回收掉.
垃圾回收器想要開啟大并發時代,標記清除演算法是不能夠滿足他的需求的,那就必須想想辦法的,那么就需要有一個演算法去替換原本的標記清除演算法吧!
重要補充:可達性分析演算法在java中的高效率是基于OopMap的資料結構去保存哪些地方存放著物件參考的,而oopMap的維護是在特定的指令的時侯,哪些指令呢?方法呼叫、回圈跳轉、例外跳轉這些指令的時候會對oopMap進行維護,而這些指令就是安全點(安全點的選擇是是否具有讓程式長時間執行的特征,因此最明顯特征就是指令序列復用,那么方法呼叫、回圈跳轉、例外跳轉就是這樣的點)
并發演算法基礎之三色標記法:三色標記演算法的出現為并發提供了標記垃圾的可能,三色分別是:黑色,灰色,白色
下文中的直接相關的意思是物件里面有b物件的這個關聯,例如:a物件有個屬性是B型別的b,那么b是a的直接相關
黑色:本身不可回收,且與他直接相關的物件沒有一個白色的物件(和他直接相關的不是黑色就是白色的物件)
灰色:本身不可回收,但是與他直接相關的物件是白色的物件
白色:沒有被分析的物件(可能是可回收,也可能是未被分析的不可回收物件)
彎彎繞繞一大堆,這里解釋一下吧:所謂三色標記法就是對標記清除演算法的一種優化,既然標記清除演算法因為狀態只有兩種不能做并發,那我就搞三種狀態.之后一邊運行其他業務執行緒,一邊去標記可回收物件唄,三色標記法的程序是這樣的.先拿三個能記錄的東西(堆,堆疊,本子,箱子什么的),一個叫專門記錄黑色的物件,一個專門記灰色的物件,一個專門記白色的物件.第一階段把GCRoot們找出來,然后灰色的堆(假如用的堆)里面把GCRoot放進去,第二階段就是把灰色的堆里面的物件放入黑色的堆里面去,怎么放呢?把灰色的物件一個一個拿出來,與他有直接相關的物件找出來全加到灰色的堆里面去,然后這個灰色物件就能去黑色堆里面去了.如此重復,直到灰色堆空掉,這個時候就標記完成了.
在CMS和G1中都是使用了三色標記法的,CMS和G1在收集時都有共同的點,初始標記和共同標記,筆者認為這就是對應了三色標記法的第一階段和第二階段**.初始標記階段是要"stop the world "的**,不過以為有oopMap的存在這個階段是很快速的,至于為什么要呢?這個怎么說吧,看看專業的解釋吧(摘自<<深入理解java虛擬機>>)

三色標記法是運用在并發的垃圾回收器上的,在并發的條件下,一定有新的物件產生,不可回收的物件變成的可回收的物件,這是必須要解決的問題
CMS中對于新的物件的產生有一個重新標記的程序,這是也是會產生"stop the world" 的,這個程序也是很快的(相對于初始標記是慢的),這個程序就是將新的物件產生給重新標記好,不讓他被回收,對不可回收的物件變成的可回收的物件的問題,也可以在此時進行解決
G1的話有一個最終標記,其中的處理看似和CMS的重新標記很相似,但是是有本質不同的,以為演算法的原因,G1的最終標記是很快速的
CMS在并發標記和并發清除的時候都是不會stw的,那么,在并發清除階段,不可回收的物件變成的可回收的物件就會變成浮動垃圾,不會在本次垃圾回收中被回收,而G1只有并發標記階段不stw,所以沒有浮動垃圾的問題
三色標記法本身還有一個問題是需要解決的,那就是物件的消失
執行緒1掃描的物件已經全部進入黑色的堆中了,所以不會再被掃描了(只有灰色物件會被掃描),此時執行緒2正在掃描灰色物件B,而灰色物件B有一個直接關聯物件C,雖然物件C是白色的不確定物件,但是他實際上是未被掃描的不可回收物件.

如果此時,業務執行緒中物件B和物件A的參考關系沒了,但是物件C也不是這樣就變得可回收了,而是被物件A參考了.但是A是黑色的了,不會再被掃描了

物件B掃描完成變為黑色的后,啟動回收,物件A因為不是黑色就會被回收,而他應該是不可回收的,這就是物件的消失(漏標)

CMS和G1都解決了物件消失的問題,CMS使用的是增量更新(Incremental Update)演算法,G1是通過起始快照演算法去解決的,對這兩種演算法筆者不做深入探討,本博客中大致說一下思路:
增量更新:當物件C插入到物件A時,將物件A變為灰色,然后在重新標記的時候stw,對A物件再次掃描下,即可
起始快照:在并發標記階段維護快照去解決的(筆者目前也還在研究中)
結
對于CMS和G1的對比,本博客只是從并發演算法的角度淺嘗一下,他們是有許多不同的地方的,奈何筆者現階段學識有限,想要深入,心有余而力不足,可能以后會來完善的,在最后的總結中說一說CMS和G1的一些缺點吧
CMS:cpu敏感,只有多cpu才能發揮的好. 浮動垃圾,沒有解決浮動垃圾問題.記憶體碎片,標記清掃演算法會產生空間碎片.
G1:cpu敏感,同上,要求記憶體要大.
后續筆者需要深入理解的一些概念:G1垃圾回收器的實作細節,記憶體細節,起始快照演算法的具體實作,G1的跨代參考使用的記憶集
歷時3天終于,寫完了,完結撒花,碼字不易,求關注三連
很重要的事:筆者剛開始系統學習jvm,難免有些理解問題,望各位大佬指出,筆者一定仔細查詢資料并改正.
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/226202.html
標籤:java
