序
學習jvm已有半月,爲了防止本身學完就忘記,寫此博客.java
jvm之運行時內存
jvm的運行時內存,是學習jvm一個不錯的切入點,在此一一列出:算法
1.虛擬機棧: 一千我的眼中有一千個哈姆雷特,一千個線程有一千個虛擬機棧,在操做系統層面看的話,用戶級線程即是分着不一樣的棧去執行的,既然操做系統老大哥都這樣,jvm的線程確定也是一個線程一個棧了.一個虛擬機棧中又有什麼呢,看看老大哥的棧中,是一個一個的棧幀,jvm天然也是棧幀了(棧幀即方法).除了棧幀,jvm還有一個小的能夠忽略的程序計數器(程序計數器記錄每一個線程運行的位置,方便線程的切換),操做系統擁有着tcb(ThreadControllerTable),能夠記錄本身運行到哪兒了,因此不須要程序計數器.所以就沒有這個概念了吧.那麼棧幀裏面又是什麼呢,這裏面jvm就分的很細緻了,操做數棧,局部變量表,動態連接,返回地址.api
操做數棧是個啥呢?操做系統中,根據指令,將須要操做的數據放入寄存器組中,以後運算出來,運算中途的數據存入寄存器裏面暫存,最後要是出告終果須要保存,那就把它保存到棧幀中(內存).那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,不免有些理解問題,望各位大佬指出,筆者必定仔細查詢資料並改正.