浮点数例外 (核心已转储)_15000 字梳理 JVM 的核心知识
前言
隨著cpu運行速度的提高和內存的增大,我們的應用程序的用戶響應時間和系統吞吐量也發生了質的提高。但是只有硬件設備的提高是不行的,軟件的性能和運行在硬件上的虛擬機的各項參數都影響著系統的質量。在越來越多的大廠面試中,jvm逐漸成為面試官青睞的考點。
本文講解了運行時數據區域,內存溢出,如何判斷對象是否存活,垃圾回收算法和垃圾收集器,類加載機制和雙親委派模型以及對象的創建存儲和訪問幾個方面,涵蓋jvm的核心考點,希望你有所收獲。
運行時數據區域
java虛擬機運行時有哪些數據區域,他們都有什么用途?
有程序計數器、java虛擬機棧、本地方法棧、堆和方法區五大模塊。請看下圖:
程序計數器
程序計數器是一塊較小的內存空間,他可以看做是當期線程所執行的字節碼的行號指令器。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。在任何一個確定的時刻,一個處理器都只會執行一條線程中的指令。因此,為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各個線程之間互不影響,獨立存儲,所以程序計數器是“線程私有的”。另外,程序計數器是唯一一個在java虛擬機規范中沒有規定OOM的區域。
Java虛擬機棧
Java虛擬機棧也是線程私有的,它的生命周期與線程相同,虛擬機棧描述的是Java方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法從調用直至執行完成的過程,對應著一個棧幀在虛擬機棧中入棧到出棧的過程。(程序員經常會把Java內存劃分為堆內存和棧內存,這種說法比較粗糙,其中的棧內存就是指虛擬機棧,或者說是虛擬機棧中的局部變量表的部分)
在Java虛擬機規范中,對這個區域規定了兩種異常:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverFlowError異常。如果虛擬機??梢詣討B擴展,如果擴展時無法申請到足夠的內存,就會拋出OOM異常。
本地方法棧
本地方法棧與虛擬機棧作用類似,他們之間的區別不過是虛擬機棧是為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。與虛擬機棧一樣,本地方法棧也會拋出StackOverFlowError異常和OOM異常。
堆
Java堆是Java虛擬機管理的最大的一塊內存,是所有線程共享的區域,在虛擬機啟動時就創建。堆用來存放對象實例,幾乎所有的對象實例都在這里分配內存(注意是幾乎所有)。這一點在Java虛擬機規范中描述為:所有的對象實例以及數組都要在堆上分配,但隨著JIT編譯器的發展和逃逸分析技術的成熟,棧上分配、標量替換技術將會導致一些微妙的變化發生,所有對象都分配在堆上也不是那么絕對了。
如果在堆中沒有內存完成實例分配,并且堆也無法再擴展時,將會拋出OOM異常。
方法區
方法區與Java堆一樣,是各個線程共享的區域,它用來存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。根據Java虛擬機規范,當方法區無法滿足內存分配的 需求時,將拋出OOM異常。
運行時常量池是方法區的一部分,用于存放編譯器生成的各種字面量和符號引用。運行時常量池相對于Class文件常量池的另外一個重要特征就是具備動態性。也就是說運行期間也可能將新的常量 放入池中,這種特性被利用的比較多的就是String類的intern()方法。
直接內存
直接內存并不是運行時數據區的一部分,也不是Java虛擬機定義的內存區域。本機直接內存的分配不受Java堆大小的限制,但是受本機總內存大小以及處理器尋址空間的限制。
內存溢出
堆內存溢出
Java堆用于存儲對象實例,只要不斷創建對象,并且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那么在對象數量達到最大堆容量限制后就會OOM。(輕易不要運行)
public class HeapOOMTest {static class OOMObject {}public static void main(String [] args) {List<OOMObject> list = new ArrayList<>();while (true) {list.add(new OOMObject());}} }Java堆內存的OOM異常是實際應用中最常見的內存溢出,當出現了咋辦?一般的手段是先通過內存映像分析工具對Dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是確認是內存泄露還是內存溢出。
如果是內存泄露,可進一步通過工具查看泄露對象到GCRoots的引用鏈,找到為什么垃圾收集器無法回收它們。如果不存在泄露,就是內存中的對象必須都存活,那就要檢查虛擬機的堆內存是否可以調大,從代碼上檢查是否某些對象生命周期過長,減少內存消耗,優化代碼。
虛擬機棧溢出
關于虛擬機棧,在java虛擬機規范中描述了兩種異常:
(1)如果線程請求的棧深度大于虛擬機所允許的最大深度,將拋出StackOverFlowError異常。
(2)如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOdMemoryError異常。
這里描述的兩種情況實際上有些重疊:當棧空間無法繼續分配的時候,到底是內存太小導致的,還是已使用的??臻g太大,本質上是一樣的。
public class StackSOFTest {private int stackLength = -1;public void stackLeak() {stackLength++;stackLeak();}public static void main(String [] args) throws Throwable {StackSOFTest oom = new StackSOFTest();try {oom.stackLeak();} catch (Throwable e) {System.out.println("stack length:"+oom.stackLength);throw e;}} }運行結果:
stack length:13980 Exception in thread "main" java.lang.StackOverflowErrorat oom.StackSOFTest.stackLeak(StackSOFTest.java:14)at oom.StackSOFTest.stackLeak(StackSOFTest.java:14)at oom.StackSOFTest.stackLeak(StackSOFTest.java:14)...后續省略實驗結果表明:在單個線程下,無論是由于棧幀太大還是虛擬機棧容量太小,當內存無法分配時,虛擬機拋出的都是StackOverflowError異常。
對象是“生”是“死”
對象的四種引用
引用分為強引用,軟引用,弱引用和虛引用四種,這四種引用強度依次逐漸減弱。
1、強引用就是指在程序代碼中普遍存在的,是指創建一個對象并把這個對象賦給一個引用變量,類似Object obj = new Object()這類的引用,只要強引用還存在,垃圾收集器就永遠不會回收被引用的對象。如果想中斷強引用和某個對象之間的關聯,可以顯示的將引用賦值為null,這樣jvm在合適的時間就會回收該對象。
2、軟引用是用來描述一些還有用但并非必需的對象。對于軟引用關聯著的對象,在系統將會發生內存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。SoftReference的特點是它的一個實例保存對一個Java對象的軟引用,該軟引用的存在不妨礙垃圾收集器線程對該Java對象的回收。
3、弱引用也是用來描述非必需對象的。當JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。在java中,用java.lang.ref.WeakReference類來表示。
4、虛引用和前面的軟引用和弱引用不同,它并不影響對象的生命周期。在java中使用PhantomReference類來表示。如果一個對象與虛引用關聯,跟沒有引用與之關聯一樣,任何時候都可能被回收。要注意的是,虛引用必須和引用隊列關聯使用。當垃圾收集器準備回收一個對象時,如果發現它還有虛引用,就會把這個虛引用加入到與之關聯的引用隊列中。為對象設置虛引用的唯一目的就是能在這個對象被垃圾收集器回收時收到一個系統通知。
引用計數法的缺陷
引用計數法就是給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1.當引用失效時,計數器的值就減一。任何時刻計數器值為0的對象就是不可能再被使用的。
優缺點:實現簡單,判斷效率高,大部分情況下是個很不錯的算法。但是致命問題是沒辦法解決對象之間相互循環引用的問題。
觀察GC日志可以看出GC發生了內存回收,意味著虛擬機并沒有因為這兩個對象相互引用就不回收它們,這也從側面說明虛擬機并沒有采用引用計數法來判斷對象是否存活。
可達性分析
這個算法的基本思想是通過一系列被稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索走過的路徑叫做引用鏈,當一個對象到GC Roots沒有任何引用鏈相連 (用圖論的話來說就是從GC Roots到這個對象不可達),則證明此對象是不可用的。
在 Java語言中,可作為GC Roots的對象包括以下幾種
(1)虛擬機棧(棧幀中的本地變量表)中引用的對象。
(2)方法區中類靜態屬性引用的對象。
(3)方法區中常量引用的對象。
(4)本地方法棧中JNI(即一般說的Native方法)引用的對象。
對象是生存還是死亡?
即使在可達性分析法中不可達的對象,也并非“非死不可”,他們還有拯救自己的機會。要宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析后沒有與GC Roots的引用鏈,那么它將會被第一次標記,并且此時需要判斷是否有必要執行finalize()方法。沒有必要的話,那么這個對象就宣告死亡,可以回收了。
如果有必要執行,那么這個對象會被放置在一個叫做F-Queue的隊列中,并在稍后由虛擬機自動建立的低優先級的Finalizer線程去執行它。finalize()是對象拯救自己的最后一次機會-只要重新與引用鏈上的 任何一個對象建立關聯即可(譬如把自己賦值給某個類變量或者對象的成員變量),那么在第二次標記時它將被移除“可回收”的集合,如果對象還沒有逃脫,基本上就真的被回收了。
具體的過程見下圖:
垃圾收集算法
標記清除算法
標記-清除算法是最基礎的算法,分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收。
它的主要不足有兩個:一是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清楚之后會產生大量不連續的內存碎片。
標記-清除算法的執行過程見下圖:
復制算法
為了解決效率問題,“復制”算法出現了。它將內存空間劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的內存用完了,就將還存活的對象復制到另外一塊上,然后再把已使用的的內存空間一次性清理掉。
這樣每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。但是這種算法的代碼是將內存空間縮小為原來的一半。
復制算法的執行過程見下圖:
標記-整理算法
標記過程仍然與標記-清除算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉邊界以外的內存。
標記-整理算法的執行過程見下圖:
分代收集算法
當前商業虛擬機的垃圾收集都采用“分代收集”的算法,這種算法只是根據對象存活周期的不同將內存劃分為幾塊,一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。
在新生代中,每次垃圾收集都發現有大批對象死去,只有少量存活,那就選用“復制算法“,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高,沒有額外空間對它進行分配擔保,就必須使用”標記-清理“算法或者”標記-整理“算法。
垃圾收集器
堆內存是垃圾收集器主要回收垃圾對象的地方,堆內存可以根據對象生命周期的不同分為新生代和老年代,分代收集,新生代使用復制算法,老年代使用標記清除或者標記整理算法。
HotSpot虛擬機提供了7中垃圾收集器,其中新生代三種:Serial/ParNew/Parallel Scavenge收集器,老年代三種:Serial Old/Parallel Old/CMS,都適用的是G1收集器。所有垃圾收集器組合 情況如下圖:
Serial收集器
最基本也是發展歷史最長的垃圾收集器,在進行垃圾收集時,必須Stop The World(暫停其他工作線程),直到收集結束。只使用一條線程完成垃圾收集,但是效率高,因為沒有線程交互的開銷,擁有更高的單線程收集效率。發生在新生代區域,使用復制算法。
ParNew收集器
Serial收集器的多線程版本。在進行垃圾收集時同樣需要Stop The World(暫停其他工作線程),直到收集結束。使用多條線程進行垃圾收集(由于存在線程交互的開銷,所以在單CPU的環境下,性能差于Serial收集器)。目前,只有Parnew收集器能與CMS收集器配合工作。發生在新生代區域,使用復制算法。
Parallel Scavenge收集器
ParNew收集器的升級版,具備ParNew收集器并發多線程收集的特點,以達到可控制吞吐量為目標。(吞吐量:CPU用于運行用戶代碼的時間與CPU總消耗時間(運行用戶代碼時間+垃圾收集時間)的比值)。該垃圾收集器能根據當前系統運行情況,動態調整自身參數,從而達到最大吞吐量的目標。(該特性成為GC自適應的調節策略)。發生在新生代,使用復制算法。
Serial Old收集器
Serial 收集器應用在老年代的版本。并發、單線程、效率高。使用標記整理算法。
Parallel Old收集器
是Parallel Scavenge應用在老年代的版本,以達到可控制吞吐量、自適應調節和多線程收集為目標,使用標記整理算法。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。使用這類收集器的應用重視服務的響應速度,希望系統停頓時間最短,以帶來更好的用戶體驗。
使用標記清除算法,一共四個步驟:初始標記、并發標記、重新標記和并發清除。詳情見下表:
下面說下CMS的優缺點:
優點:(1)并行:用戶線程和垃圾收集線程同時進行。
(2)單線程收集:只使用一條線程完成垃圾收集。
(3)垃圾收集停頓時間短:獲取最短的回收停頓時間,即希望系統停頓的時間最短,提高響應速度。
缺點:
(1)總吞吐量會降低:因為該收集器對CPU資源非常敏感,在并發階段不會導致停頓用戶線程,但會因為占用部分線程(CPU資源)導致應用程序變慢,總吞吐量會降低。
(2)無法處理浮動垃圾:由于并發清理時用戶線程還在運行,所以會有新的垃圾不斷產生,只能等到下一次GC時再清理。(因為這一部分垃圾出現在標記過程之后,所以CMS 無法在當次GC中處理他們,因此CMS無法等到老年代填滿再進行Full GC,CMS需要預留一部分空間)。
(3)垃圾收集后會產生大量的內存碎片:因為CMS收集器是使用標記-清除算法的。
下面一張圖了解下CMS的工作過程:
G1收集器
G1收集器是最新最前沿的垃圾收集器。特點如下:
(1)并行:用戶線程和垃圾收集線程同時進行。
(2)多線程:即使用多條垃圾收集線程進行垃圾回收。(并發和并行充分利用多CPU和多核環境的硬件優勢來縮短垃圾收集的停頓時間)
(3)垃圾收集效率高:G1收集器是針對性對Java堆內存區域進行垃圾收集,而非每次都對整個區域進行收集。即G1除了將Java堆內存分為新生代和老年代之外,還會細分為許多個 大小相等的獨立區域(Region),然后G1收集器會跟蹤每個Region里的垃圾代價值大小,并在后臺維護一個列表。每次回收時,會根據允許的垃圾收集時間優先回收價值最大的 Region,從而避免了對整個Java堆內存區域的回收,提高了效率。因為上述機制,G1收集器還能建立可預測的時間模型:即讓使用者明確執行一個長度為M毫秒的時間片段,消耗在 垃圾收集上的時間不得超出N毫秒。即具備實時性。
(4)不會產生內存碎片。從整理上看,G1收集器是基于標記-整理算法的,從局部看是基于復制算法的。在新生代使用復制算法,在老年代使用標記-整理算法。
下面了解下工作流程,跟CMS有點像。
下面是G1的工作過程:
類加載過程
在java中編譯并不進行鏈接工作,類型的加載、鏈接和初始化工作都是在jvm執行過程中進行的。在Java程序啟動時,jvm通過加載指定的類,然后調用該類的main方法而啟動。在JVM啟動過程中, 外部class字節碼文件會經過一系列過程轉化為JVM中執行的數據,這一系列過程我們稱為類加載過程。
類加載整體流程
從類被JVM加載到內存開始到卸載出內存為止,整個生命周期包括:加載、鏈接、初始化、使用和卸載五個過程。其中鏈接又包括驗證、準備和解析三個過程。如下圖所示:
類加載時機
java虛擬機規范通過對初始化階段進行嚴格規定,來保證初始化的完成,而作為其之前的必須啟動的過程,加載、驗證、準備也需要在此之前開始。
Java虛擬機規定,以下五種情況必須對類進行初始化:1、虛擬機在用戶指定包含main方法的主類后啟動時,必須先對主類進行初始化。
2、當使用new關鍵字對類進行實例化時、讀取或者寫入類的靜態字段時、調用類的靜態方法時,必須先觸發對該類的實例化。
3、使用反射對類進行反射調用時,如果該類沒有初始化先對其進行初始化。
4、初始化一個類,而該類的父類還未初始化,需要先對其父類進行初始化。
5、在JDK1.7之后的版本中使用動態語言支持,java.lang.invoke.MethodHandle實例解析的結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而該句柄對應的類 還未初始化,必須先觸發其實例化。
加載
在加載階段,虛擬機需要完成三件事:
1、通過一個類的全限定名來獲取此類的class字節碼二進制流。
2.將這個字節碼二進制流中的靜態存儲結構轉化為方法區中的運行時數據結構。
3、在內存中生成一個代表該類的java.lang.Class對象,作為方法區中這個類的各種數據的訪問入口。
對于Class對象,Java虛擬機規范并沒有規定要存儲在堆中,HotSpot虛擬機將其存放在方法區中。
驗證
驗證作為鏈接的第一步,大致會完成四個階段的檢驗:
1、文件格式驗證:該階段主要在字節流轉化為方法區中的運行時數據時,負責檢查字節流是否符合Class文件規范,保證其可以正確的被解析并存儲在方法區中。后面的檢查都是基于方法區的 存儲結構進行檢驗,不會再直接操作字節流。
2、元數據驗證:該階段負責分析存儲于方法區的結構是否符合Java語言規范。此階段進行數據類型的校驗,保證符合不存在非法的元數據信息。
3、字節碼驗證:元數據驗證保證了字節碼中的數據符合語言的規范,該階段則負責分析數據流和控制流,確定方法體的合法性,保證被校驗的方法在運行時不會危害虛擬機的運行。
4、符號引用驗證:在解析階段會將虛擬機中的符號引用轉化為直接引用,該階段則負責對各種符號引用進行匹配性校驗,保證外部依賴真實存在,并且符合外部依賴類、字段、方法的訪問性。
準備
準備階段正式為類的字段變量(被static修飾的類變量)分配內存并設置初始值。這些變量存儲在方法區中。當類字段為常量類型(即被static final修飾),由于字段的值已經確定,并不會在后面修改,此時會直接賦值為指定的值。
解析
解析階段將常量池中的符號引用替換為直接引用。在字節碼文件中,類、接口、字段、方法等類型都是由一組符號來表示。其形式由java虛擬機規范中的Class文件格式定義。在虛擬機執行 指定指令之前,需要將符號引用轉化為目標的指針、相對偏移量或者句柄,這樣可以通過此類直接引用在內存中定位調用的具體位置。
初始化
在類的class文件中。包含兩個特殊的方法:clinit和init,這兩方法由編譯器自動生成,分別代表類構造器和構造函數,其中構造函數編程實現,初始化階段就是負責調用類構造器,來初始化 變量和資源。
clinit方法由編譯器自動收集類的賦值動作和靜態語句塊(static)中的語句合并生成的,有以下特點:
1、編譯器收集順序又代碼順序決定,靜態語句塊只能訪問它之前定義的變量,在它之后定義的變量只能進行賦值不能訪問。
2、虛擬機保證在子類的clinit方法執行前,父類的clinit已經執行完畢。
3、clinit不是必須的,如果一個類或接口沒有變量賦值和靜態代碼塊,則編譯器可以不生成clinit。
4、虛擬機會保證clinit方法在多線程中被正確的加鎖和同步。如果多個線程同時初始化一個類,那么只有一個線程執行clinit,其他線程會被阻塞。
雙親委派模型
類加載器
1、定義:實現類加載階段的“通過一個里的全限定名來獲取描述此類的二進制字節流”的動作的代碼模塊成為“類加載器”。對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。比較兩個類是否“相等”,只有在這兩個類是同一個類加載器加載的前提下才有意義。
2、類加載器種類
從Java虛擬機的角度只有兩種類加載器:
(1)啟動類加載器(BootStrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分。
(2)另一種就是所有其他類的加載器,這些類加載器都是由Java語言實現,獨立于虛擬機外部,并且都繼承自抽象類java.lang.ClassLoader。
從Java開發人員的角度,類加載器還可分為3種系統提供的類加載器和用戶自定義的類加載器。
(1)啟動類加載器(BootStrap ClassLoader):負責加載存放java_homelib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的類。
(2)擴展類加載器(Extension ClassLoader):這個加載器sun.misc.LauncherExtClassLoader實現,它負責加載javahomelibext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
(3)應用程序類加載器(ApplicationClassLoader):這個類加載器由sun.misc.LauncherExtClassLoader實現,它負責加載java_homelibext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。 如果應用程序中沒有自定義的類加載器,一般情況下 這個就是程序中默認的類加載器。
(4)自定義類加載器(User ClassLoader):用戶自定義的類加載器。用戶在編寫自己定義的類加載器時,如果需要把請求委派給引導類加載器,那直接使用numm代替即可。要創建用戶自己 的類加載器,只需要繼承java.lang.ClassLoader,然后覆蓋它的findClass(String name)方法即可。如果要符合雙親委派模型,則重寫findClass()方法。如果要破壞的話,則重寫 loadClass()方法。
雙親委派模型
上圖展示的類加載器之間的這種層次關系稱為類加載器的雙親委派模型。1、雙親委派模型要求除了頂層的啟動類加載器之外,其余的類加載器都應當有自己的父類加載器。
2、類加載器的雙親委派模型在jdk1.2被引入,但它不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類加載器的實現方式。
雙親委派模型的工作過程如下:
1、如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成。
2、每一層的類加載器都重復第一步,因此所有的類加載請求最終都傳送到了頂層的類加載器中。
3、只有父類加載器返回自己無法完成這個加載請求,子加載器才會嘗試自己去加載。
對象的創建、存儲和訪問
對象的創建
1、類加載檢查:虛擬機遇到一條new指令,首先檢查這個指令的參數是否能在常量池中(Class文件的靜態常量池)定位到這個類的符號引用,并且檢查這個符號引用代表的類是否 已經被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
2、分配內存:對象所需內存大小在類加載完成后便可完全確定,為對象分配空間的任務等同于把一塊確定大小的內存從Java堆中劃分出來。但是不同垃圾回收器的算法會導致堆內存存在兩種情況:絕對規整和相互交錯。(比如標記清楚算法和標記整理算法)
(1)指針碰撞:假設Java堆內存是絕對規整的,所有用過的內存都存放在一起,空閑的內存存放在另一邊,中間放著一個指示器作為分界點的指示器,所分配的內存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式成為”指針碰撞“。
(2)空閑列表:如果是相互交錯的,那么虛擬機會維護一個列表,記錄哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃給對象實例,并更新列表上的記錄。這種分配方式成為”空閑列表“。
3、分配內存的并發問題:即使是僅僅修改一個指針所指向的位置,在并發情況下也不是線程安全的,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的 指針來分配內存的情況。針對這個問題有兩種解決方案:
(1)失敗重試:對分配內存空間的動作進行同步處理,虛擬機采用CAS和失敗重試機制保證更新操作的原子性。
(2)本地線程分配緩存:哪個線程要分配內存,就在哪個線程的TLAB(Thread Local Allocation Buffer)上分配,只有TLAB用完并分配新的TLAB時,才需要同步鎖定。
4、內存空間初始化零值:內存分配完成后,虛擬機需要將分配到的內存空間都初始化零值,這一步操作保證了對象的實例字段(成員變量)在Java代碼中可以不賦值就直接使用,程序能夠訪問到這些字段的數據類型所對應的零值。
5、對象設置:接下來虛擬機會對對象進行必要的設置,例如這個對象是哪個類的實例,如何才能找到類的元數據信息、對象的哈希嗎、對象的GC分代年齡等信息。這些信息存放在對象頭中。至此一個新的對象產生了。
6、實例構造器的init方法:雖然對象產生了,但是init方法并沒有執行,所欲字段還需要賦值(包括成員變量賦值,普通語句塊執行,構造函數執行等。)
Clinit和init
Clinit
類構造器的方法,與類的初始化有關。例如靜態變量(類變量)和靜態對象賦值,靜態語句塊的執行。如果一個類中沒有靜態語句塊,也沒有靜態變量或靜態對象的賦值, 那么編譯器可以不為這個類生成方法。
init
實例構造器(即成員變量,成員對象等),例如成員變量和成員對象的賦值,普通語句塊的執行,構造函數的執行。
對象的內存布局
在HotSpot虛擬機中,對象在內存中存儲的布局可以分為三個區域:對象頭、實例數據和對齊填充。
對象頭
對象頭包括兩部分信息:運行時數據和類型指針。
運行時數據
第一部分用于存儲對象自身的運行時數據,如哈希嗎(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等。
下面是HotSpot虛擬機對象頭Mark Word:
類型指針
對象頭的另一部分是類型指針,即對象指向他元數據的指針,虛擬機可以通過這個指針確定這個對象是哪個類的實例。但是如果對象是一個Java數組,那么在對象頭中還必須有一塊用于記錄數據長度的數據。
對象的實例數據
接著數據頭的是對象的實例數據,這部分是真正存儲的有效信息。無論是從父類中繼承下來的還是在子類中定義的,都需要記錄下來。
對齊填充
最后一部分對齊填充并不是必然存在的,也沒有特別的含義,僅僅起著占位符的作用。由于HotSpot虛擬機的自動內存管理系統要求對象的起始地址必須是8字節的整數倍,也就是 對象的大小必須是8字節的整數倍。而對象頭部分是8字節的倍數,當實例數據沒有對齊的時候,需要對齊填充湊夠8字節的整數倍。
對象的訪問定位
建立對象是為了使用對象,我們的Java程序需要通過棧上的引用數據來操作堆上的具體對象。對象的訪問方式取決于虛擬機的實現,目前主流的訪問方式有使用句柄和直接指針兩種。
句柄引用和直接引用不同在于:使用句柄引用的話,那么Java對堆中將會劃分出一塊內存來作為句柄池,引用中存儲的就是對象的句柄地址,但是直接引用引用中存儲的直接就是對象地址。Java使用的是直接指針訪問對象的方式,因為它最大的好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象的訪問在Java中非常頻繁,因此這類開銷積少成多后也是一項 非??捎^的執行成本。
下面是通過直接指針訪問對象
總結
本文講解了運行時數據區域,內存溢出,如何判斷對象是否存活,垃圾回收算法和垃圾收集器,類加載機制和雙親委派模型以及對象的創建存儲和訪問幾個方面,涵蓋jvm的核心考點,希望你有所收獲。
來源:掘金
作者:堅持就是勝利
鏈接:https://juejin.im/user/1943592288657022
總結
以上是生活随笔為你收集整理的浮点数例外 (核心已转储)_15000 字梳理 JVM 的核心知识的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mybatisplus or查询_Myb
- 下一篇: filesaver.js 保存文件路径_