java中就虛擬機器是其他語言編寫的(C語言+組合語言,因此,JVM最常出現的攻擊就是buffer overflow),如javac命令等,而java api是java寫的,大多開源在openjdk,jdk中有一個src.jar,就是JDk的原始碼,本文是JVM基礎知識的一個彙總,方便查閱,內容較多,以下是內容目錄,可以直接跳到感興趣的章節。
JDK7記憶體模型(圖來自於網路):
JDK8記憶體模型(圖來自於網路):
JVM記憶體模型說明:
JDK7中記憶體模型包括:方法區,堆區,棧區,本地方法棧,計數器,分為兩個型別的區域,一種是執行緒私有的,一種是執行緒共享的,其中,方法區和堆區是執行緒共享的,其他都是執行緒私有的,堆區是被GC的主要區域,方法區有部分廢棄的常量可以被GC,下面詳細的說明:
(1)計數器,執行緒私有,當前執行緒所執行的位元組碼的行號指示器,標記當前執行到了哪一行指令
(2)虛擬機器棧,執行緒私有,生命週期和執行緒相同,存區域性變數表、運算元棧、動態連結、方法出口(即方法返回地址)資訊等
其中,區域性變數表存編譯器可知的各種資料型別,包括boolean、char、byte、short、int、float、double,及物件的引用
(3)本地方法棧,和虛擬機器棧所發揮的作用非常相似,區別是,虛擬機器棧為虛擬機器執行java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的native方法服務,在 HotSpot 虛擬機器中和 Java 虛擬機器棧合二為一。本地方法被執行的時候,在本地方法棧也會建立一個棧幀,用於存放該本地方法的 區域性變數表、運算元棧、動態連結、出口資訊
(4)堆,java虛擬機器所管理的記憶體中最大的一塊,在虛擬機器啟動時建立,此記憶體區域的唯一目的就是存放物件例項,幾乎所有的對 象例項以及陣列都在這裡分配記憶體
(5)方法區,用於儲存已被虛擬機器載入的類的資訊(欄位、方法)、常量、靜態變數、即時編輯器編譯後的程式碼等資料,執行時常量池:屬於方法區,包含字面量(字串、final常量)、符號引用
其中,方法區的詳細說明:
方法區,Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫 做 Non-Heap(非堆),目的應該是與 Java 堆區分開來,方法區也被稱為永久代,但方法區和永久代並不完全等同,只是為了便於管理,這樣,HotSpot虛擬機器的垃圾收集器就可以像管理java堆一樣管理這部分記憶體,方法區可被GC回收的那部分記憶體是廢棄的常量,但這樣管理並不好,會容易產生記憶體溢位,所以在JDK8中,移除了方法區,將方法區中的常量池移至堆中(字串池和類的靜態變數放入java堆中),其他移到直接記憶體中,命名為“元空間”,解決了永久代會出現記憶體溢位的問題,但是要注意,需要設定元空間的最大大小(-XX:MaxMetaspaceSize設定),否則,如果不指定大小的話,隨著更多類的建立,虛擬機器會耗盡所有可用的系統記憶體
(6)直接記憶體,不屬於JVM執行時資料區,JVM的NIO方法可以分配堆外記憶體如使用 DirectByteBuffer
(1)堆記憶體溢位OutOfMemoryError:java heap space
產生原因:java堆用於儲存物件例項,只要不斷的建立物件,並保證GC roots到物件間有可達路徑避免這些物件的GC,那麼,當物件數量到達堆的最大容量限制後就會產生OOM
解決辦法:
- 通過引數 -XX:HeapDumpOnOutOfMemoryError 可以讓虛擬機器在記憶體溢位異常時Dump當前記憶體堆轉儲快照
- 通過記憶體映像分析工具(如:Eclipse Memory Analyzer)對Dump出的堆轉儲快照分析,判斷是記憶體洩露還是記憶體溢位
- 如果是記憶體洩露:通過工具檢視洩露物件的型別資訊和它們到 GC Roots 的引用鏈資訊,分析GC收集器無法自動回收它們的原因,定位記憶體洩露的程式碼位置
- 如果是記憶體溢位:檢查堆引數 -Xms和-Xmx,看是否可調大;程式碼上檢查某些物件生命週期過長,持有時間過長的情況,嘗試減少程式執行期間記憶體消耗
(2)除程式計數器外,JVM其他幾個執行時區域都可能發生OutOfMemoryError異常
(3)棧的兩種異常
1. StackOverFlowError異常:執行緒請求的棧深度大於虛擬機器所允許的最大深度
一個會發生stackOverFlow的場景:無限遞迴,沒有出口
2.OutOfMemoryError異常:虛擬機器擴充套件棧時無法申請足夠的記憶體空間
一個會發生OutOfMemory的場景:list,無限新增元素
解決虛擬機器棧兩種異常的辦法:
1.檢查程式碼中是否有死遞迴
2.配置 -Xss 增大每個執行緒的棧記憶體容量,但會減少工作執行緒數,需要權衡
(1)-Xss,設定棧的大小,不熟悉最好使用預設值,當棧中儲存資料比較多時,需要適當調大這個值,否則會出現java.lang.StackOverflowError異常
(2) 堆記憶體設定:
① -Xms,初始堆大小,Server端JVM最好將-Xms和-Xmx設為相同值,開發測試機JVM可以保留預設值
② -Xmx,最大堆大小,預設為實體記憶體的1/4,最佳設值應該視實體記憶體大小及計算機內其他記憶體開銷而定
預設空餘堆記憶體小於 40%時,JVM就會增大堆直到-Xmx的最大限制;空餘堆記憶體大於70%時,JVM會減少堆直到-Xms的最小限制。因此伺服器一般設定-Xms、 -Xmx相等以避 免在每次GC 後調整堆的大小。
③ -Xmn:年輕代大小(young區大小),通常為 Xmx 的 1/3 或 1/4,不熟悉最好保留預設值
④ -XX:SurvivorRatio,設定年輕代Eden和單個S區的比例,預設為8,即Eden為8/10,兩個survivor分別為1/10,其中一個survivor閒置,用於複製,所以年輕代實際可用的記憶體 大小為-Xmn設定值的9/10,Eden設定的太大的話,會導致GC變慢,並且沒有足夠的survivor倖存者空間,會導致GC直接到達老年代,老年代滿的更快,會更早觸發FullfGC, Eden設定的過小,則MinorGC頻繁,會影響線上程式執行,因為GC會導致應用程式暫停
⑤ -XX:MaxTenuringThreshold,設定年輕代中回收區物件的年齡,預設15,可通過命令指定,如果設定為0,表示Eden回收時,不經過Survivor,直接到達老年代
(3) 持久代設定:
① -XX:PermSize,方法區(永久代,或稱非堆區)初始分配的記憶體大小,其全稱為permanent size(持久化記憶體)
② -XX:MaxPermSize=64m,永久代的最大大小,超過這個值,將會丟擲OutOfMemoryError:PermGen
在配置之前一定要慎重的考慮一下自身軟體所需要的非堆區記憶體大小,因為持久代記憶體是不會被java垃圾回收機制進行處理的地方。
最大堆記憶體與最大非堆記憶體的和絕對不能夠超出作業系統的可用記憶體。
(4) 元空間的設定:
JDK 1.8 的時候,方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始 了),取而代之是元空間,元空間使用的是直接記憶體。配置如下:
-XX:MetaspaceSize=N //設定 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //設定 Metaspace 的最大大小
與永久代很大的不同就是,元空間解決了永久代易發生記憶體溢位的問題,但是要注意,如果不指定元空間的大小,隨著更多類的建立,虛擬機器會耗盡所有可用的系統記憶體。
因為GC垃圾回收的主要區域是堆區,從GC的角度來說,java堆又細分為新生代和老年代,另外有一部分是持久代,來代表方法區,是為了方便管理,是方法區在堆區開闢出來的一塊邏輯空間,下圖為分代示意圖(圖片來自於網路)
(1)新生代,新生代又分為三個區域,包括Eden區和兩個Survivor倖存者區,Eden區主要存放new出來的物件,Survivor主要存放上一次GC之後的倖存者,作為這一次GC的被掃描者,分別對應圖中的S0和S1,兩個Survivor區等大,其中一個閒置,所以新生代實際可用的記憶體大小要減去其中一個倖存者區域的大小
(2)老年代,老年代主要存放大物件,或從MinorGC過來的到達一定年齡仍然倖存的物件
(3)持久代,即方法區,用於存放已被虛擬機器載入的類的資訊,靜態變數,常量,和編譯後的程式碼等資訊,持久代能被GC的是廢棄的常量,持久代對垃圾回收沒有顯著影響
(1) MinorGC的過程:MinorGC採用複製演算法,首先,把Eden區域和使用的倖存者區域(SurvivorFrom)中存活的物件複製到另一塊空閒的倖存者區(SurvivorTo)中,同時,把這些物件的年齡+1,如果有物件的年齡已經達到了老年代的標準,則賦值到老年代區,如果空閒的倖存者區(SurvivorTo)不夠存放Eden和使用的倖存者區(SurvivorFrom)移動過來的資料,則直接放到老年代,然後,清空Eden和使用的倖存者區中(SurvivorFrom)的物件,然後,倖存者區互換,SurvivorTo中的資料換到SurvivorFrom中,SurvivorFrom繼續等待下一次GC,Survivor區每熬過一次MinorGC,就將物件的年齡+1,當物件的年齡到達某個值時(預設時15,可用通過引數-XX:MaxTenuringThreshold 來設定),這些物件就會成為老年代
(2) MajorGC的過程:老年代的回收,不會那麼頻繁,老年代使用的回收演算法是標記-清除或標記-整理演算法,標記-清除演算法會產生記憶體碎片,即不連續的空間,如果此時,有大的物件進來,記憶體中沒有足夠的連續空間時,會提前觸發FullGC(這是一個優化點),一次FullGC的時間要比一次MinorGC的時間長,當年老代也裝不下,就會丟擲OOM(Out Of Memory)異常 (3) Full GC的觸發:老年代滿了而無法容納更多的物件,會觸發Full GC,Full GC 清理整個記憶體堆,包括年輕代和老年代。
(1) 標記-清除演算法,缺點是容易產生碎片,且效率不高,標記過程和清除過程效率都不高
標記-清除演算法的過程,分為兩個過程,標記過程和清除過程
① 標記過程,遍歷所有的GC-roots,然後將所有GC-roots可達的物件標記為存活的物件(記為1)
② 清除過程,遍歷堆中的所有物件,將沒有標記的物件全部清除掉(沒有標記過的,記為0)
③ 清除過後,被標記過的物件留下,標誌位重新歸0
以下是標記-清除演算法的圖示(圖片來自於網路)
(2) 複製演算法,用於年輕代,需要額外的空間來進行復制操作
複製演算法的過程,就是把記憶體分為2塊等同大小的記憶體空間(A和B),使用A進行記憶體的使用,當A記憶體不足以分配物件而引起記憶體回收時,就會把存活的物件從A記憶體塊放到B內 存塊,然後把A記憶體塊中的物件全部清除掉,然後在B記憶體塊中使用,當B記憶體不足以分配物件而引起記憶體回收時,就會把存活的物件從B記憶體塊放到A記憶體塊中,然後把B記憶體 塊中的物件全部清除掉,如此迴圈
複製演算法的好處是,避免的空間碎片(記憶體中不連續的空間),缺點是浪費了一半的空間,降低空間使用率
以下是複製演算法的圖示(圖片來自於網路)
(3) 標記-整理演算法(Mark-Compact),用於老年代
標記-整理演算法的過程,標記過程仍然與標記-清除演算法一樣,但後續步驟不是直接 對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以 外的記憶體
以下是標記-整理演算法的圖示(圖片來自於網路)
從圖中可以看出,標記-整理演算法,避免了標記-清除演算法產生記憶體碎片的問題, 也避免了複製演算法中記憶體浪費的問題,存在的問題就是效率問題,比前兩者效率低
(4) 分代收集,JVM實際GC中使用的,根據物件存活週期的不同,分新生代和老年代,新生代使用複製演算法,因為每次GC只有少量的物件存活,用複製演算法只需要付出少量存活對 象的複製成本就可以完成收集,老年代使用標記-整理演算法或標記-清除演算法,老年代中,物件存活率高,沒有額外的擔保空間,就必須使用標記-清除或標記-整理演算法
以下是分代收集演算法的圖示(圖片來自於網路)
1、收集器
(1) serial收集器,是單執行緒收集器,用於新生代,使用複製演算法,在GC時,會暫停其他所有工作的執行緒,直到GC結束,常用於client模式下的虛擬機器
(2) parNew收集器,是serial的多執行緒版本,也用於新生代,使用複製演算法
parNew和serial都可以且只能和老年代的CMS和serial old組合使用
(3) Parallel Scarenge收集器,用於新生代,使用複製演算法,主要關注吞吐量,適合在後臺運算且不需要太多互動的任務
(4) Serial old收集器,是serial收集器的老年代版本,是單執行緒收集器,使用標記-整理演算法,也是給client下的虛擬機器用
(5) Parallel old,用於老年代,使用標記-整理演算法,注重吞吐量,及CPU敏感的場合優先考慮使用parallel + parallel old組合
(6) CMS(concurrent Mark Sweep)收集器,特點是獲取最短回收停頓時間為目標的收集器,使用標記-清除演算法,支援併發、低停頓,缺點是對CPU資源敏感(在併發階段,雖然不會導致使用者執行緒停頓,但會因為佔用一部分執行緒(或CPU資源),而導致應用程式變慢),會導致吞吐量降低,且無法收集浮動垃圾(標記-清除演算法,會產生大量的碎片),會導致FullGC,可以用serial old臨時替代
(7) G1,JDK7中新增的回收器,是JDK9預設的回收器,特點是,面向服務端應用的垃圾收集器,支援併發與並行,充分利用CPU多核,縮短stop時間,且可預測停頓,採用分代收集,整體是標記-整理演算法,區域性是複製演算法,不會產生碎片
2、G1回收器的工作原理
(1)G1的分代收集,如下圖(圖片來自於網路)
G1的分代收集,將整個堆分成n個大小相等的Region區域,每個Region佔用一塊連續的虛擬記憶體地址,新生代和老生代不再是物理隔離,而是一部分Region的集合,Region的大小可以通過-XX:G1HeapRegionSize設定,如果未設定,預設是2048份,G1仍是分代收集,除Eden、Survivor、Old區域外,還包含Humongous,是專門用來存放巨型物件的,即佔用空間>50%分割槽容量的物件,以此減少短期存在的巨型物件對垃圾收集造成的負面影響。
G1的回收過程:
(1)標記過程,G1的標記分為幾個階段,包括全域性併發標記,初始標記,併發標記,最終標記
i. 全域性併發標記:基於 STAB(snapshot-at-the-beginning)形式的併發標記,標記完成後,G1知道哪個區域是空的,它首先會收集那些產生大量空閒空間的區域
ii.初始標記STW:耗時很短,標記GC-roots能直接關聯的物件,壓入掃描棧
iii.併發標記:與使用者執行緒併發執行,耗時較長,GC執行緒不斷從掃描棧中取出引用,然後遞迴標記,直到掃描棧清空
iv.最終標記STW:重新標記併發標記期間因使用者程式執行而導致引用發生變動的那部分標記,寫入屏障Write Barrier標記的物件
(2)清理過程:統計各個Region被標記存活的物件有多少,如果發現沒有存活,就會整體回收到可分配的Region中
(3)拷貝存活物件:將Region中的存活物件拷貝到空Region裡去,回收原Region空間,繼續使用
G1不存在Full GC,分為Yong GC和Mix GC兩種,Yong GC是新生代的回收,Mix GC是老年代的收集
這是很重要的一部分,JVM引數平時不需要頻繁的去調整,可以通過觀察應用環境的執行,定期的調整,主要有兩方面,一個是JVM引數的調整,一個是GC的優化,GC優化的目標是儘量減少GC次數,儘量在Yong區完成GC,儘量減少Full GC的次數,以減少GC對應用程式帶來的影響
(1)JVM引數(這裡列出所有可調整的JVM引數,可根據各自的環境酌情設定)
-Xms4G 是指: JVM啟動時整個堆(包括年輕代,年老代)的初始化大小
-Xmx4G 是指: JVM啟動時整個堆的最大值,預設為實體記憶體的1/4
-Xms和-Xmx的設定,預設空餘堆記憶體小於 40%時,JVM就會增大堆直到-Xmx的最大限制;空餘堆記憶體大於70%時,JVM會減少堆直到-Xms的最小限制。因此伺服器一般 設定-Xms、 -Xmx相等以避免在每次GC 後調整堆的大小
-Xmn2G是指:年輕代的空間大小,通常為 Xmx 的 1/3 或 1/4,剩下的是年老代的空間
-XX:SurvivorRatio=1是指:年輕代Eden區和單個S區的比例,預設是8,即Eden佔8/10,兩個Survivor分別佔1/10,其中一個Survivor閒置,用於複製,所以年輕代實際可用的記憶體大小未-Xmn設定值的9/10,Eden設定的太答的話,會導致GC變慢,並且沒有足夠的倖存者空間,會導致GC直接到達老年代,老年代滿的更快,會更早觸發Full GC,Eden設定的過小,則Minor頻繁,會影響線上程式執行,因為GC會導致應用程式暫停
-XX:MaxTenuringThreshold,設定年輕代中回收區物件的年齡,預設15,可通過命令指定,如果設定為0,表示Eden回收時,不經過Survivor,直接到達老年代
-XX:NewRatio,設定新生代與老年代的比例,如 –XX:NewRatio=2,則新生代佔整個堆空間的1/3,老年代佔2/3
(2)GC的引數優化
i.CMS回收器的引數優化
-XX:CMSInitiatingOccupancyFraction=70,該值代表老年代堆空間的使用率,預設值是92,假如設定為70,就表示第一次 CMS 垃圾收集會在老年代佔用 70% 時觸發。過大會使 STW 時間過長,過小會影響吞吐率
-XX:+UseCMSCompactAtFullCollection,-XX:CMSFullGCsBeforeCompaction=4:執行4次不壓縮的 Full GC 後,會執行一次記憶體壓縮的過程,用來消除記憶體碎片
-XX:+ConcGCThreads,併發 CMS 過程執行時的執行緒數,CMS 預設回收執行緒數是(CPU+3) / 4。更多的執行緒會加快併發垃圾回收過程,但會帶來額外的同步開銷。
ii.G1回收器的引數優化
-XX:G1HeapRegionSize,指定G1中Rigion的大小,如果未設定,預設將堆記憶體平均分為 2048 份
-XX:MaxGCPauseMillis=n,設定GC時最大暫停時間,這個目標不一定能滿足,JVM會盡最大努力實現它,不建議設定的過小(<50ms)
-XX:InitiatingHeapOccupancyPercent=n,觸發G1啟動 Mixed GC,表示垃圾物件在整個G1 堆記憶體空間的佔比
避免使用 -Xmn 或 -XX:NewRatio等其他顯式設定年輕代大小的選項,固定年 輕代大小,會覆蓋暫停時間目標
JVM自帶的記憶體分析小工具:jconsole、jhat、jmap、jstack、jstat、jstatd、jvisualvm
linux工具:pidstat、vmstat、iostat
eclipse的分析工具:mat
收費工具:Jporfiler,yourkit
(1) jconsole,jvm自帶記憶體分析工具,位於jdk的bin目錄下,它提供了圖形介面。可以檢視到被監控的jvm的記憶體資訊,執行緒資訊,類載入資訊,MBean資訊。
(2) jhat,jvm自帶記憶體分析工具,位於jdk的bin目錄下,jdk6+版本自帶,能夠分析dump檔案,執行 jhat -J -Xmx512m [file] ,file就是dump檔案路徑。
(3) jmap,jvm自帶記憶體分析工具,位於jdk的bin目錄下,傾向於分析jvm記憶體中物件資訊,jmap -histo <pid>在螢幕上顯示出指定pid的jvm記憶體狀況,太簡單。
jmap -dump:file=c:\dump.txt 340 匯出dump檔案,用專門的dump分析工具分析。
(4) jstack,jvm自帶記憶體分析工具,位於jdk的bin目錄下,會顯示執行緒優先順序,執行緒ID,native執行緒ID,執行緒棧起始地址
(5) jstat,jvm自帶記憶體分析工具,位於jdk的bin目錄下,傾向於分析jvm記憶體的gc情況,常用引數-gcutil,這個引數的作用不斷的顯示當前指定的jvm記憶體的垃圾收集的資訊。 jstat -gcutil 340 10000,這個命令是每個10秒鐘輸出一次jvm的gc資訊,10000指的是間隔時間為10000毫秒。
(6) jstatd,jvm自帶記憶體分析工具,位於jdk的bin目錄下,一個RMI的server,它可以監控Hotspot的JVM的啟動和結束,同時提供介面可以讓遠端機器連線到JVM。 比如 jps jstat都可以通過jstatd來遠端觀察JVM的執行情況。
(7) jvisualvm,jvm自帶記憶體分析工具,位於jdk的bin目錄下,JDK6 update 7之後推出,,java視覺化虛擬機器,它不但提供了jconsole類似的功能,還提供了jvm記憶體和cpu實時診斷,還有手動dump出jvm記憶體情況,手動執行gc。和jconsole一樣,執行jviusalvm,在jdk的bin目錄下執行jvisualvm,windows下是jvisualvm.exe,linux和unix下是jvisualvm.sh。
(8) pidstat,linux系統下使用,需要安裝,yum install sysstat,要檢視Linux下面程式、程式組、執行緒的資源消耗的統計資訊,可以使用pidstat,它可以收集並報告程式的統計資訊。
(9) vmstat,linux系統下使用,需要安裝,vmstat是Virtual Meomory Statistics(虛擬記憶體統計)的縮寫,可對作業系統的虛擬記憶體、程式、CPU活動進行監控。是對系統的整體情況進行統計,不足之處是無法對某個程式進行深入分析。
(10) iostat,linux系統下使用,需要安裝,yum install sysstat,iostat是I/O statistics(輸入/輸出統計)的縮寫,iostat工具將對系統的磁碟操作活動進行監視。它的特點是彙報磁碟活動統計情況,同時也會彙報出CPU使用情況。iostat也有一個弱點,就是它不能對某個程式進行深入分析,僅對系統的整體情況進行分析
(11) MAT,可以通過MAT分析記憶體洩漏的原因
在語言層面,建立物件有四種方式:1) clone 2)反序列化 3)反射 4) New
而在虛擬機器中,物件建立的過程是如何呢?
JAVA編譯解釋的過程:.java檔案->javac編譯成.class位元組碼檔案->jvm解釋執行。
Java很特殊,Java程式需要編譯但是沒有直接編譯成機器語言,即二進位制語言,而是編譯成位元組碼(.class)再用解釋方式執行。java程式編譯以後的class屬於中間代碼,並不是可執行程式exe,不是二進位制檔案,所以在執行的時候需要一箇中介來解釋中間程式碼,這既是java直譯器,也就是所謂的java虛擬機器(JVM),也叫JDK。
JVM中,物件建立過程(New)分三步:1) 類載入 2) 為新物件分配記憶體 3)初始化
在虛擬機器遇到new指令時:
1) 類載入:確保常量池中存放的時已解釋的類,且物件所屬型別已經初始化過,如果沒有,則先執行類載入
2) 為新生物件分配記憶體:物件所需記憶體大小在類載入時可以確定,將確定大小的記憶體從Java堆中劃分出來
- 分配空閒記憶體方法:
- 指標碰撞:假如堆是規整的,用過的記憶體和空閒的記憶體各一邊,中間使用指標作為分界點,分配記憶體時將指標移動物件大小的距離
- 空閒列表:假如堆是不規整的,虛擬機器需要維護哪些記憶體塊是可用的列表,分配時候從列表中找出足夠大的空閒記憶體劃分,並更新列表記錄
- 物件建立在併發情況下保證執行緒安全:例如,正在給物件A分配記憶體,指標還沒修改,物件B同時使用了原來的指標來分配記憶體
- CAS配上失敗重試
- 本地執行緒分配緩衝TLAB(ThreadLocal Allocation Buffer):將記憶體分配動作按執行緒劃分到不同空間中進行,即每個執行緒在Java堆中預先分配一塊記憶體
3) 將分配的記憶體空間初始化為零值:保證物件的例項在Java程式碼中可以不賦值就可 直接使用,能訪問到這些欄位的資料型別對應的零值(例如,int型別引數預設為0)
4) 設定物件頭:設定物件的類的後設資料資訊、雜湊碼、GC分代年齡等
5) 執行<init>方法初始化:將物件按照程式設計師的意願初始化
在HotSpot虛擬機器中,物件在記憶體中儲存的佈局分為3個區域,如下圖:
(1)物件頭(Header):
i. MarkWord:儲存物件自身的執行時資料,例如:雜湊碼HashCode、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID等。考慮空間效率,Mark設計為非固定的資料結構,它根據物件的不同狀態複用自己的空間,如下表格:
ii. 指向Class的指標:即物件指向它的類的後設資料的指標,虛擬機器通過這個指標來確定是哪個類的例項
iii. 如果物件是Java陣列,物件頭中還需要一塊記錄陣列長度的資料
(2) 例項資料(Instance Data):物件真正儲存的有效資訊,也是程式程式碼中定義的各種型別欄位的內容
(3) 對齊填充(Padding):起佔位符的作用,因為HotSpot VM的要求物件起始地址必須是8位元組的整數倍,也就是物件的大小必須是8位元組的整數倍,當物件例項資料部分沒有對齊時,需要對齊填充來補充
什麼是物件監視器,監視器是一種同步結構,它基於互斥鎖,允許執行緒同時互斥(使用 鎖)和協作
互斥是,當一個執行緒訪問受保護的資料時,如果沒有其他執行緒在等待, 執行緒獲取鎖 並繼續執行。當執行緒完成執行時,它釋放鎖並退出監視器。但如果此時另一個執行緒已經擁有監視器時,它必須在entry-set中等待。當前面的執行緒 執行完畢退出監視器時,新到達的執行緒必須與在入口集中等待的其他執行緒競爭。只有一 個執行緒能贏得競爭並擁有鎖。
協作是,當一個執行緒需要資料在某一個狀態下它才能執行,那麼另一個執行緒負責將資料 改變到此狀態
物件監視器的一個理解:物件監視器,任意執行緒對Object的訪問,首先要先獲得Object 的監視器。如果獲取失敗了,執行緒進入同步佇列,執行緒狀態變為BLOCKED。當訪問Object 的執行緒(獲得了所的執行緒)釋放了鎖,則該釋放操作喚醒在同步佇列中的執行緒,使其重 新嘗試對監視器的獲取。Thread類提供一個holdsLock(Object obj)方法,當且僅當物件 obj的監視器被某條執行緒持有的時候才返回true,注意這是一個static方法,意味著某 條執行緒指的是當前執行緒
常見的如生產者/消費者的問題,當讀執行緒需要緩衝區處於“不空”的狀態它才可以從 緩衝區中讀取任何資料,如果它發現緩衝區為空,則進入wait-set等待。待寫執行緒用數 據填充緩衝區,再通知讀執行緒進行讀取。這種機制被稱為“Wait and Notify”或“Signal and Continue”
下圖描述了物件、物件的監視器、同步佇列和執行執行緒之間的關係。
從上圖中可以看到,任意執行緒對Object(Object由synchronized保護)的訪問,首先 要獲得Object的監視器。如果獲取失敗,執行緒進入同步佇列,執行緒狀態變為BLOCKED。 當訪問Object的前驅(獲得了鎖的執行緒)釋放了鎖,則該釋放操作喚醒阻塞在同步隊 列中的執行緒,使其重新嘗試對監視器的獲取。
那麼,物件的監視器到底是做什麼的,用在哪裡,起什麼作用?
為什麼使用術語“監視器”而不是“鎖定”?嚴格來說,確實不同,“鎖”是指具有獲取和釋放原語的東西,這些原語和原語保持某些鎖屬性。例如,獨 佔使用或單作者/多讀者。
“監視程式”是一種機制,可確保在任何給定時間只有一個執行緒可以執行給定的程式碼 段。可以使用鎖(和“條件變數”,允許執行緒等待或向其他執行緒傳送滿足條件的通知) 來實現此功能,但它不僅僅是鎖。實際上,在Java情況下,不能直接訪問監視器 使用的實際鎖。(您不能說“ Object.lock()”來阻止其他執行緒獲取它,就像使用Java Lock例項一樣。)
簡而言之,如果要學究的話,“ monitor”實際上是比“ lock”更好的術語,用於描述 Java提供的特性。但是實際上,這兩個術語幾乎可以互換使用。
(1)類載入器,其作用就是,負責將class檔案載入到記憶體中
java中,先通過javac將java檔案編譯為class檔案, 然後使用ClassLoader類載入器載入class檔案到記憶體中,當一個類被使用時,就會載入到記憶體中,類載入的過程包括:載入,驗證,準備,初始化
(2) 雙親委派模型
每一個類都有一個相對應的類載入器,系統中的ClassLoader在協同工作會預設使用雙親委派模型,類載入的過程,是從父類到子類,類驗證的過程,是從子類到父類,也就是說,載入的時候,首先把該類委派給該父類載入器的loadClass()處理,因此所有的請求最終都應該傳送到頂層的啟動類載入器BootstrapClassLoader中,然後驗證,即在類載入的時候,系統就會首先判斷當前類是否被載入過,如果已經載入的類會直接返回,否則都會嘗試載入,即當父類載入器無法處理時,都由自己來處理,當父類載入器為null時,會使用啟動類載入器BoostrapClassLoader作為父類載入器。
雙親委派保證了java程式的穩定執行,避免了類的重複載入。
除了BootstrapClassLoaderader其他類載入器均由Java實現且全部繼承於java.lang.ClassLoader,如果要自定義類載入器,就要繼承ClassLoader。
(3)反射
反射是把.class檔案載入進記憶體,把.class中的所有內容,封裝成 一個一個的物件的過程,在 程式中可以通過這些物件動態的完成方法的呼叫,成員變數的賦值,物件的建立!
反射獲取Class物件的三種方式:
i、Class.forName(全類名)
ii、類名.class
iii、物件名.getClass();