java架構之路-(11)JVM的物件和堆
上次部落格,我們說了jvm執行時的記憶體模型,堆,棧,程式計數器,元空間和本地方法棧。我們主要說了堆和棧,棧的流程大致也說了一遍,同時我們知道堆是用來存物件的,分別年輕代和老年代。但是具體的堆是怎麼來存放物件的呢?什麼時候可以將物件放置在老年代呢。下面我來看一下。
如果都為預設設定,大致就是這樣的。假設我們設定記憶體堆的大小為600M,那麼老年代就大概是400M,我們的年輕代就是200M,然後年輕代的eden區域佔160M也就是200M的8/10,一般新建的物件都在這,我是說一般啊。後面會用這個600M來詳細說明,from和 to區域各佔20M,也就是Survivor區域佔用40M,每次做完minor GC,物件就放在這個區域。
我剛才說到,有時候物件不在年輕代,那麼我來具體分析一下,什麼情況放置在年輕代,而什麼時候又放置在老年代。
1,Minor GC之後,存活的物件Survivor區域放不下。
public class Main { public static void main(String[] args) { byte[] bt1; bt1 = new byte[60000 * 1024]; } }
加入堆記憶體日誌,我們得到列印結果為:
我們得到bt1新建以後,我們的堆記憶體幾乎佔滿了,現在已經99%了,那麼我們再來看一下。
public class Main { public static void main(String[] args) { byte[] bt1,bt2; bt1 = new byte[60000 * 1024]; bt2 = new byte[10000 * 1024]; } }
從程式碼裡我們可以得知,我們新建了bt1之後又新建了bt2,這時我們的eden區域應該不夠用了,那麼我們的記憶體會怎麼來處理呢。我們來看一下結果
我們可以看到已經做了一次GC了,但是還是放不下,那麼我們直接將較大的物件直接放置在了堆記憶體上。
2,長期存活的物件移到老年代。也就是經過多次minorGC以後,物件還是存活的,我們將該物件移置老年代,一般是15次,也就是物件頭內的分代年齡達到15歲時,我們將該物件移置老年代。
3,物件動態年齡判斷。
這個很重要的一個理論知識,大概來說一下,當我們做完minorGC以後,物件放在to區域,也就是我們Survivor的to區域,可能物件是放不下的,這時會來計算分類年齡,大致是這樣來算的將所有分代年齡為1的相加,再加上分代年齡為2的,再加分代年齡為3的,依次相加,一直加到最大的分代年齡,但在相加過程中,你會發現加到分代年齡為m的物件,總大小已經放滿了to區域,這時就將m到n分代年齡的物件都移置到老年代,包含m。也就是大於Survivor區域的50%時,則後面的物件,包含該年齡的物件都放置在老年代。
4,大物件直接放在老年代。再來看段程式碼。
public class Main { public static void main(String[] args) { byte[] bt1; bt1 = new byte[90000 * 1024]; } }
上面我知道我們建立一個大概600M的物件放置在eden時,佔了99%,那麼我們建立大於600M的物件,eden一定放不下了。那麼直接放置在老年代。這裡引數也是可以設定的。我來設定一個引數再看看,設定引數為
-XX:PretenureSizeThreshold=10000000 -XX:+UseSerialGC -XX:+PrintGCDetails
public class Main { public static void main(String[] args) { byte[] bt1; bt1 = new byte[20000 * 1024]; } }
我們設定了引數,宣告10M的物件就為大物件,我們建立了一個大概20M的物件,就直接放置在了老年代上。就是物件經歷那麼多次的minorGC了,jvm虛擬機器會認為你可能會一直存活,趁著這次放不下了,你就趁早過來吧,來我們老年代混吧。
5,老年代空間分配擔保機制。
其實我們每次進行minorGC前,會有一系列操作的,可能會進行full GC的,那麼我們來看一下流程吧。
我來解釋一下上面那個五彩繽紛的圖。等我們的eden區滿時,需要進行minorGC,這時會優先看一下老年代的剩餘空間大小,如果老年代剩餘的空間不多了,我們就可能進行full GC,也就是我們老年代的剩餘空間小於我們的eden區內將要進行minorGC物件的總和。
如果真的小了,那麼我們往下走,我們會判斷時候配置了-XX:-HandlePromotionFailure (jdk8以上預設設定)這個引數,如果沒配置,直接進行fullGC,如果配置了就去判斷老年代的剩餘空間是否小於我們每次minorGC後每次要放在老年代物件大小的平均值,如果老年代小於minorGC了,那麼進行fullGC。否則不需要進行full GC。
eden和Survivor(from和to)預設比例是8:1:1,但是jvm可能會將我們的引數優化,也就是-XX:+UseAdaptiveSizePolicy這個預設引數,我將其改為-XX:-UseAdaptiveSizePolicy不進行優化,保持8:1:1的比例了。
我們再來看一下什麼樣的物件是可以被回收的。
1,引用計數法(基本不用,迴圈引用物件永遠無法銷燬,可能記憶體溢位)
給物件中新增一個引用計數器,每當有一個地方引用它,計數器就加1;當引用 失效,計數器就減1;任何時候計數器為0的物件就是不可能再被使用的。
GC Roots根節點一般為執行緒棧的本地變數、靜態變數、本地方法棧的變數等等。
2,可達性分析演算法。
這個演算法的基本思想就是通過一系列的稱為 “GC Roots” 的物件作為起點, 從這些節點開始向下搜尋,找到的物件都標記為非垃圾物件,其餘未標記的物件都是垃圾物件
3,常見的引用型別。
java的引用型別一般分為四種:強引用、軟引用、弱引用、虛引用
import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; public class Main { public static void main(String[] args) { User user = new User();//強引用 WeakReference<User> user2 = new WeakReference<User>(new User());//弱引用 SoftReference<User> user3 = new SoftReference<User>(new User());//軟引用 } }
一般將物件用SoftReference軟引用型別的物件包裹,正常情況不會被回收,但是GC做完後發現釋放不出空間存放新的物件,則會把這些軟引用的物件回收掉。軟引用可用來實現記憶體敏感的快取記憶體。
4,finalize最終判斷物件存活。
finalize是在物件馬上要被收回之前執行的最後一個方法,可以寫邏輯,但是完全不建議去這樣去寫,很可能出現物件永遠不會被回收,造成記憶體溢位,也就是說在finalize方法內還可能“救活”我們的物件。
即使在可達性分析演算法中不可達的物件,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個物件死亡,至少要經歷再次標記過程。
如何判斷一個類是無用的類
1.該類所有的例項都已經被回收,也就是 Java 堆中不存在該類的任何 例項。
2.載入該類的 ClassLoader 已經被回收。
3.該類對應的 java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
最後我們來看一下逃逸分析。
JVM的執行模式用三種,分別是解釋模式,編譯模式和混合模式,這裡簡單說一下這個問題,不然後面會蒙圈的。
解釋模式就是執行一行JVM位元組碼就編譯一行為機器碼,這樣的好處就是很節省記憶體空間,不用把所有的位元組碼都塞到記憶體裡面去,執行效率低,但是啟動快。
編譯模式和解釋模式恰恰相反,是先將所有JVM位元組碼一次編譯為機器碼,然後一次性執行所有機器碼。這樣會提高我們的執行效率,但是消耗空間資源。
混合模式是上面的總和,依然使用解釋模式執行程式碼,但是對於一些 "熱點" 程式碼採用編譯模式執行,JVM一般採用混合模式執行程式碼。
我們來看一段程式碼。
public class Main { public User getUserBeanTest() { User user = new User();//放置在堆上 return user; } public void userBeanTest() { User user = new User();//優先和方法一起發放置在棧上. } }
也就是說明,物件也是有很小的可能放置在棧上的。中秋放假了,明天補一下mybatis的底層是實現原理。過幾天繼續來說我們的jvm優化