標籤:
JIT編譯器,英文寫作Just-In-Time Compiler,中文意思是即時編譯器
類欄位不需要初始化的原因:
記憶體配置完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括對象頭),如果使用TLAB,這一工作過程也可以提前至TLAB分配時進行。這一步操作保證了對象的執行個體欄位在Java代碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料類型所對應的零值。
虛擬機器產生java對象的過程:
1)首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、解析和初始化過。如果沒有,那必須先執行相應的類載入過程
2)在類載入檢查通過後,接下來虛擬機器將為新生對象分配記憶體(指標碰撞、空閑列表)(由java堆是否規整決定)
(一種是對分配記憶體空間的動作進行同步處理——實際上虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性;另一種是把記憶體配置的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊記憶體,稱為本地線程分配緩衝(Thread Local Allocation Buffer,TLAB))
3)記憶體配置完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括對象頭)
4)接下來,虛擬機器要對對象進行必要的設定,例如這個對象是哪個類的執行個體、如何才能找到類的中繼資料資訊、對象的雜湊碼、對象的GC分代年齡等資訊。這些資訊存放在對象的對象頭(Object Header)之中。
5)在上面工作都完成之後,從虛擬機器的視角來看,一個新的對象已經產生了,但從Java程式的視角來看,對象建立才剛剛開始——<init>方法還沒有執行,所有的欄位都還為零。所以,一般來說(由位元組碼中是否跟隨invokespecial指令所決定),執行new指令之後會接著執行<init>方法,把對象按照程式員的意願進行初始化,這樣一個真正可用的對象才算完全產生出來
虛擬機器中對象的記憶體布局:
對象頭、執行個體資料、對齊填充
HotSpot虛擬機器的對象頭包括兩部分資訊,第一部分用於儲存物件自身的運行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳記等;對象頭的另外一部分是類型指標,即對象指向它的類別中繼資料的指標,虛擬機器通過這個指標來確定這個對象是哪個類的執行個體。(並不是所有的虛擬機器實現都必須在對象資料上保留類型指標,換句話說,尋找對象的中繼資料資訊並不一定要經過對象本身)。
執行個體資料部分是對象真正儲存的有效資訊,也是在程式碼中所定義的各種類型的欄位內容(HotSpot虛擬機器預設的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中可以看出,相同寬度的欄位總是被分配到一起。)
第三部分對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起著預留位置的作用(由於HotSpot VM的自動記憶體管理系統要求對象起始地址必須是8位元組的整數倍,換句話說,就是對象的大小必須是8位元組的整數倍。而對象頭部分正好是8位元組的倍數(1倍或者2倍),因此,當對象執行個體資料部分沒有對齊時,就需要通過對齊填充來補全。)
對象的訪問定位:
Java程式需要通過棧上的reference資料來操作堆上的具體對象
目前主流的訪問方式有使用控制代碼和直接指標兩種:
使用控制代碼訪問的話,那麼Java堆中將會劃分出一塊記憶體來作為控制代碼池,reference中儲存的就是對象的控制代碼地址,而控制代碼中包含了對象執行個體資料與類型資料各自的具體地址資訊
使用直接指標訪問,那麼Java堆對象的布局中就必須考慮如何放置訪問類型資料的相關資訊,而reference中儲存的直接就是對象地址
使用控制代碼來訪問的最大好處就是reference中儲存的是穩定的控制代碼地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變控制代碼中的執行個體資料指標,而reference本身不需要修改;使用直接指標訪問方式的最大好處就是速度更快,它節省了一次指標定位的時間開銷,由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。
Java堆溢出:
Java堆用於儲存物件執行個體,只要不斷地建立對象,並且保證GC Roots到對象之間有可達路徑來避免記憶體回收機制清除這些對象,那麼在對象數量到達最大堆的容量限制後就會產生記憶體溢出異常。
重點是確認記憶體中的對象是否是必要的,就是要先分清楚到底是出現了記憶體流失還是記憶體溢出。
記憶體溢出:代碼限制Java堆的大小為20MB,不可擴充(將堆的最小值-Xms參數與最大值-Xmx參數設定為一樣即可避免堆自動擴充),通過參數-XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機器在出現記憶體溢出異常時Dump出當前的記憶體堆
轉儲快照以便事後進行分析[1]
記憶體泄露:泄露對象是通過路徑與GC Roots相關聯並導致垃圾收集器無法自動回收它們的(常說的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的對象,GC會收集那些不是GC roots且沒有被GC roots引用的對象。)
如果不存在泄露,換句話說,就是記憶體中的對象確實都還必須存活著,那就應當檢查虛擬機器的堆參數(-Xmx與-Xms),與機器實體記憶體對比看是否還可以調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的情況,嘗試減少程式運行期的記憶體消耗。
虛擬機器棧和本地方法棧溢出:
如果線程請求的棧深度大於虛擬機器所允許的最大深度,將拋出StackOverflowError異常。如果虛擬機器在擴充棧時無法申請到足夠的記憶體空間,則拋出OutOfMemoryError異常。(這裡把異常分成兩種情況,看似更加嚴謹,但卻存在著一些互相重疊的地方:當棧空間無法繼續分配時,到底是記憶體太小,還是已使用的棧空間太大,其本質上只是對同一件事情的兩種描述而已)
增強類(或動態類)越多,就需要越大的方法區來保證動態產生的Class可以載入入記憶體。JVM上的動態語言(例如Groovy等,java反射)通常都會持續建立類來實現語言的動態性,隨著這類語言的流行,也越來越容易遇到方法區記憶體溢出。
本機直接記憶體溢出:
DirectMemory容量可通過-XX:MaxDirectMemorySize指定,如果不指定,則預設與Java堆最大值(-Xmx指定)一樣
雖然使用DirectByteBuffer分配記憶體也會拋出記憶體溢出異常,但它拋出異常時並沒有真正向作業系統申請分配記憶體,而是通過計算得知記憶體無法分配,於是手動拋出異常,真正申請分配記憶體的方法是unsafe.allocateMemory()。
所有和GC Roots相關聯的對象都不能被GC回收;
可以作為GC Roots 的對象有以下幾種:
虛擬機器棧(棧幀中的本地變數表)中引用的對象;
方法區中類靜態屬性引用的對象;
方法區中常量引用的對象;
本地方法棧中JNI(即一般說的Native方法)引用的對象;
Java的參考型別:
引用分為強引用(StrongReference)、軟引用(SoftReference)、弱引用(Weak Reference)、虛引用(PhantomReference)4種,這4種引用強度依次逐漸減弱。
編譯期三種編譯器:
前端編譯器:Sun的Javac、Eclipse JDT中的增量式編譯器(ECJ)[1]。(把*.java檔案轉變成*.class檔案的過程)
JIT編譯器:HotSpot VM的C1、C2編譯器。(把位元組碼轉變成機器碼的過程)
AOT編譯器:GNU Compiler for the Java(GCJ)[2]、Excelsior JET[3]。(直接把*.java檔案編譯成本地機器代碼的過程)
從Sun Javac的代碼來看,編譯過程大致可以分為3個過程,分別是:
解析與填充符號表過程。
插入式註解處理器的註解處理過程。
分析與位元組碼產生過程。
深入理解java虛擬機器讀後感(一)