JVM内存进阶
寫在前面:博主是一位普普通通的19屆雙非軟工在讀生,平時最大的愛好就是聽聽歌,逛逛B站。博主很喜歡的一句話花開堪折直須折,莫待無花空折枝:博主的理解是頭一次為人,就應該做自己想做的事,做自己不后悔的事,做自己以后不會留有遺憾的事,做自己覺得有意義的事,不浪費這大好的青春年華。博主寫博客目的是記錄所學到的知識并方便自己復習,在記錄知識的同時獲得部分瀏覽量,得到更多人的認可,滿足小小的成就感,同時在寫博客的途中結交更多志同道合的朋友,讓自己在技術的路上并不孤單。
目錄:
1.堆內內存與堆外內存
???? ?? 堆內內存
???? ?? 堆外內存
???? ?? 堆外內存的優缺點
2.直接內存
3.對象創建內存的內存分析
???? ?? 對象創建的初始化工作
???? ?? 對象分配內存方式的兩種方式
???? ?? 對象分配內存的線程問題
???? ?? 對象實例化
4.對象的內存布局
???? ?? 對象的內存布局的簡介
???? ?? 對象頭
???? ?? 實例數據
???? ?? 對齊填充
5.對象的訪問定位
1.堆內內存與堆外內存
1.1堆內內存
Java 堆內內存是用來存放對象實例及數組,也就是說我們代碼中通過 new 關鍵字 new 出來的對象都存放在這里。
在使用堆內內存(on-heap memory)的時候,完全遵守JVM虛擬機的內存管理機制,采用垃圾回收器(GC)統一進行內存管理,GC會在某些特定的時間點進行一次徹底回收,也就是Full GC,GC會對所有分配的堆內內存進行掃描,在這個過程中會對JAVA應用程序的性能造成一定影響,還可能會產生Stop The World。
1.2堆外內存
堆外內存和堆內內存是相對的兩個概念,和堆內內存相對應,堆外內存就是把內存對象分配在Java虛擬機的堆以外的內存,這些內存直接受操作系統管理(而不是虛擬機),這樣做的結果就是能夠在一定程度上減少垃圾回收對應用程序造成的影響。我們經常用java.nio.DirectByteBuffer對象進行堆外內存的管理和使用,它會在對象創建的時候就分配堆外內存。
1.3堆外內存的優缺點
優點:
- 減少了垃圾回收提高效率(因為垃圾回收會暫停其他的工作)
- 堆內在flush到遠程時,會先復制到直接內存,然后在發送;而堆外內存相當于省略掉了這個工作
缺點:
- 堆外內存的缺點就是內存難以控制,使用了堆外內存就間接失去了JVM管理內存的可行性,改由自己來管理,當發生內存溢出時排查起來非常困難。
2.直接內存
直接內存是一塊堆外內存。直接內存并不是虛擬機運行時數據區的一部分,但是被經常的使用,而且也會導致OutOfMemoryError異常
JDK1.4的時候加入NIO類,引入了一種基于通道與緩沖區的I/O方法。它可以使用Native函數庫直接分配堆外內存,然后通過Java堆里邊的DirectByteBuffer對象來作為這一塊內存的引用進行操作,能夠在一些場景顯著提高性能,因此避免了Java堆和Native堆來回復制數據。
Native函數庫:
Native關鍵字說明其修飾的方法是一個原生態方法,方法對應的實現不是在當前文件,而是在用其他語言(如C和C++)實現的文件中。Java語言本身不能對操作系統底層進行訪問和操作,但是可以通過JNI接口調用其他語言來實現對底層的訪問。
3.對象創建的內存的內存分析
3.1對象創建的初始化工作
當JVM遇到一條字節碼new指令時,首先會去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,并檢查這個符號引用代表的類是否被加載過,沒有的話執行類加載過程
3.2對象分配內存方式的兩種方式
方式1:指針碰撞
當Java堆是絕對規整的,即所有使用的內存放在一邊,沒有使用的內存放在另一邊。中間放一個指針作為指針的分界點指示器,那所分配內存是把那個指針向空閑方向挪動一段與對象大小相等的距離。這就是指針碰撞
方式2:空閑列表
當Java堆不是規整的,已被使用和沒被使用的內存交織在一塊,虛擬機會維護一個列表,記錄那些內存是可用的,那些是不可用的,在列表里找到一塊足夠大的空間給對象,并更新列表里邊的數據
總的來說怎么分配是根據 Java堆是否規整 決定的,是否規整有是根據虛擬機所采用的垃圾垃圾收集器 是否具有空間壓縮整理的能力 來決定
3.3對象分配內存的線程問題
當我們使用指針碰撞的方式來給對象A分配內存,指針還沒來得及進行修改對象B又用了原來的指針來分配內存,會引發線程問題
解決方法1:
一種是對分配內存空間的動作進行同步處理——實際上虛擬機是采用CAS配上失敗 重試的方式保證更新操作的原子性
解決方法2:
另外一種是把內存分配的動作按照線程劃分在不同的空間之中進 行,即每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩沖(TLAB)。哪個線程要分配內存,就在哪個線程的本地緩沖區中分配,只有本地緩沖區用完 了,分配新的緩存區時才需要同步鎖定。虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來 設定
內存分配完成之后,虛擬機必須將分配到的內存空間(但不包括對象頭)都初始化為零值,如果 使用了TLAB的話,這一項工作也可以提前至TLAB分配時順便進行。這步操作保證了對象的實例字段 在Java代碼中可以不賦初始值就直接使用,使程序能訪問到這些字段的數據類型所對應的零值
3.4對象實例化
我們對象的內存雖然分配好了但是所有的字段都 為默認的零值,對象需要的其他資源和狀態信息也還沒有按照預定的意圖構造好
,Java編譯器會在遇到new關鍵字的地方同時生成 這兩條字節碼指令.new指令之后會接著執行<init> ()方法,按照程序員的意愿對對象進行初始化,這樣一個真正可用的對象才算完全被構造出來。
4.對象的內存布局
4.1對象的內存布局的簡介
對象在堆內存中的存儲布局可以劃分為三個部分:
- 對象頭(Header)
- 實例 數據(Instance Data)
- 對齊填充(Padding)
4.2對象頭
對象頭部分包括兩類信息。第一類是:用于存儲對象自身的運行時數據,如哈 希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部 分數據的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32個比特和64個比特。對象頭的另外一部分是類型指針,即對象指向它的類型元數據的指針,Java虛擬機通過這個指針 來確定該對象是哪個類的實例,并不是所有的虛擬機實現都必須在對象數據上保留類型指針,換句話 說,查找對象的元數據信息并不一定要經過對象本身,
4.3實例數據
來實例數據部分是對象真正存儲的有效信息,即我們在程序代碼里面所定義的各種類型的字 段內容,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄起來
4.4對齊填充
這并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作 用。由于HotSpot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是 任何對象的大小都必須是8字節的整數倍。對象頭部分已經被精心設計成正好是8字節的倍數(1倍或者 2倍),因此,如果對象實例數據部分沒有對齊的話,就需要通過對齊填充來補全。
5.對象的訪問定位
創建對象自然是為了后續使用該對象,我們的Java程序會通過棧上的reference(引用)數據來操作堆上的具 體對象 但是并沒有定義 這個引用應該通過什么方式去定位、訪問到堆中對象的具體位置
所以對象訪問方式也是由虛擬機實 現而定的,主流的訪問方式主要有使用句柄和直接指針兩種:
- 1.使用句柄法:如果使用句柄訪問的話,Java堆中將可能會劃分出一塊內存來作為句柄池,reference中存儲的就 是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自具體的地址信息
- 2.指針法:如果使用直接指針訪問的話,Java堆中對象的內存布局就必須考慮如何放置訪問類型數據的相關 信息,reference中存儲的直接就是對象地址,如果只是訪問對象本身的話,就不需要多一次間接訪問 的開銷
總結
- 上一篇: JVM与Java的体系结构(JVM入门知
- 下一篇: 二分查找(循序渐进由0到1掌握二分)