深入理解java虛擬機---學習總結:
1.Java內存區域
1.1 java運行時數據區
Java 虛擬機所管理的內存如下圖所示,基於JDK1.6。
基於jdk1.8畫的JVM的內存模型
(1) 程序計數器:當前線程所執行的字節碼的行號指示器,內存空間小,線程私有。
內存溢出情況:唯一一個在 Java 虛擬機規范中沒有規定任何 OutOfMemoryError 情況的區域。
(2) 虛擬機棧:描述的是 Java 方法執行的內存模型:每個方法在執行時都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行結束,就對應着一個棧幀從虛擬機棧中入棧到出棧的過程。線程私有,生命周期和線程一致。
局部變量表:存放了編譯期可知的各種基本類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型)和 returnAddress 類型(指向了一條字節碼指令的地址)。
內存溢出情況:StackOverflowError:線程請求的棧深度大於虛擬機所允許的深度。
OutOfMemoryError:如果虛擬機棧可以動態擴展,而擴展時無法申請到足夠的內存。
(3) 本地方法棧:本地方法棧是為虛擬機使用到的Native方法服務。在Hotspot虛擬機中本地方法棧與虛擬機棧中合二為一。
內存溢出情況: StackOverflowError 和 OutOfMemoryError 異常。
(4) 堆:存放對象實例和數組,幾乎所有的對象實例都在這里分配內存。
內存溢出情況:OutOfMemoryError
(5) 方法區:存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。(元空間)
運行時常量池:是方法區的一部分,class文件除了有類的字段、接口、方法等描述信息之外,還有
常量池用於存放編譯期間生成的各種字面量和符號引用。
簡單描述:
堆:存放對象的實例以及對象的屬性和方法
棧:儲存基本數據類型的值、執行的方法、方法中聲明的變量、數組、對象的引用(reference類型)
方法區:存儲已被虛擬機加載的類元數據信息(元空間)
運行時常量池:常量(final)、字符串
關於常量池,下面這兩篇文章寫得很好:
https://blog.csdn.net/wangbiao007/article/details/78545189
https://blog.csdn.net/vegetable_bird_001/article/details/51278339
在 JDK 1.8 中,HotSpot 已經沒有 “PermGen space”這個空間了,取而代之是一個叫做 Metaspace(元空間) 的東西。
Java7中已經將字符串常量池從永久代移除,在Java 堆(Heap)中開辟了一塊區域存放字符串常量池。而在Java8中,已經徹底沒有了永久代,將方法區直接放在一個與堆不相連的本地內存區域,這個區域被叫做元空間。
Metaspace(元空間):
其實,移除永久代的工作從JDK1.7就開始了。JDK1.7中,存儲在永久代的部分數據就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒完全移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap。
元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過以下參數來指定元空間的大小。
2.對象的創建過程
2.1 Java對象的創建過程
對象創建過程:類加載檢查、分配內存、初始化零值、設置對象頭、執行init()方法。
(1) 類加載檢查:當虛擬機遇到一條new指令時,首先去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,
並檢查這個符號引用所代表的類是否已經被加載、解析和初始化過。如果沒有,則執行相應的類加載過程。
(2) 分配內存:把一塊確定大小的內存從java堆中分配出來
分配方式:指針碰撞和空閑列表
用哪種方式取決於堆的規整性,而堆的規整性又取決於采用的GC收集器算法是"標記-清除"還是"標記-整理",
值得注意的是,復制算法內存也是規整的。
指針碰撞: 使用場合:堆內存規整的情況下,既沒有內存碎片
原理: 將用過的內存全部移動到一端,沒用過的內存移動到另一端,中間有個分界值指針,
只需要沿着沒用過的內存移動指針對象即可。
GC收集器: Serial、ParNew
空閑列表: 使用場合:堆內存不規整
原理:虛擬機會維護一個列表,該列表會記錄哪些內存是可用的,在分配的時候會找一塊足夠大
的內存來分配給對象,最后更新列表記錄。
GC收集器:CMS收集器(concurrent mark sweep 並發標記清除算法)
內存分配並發問題:在創建對象的時候回遇到一個很重要的問題,就是線程安全問題
虛擬機采用兩種方法保證線程安全:
CAS+失敗重試:CAS(Compare And Swap 比較並替換)是樂觀鎖的一種實現方式。所謂的樂觀鎖就是,每次不加鎖而假設沒有沖突去完成某項操作,
如果因為沖突失敗就重試,直到成功為止。虛擬機采用CAS+失敗重試的方式更新操作的原子性。
TLAB:為每一個線程預先在eden區分配一塊內存,jvm在給線程中的對象分配內存時,首先在tlab分配,當對象大於
TLAB的剩余空間或TLAB的內存滿了時,再采用上述的CAS進行內存分配。
(3) 初始化零值:將內存空間都初始化為零值,讓對象的實例字段在代碼中不用賦初始值就可以直接使用。
(4) 設置對象頭:對象頭中保存着對象的哈希碼、GC分代年齡、鎖的狀態標志等。
(5) 執行init()方法:執行init()方法,把對象按照程序員的意願進行初始化。
2.2 對象的訪問定位(使用句柄和直接指針的方式)
創建對象就是為了使用對象,java程序通過棧上的reference數據來操作堆上的具體對象,主流的對象訪問
方式有兩種:1.使用句柄 2.直接指針
1.使用句柄 :如果使用句柄,那么java會在堆中開辟一塊內存來當作句柄池,reference中儲存着句柄的地址,
而句柄包含了對象的實例數據和對象的類型數據各個的地址信息。
2.直接指針:直接儲存對象的地址。
區別:使用句柄最大的好處是reference中儲存的是穩定的句柄地址,當對象被移動時只會修改句柄中的實例數據指針,
而不用修改reference。使用直接指針的好處是速度快。
3.垃圾回收機制和內存分配策略
3.1 哪些內存需要回收?
由於程序計數器、虛擬機棧、本地方法棧的生命周期都跟隨線程的生命周期,當線程銷毀了,
內存也就回收了,所以這幾個區域不用過多地考慮內存回收。由於堆和方法區的內存都是動態
分配的,而且是線程共享的,所以內存回收主要關注這部分區域。
3.2 如何判斷對象是否存活?
(1)引用計數法:
給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,如果引用失效,計數器值減1,
所以當該計數器的值為0時,就表示該對象可以被回收了。但是存在兩個對象之間相互循環引用的問題。
(2)可達性分析算法:
通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索的路徑稱為引用鏈,
當一個對象到“GC Roots”沒有任何引用鏈相連的話,也就是GC Roots到這個對象不可達時,證明此對象
已經不可用,可以被回收了。
二次標記:
在可達性分析算法中被判斷是對象不可達時不一定會被垃圾回收機制回收,因為要真正宣告一個對象
的死亡,必須經歷兩次標記的過程。如果發現對象不可達時,將會進行第一次標記,此時如果該對象
調用了finalize()方法,那么這個對象會被放置在一個叫F-Queue的隊列之中,如果在此隊列中該對象
沒有成功拯救自己(拯救自己的方法是該對象有沒有被重新引用),那么GC就會對F-Queue隊列中的對象
進行小規模的第二次標記,一旦被第二次標記的對象,將會被移除隊列並等待被GC回收,所以finalize()
方法是對象逃脫死亡命運的最后一次機會。
可作為 GC Roots 的對象:
(1)虛擬機棧(棧幀中的本地變量表)中引用的對象
(2)方法區中類靜態屬性引用的對象
(3)方法區中常量引用的對象
(4)本地方法棧中 JNI(即一般說的 Native 方法) 引用的對象
3.3 垃圾收集算法
(1)標記—清除算法:
標記階段:先通過根節點,標記所有從根節點開始的對象,未被標記的為垃圾對象。
清除階段:清除垃圾對象。
缺點:效率不高、會產生空間碎片。
(2)復制算法:將內存分為大小相同的兩塊,每次只對其中一塊進行 GC。當這塊內存使用完時,就將還存活的對象復制到另一塊上面。
特點:並不會產生內存碎片,但是代價是把內存縮小了一半,效率比較低。
(3)標記-整理算法:標記-清除算法一樣,區別是清除的時候會把所有存活的對象移到一端。
(4)分代回收算法:根據存活對象划分幾塊內存區,一般是分為新生代和老年代。然后根據各個年代的特點制定相應的回收算法。
新生代:每次垃圾回收都有大量對象死去,只有少量存活,選用復制算法比較合理。
老年代:老年代中對象存活率較高、沒有額外的空間分配對它進行擔保。所以必須使用標記—清除算法或者標記—整理算法。
3.4 垃圾回收器:
(1)Serial 收集器:單線程收集器,在進行垃圾回收時必須暫停其它所有的工作線程直到收集結束。
新生代:使用復制算法。 老年代:標記整理算法。
(2)ParNew 收集器:可以認為是 Serial 收集器的多線程版本。
新生代:使用復制算法。 老年代:標記整理算法。
(3)Parallel Scavenge 收集器:這是一個新生代收集器,也是使用復制算法實現,同時也是並行的多線程收集器。
(4)Serial Old 收集器:收集器的老年代版本,單線程,使用標記整理算法。
(5)Parallel Old 收集器:Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多線程,使用標記整理算法。
(6)CMS 收集器:CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。基於標記-清除算法實現。
回收過程主要分為四個步驟:
(1)初始標記:標記一下GC Roots能直接關聯到的對象,速度很快;
(2)並發標記:進行GC Roots Tracing的過程,也就是標記不可達的對象,相對耗時;
(3)重新標記:修正並發標記期間因用戶程序繼續運作導致的標記變動,速度比較快;
(4)並發清除:對標記的對象進行統一回收處理,比較耗時;
缺點:對 CPU 資源敏感、無法收集浮動垃圾、有大量的空間碎片
(7)G1 收集器:面向服務端的垃圾回收器,基於“標記-整理”算法實現。
回收過程主要分為四個步驟:
(1)初始標記:標記一下GC Roots能直接關聯到的對象,速度很快;
(2)並發標記:進行GC Roots Tracing(搜索標記)的過程,也就是標記不可達的對象,相對耗時 ;
(3)最終標記:修正並發標記期間因用戶程序繼續運作導致的標記變動,速度比較快;
(4)篩選回收:首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計划;
G1收集器的特點
(1)並發與並行:機型垃圾收集時可以與用戶線程並發運行;
(2)分代收集:能根據對象的存活時間采取不同的收集算法進行垃圾回收;
(3)不會產生內存碎片:基於標記——整理算法和復制算法保證不會產生內存空間碎片;
(4)可預測的停頓:G1除了追求低停頓時間外,還能建立可預測的停頓時間模型,便於用戶的實時監控;
3.5 內存分配策略:
內存分配策略:
(1)對象優先在 Eden 分配
(2)大對象直接進入老年代
(3)長期存活的對象將進入老年代
(4)動態對象年齡判定
(5)空間分配擔保
堆分為:新生代、老年代,
新生代又分為:Eden、From、To區
新生代:老年代=1/3:2/3 ,Eden:From:To=8:1:1
對象都會首先在 Eden 區域分配,當 eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC(年輕代GC)。
在GC前,對象存在於eden區和From區,在一次新生代垃圾回收后,如果對象還存活,則會進入To區,並且對象的年齡
還會加 1,當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。經過這次GC后,Eden區和"From"區已經被清空。
這個時候,"From"和"To"會交換他們的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。
不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重復這樣的過程,直到“To”區被填滿,"To"區被填滿之后,
會將所有對象移動到年老代中。
3.6 引用類型
強引用:指向使用new關鍵字創建的對象的引用都是強引用,只要該對象的強引用還在,該對象永遠都不會被GC回收;
軟引用:當內存不足時,就會被回收;
弱引用:只要發生GC,就會被回收;
虛引用:隨時都會被回收;
4.類加載機制
4.1 類加載的過程
七個步驟:加載->驗證->准備->解析->初始化->使用->卸載
類加載主要三步過程:加載->連接->初始化,而連接又包括驗證、准備、解析三個階段。
其中加載、驗證、准備、初始化和卸載這五個階段的順序是確定的
准備階段:是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有以下幾點需要注意:
(1)這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在 Java 堆中。
(2)這里所設置的初始值"通常情況"下是數據類型默認的零值(如0、0L、null、false等),比如我們定義了public static int value=111 ,
那么 value 變量在准備階段的初始值就是 0 而不是111(初始化階段才會復制)。特殊情況:比如給 value 變量加上了 fianl 關
鍵字public static final int value=111 ,那么准備階段 value 的值就被復制為 111。
對於初始化階段,虛擬機嚴格規范了有且只有5種情況下,必須對類進行初始化:
(1)創建類的實例(new 的方式)。訪問某個類或接口的靜態變量,或者對該靜態變量賦值,調用類的靜態方法
(2)使用反射的方法對類進行反射調用的時候。
(3)當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需先觸發其父類的初始化。
(4)當虛擬機啟動時,用戶需指定一個要加載的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類。
(5)當使用 JDK 1.7 的動態語言支持時。
4.2 類加載器
JVM 中內置了三個重要的 ClassLoader,除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部
繼承自java.lang.ClassLoader。
類加載器分為:啟動類加載器、擴展類加載器、應用程序類加載器。
(1)啟動類加載器:最頂層的加載類,負責加載 %JAVA_HOME%/jre/lib目錄下rt.jar。
(2)擴展類加載器:主要負責加載目錄 %JRE_HOME%/jre/lib/ext 目錄下的jar包和類。
(3)應用程序類加載器 :面向我們用戶的加載器,負責加載當前應用classpath下的所有jar包和類。
4.3 雙親委派模型 (雙親委派模型工作原理):
(1)雙親委派模型工作原理:
在類加載的時候,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則才會嘗試去加載。在類加載的時候,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完全這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
(2)雙親委派模型的好處:避免類的重復加載,也保證了 Java 的核心 API 不被篡改。
(3)如果我們不想用雙親委派模型怎么辦?
為了避免雙親委托機制,我們可以自己定義一個類加載器,然后重寫 loadClass() 即可。