本文主要講對(duì)象相關(guān)(對(duì)象實(shí)例化、內(nèi)存布局、訪問定位)和直接內(nèi)存相關(guān)的內(nèi)容。
目錄
?1 對(duì)象的實(shí)例化內(nèi)存布局與訪問定位
??1.1 對(duì)象的實(shí)例化
???1.1.1 創(chuàng)建對(duì)象的方式
???1.1.2 創(chuàng)建對(duì)象的步驟
????1.1.2.1判斷對(duì)象對(duì)應(yīng)的類是否加載、鏈接、初始化
????1.1.2.2 為對(duì)象分配內(nèi)存
????1.1.2.3 處理并發(fā)安全問題
????1.1.2.4 初始化分配到的空間
????1.1.2.5 設(shè)置對(duì)象的對(duì)象頭
????1.1.2.6 執(zhí)行init方法進(jìn)行初始化
??1.2 對(duì)象的內(nèi)存布局
??1.3 對(duì)象的訪問定位
?2 直接內(nèi)存
1 對(duì)象的實(shí)例化內(nèi)存布局與訪問定位
1.1 對(duì)象的實(shí)例化
1.1.1 創(chuàng)建對(duì)象的方式
1.1.2 創(chuàng)建對(duì)象的步驟
1.1.2.1 判斷對(duì)象對(duì)應(yīng)的類是否加載、鏈接、初始化
- 虛擬機(jī)遇到一條new指令,首先去檢查這個(gè)指令的參數(shù)能否在Metaspace的常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已經(jīng)被加載、解析和初始化。( 即判斷類元信息是否存在)。
- 如果沒有,那么在雙親委派模式下,使用當(dāng)前類加載器以ClassLoader+包名+類名為Key查找對(duì)應(yīng)的.class文件。如果沒有找到文件,則拋出ClassNotFoundException異常,如果找到,則進(jìn)行類加載,并生成對(duì)應(yīng)的Class類對(duì)象
1.1.2.2 為對(duì)象分配內(nèi)存
首先計(jì)算對(duì)象占用空間大小,接著在堆中劃分一塊內(nèi)存給新對(duì)象。
如果實(shí)例成員變量是引用變量,僅分配引用變量空間即可,即4個(gè)字節(jié)大小。
-
如果內(nèi)存是規(guī)整的,那么虛擬機(jī)將采用的是
指針碰撞(BumpThePointer)
來為對(duì)象分配內(nèi)存。意思是所有用過的內(nèi)存在一邊,空閑的內(nèi)存在另外一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,分配內(nèi)存就僅僅是把指針向空閑那邊挪動(dòng)一段與對(duì)象大小相等的距離罷了。如果垃圾收集器選擇的是Serial、ParNew這種基于標(biāo)記壓縮算法的,虛擬機(jī)采用這種分配方式。一般使用帶有compact (整理)過程的收集器時(shí),使用指針碰撞。
指針碰撞 如果內(nèi)存不是規(guī)整的,已使用的內(nèi)存和未使用的內(nèi)存相互交錯(cuò),那么虛擬機(jī)將采用的是
空閑列表(Free List)
來為對(duì)象分配內(nèi)存。意思是虛擬機(jī)維護(hù)了一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,再分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的內(nèi)容。
說明:選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
1.1.2.3 處理并發(fā)安全問題
在分配內(nèi)存空間時(shí),要保證new對(duì)象時(shí)候的線程安全性:創(chuàng)建對(duì)象是非常頻繁的操作,虛擬機(jī)需要解決并發(fā)問題。虛擬機(jī)采用 了兩種方式解決并發(fā)問題:
-
CAS ( Compare And Swap )
失敗重試、區(qū)域加鎖:保證指針更新操作的原子性;
-
本地線程分配緩沖區(qū)(TLAB ,Thread Local Allocation Buffer)
,把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存。虛擬機(jī)是否使用TLAB,可以通過-XX:+/-UseTLAB參數(shù)來 設(shè)定。
1.1.2.4 初始化分配到的空間
內(nèi)存分配結(jié)束,虛擬機(jī)將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭)。這一步保證了對(duì)象的實(shí)例字段在Java代碼中可以不用賦初始值就可以直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。
給對(duì)象的屬性賦值的操作:
① 屬性的默認(rèn)初始化
② 顯式初始化
③ 代碼塊中初始化
④ 構(gòu)造器中初始化
①是在初始化分配到的空間
這一步做的,而②③④是在執(zhí)行init方法進(jìn)行初始化
做的
1.1.2.5 設(shè)置對(duì)象的對(duì)象頭
將對(duì)象的所屬類(即類的元數(shù)據(jù)信息)、對(duì)象的HashCode和對(duì)象的GC信息、鎖信息等數(shù)據(jù)存儲(chǔ)在對(duì)象的對(duì)象頭中。這個(gè)過程的具體設(shè)置方式取決于JVM實(shí)現(xiàn)。
1.1.2.6 執(zhí)行init方法進(jìn)行初始化
- 在Java程序的視角看來,初始化才正式開始。初始化成員變量,執(zhí)行實(shí)例化代碼塊,調(diào)用類的構(gòu)造方法,并把堆內(nèi)對(duì)象的首地址賦值給引用變量。
- 因此一般來說,new指令之 后會(huì)接著執(zhí)行方法(由字節(jié)碼中是否跟隨有invokespecial指令所決定),把對(duì)象按照程序員的意愿進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全創(chuàng)建出來。
/**
* 測(cè)試對(duì)象實(shí)例化的過程 example01
* ① 加載類元信息 - ② 為對(duì)象分配內(nèi)存 - ③ 處理并發(fā)問題 - ④ 屬性的默認(rèn)初始化(零值初始化)
* - ⑤ 設(shè)置對(duì)象頭的信息 - ⑥ 屬性的顯式初始化、代碼塊中初始化、構(gòu)造器中初始化
*
*
* 給對(duì)象的屬性賦值的操作:
* ① 屬性的默認(rèn)初始化 - ② 顯式初始化 / ③ 代碼塊中初始化 - ④ 構(gòu)造器中初始化
*/
public class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名客戶";
}
public Customer(){
acct = new Account();
}
}
class Account{
}
1.2 對(duì)象的內(nèi)存布局
對(duì)齊填充
:對(duì)齊填充是用于確保對(duì)象的內(nèi)存的總長(zhǎng)度為8字節(jié)的整數(shù)倍。
為什么要是確保是8字節(jié)的整數(shù)倍呢?
??因?yàn)閔otspot要求對(duì)象起始地址為8字節(jié)的整數(shù)倍以便于自動(dòng)內(nèi)存管理,換句話說,對(duì)象的總長(zhǎng)度要為8字節(jié)的整數(shù)倍才能保證如此。而又因?yàn)閷?duì)象頭正好是8字節(jié)(32位或64位)的整數(shù)倍,但是實(shí)例數(shù)據(jù)長(zhǎng)度是任意的,因此需要對(duì)齊補(bǔ)充來確保整個(gè)對(duì)象總長(zhǎng)度為8字節(jié)的整數(shù)倍
Java對(duì)象內(nèi)存布局對(duì)齊填充等價(jià)形式推導(dǎo)
/**
* example02
*/
public class CustomerTest {
public static void main(String[] args) {
Customer cust = new Customer();
}
}
1.3 對(duì)象的訪問定位
①JVM是如何通過棧幀中的對(duì)象引|用訪問到其內(nèi)部的對(duì)象實(shí)例的呢?
定位,通過棧上reference訪問
②對(duì)象訪問的主要方式有幾種?
-
句柄訪問
句柄訪問 -
直接指針(HotSpot采用)
直接指針
③對(duì)象訪問兩種方式的優(yōu)點(diǎn)?
對(duì)象訪問方式 | 優(yōu)點(diǎn) |
---|---|
句柄訪問 | 對(duì)象移動(dòng)(垃圾收集對(duì)象移動(dòng)很普遍)時(shí)改變句柄到對(duì)象實(shí)例的指針即可 |
直接指針 | 節(jié)省空間;效率快 |
2 直接內(nèi)存(Direct Memory)
- 不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機(jī)規(guī)范》中定義的內(nèi)存區(qū)域
- 直接內(nèi)存是Java堆外的、直接向系統(tǒng)申請(qǐng)的內(nèi)存區(qū)間
- 來源于NIO,通過存在堆中的DirectByteBuffer操作Native內(nèi)存
/**
* IO NIO (New IO / Non-Blocking IO)
* byte[] / char[] Buffer
* Stream Channel
*
* 查看直接內(nèi)存的占用與釋放 example03
*/
public class BufferTest {
private static final int BUFFER = 1024 * 1024 * 1024;//1GB
public static void main(String[] args){
//直接分配本地內(nèi)存空間
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
System.out.println("直接內(nèi)存分配完畢,請(qǐng)求指示!");
Scanner scanner = new Scanner(System.in);
scanner.next();
System.out.println("直接內(nèi)存開始釋放!");
byteBuffer = null;
System.gc();
scanner.next();
}
}
-
通常,訪問直接內(nèi)存的速度會(huì)優(yōu)于Java堆。即讀寫性能高
??a.因此出于性能考慮,讀寫頻繁的場(chǎng)合可能會(huì)考慮使用直接內(nèi)存
??b.Java的NIO庫允許Java程序使用直接內(nèi)存,用于數(shù)據(jù)緩沖區(qū)
也可能導(dǎo)致OutOfMemoryError異常:OutOfMemoryError: Direct buffer memory
/**
* 本地內(nèi)存的OOM: OutOfMemoryError: Direct buffer memory example04
*/
public class BufferTest2 {
private static final int BUFFER = 1024 * 1024 * 20;//20MB
public static void main(String[] args) {
ArrayList<ByteBuffer> list = new ArrayList<>();
int count = 0;
try {
while(true){
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
list.add(byteBuffer);
count++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
System.out.println(count);
}
}
}
由于直接內(nèi)存在Java堆外,因此它的大小不會(huì)直接受限于一Xmx指定的最大
堆大小,但是系統(tǒng)內(nèi)存是有限的,Java堆和直接內(nèi)存的總和依然受限于操作系統(tǒng)能給出的最大內(nèi)存。缺點(diǎn)
??a.分配回收成本較高
??b.不受JVM內(nèi)存回收管理直接內(nèi)存大小可以通過
MaxDirectMemorySize
設(shè)置如果不指定,默認(rèn)與堆的最大值-Xmx參數(shù)值一致
簡(jiǎn)單理解:java process memory = java heap + native memory