加群聯系作者vx:xiaoda0423
倉庫地址:https://webvueblog.github.io/JavaPlusDoc/
https://1024bat.cn/
JVM 內存結構
Java 虛擬機的內存空間分為 5 個部分:
· 程序計數器
· Java 虛擬機棧
· 本地方法棧
· 堆
· 方法區
JDK 1.8 同 JDK 1.7 比,最大的差別就是:元數據區取代了永久代。元空間的本質和永久代類似,都是對 JVM 規范中方法區的實現。不過元空間與永久代之間最大的區別在于:元數據空間并不在虛擬機中,而是使用本地內存。
程序計數器(PC 寄存器)
程序計數器的定義
程序計數器是一塊較小的內存空間,是當前線程正在執行的那條字節碼指令的地址。若當前線程正在執行的是一個本地方法,那么此時程序計數器為Undefined。
程序計數器的作用
· 字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制。
· 在多線程情況下,程序計數器記錄的是當前線程執行的位置,從而當線程切換回來時,就知道上次線程執行到哪了。
程序計數器的特點
· 是一塊較小的內存空間。
· 線程私有,每條線程都有自己的程序計數器。
· 生命周期:隨著線程的創建而創建,隨著線程的結束而銷毀。
· 是唯一一個不會出現 OutOfMemoryError 的內存區域。
Java 虛擬機棧(Java 棧)
Java 虛擬機棧的定義
Java 虛擬機棧是描述 Java 方法運行過程的內存模型。
Java 虛擬機棧會為每一個即將運行的 Java 方法創建一塊叫做“棧幀”的區域,用于存放該方法運行過程中的一些信息,如:
· 局部變量表
· 操作數棧
· 動態鏈接
· 方法出口信息
壓棧出棧過程
當方法運行過程中需要創建局部變量時,就將局部變量的值存入棧幀中的局部變量表中。
Java 虛擬機棧的棧頂的棧幀是當前正在執行的活動棧,也就是當前正在執行的方法,PC 寄存器也會指向這個地址。只有這個活動的棧幀的本地變量可以被操作數棧使用,當在這個棧幀中調用另一個方法,與之對應的棧幀又會被創建,新創建的棧幀壓入棧頂,變為當前的活動棧幀。
方法結束后,當前棧幀被移出,棧幀的返回值變成新的活動棧幀中操作數棧的一個操作數。如果沒有返回值,那么新的活動棧幀中操作數棧的操作數沒有變化。
由于 Java 虛擬機棧是與線程對應的,數據不是線程共享的(也就是線程私有的),因此不用關心數據一致性問題,也不會存在同步鎖的問題。
局部變量表
定義為一個數字數組,主要用于存儲方法參數、定義在方法體內部的局部變量,數據類型包括各類基本數據類型,對象引用,以及 return address 類型。
局部變量表容量大小是在編譯期確定下來的。最基本的存儲單元是 slot,32 位占用一個 slot,64 位類型(long 和 double)占用兩個 slot。
對于 slot 的理解:
· JVM 虛擬機會為局部變量表中的每個 slot 都分配一個訪問索引,通過這個索引即可成功訪問到局部變量表中指定的局部變量值。
· 如果當前幀是由構造方法或者實例方法創建的,那么該對象引用 this,會存放在 index 為 0 的 slot 處,其余的參數表順序繼續排列。
· 棧幀中的局部變量表中的槽位是可以重復的,如果一個局部變量過了其作用域,那么其作用域之后申明的新的局部變量就有可能會復用過期局部變量的槽位,從而達到節省資源的目的。
在棧幀中,與性能調優關系最密切的部分,就是局部變量表,方法執行時,虛擬機使用局部變量表完成方法的傳遞局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收。
操作數棧
·?棧頂緩存技術:由于操作數是存儲在內存中,頻繁的進行內存讀寫操作影響執行速度,將棧頂元素全部緩存到物理 CPU 的寄存器中,以此降低對內存的讀寫次數,提升執行引擎的執行效率。
· 每一個操作數棧會擁有一個明確的棧深度,用于存儲數值,最大深度在編譯期就定義好。32bit 類型占用一個棧單位深度,64bit 類型占用兩個棧單位深度操作數棧。
· 并非采用訪問索引方式進行數據訪問,而是只能通過標準的入棧、出棧操作完成一次數據訪問。
方法的調用
· 靜態鏈接:當一個字節碼文件被裝載進 JVM 內部時,如果被調用的目標方法在編譯期可知,且運行時期間保持不變,這種情況下將調用方的符號引用轉為直接引用的過程稱為靜態鏈接。
· 動態鏈接:如果被調用的方法無法在編譯期被確定下來,只能在運行期將調用的方法的符號引用轉為直接引用,這種引用轉換過程具備動態性,因此被稱為動態鏈接。
· 方法綁定
o 早期綁定:被調用的目標方法如果在編譯期可知,且運行期保持不變。
o 晚期綁定:被調用的方法在編譯期無法被確定,只能夠在程序運行期根據實際的類型綁定相關的方法。
· 非虛方法:如果方法在編譯期就確定了具體的調用版本,則這個版本在運行時是不可變的,這樣的方法稱為非虛方法靜態方法。私有方法,final 方法,實例構造器,父類方法都是非虛方法,除了這些以外都是虛方法。
· 虛方法表:面向對象的編程中,會很頻繁的使用動態分配,如果每次動態分配的過程都要重新在類的方法元數據中搜索合適的目標的話,就可能影響到執行效率,因此為了提高性能,JVM 采用在類的方法區建立一個虛方法表,使用索引表來代替查找。
o 每個類都有一個虛方法表,表中存放著各個方法的實際入口。
o 虛方法表會在類加載的鏈接階段被創建,并開始初始化,類的變量初始值準備完成之后,JVM 會把該類的方法也初始化完畢。
· 方法重寫的本質
o 找到操作數棧頂的第一個元素所執行的對象的實際類型,記做 C。如果在類型 C 中找到與常量池中描述符和簡單名稱都相符的方法,則進行訪問權限校驗。
o 如果通過則返回這個方法的直接引用,查找過程結束;如果不通過,則返回 java.lang.IllegalAccessError 異常。
o 否則,按照繼承關系從下往上依次對 C 的各個父類進行上一步的搜索和驗證過程。
o 如果始終沒有找到合適的方法,則拋出 java.lang.AbstractMethodError 異常。
Java 中任何一個普通方法都具備虛函數的特征(運行期確認,具備晚期綁定的特點),C++ 中則使用關鍵字 virtual 來顯式定義。如果在 Java 程序中,不希望某個方法擁有虛函數的特征,則可以使用關鍵字 final 來標記這個方法。
Java 虛擬機棧的特點
· 運行速度特別快,僅僅次于 PC 寄存器。
· 局部變量表隨著棧幀的創建而創建,它的大小在編譯時確定,創建時只需分配事先規定的大小即可。在方法運行過程中,局部變量表的大小不會發生改變。
· Java 虛擬機棧會出現兩種異常:StackOverFlowError 和 OutOfMemoryError。
o StackOverFlowError 若 Java 虛擬機棧的大小不允許動態擴展,那么當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度時,拋出 StackOverFlowError 異常。
o OutOfMemoryError 若允許動態擴展,那么當線程請求棧時內存用完了,無法再動態擴展時,拋出 OutOfMemoryError 異常。
· Java 虛擬機棧也是線程私有,隨著線程創建而創建,隨著線程的結束而銷毀。
· 出現 StackOverFlowError 時,內存空間可能還有很多。
常見的運行時異常有:
· NullPointerException - 空指針引用異常
· ClassCastException - 類型強制轉換異
· IllegalArgumentException - 傳遞非法參數異常
· ArithmeticException - 算術運算異常
· ArrayStoreException - 向數組中存放與聲明類型不兼容對象異常
· IndexOutOfBoundsException - 下標越界異常
· NegativeArraySizeException - 創建一個大小為負數的數組錯誤異常
· NumberFormatException - 數字格式異常
· SecurityException - 安全異常
· UnsupportedOperationException - 不支持的操作異常
本地方法棧(C 棧)
本地方法棧的定義
本地方法棧是為 JVM 運行 Native 方法準備的空間,由于很多 Native 方法都是用 C 語言實現的,所以它通常又叫 C 棧。它與 Java 虛擬機棧實現的功能類似,只不過本地方法棧是描述本地方法運行過程的內存模型。
棧幀變化過程
本地方法被執行時,在本地方法棧也會創建一塊棧幀,用于存放該方法的局部變量表、操作數棧、動態鏈接、方法出口信息等。
方法執行結束后,相應的棧幀也會出棧,并釋放內存空間。也會拋出 StackOverFlowError 和 OutOfMemoryError 異常。
如果 Java 虛擬機本身不支持 Native 方法,或是本身不依賴于傳統棧,那么可以不提供本地方法棧。如果支持本地方法棧,那么這個棧一般會在線程創建的時候按線程分配。
堆
堆的定義
堆是用來存放對象的內存空間,幾乎所有的對象都存儲在堆中。
堆的特點
· 線程共享,整個 Java 虛擬機只有一個堆,所有的線程都訪問同一個堆。而程序計數器、Java 虛擬機棧、本地方法棧都是一個線程對應一個。
· 在虛擬機啟動時創建。
· 是垃圾回收的主要場所。
· 堆可分為新生代(Eden 區:From Survior,To Survivor)、老年代。
· Java 虛擬機規范規定,堆可以處于物理上不連續的內存空間中,但在邏輯上它應該被視為連續的。
· 關于 Survivor s0,s1 區: 復制之后有交換,誰空誰是 to。
不同的區域存放不同生命周期的對象,這樣可以根據不同的區域使用不同的垃圾回收算法,更具有針對性。
堆的大小既可以固定也可以擴展,但對于主流的虛擬機,堆的大小是可擴展的,因此當線程請求分配內存,但堆已滿,且內存已無法再擴展時,就拋出 OutOfMemoryError 異常。
Java 堆所使用的內存不需要保證是連續的。而由于堆是被所有線程共享的,所以對它的訪問需要注意同步問題,方法和對應的屬性都需要保證一致性。
新生代與老年代
· 老年代比新生代生命周期長。
· 新生代與老年代空間默認比例 1:2:JVM 調參數,XX:NewRatio=2,表示新生代占 1,老年代占 2,新生代占整個堆的 1/3。
· HotSpot 中,Eden 空間和另外兩個 Survivor 空間缺省所占的比例是:8:1:1。
· 幾乎所有的 Java 對象都是在 Eden 區被 new 出來的,Eden 放不了的大對象,就直接進入老年代了。
對象分配過程
· new 的對象先放在 Eden 區,大小有限制
· 如果創建新對象時,Eden 空間填滿了,就會觸發 Minor GC,將 Eden 不再被其他對象引用的對象進行銷毀,再加載新的對象放到 Eden 區,特別注意的是 Survivor 區滿了是不會觸發 Minor GC 的,而是 Eden 空間填滿了,Minor GC 才順便清理 Survivor 區
· 將 Eden 中剩余的對象移到 Survivor0 區
· 再次觸發垃圾回收,此時上次 Survivor 下來的,放在 Survivor0 區的,如果沒有回收,就會放到 Survivor1 區
· 再次經歷垃圾回收,又會將幸存者重新放回 Survivor0 區,依次類推
· 默認是 15 次的循環,超過 15 次,則會將幸存者區幸存下來的轉去老年區 jvm 參數設置次數 : -XX:MaxTenuringThreshold=N 進行設置
· 頻繁在新生區收集,很少在養老區收集,幾乎不在永久區/元空間搜集
Full GC /Major GC 觸發條件
· 顯示調用System.gc(),老年代的空間不夠,方法區的空間不夠等都會觸發 Full GC,同時對新生代和老年代回收,FUll GC 的 STW 的時間最長,應該要避免
· 在出現 Major GC 之前,會先觸發 Minor GC,如果老年代的空間還是不夠就會觸發 Major GC,STW 的時間長于 Minor GC
逃逸分析
標量替換
o 標量不可在分解的量,java 的基本數據類型就是標量,標量的對立就是可以被進一步分解的量,而這種量稱之為聚合量。而在 JAVA 中對象就是可以被進一步分解的聚合量
o 替換過程,通過逃逸分析確定該對象不會被外部訪問,并且對象可以被進一步分解時,JVM 不會創建該對象,而會將該對象成員變量分解若干個被這個方法使用的成員變量所代替。這些代替的成員變量在棧幀或寄存器上分配空間。
對象和數組并非都是在堆上分配內存的
《深入理解 Java 虛擬機中》關于 Java 堆內存有這樣一段描述:隨著 JIT 編譯期的發展與逃逸分析技術逐漸成熟,棧上分配,標量替換優化技術將會導致一些變化,所有的對象都分配到堆上也漸漸變得不那么"絕對"了。
這是一種可以有效減少 Java 內存堆分配壓力的分析算法,通過逃逸分析,Java Hotspot 編譯器能夠分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上。
當一個對象在方法中被定義后,它可能被外部方法所引用,如作為調用參數傳遞到其他地方中,稱為方法逃逸。
再如賦值給類變量或可以在其他線程中訪問的實例變量,稱為線程逃逸
使用逃逸分析,編譯器可以對代碼做如下優化:
同步省略:如果一個對象被發現只能從一個線程被訪問到,那么對于這個對象的操作可以不考慮同步。
將堆分配轉化為棧分配:如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象可能是棧分配的候選,而不是堆分配。
分離對象或標量替換:有的對象可能不需要作為一個連續的內存結構存在也可以被訪問到,那么對象的部分(或全部)可以不存儲在內存,而是存儲在 CPU 寄存器中。
public static StringBuffer createStringBuffer(String s1, String s2) {StringBuffer s = new StringBuffer();s.append(s1);s.append(s2);return s;}
s 是一個方法內部變量,上邊的代碼中直接將 s 返回,這個 StringBuffer 的對象有可能被其他方法所改變,導致它的作用域就不只是在方法內部,即使它是一個局部變量,但還是逃逸到了方法外部,稱為方法逃逸。
還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實例變量,稱為線程逃逸。
· 在編譯期間,如果 JIT 經過逃逸分析,發現有些對象沒有逃逸出方法,那么有可能堆內存分配會被優化成棧內存分配。
· jvm 參數設置,-XX:+DoEscapeAnalysis :開啟逃逸分析 ,-XX:-DoEscapeAnalysis : 關閉逃逸分析
· 從 jdk 1.7 開始已經默認開始逃逸分析。
TLAB
· TLAB 的全稱是 Thread Local Allocation Buffer,即線程本地分配緩存區,是屬于 Eden 區的,這是一個線程專用的內存分配區域,線程私有,默認開啟的(當然也不是絕對的,也要看哪種類型的虛擬機)
· 堆是全局共享的,在同一時間,可能會有多個線程在堆上申請空間,但每次的對象分配需要同步的進行(虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性)但是效率卻有點下降
· 所以用 TLAB 來避免多線程沖突,在給對象分配內存時,每個線程使用自己的 TLAB,這樣可以使得線程同步,提高了對象分配的效率
· 當然并不是所有的對象都可以在 TLAB 中分配內存成功,如果失敗了就會使用加鎖的機制來保持操作的原子性
· -XX:+UseTLAB 使用 TLAB,-XX:+TLABSize 設置 TLAB 大小
四種引用方式
· 強引用:創建一個對象并把這個對象賦給一個引用變量,普通 new 出來對象的變量引用都是強引用,有引用變量指向時永遠不會被垃圾回收,jvm 即使拋出 OOM,可以將引用賦值為 null,那么它所指向的對象就會被垃圾回收。
· 軟引用:如果一個對象具有軟引用,內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。
· 弱引用:非必需對象,當 JVM 進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。
· 虛引用:虛引用并不會決定對象的生命周期,如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
方法區
方法區的定義
Java 虛擬機規范中定義方法區是堆的一個邏輯部分。方法區存放以下信息:
· 已經被虛擬機加載的類信息
· 常量
· 靜態變量
· 即時編譯器編譯后的代碼
方法區的特點
· 線程共享。 方法區是堆的一個邏輯部分,因此和堆一樣,都是線程共享的。整個虛擬機中只有一個方法區。
· 永久代。 方法區中的信息一般需要長期存在,而且它又是堆的邏輯分區,因此用堆的劃分方法,把方法區稱為“永久代”。
· 內存回收效率低。 方法區中的信息一般需要長期存在,回收一遍之后可能只有少量信息無效。主要回收目標是:對常量池的回收;對類型的卸載。
· Java 虛擬機規范對方法區的要求比較寬松。 和堆一樣,允許固定大小,也允許動態擴展,還允許不實現垃圾回收。
運行時常量池
方法區中存放:類信息、常量、靜態變量、即時編譯器編譯后的代碼。常量就存放在運行時常量池中。
當類被 Java 虛擬機加載后, .class 文件中的常量就存放在方法區的運行時常量池中。而且在運行期間,可以向常量池中添加新的常量。如 String 類的 intern() 方法就能在運行期間向常量池中添加字符串常量。
直接內存(堆外內存)
直接內存是除 Java 虛擬機之外的內存,但也可能被 Java 使用。
操作直接內存
在 NIO 中引入了一種基于通道和緩沖的 IO 方式。它可以通過調用本地方法直接分配 Java 虛擬機之外的內存,然后通過一個存儲在堆中的DirectByteBuffer對象直接操作該內存,而無須先將外部內存中的數據復制到堆中再進行操作,從而提高了數據操作的效率。
直接內存的大小不受 Java 虛擬機控制,但既然是內存,當內存不足時就會拋出 OutOfMemoryError 異常。
直接內存與堆內存比較
· 直接內存申請空間耗費更高的性能
· 直接內存讀取 IO 的性能要優于普通的堆內存
· 直接內存作用鏈: 本地 IO -> 直接內存 -> 本地 IO
· 堆內存作用鏈:本地 IO -> 直接內存 -> 非直接內存 -> 直接內存 -> 本地 IO
服務器管理員在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,但經常忽略直接內存,使得各個內存區域總和大于物理內存限制,從而導致動態擴展時出現OutOfMemoryError異常。