為什麼要「除夕」,原來是內存爆了
傳說古代有一隻四角四足的怪獸:名叫
夕
。因冬天大雪導致夕沒東西吃,所以夕經常到附近的村裡找吃的,因其身體龐大、脾氣暴躁、兇猛異常,給村民帶來了很大的災難。後來有一位聰明的孩子,他叫做
年
,教給大家除掉「夕」的方法:用爆竹,輕則趕走它,重則傷它。每年臘月三十
,夕都會來村裡,村名就守着夜
,放着鞭炮趕走夕。除夕
由此而來。
我們把「夕」想像成一個不斷吃機器內存的 Java 程序,就稱它為 年獸
吧。掌管 Java 虛擬機內存的就是「年」,我們稱它為年哥
吧。
年哥的地盤
年哥
管理的地盤主要分為五大區:堆、方法區、虛擬機棧、本地方法棧、程序計數器。大家可以把圖上的線程想像成村民,而堆是作為村民共享使用的區域。
如下圖所示:
堆又可以進行細分,分為新生代
和老年代
,新生代和老年代的比例是 1:2,而新生代又可以進行細分,分為伊甸園(Eden)區和兩個 Survivor
, 其中 Eden
區大小和 Survivor
區大小是 8:1。
如下圖所示,年獸和村民都是共享
堆內存這塊地盤的,管理員年哥是管理堆內存的。其中的數字 1、8、20 分別代表佔用內存的份數。
年獸的胃口
年獸的胃口是村民的幾百倍,年獸假扮村名逃過了管理員年哥的檢查,年哥對於這種大胃王都是直接分配到老年代
去的,因為大胃王需要連續的內存給它吃,而新生代的碎片比較多不滿足條件。在 Java 的世界中,最典型的大胃王就是大對象:如很長的字符串,或者元素數量很龐大的數組。
如下圖所示,村民分配到新生代吃內存,年獸被直接分配到老年代。
大量年獸入侵
年獸嘗到甜頭後,就開始不斷地呼叫它的親戚朋友,大量年獸被分配到了老年代,直接導致老年代的內存空間不足了,如下圖所示:
代碼演示
我們用代碼來演示下年獸入侵:
- 創建了 3 個年獸,都佔用 10 MB 內存。
public class SpringFestivalOOM {
public static void main(String[] args) {
// 年獸1/2/3,都佔用 10 MB 內存
byte[] nianShou1 = new byte[10 * 1024 * 1024];
byte[] nianShou2 = new byte[10 * 1024 * 1024];
byte[] nianShou3 = new byte[10 * 1024 * 1024];
}
}
- 編譯這段程序。
javac SpringFestivalOOM.java
- 執行這段程序,同時設置堆內存最大為 20 MB。
java -Xms20M -Xmx20M SpringFestivalOOM
因為 3 個年獸佔用的內存 30 MB 大於堆的最大內存 20 MB,所以拋出堆內存溢出異常,如下圖所示:
這個時候年哥和村民才發現,原來有這麼多年獸佔了我們的地盤,趕快消滅它們!
打走年獸
村民們和年哥湊到一塊,討論了下該如何解決這個問題,究其原因就是年獸太多了,要減少他們呼朋喚友來吃內存。
放到我們的 Java 世界中,就是減少大對象的頻繁創建。
我們程序員經常出現本地寫完代碼後沒什麼問題,到線上後就出問題,很可能的原因就是線上環境的數據量大,很容易出現大對象的頻繁創建,比如大型促銷活動時,短時間
內需要創建大量訂單數據,而訂單數據又比較複雜,有很多字段,可能會佔用大量的內存空間,最終導致頻繁觸發垃圾回收
,而垃圾回收時間又會出現 Stop the world
現象,應用程序的性能就降下來了。
守歲
在除夕晚上,都會進行「守歲」,村民們齊聚一堂吃着年夜飯,一起等待除夕的鐘聲。等到天亮再拜訪親戚鄰居。
而守歲這個過程只能待在家裡,不能做其他事情,所以可以看成是垃圾回收時,其他線程不能工作,也就是 Stop the world 的由來。
如下圖所示,除夕之前,村民可以去其他地方活動,除夕夜就只能待在家裡守歲了,到了第二天早上就可以串門拜年了。
總結
本篇通過除夕的故事來講解 Java 中垃圾回收機制,因故事較為簡單,所以並沒有對垃圾回收算法進行深入講解,本篇只能算作垃圾回收的入門,希望能給大家帶來一定啟發作用,對 JVM 很熟的同學就當學習下除夕的來歷吧~
- 村民作為小對象使用堆區的
新生代
,年獸作為大對象直接使用堆區的老年代
。 - 除夕當晚,大量
年獸入侵
老年代,導致堆區內存不足
,觸發垃圾回收
機制。 - 守歲就是待在家裡守着過新年,而垃圾回收時,
又會停止其他線程
,也就是 Stop the world。 - 避免代碼中
頻繁複制或創建
大對象是必須做的事情,以免上線後出現問題。 - 除夕也代表着
辭舊迎新
,這不正是執行垃圾回收嗎?
悟空在這裡預估大家除夕快樂,新年快樂,2021 心想事成,好事成雙~