JVM內存模型與垃圾收集

JVM內存模型與垃圾收集

  • JVM堆內存模型
    JVM的垃圾收集主要指的是堆內存空間,那麼在每一次執行GC的時候需要區分出哪些堆內存空間需要被回收,哪些不需要被回收,所以爲了整體的回收處理方便,JVM將堆內存分爲如下的幾個組成部分,而這幾個組成部分還需要去考慮JDK版本,現在的JVM內存劃分就必須考慮JDK1.8之前和JDK1.8之後的問題了

這裏寫圖片描述

jdk1.8之前主要分爲
新生代、Eden(伊甸園)、S0、S1(S0和S1合成爲存活區)
老年代、
永久代
永久代:jdk1.8之後被幹掉
如果簡化點來理解的話:
1)新生代:那些剛剛創建的對象,剛剛創建的對象有可能會存在有許多的垃圾對象,這些對象應該是被優先回收的。
2)老年代:老不死的那類對象,經過了很多次的清理之後你會發現該對象依然有用。
3)永久代:intern()方法進行入池的對象實際上就在永久代之中,永久代不會被回收,除非JVM崩潰死機,因爲其本身屬於bug性質的存在,所以在jdk1.8之後將其更換爲元空間(電腦的直接內存)
100g電腦內存80g給了java,剩餘的20g稱爲元空間

這裏寫圖片描述

每一個空間上都有一個伸縮區
在整個內存的組成過程之中每一代的內存空間都會有一個伸縮區,那麼該區域就可以由JVM根據空間使用情況動態擴充。
如果伸縮區內存大小有限,每次擴充的時候會進行判斷,這樣會降低性能。
當我們適當合理的設置了伸縮區的內存大小之後,那麼就可以得到良好的性能提升,也就是說最容易的性能提升就是改變伸縮區的內存大小設置。
JVM的垃圾回收實際上本質上就是如何讓JVM在電腦上發揮出最大的性能優勢

  • java對象的創建和垃圾回收的流程
    在java之中支持有GC的概念,那麼GC有兩種調用的形式:
    1.自動調用
    2.手動調用(Runtime.getRuntime().gc()):很少使用
    那麼什麼時候會進行自動的GC調用呢,這裏面就牽扯到了兩類的GC操作環境。

這裏寫圖片描述

1.當現在程序之中需要產生新的實例化對象(關鍵字new 、對象克隆、反射實例化(包含classloader))的時候,那麼就一定要進行內存空間的開闢,所以此時需要申請新的內存空間,
2.新對象要申請的對象空間默認都是在伊甸園區(新生代區)進行開闢,所以首先要判斷伊甸園是否有空餘的內存空間,如果有空餘的內存空間,則直接在伊甸園區開闢新的堆內存空間,此時不會發生有GC處理;
3.如果新對象無法在伊甸園區申請出新的空間,那麼就表示現在的伊甸園區的內存空間不足,不足就需要將那些無用的新對象進行回收(Minor GC);—JVM回收不活躍對象,當回收完成後要繼續判斷該空間是否還有剩餘的空間可以容納下新的對象,如果可以則開闢新空間,保存新對象;
4.如果此時伊甸園區即使執行了Minor GC之後發現仍然沒有可以被回收的對象,(或者說伊甸園內存仍不足以創建新對象),那麼這個時候將繼續判斷存活區是否有空間,(存活區一般與伊甸園區的比率:1:1:8)S0:S1:Eden ,如果存活區有空餘空間,則將那些活躍的伊甸園區的部分對象直接保存給存活區,這樣就相當於伊甸園區可以騰出部分的空間(這個空間非常小)來,供新對象使用。
5.如果此時存活區依然滿了(空間不足),則繼續向老年代進行內存空間的申請,首先會判斷老年代的空間是否空餘,如果有空餘的空間,則將存活區中的活躍對象保存在老年代區,而後存活區得到了空間釋放,伊甸園區也得到了空間釋放,則對象空間申請成功。

6.如果老年代內存空間也是滿的,那麼這個時候會執行FULL GC(完全GC、major GC)進行老年代的內存釋放,如果可以釋放成功,則可以進行對象的保存,如果釋放不成功,則表示已經沒有無用的內存空間了,那麼就會拋出OOM(OutOfMemoryError)

這些是GC自動回收觸發條件。

面試題:請問自動的GC什麼時候會觸發:
最笨的回答:內存不夠了,觸發GC
GC的觸發有兩類:Minor GC 和FULL GC
Minor GC發生在新生代內存空間不足的情況下,當新生代內存空間不足時,會進行觸發,釋放新生代中不活躍的對象
FULL GC發生在老年代內存空間不足的情況下,當老年代的內存空間不足時會自動觸發FULL GC,如果觸發FULL GC之後內存空間仍然不足,則會產生OutOfMemoryError錯誤提示信息,表示已無內存可以被創建。

  • Java堆內存調整參數
    本質:GC發生的越頻繁,對系統性能越大
    GC雖然可以進行內存空間的釋放,但同時頻繁的GC一定會影響系統性能,如何纔可以不頻繁的發生GC呢?(GC發生頻率低一點):目標:伊甸園區、存活區、老年代空間足夠大,GC發生頻率會低。
    內存越大,GC發生的頻率就越低,系統的性能就越高。
    代碼中減少不必要的對象的產生。

    Java內存調整策略(優化策略)

    範例:首先來取得當前可用的內存空間量。

 Runtime run = Runtime.getRuntime();//此處是單例模式
 System.out.println("MAX_MEMORY = " + run.maxMemory());
 System.out.println("TOTAL_MEMORY = " + run.totalMemory());
 System.out.println("FREE_MEMORY = " + run.freeMemory());

這裏寫圖片描述

結果返回的是字節:

 Runtime run = Runtime.getRuntime();//此處是單例模式
 //默認情況下分配給JVM的最大內存空間:大約總內存的四分之一(電腦爲64g內存,此結果大約是14g)
 System.out.println("MAX_MEMORY = " + run.maxMemory() +"("+((double)run.maxMemory() / 1024 / 1024)+"M)");
 //默認情況下除了伸縮區之外的可用內存空間
 System.out.println("TOTAL_MEMORY = " + run.totalMemory() +"("+((double)run.totalMemory() / 1024 / 1024)+"M)");
 System.out.println("FREE_MEMORY = " + run.freeMemory() +"("+((double)run.freeMemory() / 1024 / 1024)+"M)");

根據結果可知:
//默認情況下分配給JVM的最大內存空間MAX_MEMORY:大約是總內存的四分之一

在整個分配給JVM使用的對內存之中返現,TOTAL表示用戶的最大可用,而MAX - TOTAL才表示伸縮區,所以每一塊的內存之中都存在伸縮區的概念。

如果說現在給用戶的使用內存空間留有伸縮區,那麼會造成以下的一種情況:
如果可用內存不足,會判斷是否伸縮區有空間,而後開闢伸縮區的空間。那麼這種伸縮區的開闢與回收操作部分就有可能產生性能下降。

參數:

這裏寫圖片描述

如果現在取消掉伸縮區的概念,讓初始的內存就是最大的可用內存空間,這樣就可以實現JVM的性能調整,避免了重複的內存控制操作,可以讓整個代碼的執行速度上升。

-Xms :設置初始分配大小,默認爲物理內存的1/64(比如內存64G,實際初始大小約爲1G)
-Xmx:最大分配內存,默認爲物理內存的1/4

double x = (double)64 *1024*1024*1024
System.out.println(x/run.totalMemory());

大約爲66

如果要想修改內存的大小,可以使用的單位:K、M、G;但是如果要改則考慮兩類情況,一個是Eclipse修改、另一個是Java運行修改

1.Eclipse修改:
右鍵-run configurations

這裏寫圖片描述

設置完成後,可以發現Runtime類能夠取得到的數據就是修改後的結果
如果是一臺java專用服務器,則把所有內存都加給jvm使用,比如擁有64g,則全部分配給JVM使用
-Xmx=64g -Xms=64g
這是一個很有用的性能調優,避免伸縮區不斷擴展和回收帶來的性能問題

2.命令行方式:在執行java程序的前面追加相應的配置信息:
這裏寫圖片描述

所有的java程序運行的參數要寫在執行類的前面。而且每個執行類都要分開配置。
一般一臺計算機上只跑一個JVM

在整體參數之中提供有一個GC處理詳情的問題:
這裏寫圖片描述

通過此操作可以詳細得取得GC處理中的執行問題的分析過程。

範例:編寫一個程序

這裏寫圖片描述

在eclipse中運行的時候添加參數:

這裏寫圖片描述

入池:一定會有新生代保存到永久代內存區(活躍對象)
這裏寫圖片描述

包含了GC和FULL GC的操作
以上就將整個程序運行中出現的GC過程進行了記錄。
以兩條記錄爲例:
1.新生代的GC處理(Minor GC處理)
分配失敗,意味着新生代沒有可用內存空間了,
會執行好幾次

這裏寫圖片描述

2.老年代的GC處理:(FULL GC)
此時老年代內存分配失敗,引發FULL GC,是一個完整的GC,
這裏寫圖片描述
eden:伊甸園處理
PSYoungGen:新生代
ParOldGen : 老年代
Metaspace :元空間

3.會詳細顯示出每一塊內存空間的內存使用情況:
但是需要注意的是:此時的內存狀態實際上取得了只是供開發者觀察的。
但如果是項目的運維人員,它是看不見這些內容的,就必須使用一些監控程序,
java有兩類監控程序:
jdk安裝目錄下bin裏邊:jmap.exe(cmd下監控)、jvisualvm.exe( 圖形化監控)兩個工具

可視化監控:

這裏寫圖片描述

這裏寫圖片描述

cmd下輸入:
jmap pid(進程號)

這裏寫圖片描述

這裏寫圖片描述

可以參考:
jmap -heap pid
比如:jmap -heap 3248
可以通過這個實現內存監控處理

面試題:你們的項目之中是如何對java進行調優?
1.在堆內存中會存在一個伸縮區的概念,默認情況下最大可用內存爲整體內存的四分之一,默認使用的內存爲整體內存的六十四分之一,這個時候只需要避免伸縮區的頻繁變更就可以提升程序的系能。
2.可以在程序執行的時候使用 -Xmx設置最大可用內存,-Xms設置初始化內存空間大小,將這兩個內容設置爲一樣的空間大小就可以提升JVM的運行性能。
3.以上調優都是相對而言,性能的提升必須建立在合理的代碼基礎之上。程序合理完善。

  • *年輕代(新生代)

年輕代調整參數:
年輕代主要分爲兩個區域:伊甸園區 、存活區(分爲兩個);
這裏寫圖片描述
所有新創建的對象都會存活在伊甸園區,但是伊甸園區的保存的空間一定是最大的,畢竟產生新對象的機率是很高的,在伊甸園區裏面由於裏面的對象經常只是臨時創建的,所以其擁有一個Minor GC的操作處理,而經過多次的MinorGC處理後依然被保留下來的對象就認爲該對象不應該被回收,則將此對象保存到存活區之中。
存活區一共分爲兩類:
存活0區(S0區、From Space):
存活1區(S1區、To Space):
這兩個存活區主要負責對象的晉級,向老年代的晉級。並且這兩個存活區有一塊是專門負責對象回收的,所以有一塊內存空間總是空的。所以有一塊內存空間總是空的(一個負責晉升,一個負責回收)

由於伊甸園區保存的數據對象一般較多,所以默認的比率是8:1:1,兩個存活區的大小是相同的。s1和s0名稱是可以互換的。由系統完成。

這裏寫圖片描述

這個算法不是固定的,取決於使用電腦的硬件環境,單CPU和多CPU是不一樣的。

存活區 :動態對象數組
再進行GC之前一定要先發生一次全對象的掃描處理操作,通過掃描纔可以知道哪些對象是垃圾空間。
年輕代中
這裏寫圖片描述

這裏寫圖片描述

BTP技術:查找剩餘空間:根據棧頂元素

在java的內存之中,所有的堆內存是線程共享的,這樣根據棧頂元素判斷剩餘空間就變得不夠合理。

所以纔有了TLAB技術:解決了多線程,但是可能會存在碎片問題。
這裏寫圖片描述

年輕代中內存還是可以通過參數來進行控制的。

這裏寫圖片描述

-Xss:一般不做設置,一個對象可能會很大,線程棧本身太小可能會發生溢出問題。

這裏寫圖片描述

年輕代的整體的空間控制的修改意義不大,其實最有用的部分就是在於線程的大小分配,但是又不能夠輕易的修改,同時由於年輕d代的算法問題,所以理論上你的電腦程序(服務器)是不可能接受無限多的線程(用戶),要想接受更多,必須具有更強的CPU和更大的內存。

  • 老年代調整
    一個對象要想真正的活到老年代,實在是太難了,因爲現在的程序都屬於多線程訪問,所有的業務操作的對象都是針對線程操作的,那麼所有的線程在整體的操作過程之中時間都是非常短的。(線程操作的生命週期長,佔用資源多,反之則短)
    那麼這些對象往往都在年輕代中開闢,很少能跑到老年代之中。

這裏寫圖片描述

FULL GC = Major GC

老年代算法:
Mark-Sweep
這裏寫圖片描述
碎片整理會發生性能問題的瓶頸

Mark-Compact
這裏寫圖片描述

這些算法是根據硬件環境動態選擇,當然也可以自己配置。

這裏寫圖片描述

默認情況下,所有對象都會通過年輕代進行創建,而後經過不斷的Minor GC之後要將其保存在老年代之中,但是如果現在有些對象的內容特別龐大,(我們在做數據庫查詢的時候,用多少數據取多少數據,否則會導致很大的對象,來回晉升可能會影響性能),那麼就不建議經過年輕代而將其直接保存到老年代之中,就可以設置以上參數:
超過512k直接進入老年代。
這裏寫圖片描述

一般一個良好的程序開發,是不可能出現這樣的問題的。
數據庫分頁:
如果通過數據庫分頁,取得數據很有限,
但如果把所有數據拿到程序中在做算法分頁的話,這樣會內存分配會出現問題。
full gc發生的機率很低,儘可能少發生,full gc 操作處理會很耗時間,會影響程序性能。

  • 永久代
    jdk1.8之後被廢除
    永久代是在jdk1.8之前的一個bug性的存在,其核心的本質在於:
    該區域中的對象不會被回收。所有的GC操作對老年代是無效的。
    或者簡單理解來說就是 方法區就是永久代。
    HotSpot虛擬機規範中是存在有永久代概念的,但是BEA和IBM的虛擬機規範之中是不包含永久代概念的。
    Oracle現在將HotSpot中的永久代取消了,那麼就意味着它現在希望HotSpot與JRockit兩個虛擬機的規範進行合併。

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

所以現在永久代已經不做考慮了。

  • 元空間
    元空間是永久代的替代品。
    從jdk1.8之後正式取消了永久代之後,取而代之的是元空間(MetaSpace),所謂的元空間的本質指的就是本機的物理內存,起作用和永久代相同,但是元空間和永久代最大的區別:
    元空間用的是物理內存(收到本機的物理內存的限制),而永久代是JVM的內存空間(本身受到JVM的限制)。
    本質上兩者都是保存那些基本上不會被清空的操作的。

調整參數:
這裏寫圖片描述

設置元空間的初始大小等。基本上不需要調整。
範例:設置元空間的操作
這裏寫圖片描述
會報錯!
這裏寫圖片描述

對於OOM的錯誤信息,可能是java的堆內存溢出(java heap space),元空間溢出(MetaSpace)、永久代溢出(PermGen Space)。
面試:請問是否知道什麼叫OOM,怎麼會出現?
OutOfMemoryError:指的是內存溢出問題,內存的溢出需要考慮以下情況:
java堆內存溢出java heap space 往往出現在FULL GC失敗之後;
永久代溢出PermGen Space 一個方法中出現的內存溢出。
元空間(MetaSpace):分配的物理內存不足,或者數據量高於物理內存。

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