再來一遍 JVM

首先我們說一下內存管理,因爲所有程序都是運行在內存之上的,有的程序需要自己管理運行內存比如C語言,有的程序就是半自動管理運行內存,例如 Java,自動的東西 當然是完成了80~90%的工作,而剩下10~20的管理就沒有管理的那麼細緻了,所以JVM內存的管理,上手簡單,說白了,也就是不需要你瞭解多少東西,就能直接用。那麼下面我們就來看看JVM內存自動管理的那部分屬性狀態吧。

JVM內存結構

          堆,棧,方法區,程序計數器    其中棧呢又包括兩個部分{虛擬機棧,本地方法棧},虛擬機棧我更樂意叫它 "線程棧"

          程序計數器:就是爲了記錄當前線程字節碼指令執行到了什麼位置,因爲在處理器中每一個時間點是隻能執行一個線程的,多線程的運行在於線程的來回切換,線程切換回來你得知道線程運行到了什麼位置,下面該要執行哪個字節碼指令了。程序計數器,做的就是這個工作。

          虛擬機棧:這東西也是線程自己的,聲明週期和線程一樣,運行每一個方法的時候,都會創建一個棧幀,方法在運行時間所需要的入參,以及方法中變量的計算賦值,這一系列動作都是在棧幀裏面進行的,當方法執行完畢,再進行出棧動作,將計算結果返回給調用者,或者直接不返回,方法在編譯期間就已經確定了要在棧幀中分配多大空間。

          本地方法棧:其實跟虛擬機棧是基本類似的,虛擬機棧是爲了JVM中的方法執行提供記錄與存儲的,本地方法棧就是爲本地方法執行提供方便的唄。

          堆:堆,存放的最多的還是對象信息,在堆裏面存放不同的對象,這時候就跟內存的自動管理掛鉤了,也就是堆要完成對於堆中存放的對象的自動管理,也就是自動的銷燬與創建或者換位置等等,按照 "代" 收集理論,主要分爲 新生代、老年代、永久代、 Eden 、Survivor區。

          方法區:對於所有線程也是共享的,方法區不是永久代,只是用永久代的概念來設計方法區內存的回收。現在基本上已經放棄了永久代這麼個說法,改用本地內存來實現方法區(元空間),所以以後來說你的本地內存多大,你的方法區就有多大。

          運行時常量池:運行時常量池其實也是方法區的一部分,主要存放編譯期生成的各種字面量與符號引用,在類加載之後,將這部分內容放在方法區裏頭。

          直接內存:直接內存的使用主要是爲了提高程序運行效率,減少磁盤數據到內存數據直接的來回複製以及線程上下文的頻繁切換,減少程序運行時間,但是開闢直接內存代價還是很高的。

      對象的創建過程:

  • 在new的時候,會去檢查執行new指令所帶的參數能不能在常量池中找到,如果能在常量池中找到就證明類已經被加載,不用再次加載類。
  • 類加載檢查之後,類在內存中需要佔用具體多大內存,就已經知道了,就開始着手給新對象分配內存,分配內存就看堆中的內存是不是足夠,是不是連續的,內存的連續與否就看內存的回收算法是怎樣的了。
  • 也可以通過線程的本地緩衝區進行創建對象的存儲。

對象中的內容主要包括兩個方面,一個是對象自身存儲的運行時數據,另一個是對象的狀態數據,比如對象的鎖狀態標誌,偏向線程ID

對象的訪問定位

1. 通過句柄訪問對象

  如果通過句柄訪問對象的話,棧中存儲着對於句柄池句柄的引用,句柄池中存儲這對於對象實例的引用和對象類型的引用

2.直接通過引用來訪問實例數據

  通過引用直接指導堆中的實例數據,實例數據中存儲這對於對象類型的引用,

使用直接指針的好處:訪問速度快,很少通過句柄訪問的,因爲多了一層定位過程。

 

棧的深度指的就是棧幀的創建個數,一次循環方法的調用,就能在一個線程的棧中不停地創建棧幀,然後壓入到棧中,有點類似於 hashMap的桶。

 

 

垃圾收集器與分配策略

如何判斷一個對象該不該在下一次GC的時候就行回收

1.引用計數法

    如果我要回收它,我就給他蓋個戳

2.可達性算法

以GC-Root爲根節點,如果能在這顆引用樹上的都不回收,剩下的都回收

GC-Root的對象

   虛擬機棧中引用的對象

   方法區中類靜態屬性引用的對象

   方法區中常量引用的對象

   本地方法棧中引用的對象

   被同步鎖持有的對象

 

  對象死不死,判斷就是要執行兩次標記過程,第一次發現在引用鏈上沒有它,就標記它,隨後再進行一次篩選,條件就是有沒有實現finalize方法,如果沒有的話,系統就判斷這個對象死了,如果說實現了finalize方法,系統會執行這個方法,對象想要復活  就在finalize方法中跟GC-Root下的對象掛上勾,這樣就死不了了。

方法區回收:

    方法區回收的主要對象就方法區裏面存放的東西,你說這不是廢話麼。那你就說說 方法區裏面放了什麼? 常量和類型嘛

   常量回收就很簡單啦,只要我虛擬機裏面沒地方調用你,並且垃圾回收器判斷有必要的話(至於垃圾回收期怎麼判斷有沒有必要 Enn....)就把你回收掉。

    類型的回收就比較麻煩了,

這個類的所有實例都已經被回收了,虛擬機裏面再沒有了
該類的類加載器也被系統回收了
該類對應的Class對象沒有任何地方引用了

 滿足上述條件,只能說你這個類型具備被回收的條件了。虛擬機提供了這個參數 -Xnoclassgc 來判斷是否要回收

類型的回收主要是在動態生成的類,反射生成的類 JSP文件的編譯生成的類,這些類型生命週期短,一般都是要及時回收的,從而保證方法區的壓力不是太大

垃圾回收算法

      1.標記清除 大法

               我給要回收的對象蓋個戳,在下一次GC的時候就把你回收掉,但是沒有回收的對象依舊保持自己的內存位置,這樣會導致內存中太多碎片。

       2.標記複製 大法

               標記-複製 大法 主要就是把整個堆內存分爲相等的2塊,一塊用來做備份space1一塊用來放新生成的對象space2,一旦space2滿了,我就把space2裏面的存活的對象全部放到space1裏面,然後把space2清空。這樣的好處是有大塊的連續的內存空間了,但是空間的利用率不高,你想嘛 一半都用來做備份了,能高麼。

       3.標記整理 大法

              將所有的存活的對象都移動到內存的一端,在GC的時候直接把存活對象之外的對象直接幹掉,這樣我空間利用率也高了,並且我還有足夠大的連續內存空間,可以用來分配大的對象。但是缺點也是很明顯滴,每次內存中存活對象的移動都會導致棧中更新對象的引用,這樣做要停下來所有的工作用來進行對象的移動。並且像活躍對象多的地方-老年代也要進行這樣的大操作,會導致系統間歇性的卡頓。

 基於以上的方法,我們可以看出,要麼 我們不移動對象,但是系統內存碎片化太嚴重,要麼我們移動對象,這樣會導致系統卡頓,那麼有沒有我們既要移動少量對象用以騰出來大的內存空間,並且不會導致系統卡頓的方法呢? 肯定有嘛,不然怎麼會這麼問。

       4.分代回收

               分代理論主要是將虛擬機中對象的存儲分爲 年輕代,老年代  年輕代又分爲  Eden區 Survivor區  Survivor區分爲兩個

Survivor1 Survivor2.  E:S:S=8:1:1  ,其中s2永遠是空的,用來存放GC之後的剩餘的在Eden區中存活的對象(是不是有點怪,既然是空的爲什麼還要存放對象呢? 因爲  他們角色扮演,會互換角色)

               新生成的對象首先默認在Eden區分配,在進行GC的時候Eden區的存活的對象進入到s2, 這時候判斷s1裏面的對象是否要進入老年代,判斷完了之後會把s2的剩餘的對象全部複製到s1裏面,同時清空s2 保證下次GC的時候,eden區中存活的對象直接扔到s2裏面。就這樣一遍一遍的GC循環,當s1裏面的對象填滿的時候,直接把s1的對象全部扔到老年代。

              對象什麼時候會直接進入老年代呢。

             1.對象過大的時候,因爲如果對象過大,對象在年輕代裏面GC的時候會大量的進行復制,佔用系統資源,這時候我們直接扔到老年代,避免大對象的複製。

             2.設置對象進入老年代的年齡限制。就像不慢18歲不能進98一樣,我修改這個門檻,讓你8歲就能進98.

             3.當s1裏面相同年齡的對象佔s1內存的一半以上,我就直接讓這部分以及年齡比這部分大的對象進入老年代,比如,規定18歲不能進98,結果全國一半的人認爲這個規定不合理(半數原則嘛),那我沒辦法,只能降低98年齡限制,放你們進來。

空間分配擔保:

       說白了就是爲了避免Survivor裏面放不下新生對象的情況,因爲新生代的內存分配是8:1:1嘛  ,如果我的8 裏面經過GC之後發現,我活着的對象還佔4成,那我的Survivor1裏面肯定是放不下的啊,這時候就要有一個區域來保證我的活着的對象有地方放,那老年代的內存區域就是我的保障,我直接把對象丟到老年代就行了。但是   這樣有風險。老年代也放不下了就涼涼,只能進行Full GC,也就是我放不下了,大家都別幹活了,先把沒用的清出去,讓我有地方放。

 

排版不好看,我是個粗人,先將就着看吧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章