物件的創建
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-R8G14XOU-1626787220433)(https://camo.githubusercontent.com/e99480df412dd718430d78094143a5485c908fa7/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f382f32322f313635363165353961343133353836393f773d39353026683d32373926663d706e6726733d3238353239#height=279&id=XjbmK&originHeight=279&originWidth=950&originalType=binary&ratio=1&status=done&style=none&width=950)]](https://img.uj5u.com/2021/10/21/276004210605441.png)
- 類加載檢查: 虛擬機遇到一條new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到這個類的符號參考,并且檢查這個符號參考代表的類是否已經被加載過、決議和初始化過,如果沒有,那必須先執行相應的類加載程序,
- 分配記憶體: 在類加載檢查通過后,虛擬機將會非新生物件分配記憶體,物件所需的記憶體大小在類加載之后就可以確定了,為物件分配空間的程序等同于把一塊確定大小的記憶體從Java堆中劃分出來,分配方式有“指標碰撞”,“空閑串列”兩種,選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又根據垃圾收集器是否帶有壓縮整理功能決定,
-
指標碰撞:假設Java堆記憶體是規整的,所有已分配的記憶體都在一邊,未分配的都在另一邊, 中間有指標指示器用來分隔二者,那么分配記憶體就是把指標指示器移動和物件大小相等的舉例,這種方式成為“指標碰撞”(Serial、ParNew垃圾收集器,使用了標記-整理演算法,系統采用的分配演算法就為指標碰撞,簡單又高效)

-
空閑串列:如果Java堆空間不是連續規整的,已分配的記憶體和空閑的記憶體混合在一起,那么就需要維護一個空閑串列,來記錄哪些記憶體是可用的,在分配記憶體的時候從串列中找到一塊足夠大的空間給物件實體,并更新串列上的記錄,這種方式稱為“空閑串列”(CMS垃圾收集器,“理論上”就只能使用較為復雜的空閑串列,為什么是理論上呢?因為在CMS里為了更快的分配記憶體,設計了一個叫Linear Allocation Buffer的分配緩沖區,通過空閑串列拿到一大塊分配緩沖區后,在它里面仍然可以使用指標碰撞方式來分配)

-
記憶體的分配方式:選擇以上兩種方式的哪一種,取決于Java記憶體是否規整,而Java記憶體是否規整取決于GC收集器的演算法是“標記-清除”還是“標記-整理”,復制演算法記憶體也是規整的
-
記憶體分配并發問題:在創建物件的時候有一個很重要的問題就是執行緒安全,因為在實際開發程序中,創建物件是很頻繁的事情,作為虛擬機來說必須要保證執行緒安全才行,通常來講,虛擬機采用兩種方式來保證執行緒安全:
- CAS+失敗重試: CAS是樂觀鎖的一種實作方式,所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作, 如果因為沖突失敗就重試,直到成功為止,虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性,
- TLAB: 把記憶體分配的動作按照執行緒劃分在不同的空間中進行,即每個執行緒在Java堆中預先分配一小塊記憶體成為本地執行緒快取(Thread Local Allocation Buffer),JVM在給執行緒中的物件分配記憶體的時候,首先在TLAB分配,當物件大于TLAB中剩余記憶體或者TLAB中記憶體用盡時,再采用上述的CAS分配(可以使用-XX:+/-UseTLAB引數來開啟或關閉TLAB),
-
3.初始化零值:記憶體分配完成后,虛擬機需要將分配到的記憶體都初始化為零值(不包括物件頭),這一步操作保證了物件的實體欄位在Java代碼中可以不賦初始值就可以使用,程式能訪問到這些欄位的資料型別對應的零值,
?
4.設定物件頭:初始化零值完成后,虛擬機要對物件進行必要的設定,例如這個物件是哪個類的實體、如何才能找到類的元資料資訊、物件的哈希碼、物件的GC分代年齡等資訊,這些資訊存放在物件頭中,另外,根據虛擬機當前運行狀態 的不同,如是否啟用偏向鎖等,物件頭會有不同的設定方式,
?
5.執行init方法:在上面的作業完成之后,從虛擬機的視角來看,一個新的物件已經產生了,但是從Java程式的視角來看,物件創建才剛開始,init方法還沒有執行,所有的欄位都還為零值,所以一般來說,執行new指令之后會緊接著執行init方法,把物件按照程式員的意愿進行初始化,這樣一個真正可用的物件才算完全產生,
物件的記憶體布局

在HotSpot虛擬機中,物件在記憶體中的布局可以分為3塊區域:物件頭、實體資料和對齊填充,
?
記憶體分析小工具:
<!-- JOL依賴 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
- 物件頭包括兩部分資訊
-
第一部分用于存盤物件自身的運行時資料(官方稱為Mark Word):哈希碼、GC分代年齡、鎖狀態標志等
-
32位的HotSpot虛擬機中,如物件未被同步鎖鎖定的狀態下,Mark Word的有25bit用于存盤物件的hashCode,4個位元用于存盤物件分代年齡,2個位元用于存盤鎖標志位,1個位元固定為0,在其他狀態下鎖標志位的狀態如下:


- 64位的HotSpot虛擬機中,Mark Word的存盤情況如下:

- 64位的HotSpot虛擬機中,Mark Word的存盤情況如下:
-
-
另一部分是型別指標,即物件指向它的類元資料的指標,虛擬機通過這個指標確定這個物件是哪個類的實體,如果物件是一個Java陣列,那在物件頭中還必須有一塊用于記錄陣列長度的資料,因為虛擬機可以通過Java物件的元資料確定Java物件的大小,如果陣列的長度不確定,將無法通過元資料資訊推斷出陣列的大小,
-
- 實體資料部分是物件真正存盤的有效資訊, 也是程式中所定義的各種型別的欄位內容,無論是從父類繼承下來的,還是在子類中定義的欄位都必須記錄下來,這部分的存盤順序會受到虛擬機分配策略引數(-XX:FieldsAllocationStype引數)和欄位在Java原始碼中定義的順序影響,HotSpot默認的分配順序為longs/doules、ints、shorts/chars,bytes/booleans、oops(Ordinary Objects Poninters,OOPs),相同大小的欄位總是被分配到一起存盤,在滿足這個條件的情況下, 父類中定義的變數會出現在子類之前,如果使用
-XX:CompactFields=true(默認為true),那子類中較小的變數也允許插入父類變數的空隙中,以省出一點點空間, - 對齊填充部分不是天然存在的,也沒有什么特別的含義,僅僅起占位作用, 因為Hotspot的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,換句話說就是物件的大小必須是8位元組的整數倍,而物件頭部分正好是8位元組的倍數(1倍或者2倍),因此,當物件的實體資料部分沒有對齊的時候,就需要通過對齊填充來補全,,
物件的訪問定位
建立物件就是為了使用物件,我們的Java程式通過堆疊上的reference資料來操作堆上的具體物件,物件的訪問方式由虛擬機實作而定,主流的訪問方式有兩種:
- 句柄:如果使用的句柄的方式,那么虛擬機的堆記憶體中還會分出一塊用作句柄池,Java虛擬機堆疊的本地變數表中reference中存盤的就是物件的句柄地址,而句柄中包含了物件實體資料與型別資料各自的具體地址資訊;

- 直接指標(HotSpot使用的):如果使用直接指標訪問,那么Java堆物件的布局中就必須考慮如果放置訪問型別資料的相關資訊,而reference中存盤的直接就是物件堆中的地址

這兩種訪問物件的方式各有優勢,使用句柄來訪問的最大的好處是reference中存盤的是穩定的句柄地址,在物件移動的時候只改變句柄中的實體資料指標,而reference本身不需要修改,使用直接指標訪問方式最大的好處就是訪問速度快,它節省了一次指標定位的時間開銷,
?
重點:String類和常量池
String創建物件的兩種方式
String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);//false
這兩種創建方式是不同的,第一種是直接從常量池中把"abcd"的地址拿出來了;第二種通過new創建,會在堆中創建物件實體“abcd”,因此str1和str2中持有的分別是常量池的地址和堆的地址,false,
String型別的常量池比較特殊,使用方式:
- 直接使用雙引號宣告出來的String物件會直接存盤在常量池中,
- 如果不使用雙引號宣告的String物件,可以使用String提供的intern()方法,String.intern()是一個Native方法,作用是:如果常量池中已經包含一個等于此String物件內容的字串,則回傳常量池中該字串的參考;如果沒有,則在常量池中創建于此String內容相同的字串,并回傳常量池中創建的字串的參考,
String s1 = new String("計算機");
String s2 = s1.intern();//s2持有的是常量池中字串的地址
String s3 = "計算機";
System.out.println(s2);//計算機
System.out.println(s1 == s2);//false,因為一個是堆記憶體中的String物件一個是常量池中的String物件,
System.out.println(s3 == s2);//true,因為兩個都是常量池中的String物件
字串拼接
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的物件
String str4 = str1 + str2; //在堆上創建的新的物件
String str5 = "string";//常量池中的物件
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
盡量避免使用拼接字串,因為這樣會創建多個物件,如果需要改變字串的話,可以使用Stringbuilder或者Stringbuffer,
String s = new String("a")創建了幾個物件
創建了兩個物件,
驗證:
String s1 = new String("abc");// 堆記憶體的地址值
String s2 = "abc";
System.out.println(s1 == s2);// 輸出false,因為一個是堆記憶體,一個是常量池的記憶體,故兩者是不同的,
System.out.println(s1.equals(s2));// 輸出true
解釋:先有字串“abc”放入常量池,然后new了一個字串“abc”放入Java堆(字串常量“abc”在編譯期就已經確定放入常量池,而Java堆上的“abc”是在運行期初始化階段才確定),然后Java虛擬機堆疊的s1指向Java堆上的“abc”,
String#intern
- JDK6中,intern方法會把首次遇到的字串實體復制到永久代的字串常量池中存盤,回傳的也是永久代里面這個字串參考,
- JDK7中,intern方法實作就不需要再拷貝字串的實體到永久代了,既然字串常量池已經移到Java堆中,那只需要在常量池中記錄一下首次出現實體的參考即可,
下面這段代碼在JDK6、7中的執行結果不同
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String str1 = new StringBuilder("計算機").append("軟體").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
JDK6:false false
str1是虛擬機堆疊區域變數參考,str1.intern()持有的是"計算機軟體"在永久代的參考,
str2持有的是虛擬機堆疊區域變數參考,
JDK7:true false
str
8種基本資料型別的包裝類和常量池
- Java基本資料型別的包裝類的大部分都實作了常量池技術,即Byte、Short、Long、Character、Boolean;這5種包裝類默認創建了數值[-128,127]的相應型別的快取資料,但是超出此范圍仍然會去創建新的物件,
- 兩種浮點數型別的包裝類Float、Double并沒有實作常量池技術,
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 輸出true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 輸出false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 輸出false
Integer快取源代碼:
/**
*此方法將始終快取-128到127(包括端點)范圍內的值,并可以快取此范圍之外的其他值,
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
應用場景:
1、Integer i1 = 40;Java在編譯的時候會直接將代碼封裝成Integer i1 = Integer.valueOf(40),從而使用常量池中的物件
2、Integer i1 =new Integer(40);這種情況下會創建新的物件;
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);//輸出false
Integer比較更豐富的一個例子:
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));
System.out.println("i1=i4 " + (i1 == i4));
System.out.println("i4=i5 " + (i4 == i5));
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));
System.out.println("40=i5+i6 " + (40 == i5 + i6));
結果:
i1=i2 true
i1=i2+i3 true
i1=i4 false
i4=i5 false
i4=i5+i6 true
40=i5+i6 true
解釋:
陳述句i4==i5 + i6,因為+這個運算子不適用于Integer物件,首先i5和i6進行自動拆箱操作,進行數值相加,即i4 == 40,然后Integer物件無法與數值進行直接比較,所以i4自動拆箱轉為int值40,最終這條陳述句轉為40 == 40進行數值比較,
本文由博客一文多發平臺 OpenWrite 發布!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/327755.html
標籤:Java
