超詳細的JVM優化方案

一.概念理解
1.1堆與棧

棧是運行時單位,解決程序的運行問題,即程序如何執行,或說如何處理數據
堆是數據的存儲單位,堆是jvm中管理內存中最大一塊。它是被共享,存放對象實例。也稱爲“gc堆”。
垃圾回收的主要管理區域

在java中一個線程就會有一個線程棧與之對應,不同的線程執行邏輯不同,因此需要一個獨立的線程棧。而堆是所有線程共性的。棧因爲是運行單位,因此裏面存儲的信息都是跟當前線程相關信息的。包括局部變量,程序運行狀態,方法返回值等等;而堆只負責存儲信息。
在這裏插入圖片描述
1.2爲什麼需要堆和棧
第一.從設計角度,棧代表了處理邏輯,而堆代表了數據。這樣分開,使得邏輯更爲清晰。分而治之的思想。這種隔離。模塊化的思想在設計的方方面面都有體現。
第二.堆與棧的分離,使得堆中的內容可以被多個棧共享(也可以理解爲多個線程訪問同一個對象)。這種共享的收益很多,一方面這種共享提供了一種有效的數據交互方式,另一方面,堆中的共享變量和緩存可以被所有棧訪問,節省空間
在這裏插入圖片描述第三.棧因爲運行時需要,比如保存形系統運行的上下文,需要進行地段的劃分。由於棧只能向上增長,因此就會限制棧儲存內容的能力。而堆不同,堆中的對象可以根據需要動態增長的。因此棧和堆拆分,使得動態增長成爲可能,相應棧中只需要記錄堆中的一個地址即可。
第四.面向對象就是堆與棧的完美結合。我們把對象開分開,對象的屬性其實就是數據,存放在堆中;而對象的行爲(運行邏輯),放在棧中。我們在編寫對象的時候,其實是編寫了數據結構,也編寫的數據處理的邏輯。在java中,Main函數是棧的起始點,也是程序的起始點

1.3存儲數據
堆中存的是對象
棧中存儲的是基本數據類型和堆中對象的引用
一個對象的大小是不可估計的,或者說是動態變化的,但是棧中,一個對象只對應了一個4byte的引用
爲什麼不把基本類型放在堆中呢?
**因爲其佔用的空間一般是1~8個字節,需要空間比較少,因爲是基本類型,不會出現動態增長的的情況(長度固定),因此在棧中存儲就足夠了。**如果把它存在在堆中是沒有意義的(還會浪費空間)。可以這麼說,基本類型和對象的引用都是存在在棧中,但是基本類型,對象引用和對象本身就有區別了,因爲一個是棧中的數據,一個是堆中的數據,會產生java中參數傳遞的問題

三.參數傳遞
1.java中只有值傳遞(基本數據類型的值,引用數據類型的值)
2.java中沒有指針的概念
3.程序運行永遠是在棧中進行的,因此參數傳遞時,只存在傳遞基本類型和對象引用的問題。不會傳對象本身。堆和棧中,棧是程序運行最根本的東西。程序運行可以沒有堆,但是不能沒有棧。而堆是爲棧進行數據存儲服務,說白了堆就是一塊共享的內存。正因爲堆和棧分離的思想,才使得java的垃圾回收稱爲可能。

java中,棧的大小通過-Xss來設置,當棧中存儲數據比較多時,需要適當調大這個值,否則會出現java.lang.StackOverflowError異常。常見的出現這個異常是無法返回的遞歸,因爲棧中保存的信息都是方法返回的記錄點。

四.對象的大小
在stack裏面,對象的大小:4byte的引用
基本數據的類型的大小是固定的。對於非基本數據類型的java對象的大小,就值得商榷。

在Java中,一個空Object對象的大小是8byte,這個大小隻是保存堆中一個沒有任何屬性的對象的大小,看下面語句:

Object ob = new Object();

這樣在程序中完成了一個java對象的生命,但是它所佔的空間爲:4byte+8byte是上面部分所說的java棧中保存引用的所需的空間。那8byte則是java堆中對象的信息。因爲所有的java非基本類型都需要默認集成Object類,因此不論什麼樣的java對象,其大小必須大於8byte。

五.引用類型不會回收強引用,其他引用會優先回收
對象的引用分爲強引用,弱引用,軟引用,虛引用。
強引用:一般生成對象時java虛擬機生成的引用,強引用條件下,垃圾回收需要嚴格判斷當前對象引用是否是強引用,如果是,則不會被垃圾回收。
軟引用:一般作爲緩存來使用,軟引用在垃圾回收時,虛擬機會根據當前系統的剩餘內存來決定是否對軟引用進行回收。如果剩餘內存比較緊張,則虛擬機會回收軟引用所引用的空間;如果剩餘內存相對富裕,則不會進行回收。
弱引用:弱引用與軟引用類似,都是作爲緩存來使用。但與軟引用不同,弱引用在進行垃圾回收時,是一定會被回收掉的,因此其生命週期只存在於一個垃圾回收週期內。

六.回收算法(堆)
6.1 標記-清除(Mark-Sweep):
1.回收算法的意思是怎麼移除在堆內存中不被使用的對象
2.遍歷堆裏面的所有對象,標記要回收的對象(不可達的對象)
3.珊瑚堆裏面標記的對象

在這裏插入圖片描述
此算法執行分兩階段:
第一階段從引用根節點開始標記所有被引用的對象
第二階段遍歷整個堆,把未標記的對象清除。此算法需要暫停整個應用,同時,會產生內存碎片。

6.2.複製(Copying):
在這裏插入圖片描述
此算法把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象複製到另外一個區域中。此算法每次只處理正在使用中的對象,因此複製成本比較小,同時複製過去以後還能進行相應的內存整理,不會出現“碎片”問題。當然,此算法的缺點也是很明顯的,就是需要兩倍內存空間。
在這裏插入圖片描述
6.3標記-整理(Mark-Compact):
在這裏插入圖片描述
此算法結合了“標記-清除”和“複製”兩個算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用對象,第二階段遍歷整個堆,清除未標記對象並且把存活對象“壓縮”到堆的其中一塊,按順序排放。此算法避免了“標記-清除”的碎片問題,同時也避免了“複製”算法的空間問題。
在這裏插入圖片描述

七.按分區對待
7.1增量收集(Incremental Collecting)
實時垃圾回收算法,即:在應用進行的同時進行垃圾回收。不知道什麼原因JDK5.0中的收集器沒有使用這種算法的。
把堆都掃描一般
7.2分代收集(Incremental Collecting)
解決了回收的數量和範圍問題
將堆裏面的內存都遍歷一般,可以達到目的,但是性能很差

有的對象:存活時間較長(IOC)
有的對象:存活時間短(局部變量)
基於對對象生命週期分析後得出的垃圾回收算法**。把堆分爲年青代、年老代、持久代,對不同生命週期的對象使用不同的算法(上述方式中的一個)進行回收。現在的垃圾回收器(從J2SE1.2開始)都是使用此算法的**

8.按系統線程分
8.1串行收集:
串行收集使用單線程處理所有垃圾回收工作, 因爲無需多線程交互,實現容易,而且效率比較高。但是,其侷限性也比較明顯,即無法使用多處理器的優勢,所以此收集適合單處理器機器。當然,此收集器也可以用在小數據量(100M左右)情況下的多處理器機器上。
總結:單核 ,或者多核,數據量少
8.2並行收集
並行收集使用多線程處理垃圾回收工作,因而速度快,效率高。而且理論上CPU數目越多,越能體現出並行收集器的優勢。
總結:在多核上的發揮較大。多核堆的數據 大
大型的應用
不管你是並行還是串行,都需要停止整個應用。15ms
8.3併發收集
也需要停止應用,但是能減少時間!10

相對於串行收集和並行收集而言,前面兩個在進行垃圾回收工作時,需要暫停整個運行環境,而只有垃圾回收程序在運行,因此,系統在垃圾回收時會有明顯的暫停,而且暫停時間會因爲堆越大而越長。
在運行時,也能回收垃圾。

九.何爲垃圾?
9.1引用計數
上面說到的“引用計數”法,通過統計控制生成對象和刪除對象時的引用數來判斷。垃圾回收程序收集計數爲0的對象即可。但是這種方法無法解決循環引用

**** 碎片問題
由於不同Java對象存活時間是不一定的,因此,在程序運行一段時間以後,如果不進行內存整理,就會出現零散的內存碎片。碎片最直接的問題就是會導致無法分配大塊的內存空間,以及程序運行效率降低。所以,在上面提到的基本垃圾回收算法中,“複製”方式和“標記-整理”方式,都可以解決碎片的問題

十.對象引用樹遍歷
從程序運行的根節點出發,遍歷整個對象引用,查找存活的對象。那麼在這種方式的實現中,垃圾回收從哪兒開始的呢?即,從哪兒開始查找哪些對象是正在被當前系統使用的。上面分析的堆和棧的區別,其中棧是真正進行程序執行地方,所以要獲取哪些對象正在被使用,則需要從Java棧開始。同時,一個棧是與一個線程對應的,因此,如果有多個線程的話,則必須對這些線程對應的所有的棧進行檢查。
在這裏插入圖片描述
在這裏插入圖片描述
同時,除了棧外,還有系統運行時的寄存器等,也是存儲程序運行數據的。這樣,以棧或寄存器中的引用爲起點,我們可以找到堆中的對象,又從這些對象找到對堆中其他對象的引用,這種引用逐步擴展,最終以null引用或者基本類型結束,這樣就形成了一顆以Java棧中引用所對應的對象爲根節點的一顆對象樹,如果棧中有多個引用,則最終會形成多顆對象樹。在這些對象樹上的對象,都是當前系統運行所需要的對象,不能被垃圾回收。而其他剩餘對象,則可以視爲無法被引用到的對象,可以被當做垃圾進行回收。

因此,垃圾回收的起點是一些根對象(java棧, 靜態變量, 寄存器…)。而最簡單的Java棧就是Java程序執行的main函數。這種回收方式,也是上面提到的“標記-清除”的回收方式

  • 同時創建和回收
    垃圾回收線程是回收內存的,而程序運行線程則是消耗(或分配)內存的,一個回收內存,一個分配內存,從這點看,兩者是矛盾的。因此,在現有的垃圾回收方式中,要進行垃圾回收前,一般都需要暫停整個應用(即:暫停內存的分配),然後進行垃圾回收,回收完成後再繼續應用。這種實現方式是最直接,而且最有效的解決二者矛盾的方式。

堆變大->回收的時間變長->應用的停止時間長(採用併發收集)

但是這種方式有一個很明顯的弊端,就是當堆空間持續增大時,垃圾回收的時間也將會相應的持續增大,對應應用暫停的時間也會相應的增大。一些對相應時間要求很高的應用,比如最大暫停時間要求是幾百毫秒,那麼當堆空間大於幾個G時,就很有可能超過這個限制,在這種情況下,垃圾回收將會成爲系統運行的一個瓶頸。爲解決這種矛盾,有了併發垃圾回收算法,使用這種算法,垃圾回收線程與程序運行線程同時運行。在這種方式下,解決了暫停的問題,但是因爲需要在新生成對象的同時又要回收對象,算法複雜性會大大增加,系統的處理能力也會相應降低,同時,“碎片”問題將會比較難解決。

分代回收(重點)
分代的垃圾回收策略,是基於這樣一個事實:不同的對象的生命週期是不一樣的。因此,不同生命週期的對象可以採取不同的收集方式,以便提高回收效率。

在Java程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關,比如Http請求中的Session對象、線程、Socket連接,這類對象跟業務直接掛鉤,因此生命週期比較長。但是還有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命週期會比較短,比如:String對象,由於其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次即可回收。

試想,在不進行對象分代的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,因爲每次回收都需要遍歷所有存活對象,但實際上,對於生命週期長的對象而言,這種遍歷是沒有效果的,因爲可能進行了很多次遍歷,但是他們依舊存在。因此,分代垃圾回收採用分治的思想,進行代的劃分,把不同生命週期的對象放在不同代上,不同代上採用最適合它的垃圾回收方式進行回收。

在這裏插入圖片描述
虛擬機中的共劃分爲三個代:年輕代(Young Generation)、年老點(Old Generation)和持久代(Permanent Generation)。
持久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對象關係不大;
年輕代和年老代的劃分是對垃圾收集影響比較大的;

年青代:
生成的對象首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生命週期
短的對象。年輕代分三個區。一個Eden區,兩個Survivor區(一般而言)。大部分對象在Eden區中生
成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個Survivor區滿
時,此區的存活對象將被複制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個
Survivor區複製過來的並且此時還存活的對象,將被複制“年老區(Tenured)”。
需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來
 對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor去過來的對
 象。而且,Survivor區總有一個是空的。同時,根據程序需要,Survivor區是可以配置爲多個的
 (多於兩個),這樣可以增加對象在年輕代中的存在時間,減少被放到年老代的可能。
年老代:
在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認爲年老代中存
放的都是一些生命週期較長的對象。
持久帶:
用於存放靜態文件,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態
生成或者調用一些class,例如Hibernate等,在這種時候需要設置一個比較大的持久代空間來存放這
些運行過程中新增的類。持久代大小通過-XX:MaxPermSize=進行設置。

堆裏面分代(重點)
1 年輕代
2 老年代
3 持久代 jdk8 裏面沒有持久化代了使用MeteSpance
在這裏插入圖片描述
年輕代:
Eedn:伊甸區
So:存活區
S1:存活區
在這裏插入圖片描述
在這裏插入圖片描述
老年代滿:(Full GC)因爲該時間較長
在這裏插入圖片描述
在這裏插入圖片描述

開始回收
7.1 Scavenge GC
一般情況下,當新對象生成,並且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,並且把尚且存活的對象移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因爲大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這裏需要使用速度快、效率高的算法,使Eden去能儘快空閒出來。
需要:速度快,效率高
7.2 Full GC
對整個堆進行整理,包括Young、Tenured和Perm。Full GC因爲需要對整個堆進行回收,所以比Scavenge GC要慢,因此應該儘可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於FullGC的調節。有如下原因可能導致Full GC:
年老代(Tenured)被寫滿

持久代(Perm)被寫滿
System.gc()被顯示調用

年輕代->老年代(OK)
當老年代滿->持久化轉化(錯誤的)
持久化轉化: 項目啓動就確定下了
就是類加載器加載到的東西 + 常量池 + jvm 內部的數據
在這裏插入圖片描述

永久:可能類特別多,但是永久代空間較少。可能發生滿

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