摘要:第章內存區域與內存溢出異常運行時數據區域虛擬機在執行程序的過程中會把它所管理的內存劃分為若干個不同的數據區域。即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
第2章 Java內存區域與內存溢出異常 2.2 運行時數據區域
Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分為若干個不同的數據區域。根據《Java虛擬機規范(Java SE 7版)》的規定,Java虛擬機所管理的內存將會包括以下幾個運行時數據區域:
2.2.1 程序計數器(Program Counter Register)每條線程都需要有一個獨立的程序計數器,互不影響,獨立存儲
較小的內存空間
記錄當前線程所執行的代碼的行號指示器
字節碼解釋器工作時通過改變程序計數器的值,來選去下一條需要執行的字節碼指令
Java虛擬機規范沒有規定此區域存在OOM
2.2.2 Java虛擬機棧(Java Virtual Machine Stacks)生命周期與線程相同
描述的是Java方法執行的內存模型
每個方法在執行的同時都會創建一個棧幀(存放局部變量表、操作數棧、動態鏈接、方法出口等)
方法調用即棧幀的出入棧
局部變量表:基本數據類型、對象引用、returnAddress類型
64位長度的long和double類型的數據會占用2個局部變量空間(Slot)
局部變量空間在編譯期分配完成;運行期間不會改變大小
Java虛擬機規范規定2種異常情況:
StackOverflowError:線程請求的棧深度 > 虛擬機所允許的深度
OutOfMemoryError:虛擬機棧動態擴展時無法申請到足夠內存
2.2.3 本地方法棧(Native Method Stack)為虛擬機調用Native方法提供服務(虛擬機棧是為虛擬機調用Java方法提供服務)
也會拋出StackOverflowError和OutOfMemoryError
2.2.4 Java堆(Java Heap)所有線程共享
虛擬機啟動時創建
存放對象實例
堆空間可以物理上不連續,邏輯上連續
OutOfMemoryError:對象實例沒有被分配,且堆無法擴展
2.2.5 方法區(Method Area)線程共享
存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據
永久代:HotSpot在1.7之前把GC分代收集擴展至方法區,即用永久代實現方法區
好處:可以像管理Heap一樣管理方法區
壞處:容易遇到內存溢出問題,永久代有-XX:MaxPermSize的上限
這區域的內存回收目標主要是針對常量池的回收和對類型的卸載
2.2.6 運行時常量池(Runtime Constant Pool)方法區的一部分
用于存放編譯期生成的各種字面量和符號引用,在類加載后進入存放
具有動態性,除了編譯期,運行期也可以將新的常量存入(例如 String.intern())
受到方法區內存的限制
2.2.7 直接內存(Direct Memory)并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域
但使用頻繁,可能導致OutOfMemoryError
分配不會受到Java堆大小的限制,但受到本機總內存(包括RAM以及SWAP區或者分頁文件)大小以及處理器尋址空間的限制
NIO使用Native函數庫直接分配對外內存,通過堆內的DirectByteBuffer對象引用該內存,因為避免了Heap與Native Heap來回復制數據,提高了性能
2.3 HotSpot虛擬機對象探秘 2.3.1 對象的創建先檢查指令參數是否在常量池中存在該類的符號引用,并檢查該符號引用是否被加載、解析和初始化
若無,則執行類加載過程
垃圾收集器帶壓縮功能(Serial、ParNew) -> Heap是連續的 -> “指針碰撞”(Bump the Pointer)分配內存
垃圾收集器不帶壓縮功能(CMS) -> Heap不是連續的 -> “空閑列表”(Free List)分配內存
同步分配內存空間2種方式:
虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性
每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)。只有TLAB用完并分配新的TLAB時,才需要同步鎖定;通過-XX:+/-UseTLAB參數來設定
內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭)
設置對象頭(Object Header)信息。包括:元數據信息、hash碼、GC分代年齡信息等
執行
HotSpot VM中,對象在內存中的布局:
對象頭(Header)
Mark Word。存儲運行時數據;如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等
類型指針。即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
實例數據(Instance Data)。對象真正存儲的有效信息
對齊填充(Padding)。僅起著占位符的作用
2.3.3 對象的訪問定位以下是Java程序通過棧上的Reference來操作堆上的具體對象。
方式一:使用句柄
優勢:reference存放的穩定句柄,對象移動不會影響到reference
劣勢:需要在堆上開辟一塊空間存放句柄信息
方式二:使用直接指針
優勢:reference存放的對象地址,訪問速度快。
劣勢:對象移動時需要更新reference。
HotSpot使用這種
2.4 實戰:OutOfMemoryError異常 2.4.1 Java堆溢出將堆的最小值-Xms參數與最大值-Xmx參數設置為一樣即可避免堆自動擴展
-XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機在出現內存溢出異常時Dump出當前的內存堆轉儲快照
2.4.2 虛擬機棧和本地方法棧溢出
在HotSpot虛擬機中并不區分虛擬機棧和本地方法棧
如果線程請求的棧深度大于虛擬機所允許的最大深度,將拋出StackOverflowError異常(單線程下居多)
如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常(多線程下居多)
不考慮虛擬機本身耗費內存、程序計數器內存(很小) 虛擬機棧和本地方法棧分配到的內存 = 進程內存 - 最大堆內存(Xmx)- 最大方法區(MaxPermSize) 所以線程數越多,單個線程內存就越小,成反比2.4.3 方法區和運行時常量池溢出
方法區主要存放Class相關的信息,當使用例如CGLib字節碼增強、動態語言時,容易導致方法區內存溢出
2.4.4 本機直接內存溢出DirectMemory可以通過-XX:MaxDirectMemorySize進行設置,不設置則等同于Heap最大值。
Heap Dump文件中不會看見明顯的異常
如果Dump文件很小,但程序有使用NIO,則可能時本機直接內存溢出
第3章 垃圾收集器與內存分配策略本章討論Heap內存的分配和回收
3.2 對象已死嗎 3.2.1 引用計數算法給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。3.2.2 可達性分析算法很難解決對象之間相互循環引用的問題
這個算法的基本思路就是通過一系列的稱為"GC Roots"的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
可作為CG Root的對象:
虛擬機棧(棧幀中的本地變量表)中引用的對象。
方法區中類靜態屬性引用的對象。
方法區中常量引用的對象。
本地方法棧中JNI(即一般說的Native方法)引用的對象。
3.2.3 再談引用強引用:類似Object() obj = new Object();,只要存在引用,便無法進行垃圾收集
軟引用:描述一些有用但非必需的對象;在系統將要內存溢出前,進行二次收集,如果還是不足,則拋出內存溢出異常。SoftReference
弱引用:描述非必需對象,只能生存到下一次垃圾收集器工作之前,不管內存是否不足。WeakReference
虛引用:無法通過其獲取對象實例,作用時當對被垃圾收集時可以獲取一個系統通知。
3.2.4 生存還是死亡當對象被檢測到沒有與GC Root可達,則將會被第一次標記,如果對象沒有覆蓋finalize(),或者finalize()已經被調用過,則不會執行
對象進入F-Queue,稍后虛擬機自動建立Finalizer線程執行它,僅觸發
GC對F-Queue中的對象進行二次標記,標記前如果對象和GC Root關聯,則可以逃脫
所以主動調用finalize()并不能立即觸發GC,它不是C++中的析構函數3.2.5 回收方法區
永久代收集內容:
廢棄常量 :常量池中沒有被引用的字面量
無用類:
所有實例都被回收
ClassLoader被回收
Class對象沒有被引用
3.3 垃圾收集算法 3.3.1 標記-清除算法(Mark-Sweep)首先標記出所有需要回收的對象
在標記完成后統一回收所有被標記的對象
不足:
效率不夠高,標記和清除兩個效率都不高
空間問題,會產生不連續的碎片內存,
3.3.2 復制算法(Coping)將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。
一塊內存用完,將存活對象復制到另一塊,然后將已使用的對象清除
不用考慮碎片問題,只要移動堆頂指針,按順序分配即可
空間利用率低
現在的商業虛擬機都采用這種收集算法來回收新生代
當復制到另一個Survivor空間不夠用時,需要依賴其他內存(這里指老年代)進行分配擔保(Handle Promotion)
3.3.3 標記-整理算法(Mark-Compact)先標記需要回收的對象
再移動存活對象到一端
最后清理
3.3.4 分代收集算法(Generational Collection)當前商業虛擬機的垃圾收集都采用“分代收集”
根據對象存活周期進行分代
新生代:復制法;大量對象存活時間短 (Eden/Survivor0/Survivor1 : 8/1/1)
老年代:標記清除法、標記整理法;存活時間長
3.4 HotSpot的算法實現 3.4.1 枚舉根節點可達性分析為保證準確性必須在一個保證一致性的快照中進行,所以導致GC進行時需要停頓所有Java線程 -- Stop The World。
CMS收集器中,枚舉根節點時也是必須要停頓的。
HotSpot通過內部實現的OopMap數據結構可以快速且準確地完成GC Roots枚舉,在類加載期和編譯期記錄下對象引用信息,方便GC掃描。
3.4.2 安全點HotSpot只在特定位置設置引用信息 -- 安全點
程序只有在安全點才會停下來執行GC
選定標準“是否具有讓程序長時間執行的特征”,即指令序列復用,例如:方法調用、循環跳轉、異常跳轉等
安全點位置選定還需考慮GC時讓所有線程都進入此
搶先式中斷:在GC發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上。(現在幾乎不采用)
主動式中斷:當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標志,各個線程執行時主動去輪詢這個標志,發現中斷標志為真時就自己中斷掛起。輪詢標志的地方和安全點是重合的
3.4.3 安全區域在線程執行到Safe Region中的代碼時,首先標識自己已經進入了Safe Region,那樣,當在這段時間里JVM要發起GC時,就不用管標識自己為Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者是整個GC過程),如果完成了,那線程就繼續執行,否則它就必須等待直到收到可以安全離開Safe Region的信號為止。
3.5 垃圾收集器 3.5.1 Serial最基本、發展歷史最悠久的收集器
它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束 -- Stop The World
默認Client模式下新生代收集器
3.5.2 ParNewSerial的多線程版本
許多Server模式下首選的新生代收集器
除了Serial收集器外,目前只有它能與CMS收集器配合工作
使用-XX:+UseConcMarkSweepGC選項后的默認新生代收集器,也可以使用-XX:+UseParNewGC選項來強制指定它。
默認開啟的收集線程數與CPU的數量相同
可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
并行(Parallel):指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態。3.5.3 Parallel Scavenge并發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是并行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行于另一個CPU上。
是一個新生代收集器,使用復制算法,并行的多線程收集器
關注的維度不同
CMS考慮停頓時間,適合交互多的程序;
Parallel Scavenge考慮吞吐量,適合高效利用CPU時間的后臺程序
吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
-XX:MaxGCPauseMillis控制最大垃圾收集停頓時間,參數是>0的毫秒數,如果停頓時間減小,吞吐量降低,收集次數增加。
-XX:GCTimeRatio直接設置吞吐量大小,大于0且小于100的整數,垃圾收集時間占總時間的比率,相當于是吞吐量的倒數
-XX:+UseAdaptiveSizePolicy GC自適應調節策略,內存管理調優過程由虛擬機完成,這是與ParNew最大的區別
3.5.4 Serial OldSerial的老年版本
單線程,使用“標記-整理”算法
Client模式下的虛擬機使用
3.5.5 Parallel OldParallel Scavenge收集器的老年代版本
使用多線程和“標記-整理”算法
在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old
3.5.6 CMS (Concurrent Mark Sweep)以獲取最短回收停頓時間為目標
“標記-清除”算法
初始標記(CMS initial mark):Stop The World,僅標記一下GC Roots能直接關聯到的對象
并發標記(CMS concurrent mark):進行GC RootsTracing的過程,可與用戶線程一起工作
重新標記(CMS remark):Stop The World,修正并發標記期間因用戶程序運作導致標記變動的對象標記記錄,時間稍長于初始標記,遠小于并發標記
并發清除(CMS concurrent sweep):可與用戶線程一起工作
缺點:
對CPU資源非常敏感,并發階段會占用一部分線程導致應用變慢,總吞吐量降低
CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現"Concurrent Mode Failure"失敗而導致另一次Full GC的產生。
產生碎片空間可能無法存放當前對象,導致進行Full GC
浮動垃圾:并發清除時用戶線程還在運行,可能在標記過程后產生部分垃圾,只能留到下次GC時清除。3.5.7 G1
面向服務端應用的垃圾收集器
并行與并發:使用多CPU來縮短Stop The World
分代收集:可以獨立管理整個GC堆
空間整合:整體是基于“標記—整理”算法,局部(兩個Region之間)是基于“復制”算法,保證不會產生碎片
可預測的停頓:它將整個Java堆劃分為多個大小相等的獨立區域(Region),跟蹤各個Region的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region,
每個Region內部維護一個Remmbered Set來記錄對象引用信息,后面可以不用通過全堆掃描來收集垃圾
G1的運作步驟:
初始標記(Initial Marking):標記GC Root到直接關聯的對象,修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序并發運行時,能在正確可用的Region中創建新對象,這階段需要停頓線程,但耗時很短。
并發標記(Concurrent Marking):從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序并發執行。
最終標記(Final Marking):修正在并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs里面,最終標記階段需要把Remembered Set Logs的數據合并到Remembered Set中,這階段需要停頓線程,但是可并行執行。
篩選回收(Live Data Counting and Evacuation):首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃,從Sun公司透露出來的信息來看,這個階段其實也可以做到與用戶程序一起并發執行,但是因為只回收一部分Region,時間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率。
3.5.9 垃圾收集器參數總結 3.6 內存分配與回收策略 3.6.1 對象優先在Eden分配大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。
-XX:+PrintGCDetails:在發生垃圾收集行為時打印內存回收日志,并且在進程退出的時候輸出當前的內存各區域分配情況
-Xms20M、-Xmx20M、-Xmn10M這3個參數限制了Java堆大小為20MB,不可擴展,其中10MB分配給新生代,剩下的10MB分配給老年代
-XX:SurvivorRatio=8決定了新生代中Eden區與一個Survivor區的空間比例是8:1
新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快
老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略里就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。
3.6.2 大對象直接進入老年代大對象:需要大量連續內存空間的Java對象;例如:很長的字符串以及數組
經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間
-XX:PretenureSizeThreshold:令大于這個設置值的對象直接在老年代分配,只對Serial和ParNew兩款收集器有效
3.6.3 長期存活的對象將進入老年代對象在Eden出生并經過第一次Minor GC后仍然存活,并且能被Survivor容納的話,對象年齡設為1
對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲
當它的年齡增加到一定程度(默認為15歲),就將會被晉升到老年代中
對象晉升老年代的年齡閾值,可以通過參數-XX:MaxTenuringThreshold設置
3.6.4 動態對象年齡判定如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡
3.6.5 空間分配擔保在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那么會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次Minor GC,盡管這次Minor GC是有風險的;如果小于,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC。
如果出現了HandlePromotionFailure失敗,那就只好在失敗后重新發起一次Full GC
在JDK 6 Update 24之后,這個測試結果會有差異,HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略
JDK 6 Update 24之后的規則變為只要老年代的連續空間大于新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。
第4章 虛擬機性能監控與故障處理工具 jps:虛擬機進程狀況工具JVM Process Status Tool
使用頻率最高的JDK命令行工具
jps[options][hostid] jps可以通過RMI協議查詢開啟了RMI服務的遠程虛擬機進程狀態,hostid為RMI注冊表中注冊的主機名jstat:虛擬機統計信息監視工具
JVM Statistics Monitoring Tool
可以顯示本地或者遠程虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據
jstat[option vmid[interval[s|ms][count]]] interval:查詢間隔 count:次數 #每250毫秒查詢一次進程2764垃圾收集狀況,一共查詢20次 jstat -gc 2764 250 20jinfo:Java配置信息工具
Configuration Info for Java
實時地查看和調整虛擬機各項參數
jinfo[option]pid # 查詢CMSInitiatingOccupancyFraction參數值 $ jinfo -flag CMSInitiatingOccupancyFraction 13435 -XX:CMSInitiatingOccupancyFraction=-1jmap:Java內存映像工具
Memory Map for Java
生成堆轉儲快照(一般稱為heapdump或dump文件)
其他方式獲得dump文件:
-XX:+HeapDumpOnOutOfMemoryError:OOM異常出現之后自動生成dump文件
-XX:+HeapDumpOnCtrlBreak:使用[Ctrl]+[Break]鍵讓虛擬機生成dump文件
kill -3:發送進程退出信號“嚇唬”一下虛擬機,也能拿到dump文件
jhat:虛擬機堆轉儲快照分析工具JVM Heap Analysis Tool
與jmap搭配使用,來分析jmap生成的堆轉儲快照
功能較簡陋
jstack:Java堆棧跟蹤工具Stack Trace for Java
生成虛擬機當前時刻的線程快照(一般稱為threaddump或者javacore文件)
定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等
jstack[option]vmid第5章 調優案例分析與實戰 5.2 案例分析 高性能硬件上的程序部署策略
在大多數網站形式的應用里,主要對象的生存周期都應該是請求級或者頁面級的,會話級和全局級的長生命對象相對很少。只要代碼寫得合理,應當都能實現在超大堆中正常使用而沒有Full GC,這樣的話,使用超大堆內存時,網站響應速度才會比較有保證。
堆外內存導致的溢出錯誤垃圾收集進行時,虛擬機雖然會對Direct Memory進行回收,但是Direct Memory卻不能像新生代、老年代那樣,發現空間不足了就通知收集器進行垃圾回收,它只能等待老年代滿了后Full GC,然后“順便地”幫它清理掉內存的廢棄對象。否則它只能一直等到拋出內存溢出異常時,先catch掉,再在catch塊里面“大喊”一聲:"System.gc()!"。要是虛擬機還是不聽(譬如打開了-XX:+DisableExplicitGC開關),那就只能眼睜睜地看著堆中還有許多空閑內存,自己卻不得不拋出內存溢出異常了。而本案例中使用的CometD 1.1.1框架,正好有大量的NIO操作需要使用到Direct Memory內存。
從實踐經驗的角度出發,除了Java堆和永久代之外,我們注意到下面這些區域還會占用較多的內存,這里所有的內存總和受到操作系統進程最大內存的限制。
Direct Memory:可通過-XX:MaxDirectMemorySize調整大小,內存不足時拋出OutOfMemoryError或者OutOfMemoryError:Direct buffer memory。
線程堆棧:可通過-Xss調整大小,內存不足時拋出StackOverflowError(縱向無法分配,即無法分配新的棧幀)或者OutOfMemoryError:unable to create new native thread(橫向無法分配,即無法建立新的線程)。
Socket緩存區:每個Socket連接都Receive和Send兩個緩存區,分別占大約37KB和25KB內存,連接多的話這塊內存占用也比較可觀。如果無法分配,則可能會拋出IOException:Too many open files異常。
JNI代碼:如果代碼中使用JNI調用本地庫,那本地庫使用的內存也不在堆中。
虛擬機和GC:虛擬機、GC的代碼執行也要消耗一定的內存。
外部命令導致系統緩慢Java的Runtime.getRuntime().exec()方法,首先克隆一個和當前虛擬機擁有一樣環境變量的進程,再用這個新的進程去執行外部命令,最后再退出這個進程。如果頻繁執行這個操作,系統的消耗會很大,不僅是CPU,內存負擔也很重
第7章 虛擬機類加載機制 7.2 類加載的時機加載、驗證、準備、初始化和卸載這5個階段的順序是確定的
解析可以在初始化之后,為了支持Java的運行時綁定(動態綁定)
因為各個階段都是相互交叉地混合式進行,所以不一定按順序完成
虛擬機規范則是嚴格規定了有且只有5種情況必須立即對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始):
遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
7.3 類加載過程 7.3.1 加載通過一個類的全限定名來獲取定義此類的二進制字節流。
將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
在內存中生成一個代表這個類的java.lang.Class對象(并沒有明確規定是在Java堆中,對于HotSpot虛擬機而言,Class對象比較特殊,它雖然是對象,但是存放在方法區里面),作為方法區這個類的各種數據的訪問入口。
7.3.2 驗證目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
驗證的4個階段:
文件格式驗證:驗證字節流是否符合Class文件格式的規范,并且能被當前版本的虛擬機處理。主要目的是保證輸入的字節流能正確地解析并存儲于方法區之內,格式上符合描述一個Java類型信息的要求。這階段的驗證是基于二進制字節流進行的,只有通過了這個階段的驗證后,字節流才會進入內存的方法區中進行存儲,所以后面的3個驗證階段全部是基于方法區的存儲結構進行的,不會再直接操作字節流。
元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規范的要求
字節碼驗證:第三階段是整個驗證過程中最復雜的一個階段,主要目的是通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。在第二階段對元數據信息中的數據類型做完校驗后,這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件
符號引用驗證:校驗發生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作將在連接的第三階段——解析階段中發生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗
7.3.3 準備準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。
這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中
7.3.4 解析解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行
符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。
直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。
7.3.5 初始化真正開始執行類中定義的Java程序代碼(或者說是字節碼)。
初始化階段是執行類構造器<clinit>()方法的過程,初始化類變量和其他資源
7.4 類加載器類加載器在虛擬機外部
7.4.1 類與類加載器每一個類加載器,都擁有一個獨立的類名稱空間;例如:兩個類來源于同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
7.4.2 雙親委派模型(Parents Delegation Model)從Java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現[1],是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都由Java語言實現,獨立于虛擬機外部,并且全都繼承自抽象類java.lang.ClassLoader。
從Java開發人員角度可以大致細分程3種:
啟動類加載器(Bootstrap ClassLoader)[不能直接使用]
<JAVA_HOME>lib
-Xbootclasspath指定目錄
虛擬機識別的類庫
擴展類加載器(Extension ClassLoader),[可直接使用]
<JAVA_HOME>libext
java.ext.dirs系統變量指定的類庫
應用程序類加載器(Application ClassLoader),[可直接使用]
ClassLoader中的getSystemClassLoader()方法的返回值
加載用戶類路徑(ClassPath)上所指定的類庫
程序中默認的類加載器
雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。這里類加載器之間的父子關系一般不會以繼承(Inheritance)的關系來實現,而是都使用組合(Composition)關系來復用父加載器的代碼。
雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
好處就是Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱為java.lang.Object的類,并放在程序的ClassPath中,那系統中將會出現多個不同的Object類,Java類型體系中最基礎的行為也就無法保證,應用程序也將會變得一片混亂。
實現雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()方法之中,先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass()方法,若父加載器為空則默認使用啟動類加載器作為父加載器。如果父類加載失敗,拋出ClassNotFoundException異常后,再調用自己的findClass()方法進行加載。
protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先判斷該類型是否已經被加載 Class c = findLoadedClass(name); if (c == null) { // 如果沒有被加載,就委托給父類加載或者委派給啟動類加載器加載 try { if (parent != null) { // 如果存在父類加載器,就委派給父類加載器加載 c = parent.loadClass(name, false); } else { // 如果不存在父類加載器,就檢查是否是由啟動類加載器加載的類,通過調用本地方法native Class findBootstrapClass(String name) c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 如果父類加載器和啟動類加載器都不能完成加載任務,才調用自身的加載功能 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }第9章 類加載及執行子系統的案例與實戰 9.2.1 Tomcat:正統的類加載器架構
主流Java Web服務器要解決的問題:
部署在同一個服務器上的兩個Web應用程序所使用的Java類庫可以實現相互隔離
部署在同一個服務器上的兩個Web應用程序所使用的Java類庫可以互相共享,如果類庫不能共享,虛擬機的方法區就會很容易出現過度膨脹的風險。
服務器需要盡可能地保證自身的安全不受部署的Web應用程序影響。基于安全考慮,服務器所使用的類庫應該與應用程序的類庫互相獨立。
Tomcat的目錄結構:
/common/*:類庫可被Tomcat和所有的Web應用程序共同使用。
/server/*:類庫可被Tomcat使用,對所有的Web應用程序都不可見。
/shared/*:類庫可被所有的Web應用程序共同使用,但對Tomcat自己不可見。
/WebApp/WEB-INF/*:類庫僅僅可以被此Web應用程序使用,對Tomcat和其他Web應用程序都不可見。
灰色:JDK默認加載器
每一個Web應用程序對應一個WebApp類加載器,每一個JSP文件對應一個Jsp類加載器
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73623.html
摘要:大家好,我是冰河有句話叫做投資啥都不如投資自己的回報率高。馬上就十一國慶假期了,給小伙伴們分享下,從小白程序員到大廠高級技術專家我看過哪些技術類書籍。 大家好,我是...
摘要:虛擬機發展史注本文大部分摘自深入理解虛擬機第二版作為一名開發人員,不能局限于語言規范,更需要對虛擬機規范有所了解。虛擬機規范有多種實現,其中是和中所帶的虛擬機,也是目前使用范圍最廣的虛擬機。世界第一款商用虛擬機。號稱世界上最快的虛擬機。 Java虛擬機發展史 注:本文大部分摘自《深入理解Java虛擬機(第二版)》 作為一名Java開發人員,不能局限于Java語言規范,更需要對Java虛...
摘要:相對于電子書,我更喜歡紙質版的書籍。過去的年一共閱讀過本技術書,下面對這些書做一個小結。源碼深度解析這本書是年購買的,年是第四次閱讀。必知必會數據庫的復習書籍,內容淺顯易懂。 相對于電子書,我更喜歡紙質版的書籍。我喜歡在拿到新書時記錄購買時間、地點、開始閱讀的時間、第一次看完的時間,算是一種學習的記錄。過去的2016年一共閱讀過15本技術書,下面對這些書做一個小結。 《深入理解Java...
摘要:一直都挺喜歡這個社區的,給人的第一感覺就是比較的專業正式,社區內氛圍不錯,各種文章的質量也很好,并且幫助了我很多。很開心能夠來到這里,記錄自己的成長,希望自己能夠多活躍一下,無論是在問答上面還是寫作上面。 一直都挺喜歡 Segmentfault 這個社區的,給人的第一感覺就是比較的專業正式,社區內氛圍不錯,各種文章的質量也很好,并且幫助了我很多。很開心能夠來到這里,記錄自己的成長,希望...
閱讀 1684·2021-11-23 09:51
閱讀 3174·2021-09-26 10:21
閱讀 798·2021-09-09 09:32
閱讀 881·2019-08-29 16:06
閱讀 3308·2019-08-26 13:36
閱讀 772·2019-08-26 10:56
閱讀 2564·2019-08-26 10:44
閱讀 1143·2019-08-23 14:04