深入學(xué)習(xí)JVM: (3) 對象創(chuàng)建與內(nèi)存分配

一. 前言

時隔多日, 我又來做知識總結(jié)了, 這次是Java對象的創(chuàng)建以及內(nèi)存的分配機制. 本來說好的這篇文章打算寫垃圾收集器, 但是想了想, 對象相關(guān)的東西作為前置知識, 還是得先說說.

二. 對象創(chuàng)建

一個對象的創(chuàng)建到底經(jīng)歷了哪些步驟? 看圖說話:

創(chuàng)建對象流程圖.png

加載類這一步, 在下就不多說了, 詳細(xì)可以看在下的上...上篇文章. 不過, 涉及到對象創(chuàng)建的語句可不只是new, 比如說還有克隆, 或者反序列化等等. 好, 還是著重說說后邊兒的步驟:

  1. 分配內(nèi)存: 為即將要創(chuàng)建的對象分配合適的內(nèi)存大小.
    也沒那么簡單, Jvm一共設(shè)計了兩種分配內(nèi)存的策略, 分別是: 指針碰撞空閑列表
    先來說指針碰撞, 接著看圖:
規(guī)整內(nèi)存.png

假設(shè)這是一塊非常規(guī)整的內(nèi)存, 粉紅色代表已使用, 白色表示未使用, 藍色是一個指針; 當(dāng)要給對象分配內(nèi)存時, 藍色的指針會根據(jù)對象的大小, 向后空閑的位置移動若干的格子. 比如這個對象計算后發(fā)現(xiàn)要占用3個格子, 則藍色的指針就會移動到當(dāng)前行的最后一個格子. 這個過程就叫做指針碰撞.

再來說空閑列表, 還是看圖:

不規(guī)則內(nèi)存.png

假設(shè)這是我們的內(nèi)存, 由于選擇了不恰當(dāng)?shù)睦占惴? 就可能出現(xiàn)這種情況. 內(nèi)存上分配的對象很凌亂, 完全不規(guī)則. 這種情況肯定就沒有辦法使用指針碰撞的方式來分配內(nèi)存了. 這時候, Jvm會采用另外一種方式來給即將要創(chuàng)建的對象分配內(nèi)存空間, 那就是空閑列表. 空閑列表是指Jvm維護在堆中的一個列表, 上面記錄了所有空閑的可用的內(nèi)存地址, 也就是上圖中的白塊兒.當(dāng)計算完該對象需要多大空間的內(nèi)存時, 則從列表選擇出相應(yīng)的連續(xù)的白塊兒用于創(chuàng)建對象.

這時候, 新的問題產(chǎn)生了. 如果多個線程同時要創(chuàng)建對象, 到堆中分配內(nèi)存怎么辦?

Jvm提供了兩種方式來解決這個問題, 分別是CAS以及TLAB.
①. CAS(compare and swap): 這是一種算法, 不知道的沒關(guān)系, 可以先百度一下. 簡而言之, 通過這種算法, 多個線程同時搶一塊內(nèi)存的時候, 只有一個會成功, 其余失敗的會進行重試.
②. TLAB(Thread Local Allocation Buffer): 這是一種機制, Jvm事先給每個線程在堆中預(yù)先分配一小塊兒內(nèi)存空間, 每個線程要創(chuàng)建對象分配內(nèi)存時, 先到自身線程分配到的內(nèi)存中嘗試分配. 如果分配失敗, 再使用CAS的方式在Eden區(qū)中分配內(nèi)存.
可以通過參數(shù): -XX:+UseTLAB來開啟TLAB(默認(rèn)開啟); 通過參數(shù): -XX:TLABSize指定TLAB的大小, 不過一般不用也不推薦去修改它.

以上是關(guān)于分配內(nèi)存的所有內(nèi)容, 下面來講初始化:

  1. 初始化: 為對象所有成員變量賦零值
    這也是為什么一個對象的成員變量不用賦值也可以使用的原因.
  1. 設(shè)置對象頭: 一個對象分為三部分信息, 分別是對象頭, 實例數(shù)據(jù), 對齊填充
    ①. 對象頭: 不知道你有沒有想過為什么使用instanceof關(guān)鍵字可以判斷一個對象的類型; 使用 對象.getClass() 可以取得該對象類對象; Jvm如何判斷一個對象經(jīng)歷了15次垃圾回收, 從而將其放入老年代. 其實這些信息都存放在對象的對象頭中.
    對象頭中也包含了三部分信息, 分別是:
    (markword): 對象hash code值宵蛀、gc分代年齡、鎖狀態(tài)標(biāo)識(這也是對象可以作為鎖的原因)
    (klass pointer): 一個指針, 指向方法區(qū)中該對象對應(yīng)的類元信息
    (數(shù)組長度): 如果該對象是一個數(shù)組, 才會有該信息, 記錄數(shù)組的長度
    ②. 實例數(shù)據(jù): 這個就不多講了, 就是對象的成員變量嘛.
    ③. 對齊填充: 我們的對象由于成員變量的數(shù)據(jù)類型不同, 都有不同的大小. 而Jvm會將這些對象大小都填充成8的倍數(shù)次方. 例如一個對象占用28字節(jié), Jvm會填充4個字節(jié), 使其變成32字節(jié)大小. 為什么要這樣呢? 了解計算機底層的朋友應(yīng)該知道, 對于8的倍數(shù)值, 計算機尋址的速度是最快的.
    ④. 還有一個指針壓縮: 我們的對象頭中有一個klass pointer, 它是一個指針, 也會占用相對應(yīng)的內(nèi)存, Jvm為了節(jié)省內(nèi)存空間會對其進行壓縮. 另外, 我們的對象中也可能包含其它對象, 這些個對象也會以指針的方式存放在內(nèi)存中. 對其壓縮后, 可以節(jié)省我們的內(nèi)存空間, 降低指針移動時所耗費的帶寬.
    關(guān)于指針壓縮, 不能在32G以上的內(nèi)存中生效. 所以這也是為什么jvm內(nèi)存配置小了不好, 配置大了也不好的原因之一.

三. 對象內(nèi)存分配

上面說的對象分配內(nèi)存是指對象創(chuàng)建時分配內(nèi)存的規(guī)則, 下面要說的是對象分配的內(nèi)存區(qū)域.

1. 棧上分配對象

看到這個的小伙伴兒可能有點兒奇怪, 哎, 你之前不是說對象在堆上分配, 然后棧內(nèi)存上只存放對象的引用嗎? 記住了, 我說的是大多數(shù)時候, 這里要扯到一個概念, 叫做對象逃逸分析, 請看偽代碼:

public void test() {
     User user = new User();
     System.out.print("User對象能在test()方法外被使用嗎?");
}

上述代碼在test()方法內(nèi)new了一個User對象, 這個對象僅在該方法內(nèi)有效. 換句話說, 這個User對象已經(jīng)逃不出test()方法的作用域了. 這個時候Jvm就有可能在棧上存放該對象.

可是, 存放對象需要一塊兒連續(xù)的內(nèi)存空間呀, 棧幀沒有怎么辦?

這就涉及到對象內(nèi)存棧上分配的一種方式, 標(biāo)量替換, 什么是標(biāo)量? 其
實就是變量, Jvm會將該對象的成員屬性拆分成一個一個的標(biāo)量存放在棧
上.

使用參數(shù): -XX:+DoEscapeAnalysis 開啟逃逸分析(默認(rèn)開啟)
使用參數(shù): -XX:+EliminateAllocations 開啟標(biāo)量替換(默認(rèn)開啟)

2. 大對象直接進入老年代

大對象直接進入老年代? 多大的對象算大對象嘞?

為了避免一些體積過大的對象頻繁在年輕代參與gc占用I/O帶寬, Jvm直接
將大對象放在老年代.
通過參數(shù): -XX:PretenureSizeThreshold=1048576(字節(jié)) 設(shè)置大對象大小

3. 長期存活的對象進入老年代

這個之前提到過, 一個對象經(jīng)歷一次minor gc, 則將對象頭中存儲的gc年齡加1(最大15), Jvm默認(rèn)當(dāng)一個對象的gc年齡達到15時將其移入老年代(不同的垃圾回收器默認(rèn)值不同).

這是為了避免一些本就應(yīng)該永久存在, 或者說長期存活的的對象一直在年輕
代頻繁經(jīng)歷minor gc卻始終回收不掉, 消耗性能.
通過參數(shù): -XX:MaxTenuringThreshold=5 可以設(shè)置移入老年代的年齡閾值

4. 對象動態(tài)年齡判斷

這個就有意思了, 前一篇文章說過, 當(dāng)年輕代的eden區(qū)滿了之后, 會執(zhí)行minor gc, 然后將存活的對象移動到survivor區(qū). 前面還說過, 每個對象都有一個gc年齡. 還是結(jié)合圖吧:

minor gc存活對象.png

當(dāng)minor gc后如果移動到幸存區(qū)的對象總大小超過了幸存區(qū)大小的50%, 則
會進行對象動態(tài)年齡判斷機制, 該機制會根據(jù)對象年齡進行排序, 然后相加并判斷是否大于了幸存區(qū)的50% ,如果大于, 則將年齡n及以上對象全部移入老年代.
以上圖舉例:

  1. 對象1 + 對象2 + 對象3 + 對象4 + 對象5 + 對象6 = 600kb
  2. 600kb > (1mb * 0.5) = true
  3. 將對象6 ~ 對象9 移入老年代.

通過參數(shù): -XX:TargetSurvivorRatio 指定判定閾值

5. 老年代空間分配擔(dān)保機制

該機制發(fā)生在minor gc前, 就是在執(zhí)行minor gc之前, 會判斷每一次minor gc移入老年代的對象的平均大小, 如果大于老年代剩余空間, 直接執(zhí)行full gc.
舉例說明: 年輕代gc完存活了一批對象要進入老年代, 而老年代放不下這么多的對象, 怎么辦? 只能接著再執(zhí)行一次full gc.

6. 對象在Eden區(qū)分配

除了上述規(guī)則, 一個對象創(chuàng)建時, 會被分配到Eden區(qū).

四. 對象內(nèi)存回收

1. 如何判定一個對象是可回收的對象?

有兩種方式可以判斷一個對象是不是一個無用的對象, 分別是: 引用計數(shù)器算法可達性分析算法, 而且目前主流在用的只有可達性分析算法.

聽上去很高大上? 聽在下解釋:
引用計數(shù)器算法: 給對象添加一個計數(shù)器, 每當(dāng)有變量引用它時, +1, gc就判斷這個對象的計數(shù)器是否為0即可. 十分簡單, 不過問題也很明顯, 比如:

A a = new A();
B b = new B();

a.b = b;
b.a = a;

只要對象相互引用一下, 好家伙, gg了...這兩對象回收不掉了.

可達性分析算法: 將棧的 局部變量 以及方法區(qū)中的 靜態(tài)變量 作為根, 沿著向下查找對象, 并將所有找到的對象標(biāo)記為非垃圾對象. 然后回收掉所有未被標(biāo)記的對象. 被作為根的對象也被稱之為"gc roots". 如下圖:

可達性分析算法.png

2. 聽說Java中有好幾種引用? 對于回收它們有什么區(qū)別?

Java中的引用一般分為 強, 軟, 弱, 虛 四種引用, 在編碼上區(qū)別如下:
強引用: 就是普通的變量直接引用, 只有在失去引用后才會被回收.

User user = new User();

軟引用: 使用SoftReference軟引用類型包裹一個對象, 即可將該對象轉(zhuǎn)換為軟引用. 該引用正常情況下不回被回收, 但如果gc之后仍沒有足夠的空間可以使用, 則會回收這類引用的對象

public static SoftReference<User> user = newSoftReference<>(new User());

弱引用: 使用WeekSoftReference弱引用類型包裹, 即可轉(zhuǎn)換引用類型為弱引用. 這類引用會在垃圾回收時直接被回收掉, 與無引用幾乎無異.

public static WeakReference<User> user = new WeakReference<>(new User());

虛引用: 在下沒弄明白這個, 完全沒有使用場景...前兩種還可以用做緩存...如果有小伙伴兒知道這個虛引用的使用場景, 歡迎評論.

3. 垃圾回收調(diào)用finalize()干了啥?

  • 我的理解, 這個finalize方法是Jvm的一個生命周期鉤子, 在對象即將被回收之前調(diào)用.
  • 其實在可達性分析算法結(jié)束后, 所有未被標(biāo)記的"垃圾對象"在真正回收前會判斷該對象是否重寫了finalize()方法, 如果重寫了, 則執(zhí)行, 如果未重寫, 直接回收.
  • 也可以在finalize()方法中給對象重新建立引用, 這樣做則可以挽救該對象被回收.
  • 需要注意的是, 每個對象的finalize()方法僅會被調(diào)用一次.

4. 如何判斷一個類是可回收的類呢?

我們的類在full gc時也可能被回收, 前面說過類的信息是存放在方法區(qū)的, 其實full gc回收方法區(qū)主要就是回收類.
而判斷一個類是否可回收必須滿足三個條件:

  1. 該類的所有實例對象被回收
  2. 該類的類對象已不存在任何引用, 也就是java.lang.class對象. 這意味著, 不能再通過反射創(chuàng)建該類的對象
  3. 加載該類的類加載器已被回收. 這一點很有意思, 因為這說明我們自己寫的類, 也就是classpath路徑下的類永遠不會被回收. 因為它們都是AppClassLoader加載的. 這也是為啥熱更新jsp需要tomcat自定義類加載器的原因之一(tomcat每次熱更新jsp都會重新創(chuàng)建一個類加載器實例, 并重新加載產(chǎn)生變化的jsp).

好了, 今天的總結(jié)就到這里. 如果有錯誤的地方, 希望大家能不吝指教, 也歡迎大家向我提問, 一起討論技術(shù).

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末侄泽,一起剝皮案震驚了整個濱河市绘趋,隨后出現(xiàn)的幾起案子误债,更是在濱河造成了極大的恐慌票编,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蟀架,死亡現(xiàn)場離奇詭異瓣赂,居然都是意外死亡榆骚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進店門煌集,熙熙樓的掌柜王于貴愁眉苦臉地迎上來妓肢,“玉大人,你說我怎么就攤上這事苫纤〉锬疲” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵卷拘,是天一觀的道長喊废。 經(jīng)常有香客問我,道長栗弟,這世上最難降的妖魔是什么污筷? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮横腿,結(jié)果婚禮上颓屑,老公的妹妹穿的比我還像新娘。我一直安慰自己耿焊,他們只是感情好揪惦,可當(dāng)我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著罗侯,像睡著了一般器腋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上钩杰,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天纫塌,我揣著相機與錄音,去河邊找鬼讲弄。 笑死措左,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的避除。 我是一名探鬼主播怎披,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼瓶摆!你這毒婦竟也來了凉逛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤群井,失蹤者是張志新(化名)和其女友劉穎状飞,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡诬辈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年酵使,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片自晰。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡凝化,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出酬荞,到底是詐尸還是另有隱情搓劫,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布混巧,位于F島的核電站枪向,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏咧党。R本人自食惡果不足惜秘蛔,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望傍衡。 院中可真熱鬧深员,春花似錦、人聲如沸蛙埂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绣的。三九已至叠赐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間屡江,已是汗流浹背芭概。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留惩嘉,地道東北人罢洲。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像文黎,于是被迫代替她去往敵國和親奏路。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容