談一談Java內(nèi)存區(qū)域和Java內(nèi)存模型的理解? / Java內(nèi)存區(qū)域和Java內(nèi)存模型是一個(gè)東西嗎?
Java內(nèi)存區(qū)域和Java內(nèi)存模型不是一個(gè)東西?。。。。?/p>
Java內(nèi)存區(qū)域,也就是Java運(yùn)行時(shí)數(shù)據(jù)區(qū)域。是指Java虛擬機(jī)在運(yùn)行時(shí)創(chuàng)建的一個(gè)內(nèi)存區(qū)域,用于存儲(chǔ)Java程序運(yùn)行時(shí)所需要的數(shù)據(jù)結(jié)構(gòu)和對(duì)象實(shí)例。Java運(yùn)行時(shí)數(shù)據(jù)區(qū)包括堆、方法區(qū)、虛擬機(jī)棧、本地方法棧和程序計(jì)數(shù)器等部分。
Java內(nèi)存模型,也就是JMM,定義了程序中各個(gè)變量的訪(fǎng)問(wèn)規(guī)則,在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這種底層細(xì)節(jié)。
JMM
JMM(Java Memory Model)是Java虛擬機(jī)規(guī)范中定義的一種內(nèi)存模型,它描述了Java程序如何在多線(xiàn)程環(huán)境下訪(fǎng)問(wèn)共享內(nèi)存。JMM主要是為了屏蔽各種硬件和操作系統(tǒng)對(duì)內(nèi)存訪(fǎng)問(wèn)的差異而定義出來(lái)的內(nèi)存模型。JMM定義了一個(gè)抽象的計(jì)算機(jī)內(nèi)存模型,包括主內(nèi)存和工作內(nèi)存兩部分。
-
主內(nèi)存
主內(nèi)存是所有線(xiàn)程共享的內(nèi)存區(qū)域,也是Java內(nèi)存模型中的核心部分。主內(nèi)存中保存著Java對(duì)象的實(shí)例數(shù)據(jù)、類(lèi)信息、方法等。
在多線(xiàn)程環(huán)境下,當(dāng)一個(gè)線(xiàn)程修改了主內(nèi)存中的共享變量時(shí),其他線(xiàn)程并不會(huì)立即看到這個(gè)變量的修改。這是因?yàn)槊總€(gè)線(xiàn)程都有自己的工作內(nèi)存,與主內(nèi)存之間存在緩存數(shù)據(jù)不一致的問(wèn)題。 -
工作內(nèi)存
每個(gè)線(xiàn)程都有自己的工作內(nèi)存(Thread Local Memory),它是線(xiàn)程私有的內(nèi)存區(qū)域。工作內(nèi)存中保存著該線(xiàn)程使用到的共享變量的副本拷貝。
當(dāng)一個(gè)線(xiàn)程需要使用某個(gè)共享變量時(shí),它會(huì)首先從主內(nèi)存中讀取該變量的值到自己的工作內(nèi)存中,并對(duì)它進(jìn)行操作。操作完成后,該線(xiàn)程再將變量的值寫(xiě)回主內(nèi)存中。在這個(gè)過(guò)程中,其他線(xiàn)程并不能直接訪(fǎng)問(wèn)到該線(xiàn)程的工作內(nèi)存。 -
內(nèi)存交互操作
Java內(nèi)存模型還定義了一些內(nèi)存交互操作,包括lock、unlock、read和write等。這些操作可以保證多線(xiàn)程環(huán)境下共享變量的可見(jiàn)性和一致性。- lock和unlock:用于對(duì)共享變量進(jìn)行加鎖和解鎖,確保同一時(shí)刻只有一個(gè)線(xiàn)程可以訪(fǎng)問(wèn)該變量。
- read:用于將工作內(nèi)存中的值傳遞到主內(nèi)存中。
- write:用于將主內(nèi)存中的值傳遞到工作內(nèi)存中。
-
內(nèi)存屏障
Java內(nèi)存模型還定義了內(nèi)存屏障(Memory Barrier),用于控制內(nèi)存交互操作的順序和可見(jiàn)性。
內(nèi)存屏障分為四種類(lèi)型:- LoadLoad屏障:保證load指令之前的所有l(wèi)oad指令已經(jīng)執(zhí)行完畢。
- StoreStore屏障:保證store指令之前的所有store指令已經(jīng)執(zhí)行完畢。
- LoadStore屏障:保證load指令之前的所有指令都已經(jīng)執(zhí)行完畢,并且能夠讀取到最新的變量值。
- StoreLoad屏障:保證store指令之前的所有指令都已經(jīng)執(zhí)行完畢,并且該指令所寫(xiě)入的變量值對(duì)于其他線(xiàn)程可見(jiàn)。
JMM的作用
保證多線(xiàn)程環(huán)境下的數(shù)據(jù)可見(jiàn)性、原子性和有序性。通過(guò)JMM規(guī)定的規(guī)范,我們可以確保多線(xiàn)程的正確性和可靠性。
synchronized、volatile、Lock等關(guān)鍵字和API有什么作用?
這些關(guān)鍵字和API是Java多線(xiàn)程編程中用來(lái)實(shí)現(xiàn)同步的機(jī)制,用于保證多線(xiàn)程環(huán)境下共享變量的可見(jiàn)性、有序性和原子性。其中synchronized關(guān)鍵字用于實(shí)現(xiàn)悲觀鎖機(jī)制,volatile關(guān)鍵字用于實(shí)現(xiàn)輕量級(jí)同步機(jī)制,Lock接口用于實(shí)現(xiàn)更加靈活的鎖機(jī)制。
Java內(nèi)存區(qū)域
1.8之前
1.8及之后
字符串常量池?
字符串常量池 JVM 為了提升性能和減少內(nèi)存消耗針對(duì)字符串(String 類(lèi))專(zhuān)門(mén)開(kāi)辟的一塊區(qū)域,主要目的是為了避免字符串的重復(fù)創(chuàng)建。
HotSpot 虛擬機(jī)中字符串常量池的實(shí)現(xiàn)是 src/hotspot/share/classfile/stringTable.cpp ,StringTable 本質(zhì)上就是一個(gè)HashSet ,容量為 StringTableSize(可以通過(guò) -XX:StringTableSize 參數(shù)來(lái)設(shè)置)。
StringTable 中保存的是字符串對(duì)象的引用,字符串對(duì)象的引用指向堆中的字符串對(duì)象。
JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和靜態(tài)變量從永久代移動(dòng)了 Java 堆中。
JDK 1.7 為什么要將字符串常量池移動(dòng)到堆中?
主要是因?yàn)橛谰么ǚ椒▍^(qū)實(shí)現(xiàn))的 GC 回收效率太低,只有在整堆收集 (Full GC)的時(shí)候才會(huì)被執(zhí)行 GC。Java 程序中通常會(huì)有大量的被創(chuàng)建的字符串等待回收,將字符串常量池放到堆中,能夠更高效及時(shí)地回收字符串內(nèi)存。
什么是直接內(nèi)存?
直接內(nèi)存是一種特殊的內(nèi)存緩沖區(qū),并不在 Java 堆或方法區(qū)中分配的,而是通過(guò) JNI 的方式在本地內(nèi)存上分配的。
直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存也被頻繁地使用。而且也可能導(dǎo)致 OutOfMemoryError 錯(cuò)誤出現(xiàn)。
JDK1.4 中新加入的 NIO(Non-Blocking I/O,也被稱(chēng)為New I/O),引入了一種基于通道(Channel)與緩存區(qū)(Buffer)的 I/O 方式,它可以直接使用 Native 函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在 Java 堆中的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣就能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽?Java 堆和 Native 堆之間來(lái)回復(fù)制數(shù)據(jù)。
直接內(nèi)存的分配不會(huì)受到 Java 堆的限制,但是,既然是內(nèi)存就會(huì)受到本機(jī)總內(nèi)存大小以及處理器尋址空間的限制。
說(shuō)一說(shuō)HotSpot 虛擬機(jī)在 Java 堆中對(duì)象分配過(guò)程?
-
類(lèi)加載檢查
虛擬機(jī)遇到一條 new 指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到這個(gè)類(lèi)的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類(lèi)是否已被加載過(guò)、解析和初始化過(guò)。如果沒(méi)有,那必須先執(zhí)行相應(yīng)的類(lèi)加載過(guò)程。 -
分配內(nèi)存
在類(lèi)加載檢查通過(guò)后,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需的內(nèi)存大小在類(lèi)加載完成后便可確定,為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從 Java 堆中劃分出來(lái)。分配方式有 “指針碰撞” 和 “空閑列表” 兩種,選擇哪種分配方式由 Java 堆是否規(guī)整決定,而 Java 堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。 -
初始化零值
內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭),這一步操作保證了對(duì)象的實(shí)例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪(fǎng)問(wèn)到這些字段的數(shù)據(jù)類(lèi)型所對(duì)應(yīng)的零值。 -
設(shè)置對(duì)象頭
初始化零值完成之后,虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例、如何才能找到類(lèi)的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的 GC 分代年齡等信息。 這些信息存放在對(duì)象頭中。 另外,根據(jù)虛擬機(jī)當(dāng)前運(yùn)行狀態(tài)的不同,如是否啟用偏向鎖等,對(duì)象頭會(huì)有不同的設(shè)置方式。 -
執(zhí)行init方法
在上面工作都完成之后,從虛擬機(jī)的視角來(lái)看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了,但從 Java 程序的視角來(lái)看,對(duì)象創(chuàng)建才剛開(kāi)始, 方法還沒(méi)有執(zhí)行,所有的字段都還為零。所以一般來(lái)說(shuō),執(zhí)行 new 指令之后會(huì)接著執(zhí)行 方法,把對(duì)象按照程序員的意愿進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全產(chǎn)生出來(lái)。
虛擬機(jī)內(nèi)存分配的兩種方式?
-
指針碰撞
- 適用場(chǎng)合:堆內(nèi)存規(guī)整(沒(méi)有內(nèi)存碎片)的情況下
- 原理:用過(guò)的內(nèi)存全部整合到一邊,沒(méi)有用過(guò)的內(nèi)存放到另一邊,中間一個(gè)分解指針,只需要想著沒(méi)用過(guò)的內(nèi)存方向?qū)⒃撝羔樢苿?dòng)對(duì)象內(nèi)存大小即可。
- 使用該分配方式的GC收集器:Serial,ParNew
-
空閑列表
- 適用場(chǎng)合:堆內(nèi)存不規(guī)整的情況下
- 原理:虛擬機(jī)會(huì)維護(hù)一個(gè)列表,該列表中會(huì)記錄哪些內(nèi)存塊是可用的,在分配的時(shí)候,找一塊兒足夠大的內(nèi)存塊兒來(lái)劃分給對(duì)象實(shí)例,最后更新列表記錄
- 使用該分配方式的 GC 收集器:CMS
選擇以上兩種方式中的哪一種,取決于 Java 堆內(nèi)存是否規(guī)整。而 Java 堆內(nèi)存是否規(guī)整,取決于 GC 收集器的算法是"標(biāo)記-清除",還是"標(biāo)記-整理"(也稱(chēng)作"標(biāo)記-壓縮"),值得注意的是,復(fù)制算法內(nèi)存也是規(guī)整的。
說(shuō)一說(shuō)對(duì)象創(chuàng)建中不安全的現(xiàn)象? / 如何解決線(xiàn)程不安全的問(wèn)題?
對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為, 即使僅僅修改一個(gè)指針?biāo)赶虻奈恢茫?在并發(fā)情況下也并不是線(xiàn)程安全的, 可能出現(xiàn)正在給對(duì)象A分配內(nèi)存, 指針還沒(méi)來(lái)得及修改, 對(duì)象B又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存的情況。
一種是對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理——實(shí)際上虛擬機(jī)是采用CAS配上失敗重試的方式保證更新操作的原子性; 另外一種是把內(nèi)存分配的動(dòng)作按照線(xiàn)程劃分在不同的空間之中進(jìn)行, 即每個(gè)線(xiàn)程在Java堆中預(yù)先分配一小塊內(nèi)存, 稱(chēng)為本地線(xiàn)程分配緩沖(Thread Local Allocation Buffer, TLAB) , 哪個(gè)線(xiàn)程要分配內(nèi)存, 就在哪個(gè)線(xiàn)程的本地緩沖區(qū)中分配, 只有本地緩沖區(qū)用完了, 分配新的緩存區(qū)時(shí)才需要同步鎖定。 虛擬機(jī)是否使用TLAB, 可以通過(guò)-XX: +/-UseTLAB參數(shù)來(lái)設(shè)定。
對(duì)象的內(nèi)存布局?
在HotSpot虛擬機(jī)里, 對(duì)象在堆內(nèi)存中的存儲(chǔ)布局可以劃分為三個(gè)部分: 對(duì)象頭(Header) 、 實(shí)例數(shù)據(jù)(Instance Data) 和對(duì)齊填充(Padding) 。
HotSpot虛擬機(jī)對(duì)象的對(duì)象頭部分包括兩類(lèi)信息。 第一類(lèi)是用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù), 如哈希碼(HashCode) 、 GC分代年齡、 鎖狀態(tài)標(biāo)志、 線(xiàn)程持有的鎖、 偏向線(xiàn)程ID、 偏向時(shí)間戳等, 這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(未開(kāi)啟壓縮指針) 中分別為32個(gè)比特和64個(gè)比特, 官方稱(chēng)它為“Mark Word”。
對(duì)象頭的另外一部分是類(lèi)型指針, 即對(duì)象指向它的類(lèi)型元數(shù)據(jù)的指針, Java虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定該對(duì)象是哪個(gè)類(lèi)的實(shí)例。
接下來(lái)實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息, 即我們?cè)诔绦虼a里面所定義的各種類(lèi)型的字段內(nèi)容, 無(wú)論是從父類(lèi)繼承下來(lái)的, 還是在子類(lèi)中定義的字段都必須記錄起來(lái)。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-414858.html
對(duì)象的第三部分是對(duì)齊填充, 這并不是必然存在的, 也沒(méi)有特別的含義, 它僅僅起著占位符的作用。 由于HotSpot虛擬機(jī)的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍, 換句話(huà)說(shuō)就是任何對(duì)象的大小都必須是8字節(jié)的整數(shù)倍。 對(duì)象頭部分已經(jīng)被精心設(shè)計(jì)成正好是8字節(jié)的倍數(shù)(1倍或者
2倍) , 因此, 如果對(duì)象實(shí)例數(shù)據(jù)部分沒(méi)有對(duì)齊的話(huà), 就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-414858.html
到了這里,關(guān)于我的面試八股(JVM篇)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!