借用一句話:Java與C++之間有一堵內存動態分配和垃圾收集技術圍成的高牆,牆外面的人想進來,牆里面的人卻想出去。
一.我們為什么要了解JAVA內存
因為虛擬機幫我們JAVA程序員管理着內存,我們在new Object()申請了內存創建對象之后,便不需要再去delete/free來釋放內存。也因此不容易出現內存泄漏和內存溢出的問題,看起來一切都很美好。
但是,如果一個程序員不了解虛擬機是怎么管理內存的,那么在排查內存相關的錯誤是便會成為一個巨大的難題。
二.內存區域有哪些
內存區域分為兩種,一種隨着虛擬機的進程啟動而存在。另一種則依賴用戶進程的啟動和結束而建立和銷毀。
1.程序計數器
一塊較小的線程私有的內存空間,可以看作是當前線程的所執行的字節碼的行號指示器。
如果線程正在執行的是一個JAVA方法,那么計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是native方法,那么計數器值為空(Undefined)。
該內存區域是唯一一個在JAVA虛擬機規范中沒有規定任何OutOfMemoryError(OOM)情況的區域。
2.虛擬機棧
線程私有的,每個Java方法在執行時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用到完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
3.本地方法棧
線程私有,同虛擬機棧,為native方法服務。在HotSpot虛擬機中,直接把虛擬機棧和本地方法棧合二為一。
4.堆
線程共享的區域。存放實例的區域,幾乎所有的對象實例都在這里分配內存。同時,因為空間固定,而用戶可能需要不斷生成實例,故該區域還是垃圾收集的主要區域。垃圾收集將在后面提到。
Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可。
5.方法區
線程共享的區域,存儲已被虛擬機加載的類信息、常量、靜態變量等數據。
很多人稱之為“永生代”,因為HotSpot使用永生代來實現方法區。Java規范中對方法區的限制十分寬松,可以選擇不實現垃圾收集。
6.運行時常量池
方法區的一部分,用於存放編譯器生成的各種字面量和符號引用,在類加載完成后進入方法區的運行時常量池中存放。
關於這快區域,有一個需要注意的地方。代碼如下:
public class t18 { public static void main(String[] args){ Integer a1 = 128 ; Integer a2 = 128 ; System.out.println(a1==a2); Integer b1 = 127; Integer b2 = 127; Integer b3 = 1 + b1; Integer b4 = a1 -1 ; System.out.println(b1==b2); System.out.println(b3==a1); System.out.println(b4 == b1); } }
上面代碼的運行結果為 false ,true ,false ,true 。這是很多人第一次見到時都無法理解的,因為這里涉及到了常量池的知識。JVM會把一些int,String等數據進行在常量池中緩存,但是重點在於,對於int型數據,只會緩存 -128~127 范圍內的數據。因此:
a1、a2超過了127,在堆中分配內存,兩者指向不同對象,返回false;
b1、b2都指向常量池中的127,故b1、b2指向地址相同,返回true;
第3、4個同理,Integer b3 = 1+b1 ----> Integer b3 =Integer.valueOf(1+b1)。
7.直接內存
JDK1.4后加入了NIO (new I/O)類,引入了基於通道與緩沖區的IO方式,可以使用native函數庫分配機器內存,如電腦8g內存,JVM可以使用電腦的剩余內存,只需要在java堆中存儲DirectByteBuffer對象作為內存的引用進行操作。這樣在某些場景中提高性能。
三.在new一個對象時發生了什么
- 當虛擬機遇到一條new 指令時,首先回去檢查能否在常量池中定位,並檢查這個類是否已經被加載、解析、初始化過,如果沒有,那么必須先執行類的加載過程。
- 類加載完成后,接下來將會為對象分配內存,即把一塊確定大小的內存從java堆中划分出來。如果java堆是連續且規整的,已分配過的內存放在一邊,空閑的在另一邊。中間的指針作為分界點的指示器,那么分配內存就是將指針向空閑的方向移動所需要的距離,(使用Serial、PalNew等帶規整過程的垃圾收集器);如果java堆是不規整的,那么虛擬機就必須維護一個記錄,分配內存的同時需要更新記錄,(如使用CMS這種基於標記-清除算法的收集器)。
- 將分配到的內存空間賦予初值,如整形變量置0,bool型置false。保證了對象字段在代碼中可以不付初值就可以直接使用。然而在實際編寫代碼中,建議采用賦初值的形式,保持一個良好的代碼習慣。另外
String s ; System.out.println(s); //未初始化,編譯器報錯
該代碼會報錯,而不是輸出null,切記切記。
- 初始化對象的對象頭數據,每個java對象都有對象頭(Object Header),里面記錄了對象是哪個類的實例、如何找到類的元數據信息、哈希碼、GC年齡、偏向鎖等信息。
- 執行init方法,把對象按照程序員的一員進行初始化,這樣,一個可用的對象才完成new操作。
四.一個對象在內存中有哪些部分
以HotSpot虛擬機為例,對象在內存中存儲的區域可以分為三個部分
1.對象頭(Object Header)
對象頭包括兩部分,一部分用於存儲對象自身的運行時數據,官方稱之為“Mark Word”,包括:HashCode、GC年齡、鎖狀態、線程持有鎖、偏向鎖線程id、偏向時間戳等。占一個字長(32bit或64bit,取決於虛擬機)。
另一部分是類型指針,對象指向的類元數據指針,通過這個來確定該對象是哪個類的實例。另外如果一個對象是一個數組,那么還有一塊用於記錄數組長度的數據。
2.對象數據
即實例中存儲的,程序員設計的應該存儲的數據。
3.對齊填充
不是必須的,僅僅起着占位的作用,HotSpot內存管理規定對象的起始地址必須是8字節的整數倍,換句話說對象的大小必須是9字節的整數倍,因此,當實例大小沒有對齊時,需要通過對齊填充來補全。
五.如何訪問定位對象
創建對象是為了使用對象,java虛擬機使用棧上的reference數據來操作堆上的具體對象,目前的訪問方式主流有兩種:
1.使用句柄訪問
Java堆中會划分出一塊內存作為句柄池,reference中存儲的是對象的句柄地址,而句柄中包含了對象的實例數據與類型數據各自的地址信息。
即訪問時refenrence(存句柄地址) --> 句柄池(堆中,存對象地址) --> 具體對象(堆或方法區中)。
2.使用直接指針訪問
直接訪問,reference(存對象地址)-->具體對象(堆中或方法區中),一次跳轉。HotSpot虛擬機使用的就是這種方式。