天天看點

Java虛拟機的Heap監獄

在Java虛拟機中,我是一個位高權重的大管家,他們都很怕我,尤其是那些Java 對象,我把他們圈到一個叫做Heap的“監獄”裡,嚴格管理,生殺大權盡在掌握。

中國人把Stack翻譯成“棧”,把Heap翻譯成“堆”, 還有人會把Stack翻譯成“堆棧”,唉,真不知道他們是怎麼想的, 不過這麼多年都過來了,你們明白就好。

碰巧我會對Heap中的Java 對象做垃圾回收,這個“堆”總是讓我聯想到垃圾堆。

說起垃圾回收,這實在是一個大負擔,原因很簡單,那些寫Java程式的人類隻管把對象給new出來,扔到Heap 中, 但是從來不管把他delete 掉, 删掉這些對象的責任就落到了我的頭上,我不嚴格管理怎麼行?

有時候我挺羨慕C和C++, 必須得手動地配置設定和釋放記憶體,出了錯都是程式員來背鍋。

在我這裡,如果任由這些對象對象肆意妄為,我那容量不高的,Java虛拟機啟動後就無法更改的Heap“監獄”很快就會被填滿, 是以我必須得派出我的得力助手,專門找到并且清理那些不用的Java 對象, 把他們占據的空間給釋放掉。

為了找到這些搗亂分子,我發明了一個叫做“可達性分析”的算法,這個算法估計大部分人已經知道了,我就不再啰嗦了,下面這張圖說明了背後的思想,聰明的你一眼就能看出來, 橙色的對象都是不可達對象,可以回收。

我抗議了很多次,讓他修改, 他說微信公衆号隻能改五個字,改不了,唉,真是沒轍。

Heap監獄

好吧,現在詳細說一下我管理的Heap“監獄”。

你可以把它想象成一大片空間,為了友善管理, 我把Heap“監獄”劃分成多個區域,然後把那些Java對象在其中搬來搬去。

我定的規矩就是: 新來的家夥們都要進入新生代待着,新生代住不下了,我就派出清理者進行垃圾回收(Minor GC),回收以後還住不下,那就把年齡大的老家夥們趕到養老院(老年代)去。

每個在Heap中的Java對象我都會設定一個年齡計數器,每次Java對象熬過一次GC,就把年齡加1, 如果老到一定程度,對不起,請進入養老院(老年代)。  實際上我還會做動态的年齡判斷,這裡按下不表。

你可能會覺得奇怪,為什麼在新生代裡分出了Eden, Survivor1, Survivor2這樣奇怪的區域?

那是因為我想在這裡實作一個所謂的“複制”算法。

最早的時候, 我是把一個記憶體的區域劃分成大小相當的兩個區域,每次隻用其中的一個。

區域1用完了,我就做垃圾回收,把存活的都搬到另外一個區域。

注意:搬過去以後,他們都會緊緊地挨在一起居住,這樣以來,被清理掉的那些紅色碎片就會重新平整成一大塊空間,友善後續使用,尤其是針對大塊頭對象來了以後。

這麼來回颠倒着使用兩個區域,雖然效率高,沒有碎片,但是浪費的空間很巨大:每次隻能用一半。

後來人類發現,大部分在新生代的對象都活不了多長時間,基本上一次垃圾回收就删除得差不多了。

是以就改進了這個隻用一半的複制算法, 把新生代分成三個部分:Eden , Survivor1, Survivor2 , 他們的比例是8:1:1。

每次隻使用Eden 和其中一個Survivor , 當垃圾回收時,把這兩塊區域中還活着的對象複制到另外一個Survivor, 如果Survivor放不下,請進養老院(老年代)吧。

如果很不幸, 連養老院都住滿了,那隻好搞一次Full GC了,這是個很慢的操作,你們最好祈禱它不要頻繁發生。

“監獄”之外,大有可為

雖然我可以在Heap監獄内作威作福,有時候我也得接觸下監獄之外的世界。

有一次要通過Socket向外發送資料,我明明把資料準備好了,就在我的Heap中,可是JVM老大竟然把資料複制了一份到Heap之外的記憶體中去,然後才能通過Socket發送。

我問他這到底是怎麼回事,為什麼要多此一舉,難道是對我這個Heap監獄的大管家不放心?

JVM老大說确實是不放心,人家底層的Socket都是C語言寫的, 關注的是實體記憶體的位址, 你垃圾回收的時候把Java對象在什麼Eden, Survivor, 老年代之間挪來挪去,對象的位址也會變來變去, 我怎麼告訴人家到底發哪個位址的資料啊?

想想也是這個理兒,有得必有失,你程式員不用管理記憶體,但是底層還得和記憶體打交道,并且還額外多了一道工序:Copy 。

老大還說:“可能你還不知道,除了你的Heap監獄,其實我在Java程序中還有一塊兒叫做“Off-Heap記憶體’的地方,資料就會複制到這裡。 為了和你區分開,我把它叫做堆外記憶體。”

沒想到這裡還有一塊我都管不着的“飛地”!

不過它和我也沒有什麼競争關系,由它去吧。

可是沒過幾天,JVM老大再次給我帶來了“驚喜”。

他說:“複制資料太麻煩了,我想了個辦法,可以在Java代碼中直接配置設定一塊屬于Off-Heap的記憶體。”

我覺得頭皮發蒙:“直接在堆外記憶體配置設定?到底怎麼配置設定?”

老大給了我一段代碼:“看看,這不就配置設定了128M的堆外存嗎? 對這個buffer的讀寫操作會直接寫入堆外記憶體, 不用再經過你來複制了。”

ByteBuffer buffer = ByteBuffer.allocateDirect(1024*1024*128);

該死的面向接口程式設計,這個ByteBuffer配置設定出來的堆外記憶體,就像一個普通的Java對象在使用,絲毫看不出它在堆内還是在堆外。

完了,這塊記憶體我是徹底管不了了。

老大看出我情緒不對,安慰道: “這個buffer也是個Java對象啊, 就在你的Heap中存着,隻不過它儲存了那128M記憶體的資訊而已。”

這還差不多 ! 既然它是個Java對象,那就得放到我的Heap監獄中,被我控制!

可以想象,這個對象被垃圾回收的時候, 它指向的直接記憶體才會被釋放。

我突然有了一個邪惡的想法:如果這樣的對象越來越多,并且一直不被垃圾回收,那對應的直接記憶體豈不也是不能釋放,然後Out of Memory ?

老大似乎看穿了我的思想:“對于這些對象,得特别小心,一定得確定能釋放。”

直接配置設定堆外記憶體的功能正式推出了,我發現配置設定起堆外記憶體要比堆内記憶體要慢一點,心想估計沒有多少人使用吧。 可沒想到的是它特别适合那些配置設定次數少,讀寫操作很頻繁的場景。于是就受到了Netty這些通信類系統的熱烈歡迎。

為了減少建立堆外記憶體的開銷,Netty 還引入了對象池的技術,就像資料庫連接配接池一樣,先配置設定一些堆外記憶體, 然後不斷地複用他們。

我沒想到堆外記憶體能玩出這麼多的花樣,但是一想到他們還是Java程式,還得用Java對象包裝,無論如何都跳不出我的手掌去,也就釋然了。

歡迎工作一到五年的Java工程師朋友們加入Java架構開發:468947140

點選連結加入群聊【Java-BATJ企業級資深架構】:https://jq.qq.com/?_wv=1027&k=5zMN6JB

本群提供免費的學習指導 架構資料 以及免費的解答

不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導