偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

New 一個(gè)對(duì)象在堆中的歷程

開(kāi)發(fā) 前端
至于 JVM 怎么知道這個(gè)空間得分配多大呢?事實(shí)上,對(duì)象所需內(nèi)存的大小在類(lèi)加載完成后就已經(jīng)可以完全確定了。

小伙伴們大家好呀,我是小牛肉~ 我寫(xiě)文章的流程一般都是先在看書(shū)和看博客的過(guò)程中做做筆記,然后過(guò)一段時(shí)間再把這些筆記總結(jié)成文章輸出出來(lái),這樣一來(lái)能夠加深影響,二來(lái)也不至于文章的質(zhì)量太低。從這篇文章的草稿筆記到現(xiàn)在決定開(kāi)始成文,其實(shí)已經(jīng)有一個(gè)月了,本來(lái)覺(jué)得趁著寒假可以順理成章地脫離惡心的深度學(xué)習(xí)然后好好地把 JVM 知識(shí)點(diǎn)全都掃一遍,正好囤幾篇文章,誰(shuí)知道回家后根本無(wú)心看書(shū),只能每天刷幾道 LeetCode 來(lái)彌補(bǔ)下日積月累的焦慮和罪惡感。

STOP,廢話結(jié)束

今天介紹兩個(gè) JVM 中的高頻基礎(chǔ)題:

  • 對(duì)象的創(chuàng)建過(guò)程(new 一個(gè)對(duì)象在堆中的歷程)
  • 對(duì)象在堆上分配的兩種方式

對(duì)象的創(chuàng)建過(guò)程分五步走,如下圖:

我感覺(jué) JVM 如果不看 GC 收集器那塊(滑稽),似乎東西還不多

老規(guī)矩,背誦版在文末。點(diǎn)擊閱讀原文可以直達(dá)我收錄整理的各大廠面試真題

類(lèi)加載檢查

對(duì)象創(chuàng)建過(guò)程的第一步,所謂類(lèi)加載檢查,就是檢測(cè)我們接下來(lái)要 new 出來(lái)的這個(gè)對(duì)象所屬的類(lèi)是否已經(jīng)被 JVM 成功加載、解析和初始化過(guò)了(具體的類(lèi)加載過(guò)程會(huì)在后續(xù)文章詳細(xì)解釋~)

具體來(lái)說(shuō),當(dāng) Java 虛擬機(jī)遇到一條字節(jié)碼 new 指令時(shí):

1)首先檢查根據(jù) class 文件中的常量池表(Constant Pool Table)能否找到這個(gè)類(lèi)對(duì)應(yīng)的符號(hào)引用

此處可以回顧一波常量池表 (Constant Pool Table) 的概念:

用于存放編譯期生成的各種字面量(字面量相當(dāng)于 Java 語(yǔ)言層面常量的概念,如文本字符串,聲明為 final 的常量值等)與符號(hào)引用。有一些文章會(huì)把 class 常量池表稱(chēng)為靜態(tài)常量池。

都是常量池,常量池表和方法區(qū)中的運(yùn)行時(shí)常量池有啥關(guān)系嗎?運(yùn)行時(shí)常量池是干嘛的呢?

運(yùn)行時(shí)常量池可以在運(yùn)行期間將 class 常量池表中的符號(hào)引用解析為直接引用。簡(jiǎn)單來(lái)說(shuō),class 常量池表就相當(dāng)于一堆索引,運(yùn)行時(shí)常量池根據(jù)這些索引來(lái)查找對(duì)應(yīng)方法或字段所屬的類(lèi)型信息和名稱(chēng)及描述符信息

2)然后去方法區(qū)中的運(yùn)行時(shí)常量池中查找該符號(hào)引用所指向的類(lèi)是否已被 JVM 加載、解析和初始化過(guò)

如果沒(méi)有,那就先執(zhí)行相應(yīng)的類(lèi)加載過(guò)程

如果有,那么進(jìn)入下一步,為新生對(duì)象分配內(nèi)存

分配內(nèi)存

類(lèi)加載檢查通過(guò)后,這個(gè)對(duì)象待會(huì)兒要是被創(chuàng)建出來(lái)得有地方放他對(duì)吧?

所以接下來(lái) JVM 會(huì)為新生對(duì)象分配內(nèi)存空間。

至于 JVM 怎么知道這個(gè)空間得分配多大呢?事實(shí)上,對(duì)象所需內(nèi)存的大小在類(lèi)加載完成后就已經(jīng)可以完全確定了。在 Hotspot 虛擬機(jī)中,對(duì)象在內(nèi)存中的布局可以分為 3 塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充。

1)Hotspot 虛擬機(jī)的對(duì)象頭包括兩部分信息:

  • 第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)(如哈希碼(HashCode)、GC 分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程 ID、偏向時(shí)間戳等,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在 32 位和 64 位的虛擬機(jī)(未開(kāi)啟壓縮指針)中分別為 32 個(gè)比特和 64 個(gè)比特,官方稱(chēng)它為 “Mark Word”。學(xué)過(guò) synchronized 的小伙伴對(duì)這個(gè)一定不陌生~)
  • 另一部分是類(lèi)型指針,即對(duì)象指向它的類(lèi)型元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例

2)實(shí)例數(shù)據(jù)部分存儲(chǔ)的是這個(gè)對(duì)象真正的有效信息,即我們?cè)诔绦虼a里面所定義的各種類(lèi)型的字段內(nèi)容,無(wú)論是從父類(lèi)繼承下來(lái)的,還是在子類(lèi)中定義的字段都必須記錄起來(lái)。

3)對(duì)齊填充部分不是必須的,也沒(méi)有什么特別的含義,僅僅起占位作用。因?yàn)?Hotspot 虛擬機(jī)的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是 8 字節(jié)的整數(shù)倍,換句話說(shuō)就是對(duì)象的大小必須是 8 字節(jié)的整數(shù)倍。而對(duì)象頭部分正好是 8 字節(jié)的倍數(shù)(1 倍或 2 倍),因此,當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒(méi)有對(duì)齊時(shí),就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。

對(duì)象在堆上的兩種分配方式

為對(duì)象分配內(nèi)存空間的任務(wù)通俗來(lái)說(shuō)把一塊確定大小的內(nèi)存塊從 Java 堆中劃分出來(lái)給這個(gè)對(duì)象用。

根據(jù)堆中的內(nèi)存是否規(guī)整,有兩種劃分方式,或者說(shuō)對(duì)象在堆上的分配有兩種方式:

1)假設(shè) Java 堆中內(nèi)存是絕對(duì)規(guī)整的,所有被使用過(guò)的內(nèi)存都被放在一邊,空閑的內(nèi)存被放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把這個(gè)指針 向 空閑空間方向 挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱(chēng)為 指針碰撞(Bump The Pointer)

2)如果 Java 堆中的內(nèi)存并不是規(guī)整的,已被使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò)在一起,那就沒(méi)有辦法簡(jiǎn)單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的連續(xù)空間劃分給這個(gè)對(duì)象,并更新列表上的記錄,這種分配方式稱(chēng)為 空閑列表(Free List)。

選擇哪種分配方式由 Java 堆是否規(guī)整決定,那又有同學(xué)會(huì)問(wèn)了,堆是否規(guī)整又由誰(shuí)來(lái)決定呢?

Java 堆是否規(guī)整由所采用的垃圾收集器是否帶有空間壓縮整理(Compact)的能力決定的(或者說(shuō)由垃圾收集器采用的垃圾收集算法來(lái)決定的,具體垃圾收集算法見(jiàn)后續(xù)文章):

因此,當(dāng)使用 Serial、ParNew 等帶壓縮整理過(guò)程的收集器時(shí),系統(tǒng)采用的分配算法是指針碰撞,既簡(jiǎn)單又高效

而當(dāng)使用 CMS 這種基于清除(Sweep)算法的收集器時(shí),理論上就只能采用較為復(fù)雜的空閑列表來(lái)分配內(nèi)存

對(duì)象創(chuàng)建時(shí)候的并發(fā)安全問(wèn)題

另外,在為對(duì)象創(chuàng)建內(nèi)存的時(shí)候,還需要考慮一個(gè)問(wèn)題:并發(fā)安全問(wèn)題。

對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,以上面介紹的指針碰撞法為例,即使只修改一個(gè)指針?biāo)赶虻奈恢?,在并發(fā)情況下也并不是線程安全的,可能出現(xiàn)某個(gè)線程正在給對(duì)象 A 分配內(nèi)存,指針還沒(méi)來(lái)得及修改,另一個(gè)線程創(chuàng)建了對(duì)象 B 又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存的情況。

解決這個(gè)問(wèn)題有兩種可選方案:

  • 方案 1:CAS + 失敗重試:CAS 大伙應(yīng)該都熟悉,比較并交換,樂(lè)觀鎖方案,如果失敗就重試,直到成功為止
  • 方案 2:本地線程分配緩沖(Thread Local Allocation Buffer,TLAB):每個(gè)線程在堆中預(yù)先分配一小塊內(nèi)存,每個(gè)線程擁有的這一小塊內(nèi)存就稱(chēng)為 TLAB。哪個(gè)線程要分配內(nèi)存了,就在哪個(gè)線程的 TLAB 中進(jìn)行分配,這樣各個(gè)線程之間互不干擾。如果某個(gè)線程的 TLAB 用完了,那么虛擬機(jī)就需要為它分配新的 TLAB,這時(shí)才需要進(jìn)行同步鎖定??梢酝ㄟ^(guò) -XX:+/-UseTLAB 參數(shù)來(lái)設(shè)定是否使用 TLAB。

初始化零值

內(nèi)存分配完成之后,JVM 會(huì)將分配到的內(nèi)存空間(當(dāng)然不包括對(duì)象頭啦)都初始化為零值,比如 boolean 字段都初始化為 false 啊,int 字段都初始化為 0 啊之類(lèi)的

這步操作保證了對(duì)象的實(shí)例字段在 Java 代碼中可以不賦初始值就直接使用,使程序能訪問(wèn)到這些字段的數(shù)據(jù)類(lèi)型所對(duì)應(yīng)的零值。

如果使用了 TLAB 的話,初始化零值這項(xiàng)工作可以提前至 TLAB 分配時(shí)就順便進(jìn)行了

設(shè)置對(duì)象頭

上面我們說(shuō)過(guò),對(duì)象在內(nèi)存中的布局可以分為 3 塊區(qū)域:對(duì)象頭(Object Header)、實(shí)例數(shù)據(jù)和對(duì)齊填充

對(duì)齊填充并不是什么有意義的數(shù)據(jù),實(shí)例數(shù)據(jù)我們?cè)谏弦徊讲僮髦羞M(jìn)行了初始化零值,那么對(duì)于剩下的對(duì)象頭中的信息來(lái)說(shuō),自然不必多說(shuō),也是要進(jìn)行一些賦值操作的:例如這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例、如何才能找到類(lèi)的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的 GC 分代年齡等信息。根據(jù)虛擬機(jī)當(dāng)前運(yùn)行狀態(tài)的不同,如是否啟用偏向鎖等,對(duì)象頭會(huì)有不同的設(shè)置方式。

執(zhí)行 init 方法

上面四個(gè)步驟都走完之后,從 JVM 的視角來(lái)看,其實(shí)一個(gè)新的對(duì)象已經(jīng)成功誕生了。

但是從我們程序員的視角來(lái)看,這個(gè)對(duì)象確實(shí)是創(chuàng)建出來(lái)了,但是還沒(méi)按照我們定義的構(gòu)造函數(shù)來(lái)進(jìn)行賦值呢,所有的字段都還是默認(rèn)的零值啊。

構(gòu)造函數(shù)即 Class 文件中的 () 方法,一般來(lái)說(shuō),new 指令之后會(huì)接著執(zhí)行 ()方法,按照構(gòu)造函數(shù)的意圖對(duì)這個(gè)對(duì)象進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全地被構(gòu)造出來(lái)了,皆大歡喜。

最后放上這道題的背誦版:

?? 面試官:講一下對(duì)象的創(chuàng)建過(guò)程

?? 小牛肉:new 一個(gè)對(duì)象在堆中的過(guò)程主要分為五個(gè)步驟:

1)類(lèi)加載檢查:具體來(lái)說(shuō),當(dāng) Java 虛擬機(jī)遇到一條字節(jié)碼 new 指令時(shí),它會(huì)首先檢查根據(jù) class 文件中的常量池表(Constant Pool Table)能否找到這個(gè)類(lèi)對(duì)應(yīng)的符號(hào)引用,然后去方法區(qū)中的運(yùn)行時(shí)常量池中查找該符號(hào)引用所指向的類(lèi)是否已被 JVM 加載、解析和初始化過(guò)

  • 如果沒(méi)有,那就先執(zhí)行相應(yīng)的類(lèi)加載過(guò)程
  • 如果有,那么進(jìn)入下一步,為新生對(duì)象分配內(nèi)存

2)分配內(nèi)存:就是在堆中給劃分一塊內(nèi)存空間分配給這個(gè)新生對(duì)象用。具體的分配方式根據(jù)堆內(nèi)存是否規(guī)整有兩種方式:

  • 堆內(nèi)存規(guī)整的話采用的分配方式就是指針碰撞:所有被使用過(guò)的內(nèi)存都被放在一邊,空閑的內(nèi)存被放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,分配內(nèi)存就是把這個(gè)指針向空閑空間方向挪動(dòng)一段與對(duì)象大小相等的距離
  • 堆內(nèi)存不規(guī)整的話采用的分配方式就是空閑列表:所謂內(nèi)存不規(guī)整就是已被使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò)在一起,那就沒(méi)有辦法簡(jiǎn)單地進(jìn)行指針碰撞了,JVM 就必須維護(hù)一個(gè)列表,記錄哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的連續(xù)空間劃分給這個(gè)對(duì)象,并更新列表上的記錄,這就是空閑列表的方式

3)初始化零值:對(duì)象在內(nèi)存中的布局可以分為 3 塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充,對(duì)齊填充僅僅起占位作用,沒(méi)啥特殊意義,初始化零值這個(gè)操作就是初始化實(shí)例數(shù)據(jù)這個(gè)部分,比如 boolean 字段初始化為 false 之類(lèi)的

4)設(shè)置對(duì)象頭:這個(gè)步驟就是設(shè)置對(duì)象頭中的一些信息

5)執(zhí)行 init 方法:最后就是執(zhí)行構(gòu)造函數(shù),構(gòu)造函數(shù)即 Class 文件中的 ()方法,一般來(lái)說(shuō),new 指令之后會(huì)接著執(zhí)行() 方法,按照構(gòu)造函數(shù)的意圖對(duì)這個(gè)對(duì)象進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全地被構(gòu)造出來(lái)了


責(zé)任編輯:武曉燕 來(lái)源: 飛天小牛肉
相關(guān)推薦

2009-05-20 09:49:15

2011-09-19 10:19:04

NoSQL

2024-04-11 08:30:05

JavaScript數(shù)組函數(shù)

2015-08-06 13:44:21

swiftcocoapods

2011-09-08 10:46:12

Widget

2023-03-15 09:00:43

SwiftUISlider

2014-05-26 09:13:46

DockerPython

2021-01-04 09:12:31

集合變量

2023-10-14 17:49:25

Java存儲(chǔ)

2017-08-17 14:38:39

JavaAbstract抽象

2020-12-04 17:21:18

前端開(kāi)發(fā)技術(shù)

2021-12-13 11:31:36

排序數(shù)組數(shù)據(jù)結(jié)構(gòu)算法

2021-11-02 14:54:41

排序數(shù)組元素

2020-06-28 08:10:00

GoGOSSAFUNC圖編程語(yǔ)言

2021-07-31 12:58:53

PodmanLinux虛擬機(jī)

2021-07-28 20:12:17

WindowsHeap內(nèi)存

2011-05-19 15:14:49

PostgreSQL

2024-08-12 08:33:05

2024-12-17 09:27:31

2020-09-29 07:24:14

Python字典數(shù)據(jù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)