《深入理解Java虚拟机(第2版)》-笔记
生活随笔
收集整理的這篇文章主要介紹了
《深入理解Java虚拟机(第2版)》-笔记
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
- 內存區域
- 虛擬機自動內存管理機制
- 運行時數據區域
- jvm在執行java程序過程中,把它所管理的內存劃分為若干個不同的數據區域。
- 1. 程序計數器(Program counter register)
- 當前線程所執行的字節碼的行號指示器(Native方法,則為空)
- 每條線程都需要一個獨立的程序計數器(線程私有內存)
- 是唯一一個在java虛擬機規范中,沒有規定任何OutOfMemoryError情況的區域。
- 2. 虛擬機棧
- 線程私有,生命周期與線程相同
- 描述Java方法執行的內存模型
- 每個方法執行同時,會創建一個棧幀(Stack Frame)
- 每個方法從調用直至完成,就對應一個棧幀在虛擬機棧中入棧到出棧的過程。
- 存儲局部變量表,操作數棧,動態鏈接,方法出口等
- 局部變量表
- 存放編譯期可知的各種基本數據類型,對象引用。
- 64位長度的long和double占用2個局部變量空間,其余數據類型只占用1個
- 當進入一個方法,需要在幀中分配多大的局部變量空間是完全確定的,運行期間不會改變。
- 線程請求棧深度大于虛擬機所允許的深度,StackOverflowError
- 如果擴展時無法申請到足夠的內存,OutOfMemoryError
- 局部變量表
- 3. 本地方法棧
- 與虛擬機棧非常相似,為Native方法服務
- HotSpot將它與虛擬機棧合二為一
- 4. 堆
- 是被所有線程共享的一塊內存區域,在虛擬機啟動時創建,唯一目的就是存放對象實例。
- 隨著JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配,標量替換使所有對象都分配到heap上,變得不那么絕對
- 垃圾收集器管理的主要區域
- 現在GC基本都采用分代收集算法
- 新生代
- Eden
- From Survivor
- To Survivor
- 老年代
- 新生代
- 從內存分配角度,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區(TLAB)
- 現在GC基本都采用分代收集算法
- Heap可以處于物理上不連續的內存空間中,只要邏輯上是連續的即可。
- (-Xmx -Xms),OutOfMemoryError
- 5. 方法區
- 是各個線程共享的內存區域,用于存儲類信息,常量,靜態變量,JIT編譯后的代碼等,alias:Non-heap,堆的一個邏輯部分
- HotSpot上被人稱為永久代(Permanent Generation,將GC分代收集擴展至方法區)
- 這樣更容易遇到內存溢出問題,(-XX:MaxPermSize)
- JDK8中已移除,變為元數據區
- 這個區域的內存回收主要是針對常量池回收和對類型卸載,但收效甚微。
- OutOfMemoryError
- 運行時常量池
- 是方法區的一部分,運行時常量池相對Class文件常量池另一個重要特征:動態性
- 不一定只有編譯期才能產生,運行期間也可能將新的常量放入池中。
- 6. 直接內存
- 并不是虛擬機運行時數據區的一部分
- NIO使用Native庫直接分配堆外內存,然后通過一個Heap中的DirectByteBuffer對象作為這塊內存的引用,避免來回復制數據。
- 各個內存區域大于物理內存限制,從而導致動態擴展時出現OutOfMemoryError。
- 1. 程序計數器(Program counter register)
- jvm在執行java程序過程中,把它所管理的內存劃分為若干個不同的數據區域。
- 對象管理
- java作為一門面向對象的編程語言,在運行過程中無時無刻都有對象被創建出來。
- 為新生對象分配內存
- 指針碰撞(Bump the Pointer)
- 假設堆中內存足夠規整,用過與空閑內存各在一方,分配內存僅僅就是向空閑內存方移動指針。
- 垃圾收集器是否帶有壓縮整理功能,決定堆是否規整。
- Serial,ParNer等帶Compact過程。
- 空閑列表(Free List)
- 維護一個列表,記錄哪些內存塊是可用的
- 使用基于Mark-Sweep算法的收集器,CMS
- 指針碰撞(Bump the Pointer)
- 并發情況下,內存分配
- 采用CAS搭配失敗重試,保證更新操作原子性
- 每個線程預先在堆中分配一塊內存(TLAB 本地線程分配緩沖),用完后,才進行上述同步鎖定(-XX:+/-UseTLAB)
- 內存分配完成后,初始化為零值(實例字段,不賦初值就能使用)
- 對象內存布局
- 1. 對象頭(Header)
- Mark Word
- 32、64bit
- 存儲對象自身的運行時數據(哈希碼,GC分代年齡,鎖狀態標志等)
- 非固定的數據結構,以便在極小的空間內存內存儲盡量多的信息(根據對象的狀態復用自己的存儲空間)
- 類型指針
- 對象指向他的類元數據指針(用來確定對象屬于哪個類的實例)
- 如果對象是一個數組,還要有一塊用于記錄數組長度。
- Mark Word
- 2. 實例數據(Instance Data)
- 對象真正存儲的有效信息
- 無論是從父類繼承下來,還是在子類中定義
- 存儲順序會受到:虛擬機分配策略參數(FieldsAllocationStyle)和字段在源碼中定義順序影響
- 相同寬度的字段總是被分配到一起
- 如果CompactFields參數值為True(默認),子類中較窄的變量可能會插入到父類變量的空隙之中。
- 3. 對齊填充(Padding)
- 并不是必然存在,起著占位符的作用
- Hotspot的自動內存管理系統要求,對象起始地址必須是8字節的整數倍,也就是對象大小必須是8字節的整數倍。
- 對象頭部分正好是8字節的倍數(1或2倍),當實例數據沒有對齊時,通過填充補全。
- 1. 對象頭(Header)
- 對象的訪問定位
- 通過棧上的reference數據來操作堆上的具體對象
- 句柄
- 堆中會劃分出一塊內存作為句柄池,reference存儲句柄地址。
- 直接指針
- 節省一次指針定位的時間開銷。
- Hotspot,使用
- 句柄
- 句柄好處就是reference中存儲句柄地址,對象被移動時,不需要改變reference。
- 通過棧上的reference數據來操作堆上的具體對象
- OutOfMemoryError
- 除了程序計數器,其他運行區域都有可能發生OOM
- 1. Java堆溢出
- java.lang.OutOfMemoryError: Java heap space
- GC Roots到對象之間有可達路徑,避免GC
- 處理方法
- dump出堆轉儲快照
- 判斷出是內存泄露(Memory Leak)
- 找到無法回收原因,定位泄露位置
- 還是內存溢出(Memory Overflow)
- 調大堆參數(-Xmx -Xms)
- 檢查代碼中對象分配是否可以優化
- 2. 虛擬機棧和本地方法棧溢出
- -Xss
- 線程請求的棧深度大于最大深度:StackOverflowError
- 在單線程下,無論是由于棧幀太大還是容量太小,都拋出該異常
- 棧深度達到1000-2000完全沒問題
- 擴展棧無法得到足夠空間:OutOfMemoryError
- 多線程下,每個線程棧分配內存越大,容易出現該異常。
- 每個進程內存有限制,(32bit-Window,2GB),2G-Xmx-MaxPermSize=jvm進程剩余容量
- 通過減少最大堆和減少棧容量,換取更多線程。
- 多線程下,每個線程棧分配內存越大,容易出現該異常。
- 3. 方法區和運行時常量池的溢出
- JDK7移除字符量常量池
- 通過List保持引用,避免回收
- java.lang.OutOfMemoryError: PermGen space
- 動態增強的類可以載入內存中(CGLIB)
- 本機直接內存溢出
- 可通過-XX: MaxDirectMemorySize指定,默認與-Xmx一樣。
- 發生OOM后,Dump文件很小,而程序中使用NIO。
- 垃圾收集器
- 程序計數器,虛擬機棧,本地方法棧,隨線程而生而滅,因為方法結束或者線程結束時,內存自然就跟著回收了。
- Java堆和方法區
- 只有在程序處于運行期間時才能知道會創建哪些對象。
- 內存的分配和回收都是動態的。
- 存活算法
- 引用計數算法
- 給對象添加一個引用計數器,每當一個地方引用它時,計數器值就加1,當引用失效時,計數器值就減1。
- 主流JVM沒有使用引用計數算法
- 它很難解決對象之間互相循環引用的問題。
- 可達性分析算法 reachability Analysis
- 通過一系列“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈Reference Chain。
- 可作為GC Roots的對象
- 1. 虛擬機棧(棧幀中的本地變量表)中引用的對象。
- 2. 方法區中類靜態屬性引用的對象。
- 3. 方法區中常量引用的對象。
- 4. 本地方法棧中JNI引用的對象。
- 引用計數算法
- 引用
- 判斷對象是否存活都與引用相關。
- 如果對象只定義為被引用或沒有被引用兩種狀態,就太過狹隘,描述一些“食之無味,棄之可惜”的對象就顯得無能為力。
- 當內存空間足夠,則保留,空間緊張,則丟棄。
- JDK1.2+
- 1. 強引用
- Object obj = new Object(); 只要強引用存在,則GC永遠不會回收該部分。
- 2. 軟引用
- SoftReference類實現
- 描述還有用但并非必需的對象
- 在系統將要發生內存溢出前,將把這些對象列入回收范圍進行二次回收。
- 3. 弱引用
- WeakReference
- 被弱引用關聯的對象只能生存到下一次GC發生前。GC發生時,都會回收該部分對象。
- 4. 虛引用,幽靈引用,幻影引用
- PhantomReference
- 無法通過虛引用來取得一個對象實例
- 為一個對象設置虛引用關聯的唯一目的,就是能在這個對象被收集器回收時收到一個系統通知。
- 1. 強引用
- 生存還是死亡
- 可達性分析算法中不可達對象,并非非死不可,只是處于緩刑階段。
- 到真正宣告一個對象死亡,至少要經歷兩次標記過程
- 1. 如果對象脫離引用鏈,那它將會被第一次標記并且進行一次篩選
- 篩選條件是此對象是否有必要執行finalize()
- 對象沒有覆蓋finalize(), 或者finalize()已被虛擬機調用過;這兩種情況被視為沒有必要執行。
- 沒必要執行finalize,則回收。
- 篩選條件是此對象是否有必要執行finalize()
- 2. 當被判定為有必要執行finalize(),那么這個對象將會放置在一個F-Queue的隊列中。
- 并在稍后由一個由虛擬機自動建立的,低優先級的Finalizer線程執行(并不承諾等待它運行結束,可能會導致F-Queue其他對象永久等待)它。
- finalize()是對象逃脫死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規模標記,只要對象在finalize中成功拯救自己(重新加入引用鏈)
- 1. 如果對象脫離引用鏈,那它將會被第一次標記并且進行一次篩選
- 一個對象的finalize方法最多只會被系統自動調用一次。
- 自救只有一次機會
- finalize(),是Java誕生時為了使C/C++程序員更容易接受它,所作出的一個妥協,它的運行代價高昂,不確定性大,無法保證各個對象的調用順序。
- 使用try-finally或其他方式可以做的更好。
- 回收方法區
- 在方法區中進行垃圾收集,性價比較低。
- 新生代中,進行一次GC,一般可以回收70%~95%的空間。
- 方法區的回收
- 1.廢棄常量
- 常量池中無其他引用到它。
- 2.無用類
- 同時滿足3個條件,才可以回收(-Xnoclassgc)
- 該類所有實例都已被回收。
- 加載該類的ClassLoader已被回收
- 該類對應的Class對象沒有被引用,無法在任何地方通過反射訪問該類方法。
- 同時滿足3個條件,才可以回收(-Xnoclassgc)
- 1.廢棄常量
- 垃圾回收算法
- 標記清除算法 Mark-Sweep
- 首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。
- 后續的收集算法都是基于這種思路,并對其不足進行改進
- 1. 效率問題
- 2.空間問題:會產生大量不連續的內存碎片。
- 復制算法 Copying
- 將可用內存劃分為大小相等的兩塊,每次只使用其中一塊,這塊內存用完了,就將還存活的對象復制到另一塊,然后這塊內存一次清理掉。
- 代價是將內存縮小為了原來的一半。
- 現在商業虛擬機都采用這種收集算法,來回收新生代。
- 新生代中的對象98%是朝生夕死的
- 將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間。
- 每次使用Eden和一塊Survivor空間,回收時,將存活對象復制到另一塊Survivor上。
- HotSpot默認Eden和Survivor比例為8:1
- 無法保證每次回收都只有不多于10%對象存活,當Survivor不夠用時,需要依賴其他內存(老年代)進行分配擔保
- 缺陷:對象存活率較高,就要進行較多的復制,效率變低。
- 將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間。
- 新生代中的對象98%是朝生夕死的
- 標記-整理算法 Mark-Compact
- 根據老年代的特點,回收時不直接對可回收對象進行整理,而是讓所有存活下來的對象都向一端移動。
- 分代收集算法 Generational Collection
- 當今商業虛擬機的垃圾收集都采用 分代收集算法,根據對象存活周期不同,將內存劃分為幾塊(新生代和老年代)。
- 根據每個年代特點,采用最適當的收集算法。
- 新生代中,大批對象死去,少量存活,選用復制算法。
- 老年代,對象存活率高,沒有額外空間進行擔保,必須使用標記清理或標記整理算法。
- 標記清除算法 Mark-Sweep
- HotSpot算法實現
- 枚舉根節點
- 可作為GC Roots的節點主要在全局性的引用(常量或類靜態屬性)與執行上下文(棧幀中的本地變量表)中。
- 如果逐個檢查引用,必然會消耗很多時間。
- GC停頓
- 可達性分析必須在一個確保一致性的快照中進行
- 看起來就像被凍結在某個時間點上,不可以出現分析過程中對象引用還在不斷變化的情況。
- 是導致GC進行時,必須停頓所有Java執行線程(Stop The World)的其中一個重要原因。
- 可達性分析必須在一個確保一致性的快照中進行
- 主流JVM使用的都是準確式GC
- 并不需要一個不漏地檢查完所有執行上下文和全局的引用變量,有辦法直接得知哪些地方存放著對象引用。
- HotSpot實現中,使用一組OopMap,到了特定的位置記錄著引用。
- 在OopMap協助下,HopSpot可以快速準確的完成GC枚舉,如果每一條指令都生成對應OopMap,那成本會很高。
- 可作為GC Roots的節點主要在全局性的引用(常量或類靜態屬性)與執行上下文(棧幀中的本地變量表)中。
- 安全點 Safepoint
- 解決如何進入GC的問題
- 程序執行時并非在所有地方都能停頓下來進行GC,只有在到達安全點時才能暫停。
- 選點不能太少,也不能過大,以是否具有讓程序長時間執行的特征為標準進行選定。
- 如何在GC發生時,讓所有線程都跑到安全點上停頓下來。
- 1. 搶先式中斷
- GC發生時,首先把所有線程全部中斷,如果發現線程中斷地方不在安全點上,就恢復線程,讓他跑到安全點上。
- 現在幾乎沒有虛擬機采用這種方式。
- 2. 主動式中斷
- 當GC時,僅僅簡單設置一個標志,各個線程主動輪詢這個標志,輪詢標志的地方和安全點是重合的。
- 1. 搶先式中斷
- 安全區域
- Safepoint機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的Safepoint。
- 當程序不執行時,(線程處于Sleep或Blocked狀態時),需要安全區域(Safe Region)
- 安全區域中引用關系不會發生變化,在這個區域中的任意地方開始GC都是安全的。
- 當線程執行到Safe Region時,標識自己進入了Safe Region,那樣,在這段時間里,GC發生,就不用管Safe Region狀態的線程,當離開SR時,線程檢查是否完成GC過程,否則必須等待直到收到可以安全離開SR的信號為止。
- 枚舉根節點
- 垃圾收集器
- 一般都會提供參數供用戶根據自己的應用特點和要去,組合出各個年代所使用的收集器。
- 1. Serial收集器
- 最基本,發展歷史最悠久的收集器
- 是個單線程收集器(不僅僅說明它只會使用一個線程去完成收集工作,還必須暫停其他所有工作線程,直到它收集結束STW)
- 依賴是JVM運行在Client模式下的默認新生代收集器。
- Serial收集器沒有線程交互開銷,專心做垃圾收集,對桌面應用管理內存不大,是個很好選擇。
- 2. ParNew收集器
- Serial的多線程版本 +XX: +UseParNewGC
- 是許多運行在Server模式下的首選新生代收集器。
- 重要原因,是除了Serial外,唯一能和CMS配合工作的。
- 默認開啟的收集線程數與CPU的數量相同。
- 3. Parallel Scavenge 收集器
- 關注點與其他收集器不同,目標是達到一個可控制的吞吐量(CPU用于運行用戶代碼時間與CPU總消耗時間的比值)。
- +XX:MaxGCPauseMillis,控制最大垃圾收集停頓時間
- GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的,導致GC頻繁。
- +XX:GCTimeRation,直接設置吞吐量大小。(0,100)
- 垃圾收集收集占總時間的比率。
- +XX:UseAdaptiveSizePolicy
- 開關參數,打開后,就不需要手工指定新生代大小(-Xmn),Eden與Survivor比例。
- 虛擬機會根據當前系統運行情況收集性能監控信息,動態調整參數,GC自適應調節策略(GC Ergonomics)。
- 吞吐量優先收集器
- 4. Serial Old收集器
- 5. Paraller Old收集器
- Paraller Scavenge老年代版本,JDK6+
- 在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old
- CMS收集器 Concurrent Mark Sweep
- 以獲取最短回收停頓時間為目標的收集器
- 基于標記清除,分為4個步驟
- 1. 初始標記
- STW,標記GC Roots能直接關聯到的對象,速度很快。
- 2. 并發標記
- GC Roots Tracing的過程。
- 3. 重新標記
- STW,為了修正并發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象標記記錄。
- 4. 并發清除
- 1. 初始標記
- 整個過程耗時最長的并發標記和并發清除過程,收集器線程都可以與用戶線程一起工作
- 并發低停頓收集器,3個缺點:
- 1. 對CPU資源非常敏感。
- 雖然不會導致用戶線程停頓,但是會因為占用一部分線程資源而導致應用變慢。
- 為了解決這種情況,提供了增量式并發收集器
- 讓GC線程與用戶線程交替運行,盡量減少GC線程獨占資源時間。
- 效果很一般,已不提倡使用。
- 為了解決這種情況,提供了增量式并發收集器
- 默認回收線程數: (CPU數量+3)/4
- 雖然不會導致用戶線程停頓,但是會因為占用一部分線程資源而導致應用變慢。
- 2. 無法處理浮動垃圾 Floating Garbage
- 并發清理階段,用戶線程還在運行,會有新垃圾不斷產生:浮動垃圾
- GC與用戶進程并行執行,就還需要預留有足夠的內存空間給用戶進程使用
- -XX:CMSInitiatingOccupancyFraction提高觸發百分比
- JDK6+,默認閥值92%,如果預留內存無法滿足需要,出現Concurrent Mode Failure失敗,臨時啟用Serial Old收集器。
- 3. 會有大量空間碎片
- 給大對象分配帶來麻煩,不得不提前觸發Full GC
- -XX:UseCMSCompactAtFullCollection,開關(默認開啟)
- 進行Full GC是,進行內存碎片合并整理。
- -XX:CMSFullGCsBeforeCompaction
- 設置執行多少次不壓縮Full GC后,跟著來一次帶壓縮(默認為0)
- 1. 對CPU資源非常敏感。
- G1收集器 Garbage-First
- 并行與并發
- 利用多個CPU來縮短STW停頓時間
- 分代收集
- 空間整合
- 不產生內存空間碎片。
- 可預測停頓
- 能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
- 可以有計劃的避免在整個堆中進行全區域的GC
- 跟蹤各個Region里垃圾堆積的價值大小,維護優先隊列,根據每次允許的收集時間,優先回收價值最大的Region
- 能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
- G1在堆中內存布局
- 分為多個大小相等的獨立區域(Region),不再是新老生代。
- 還保留新老生代的概率,都是一部分Region的集合(不需要連續)
- 使用每個Region對應一個Remembered Set,保證不對全堆進行掃描。
- 四個階段
- 1. 初始標記
- 2. 并發標記
- 3. 最終標記
- 4. 篩選回收
- 如果追求低停頓,是一個可以嘗試的選擇,追求吞吐量,則不會有什么特別的好處。
- 并行與并發
- 理解GC日志
- 開頭[Full GC,說明此次GC發生STW
- 內存分配與回收策略
- 自動內存管理,給對象分配內存以及回收分配給對象的內存。
- 1. 對象優先在Eden分配
- 當Eden沒有足夠空間進行分配時,JVM發起一次Minor GC。
- -XX:+PrintGCDetails,發生垃圾收集行為時打印內存回收日志
- Minor GC與Full GC/Major GC的區別
- Minor GC發生在新生代上,非常頻繁,回收速度快
- Full GC發生在老年代,一般比Minor GC慢10倍以上。
- 2. 大對象直接進入老年代
- 大對象:需要大量連續內存空間的Java對象,對內存分配來說是個壞消息,提前確保有足夠連續空間安置他們。
- -XX:PretenureSizeThreshold:令大于這個設置值的對象直接在老年代分配。只對Serial和ParNew有效
- 避免Eden與Survivor發生大量內存復制
- 3. 長期存活的對象將進入老年代
- 虛擬機給每個對象定義了一個對象年齡計數器。
- 對象在Eden出生,并經過第一次Minor GC存活到Survivor中,每熬過一次Minor GC年齡就加一
- -XX:MaxTenuringThreshold配置年齡閾值,默認15歲時,就被晉升到老年代中。
- 4. 動態對象年齡判定
- 并不是永遠要求對象年齡必須達到MaxTenuringThreshold才能晉升老年代。
- 如果Survivor空間中相同年齡所有對象大小總和 大于Survivor空間的一半,年齡大于或等于該年齡的對象就直接進入老年代。
- 5. 空間分配擔保
- 在發生Minor GC前,JVM會先檢查老年代最大可用的連續空間是否大于新生代所有對象空間。
- 如果成立,則Minor GC可以確保是安全的
- 如果不成立,JVM查看HandlePromotionFailure設置值是否允許擔保失敗
- 允許,繼續檢查老年代最大可用連續空間是否大于歷次晉升到老年代對象的平均大小。
- 如果大于,將進行一次Minor GC,存在風險
- 風險:老年代要確保還有容納這些對象的剩余空間。
- 小于或HandlePromotionFailure不允許,則改為進行一次Full GC
- 如果大于,將進行一次Minor GC,存在風險
- 允許,繼續檢查老年代最大可用連續空間是否大于歷次晉升到老年代對象的平均大小。
- 取平均值進行比較,是一種動態概率手段
- 如果擔保失敗,則重新發起一次Full GC
- 大部分情況還是會將HandlePromotionFailure開關打開,避免Full GC過于頻繁。
- 在發生Minor GC前,JVM會先檢查老年代最大可用的連續空間是否大于新生代所有對象空間。
- 內存回收和垃圾收集器很多時候都是影響系統性能,并發能力的主要因素之一。
- 類加載機制
- Java里天生可以動態擴展的語言特性,就是依賴運行期間動態加載和動態連接這個特點實現的。
- 面向接口的應用程序,可以等到運行時再指定其實際的實現類。
- 類加載時機
- 類從被加載到虛擬機內存中開始,到卸載出內存為止,生命周期包括7個階段:
- 加載,驗證,準備,初始化和卸載五個階段的順序是確定的。
- 而解析階段則不一定
- 正常在準備階段后開始
- 為了支持Java語言的運行時綁定,在初始化階段后再開始
- 這些階段通常都是互相交叉地混合式進行的,通常會在一個階段執行的過程中調用激活另一個階段。
- 按部就班的開始,不意味著完成后才開始。
- 1. 加載
- 在加載階段,虛擬機需要完成以下3件事
- 1. 通過一個類的全限定名,來獲取定義此類的二進制字節流。
- 許多舉足輕重的Java技術都建立在這一基礎上。
- JAR,reflect,JSP...
- 這個動作放到Java虛擬機外部實現,以便讓應用程序自己決定如何去獲取所需要的類,“類加載器”
- 對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性。
- 比較兩個類是否相等,只有在這個兩個類是由同一個類加載器加載的前提下,才有意義。
- 許多舉足輕重的Java技術都建立在這一基礎上。
- 2. 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
- 3. 在內存中生成一個代表這個類的Class對象,作為方法區這個類的各種數據訪問入口。
- 1. 通過一個類的全限定名,來獲取定義此類的二進制字節流。
- 數組類本身不通過類加載器創建,由虛擬機直接創建,但數組的元素類型最終是要靠類加載器去創建的。
- 加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中
- 然后在內存中實例化一個Class對象,HotSpot存放在方法區中。
- 這個對象將作為程序訪問方法區中的這些類型數據的外部接口。
- 在加載階段,虛擬機需要完成以下3件事
- 2. 驗證
- 確保Class文件的字節流中包含的信息符合當前虛擬機的要求
- 如果輸入字節流不符合約束,則拋出java.lang.VerifyError。
- 4個階段檢驗:
- 1. 文件格式驗證
- 基于二進制字節流,驗證字節流是否符合Class文件格式的規范,并且能被當前版本的虛擬機處理。
- 只有通過了這個階段的驗證后,字節流才會進入內存的方法區中進行存儲。
- 后面的3個階段都是基于方法區的存儲結構進行,不會再直接操作字節流。
- 2. 元數據校驗
- 對字節碼描述的信息進行語義分析。
- 是否繼承final類...
- 對字節碼描述的信息進行語義分析。
- 3. 字節碼驗證
- 最復雜的一個階段 ,通過數據流和控制流分析,確定程序語義是合法的,符合邏輯的。
- Halting Problem:通過程序去校驗程序邏輯是無法做到絕對準確的。
- 由于數據流驗證的高復雜性,JDK6+后,給方法體中新增StackMapTable屬性,記錄應有的狀態,這樣就將類型推導轉變為類型檢查,從而節省時間。
- 4. 符號引用驗證
- 發生在虛擬機將符號引用轉化為直接引用的時候,在解析階段發生。
- 訪問權限是否滿足 ...
- 1. 文件格式驗證
- 如果所運行的全部代碼都已經被反復使用和驗證過,可在實施階段使用-Xverify:none關閉大部分類驗證措施。
- 3. 準備
- 是正式為類變量分配內存并設置類變量初始值的階段
- 將在方法區中進行分配
- 這時候進行內存分配的僅包括類變量(static),不包括實例變量。
- public static int value = 123, 準備階段過后,value值為0,賦值123將在初始化階段執行。
- 若存在ConstantValue屬性,public static final int value=123;將在準備階段被賦值123.
- 是正式為類變量分配內存并設置類變量初始值的階段
- 4. 解析
- 虛擬機將常量池中符號引用替換為直接引用的過程。
- 1. 類或接口解析
- 2.字段解析
- 實現接口,將會按照繼承關系從下往上遞歸搜索。
- 找不到則拋出NoSuchFieldError異常
- 若對字段無權限,則拋出IllegalAccessError
- 3. 類方法解析
- 4. 接口方法解析
- 5. 初始化階段
- 到了初始化階段,才真正開始執行類中定義的Java代碼。
- 初始化階段是執行類構造器<clinit>()方法的過程。
- 由編譯器自動收集類中,所有的類變量的賦值動作和靜態語句塊的語句合并產生的。
- 收集順序由語句在源文件中出現的順序所決定
- 非法向前引用
- 靜態語句塊只能訪問到定義在靜態語句塊之前的變量,定義在它之后的變量,在前面的靜態語句塊可以賦值,但不能訪問。
- 不需要顯示調用父類構造器,虛擬機會保證在子類<clinit>方法執行前,父類的<clinit>已經執行完畢
- 對于類或接口不是必須的。
- 虛擬機保證<clinit>方法在多線程環境下,被正常加鎖同步,其余線程都需要阻塞等待。
- 由編譯器自動收集類中,所有的類變量的賦值動作和靜態語句塊的語句合并產生的。
- 有且只有5種情況,必須立即對類進行初始化(對一個類進行主動引用)
- 1. 遇到new,getstatic,putstatic,invokestatic這4條字節碼指令時。
- 使用new實例對象,讀取或設置一個類靜態變量(被final修飾,在編譯期把結果放入常量池的靜態字段除外),以及調用一個類的靜態方法。
- 2. 使用reflect包方法,對類進行反射調用時,如果類沒有進行過初始化,則需要先觸發其初始化。
- 3. 當初始化一個類,發現父類還未初始化,先觸發其父類初始化
- 4. 當虛擬機啟動,用戶需要指定一個要執行的主類(包含main方法的那個類),先初始化這個主類。
- 5. 使用JDK7的動態語言支持時,java.lang.invoke.MethodHandle,方法的句柄所對應類沒有進行初始化時
- 1. 遇到new,getstatic,putstatic,invokestatic這4條字節碼指令時。
- 被動引用
- 除主動引用外,所有引用類的方法都不會觸發初始化
- 1. 通過子類引用父類的靜態字段,不會導致子類初始化。
- 對于靜態字段,只有直接定義這個字段的類才會初始化。
- HotSpot通過-XX:+TraceClassLoading可觀察到此操作會導致子類的加載。
- 2. 通過數組定義來引用類,不會觸發類的初始化
- 由虛擬機自動生成的,直接繼承于Object的子類,創建動作有字節碼指令newarray觸發。
- 這個[L...數組專屬類,封裝了數組中屬性和方法,是Java中對數組訪問比C/C++相對安全的原因。
- 3. 常量在編譯階段就存入調用類的常量池中,調用時不會觸發初始化
- 編譯階段的常量傳播優化。
- 接口與類初始化場景,存在一處不同
- 當類被初始化時,要求其父類都被初始化過了,而接口初始化時,并無此要求,只有在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化。
- 雙親委派模型
- 從JVM角度,只存在兩種不同的類加載器
- 1.啟動類加載器(Bootstrap ClassLoader),由C++實現。
- 負責將存在<JAVA_HOME>/lib目錄中或-Xbootclasspath的被JVM識別的類庫,加載到虛擬機內存中。
- 無法被Java程序直接引用,如果需要把加載請求委派給它,直接使用null替代即可。
- 2. 所有其他的類加載器,都繼承自ClassLoader,獨立于虛擬機外部。
- 擴展類加載器(Extension ClassLoader)
- 負責加載<JAVA_HOME>\lib\ext目錄中或被java.ext.dirs系統變量所指定的類庫,開發者可直接使用。
- 應用程序類加載器(Application ClassLoader)
- 加載用戶類路徑(ClassPath)上所指定的類庫,如果沒有自定義過自己的類加載器,程序中默認的類加載器。
- 擴展類加載器(Extension ClassLoader)
- 1.啟動類加載器(Bootstrap ClassLoader),由C++實現。
- 除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。
- 這里的類加載器之間的父子關系,一般不會以繼承實現,而是使用組合。
- 并不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類加載器實現方式。
- 工作過程
- 如果一個類加載器收到了類的加載請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類的加載器去完成。
- 因此所有的加載請求都應該傳送到頂層的啟動類加載器中。
- 只有父加載器無法完成這份加載請求,子加載器才會嘗試自己去加載。
- 好處
- Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。
- 與rt .jar類庫中重名的Java類,將會正常編譯,但永遠無法被加載運行。
- 如果父類加載器加載失敗,則拋出ClassNotFoundException
- 破壞雙親委派模型
- 1. 第一次被破壞是在JDK2之前,還未出現雙親委派模型。
- 不提倡用戶覆蓋loadClass方法(實現雙親委派模型),而應當把自己的類加載器寫到findClass方法中。
- 2. 由這個模型自身的缺陷所導致。
- 基礎類又要調用用戶自己的代碼。
- JNDI,線程上下文加載器。通過Thread類的setContextClassLoaser方法設置。
- 如果創建線程時還未設置,它將會從父線程中繼承一個,如果全局范圍都沒有設置,則這個類加載器默認就是應用程序類加載器。
- 父類加載器,請求子類加載器完成類加載動作,所有涉及SPI的加載,如JNDI,JDBC等
- 3. 由于用戶對程序動態性的追求而導致的。
- 1. 第一次被破壞是在JDK2之前,還未出現雙親委派模型。
- 從JVM角度,只存在兩種不同的類加載器
- 除了加載階段用戶程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制。
- Java里天生可以動態擴展的語言特性,就是依賴運行期間動態加載和動態連接這個特點實現的。
- 常用命令行
- 幾乎所有工具的體積基本上都穩定在27kb左右,僅僅是jdk/lib/tools.jar的一層薄封裝。
- jps,JVM Process Status
- jps -lv
- l: 輸出主類的全名
- v: 輸出虛擬機進程啟動時JVM參數
- jstat ,JVM Statistics Monitoring Tool
- 用于監視虛擬機各種運行狀態信息的工具,定位虛擬機性能問題首選工具
- jstat [option vmid [interval[s|ms] [count]] ]
- option: 分為3類:類加載,垃圾收集,運行期編譯狀況
- -gcutil 已使用空間占總空間的百分比
- -gc 監視Java堆狀況
- jstats -gcutil 2032
- jinfo, Configuration Info for Java
- 實時查看和調整虛擬機各項參數,常用于未被顯式指定的參數
- -flag name 未被顯示指定參數
- -sysprops System.getProperties()的信息
- jinfo [option] pid
- jinfo -flag CMSInitiatingOccupancyFraction 123
- jmap, Memory Map for Java
- 用于生成堆轉儲快照(獲取dump文件,查詢finalize隊列,Java堆信息等)
- 可使用-XX: +HeapDumpOnOutOfMemoryError/ -XX:+HeapDumpOnCtrlBreak,生成dump文件
- jmap [option] vmid
- -dump:[live,]format=b, file=<filename>
- -heap,顯示堆詳細信息
- -histo 顯示堆中對象統計信息
- jmap -dump:format=b,file=log.bin 3500
- jhat,JVM Heap Analysis Tool
- 搭配jmap使用,內置微型HTTP/HTML服務器
- 分析工作耗時而且消耗硬件資源的過程,在服務器上使用較少
- jstack,Stack Trace for Java
- 生成虛擬機當前時刻的線程快照,每一條線程正在執行的方法堆集合
- 定位線程出現長時間停頓的原因
- jstack -l vmid
- -l: 除堆棧外,顯示關于鎖的附加信息
- 線程模型
- happens-before
- 判斷數據是否存在競爭,線程是否安全的主要依據。
- 無須任何同步手段保障。
- 8種JMM先行關系
- 程序次序規則,一個線程內,按照控制流順序執行。
- 管程鎖定規則,同一個鎖,unlock先行發生于lock。
- volatile變量規則,volatile變量的寫操作先行于讀。
- 線程啟動規則,線程的start先行于此線程的每個動作。
- 線程終止規則,線程所有操作先行于對此線程的終止檢測(join,isAlive()).
- 線程中斷規則,對線程的interrupt()方法的調用,先行于檢測中斷時間的發生(Thread.interrupted())
- 對象終結規則,對象的初始化完成先行于finalize()方法開始
- 傳遞性,a先于b,b先于c,a必先于c。
- 時間先后順序與先行發生原則沒有太大關系,一切必須以先行發生原則為準。
- Thread類中所有關鍵方法都是Native,實現線程3種方式:
- 線程模型,只對線程的并發規模和操作成本產生影響,對編碼和運行,這些差異都是透明的。
- 1. 使用內核線程(KLT),Multi-Threads-Kernel通過操縱調度器對線程進行調度,并負責將線程任務映射到各個處理器上。
- 程序不直接使用內核線程,使用內核線程的高級接口(輕量級進程Light Weight Process LWP)
- 輕量級進程與內核進程,一對一的線程模型
- 優勢:LWP成為獨立調度單元,單個阻塞也不影響整個進程繼續。
- 缺陷:
- 系統調用代價高,需要在用戶態和內核態中來回切換
- 系統支持輕量級進程的數量是有限的,LWP要消耗一定的內核資源。
- 2. 用戶線程,完全建立在用戶空間線程庫上,系統內核無感知。
- 只要程序實現得當,就不需要切換到內核態,操作非常快速低耗。
- 進程與用戶線程間一對多的線程模型
- 優勢與劣勢都在于有無系統內核的支援。
- 所有的線程操作都需要用戶程序自己處理,異常麻煩。
- Java曾經使用過,已放棄(除了DOS中)
- 3. 用戶進程加輕量級進程混合實現
- 用戶線程仍創建在用戶空間中,通過輕量級進程作為與內核線程的橋梁。
- 可以使用內核提供的線程調度和處理器映射,與用戶線程的廉價操作。
- 用戶線程與輕量級進程間N:M模型
- 4. java線程實現
- JDK1.2之前
- 基于用戶線程實現Green Threads
- JDK1.2+
- 替換為基于操作系統原生線程模型實現
- Windows/Linux 使用一對一的線程模型
- Solaris可配置一對一/多對多模型
- JDK1.2之前
- 線程調度
- 系統為線程分配處理器使用權的過程
- 1. 協同式Cooperative
- 線程使用時間由線程本身控制,線程把自己的事干完后進行線程切換,切換操作對線程自己可知。
- 缺陷:執行時間不可控制,可能導致一直阻塞。
- 2. 搶占式Preemptive
- 每個線程將由系統來分配執行時間,系統可控。
- java使用此。
- 設置優先級,可對分配時間提出建議。
- 10個級別,優先級越高,越容易被選中執行。
- 不靠譜,線程調度最終還是取決于操作系統,每個平臺自身的線程優先級與jvm提供的不對應。
- 優先級推進器(Priority Boosting)
- 5類操作共享的數據
- 1. 不可變Immutable,只要不可變對象被正確的構建出來,并沒有發生this引用逃逸,就一定是線程安全的。
- 2. 絕對線程安全,不意味著調用它時不需要,e.g. Vector多線程remove()
- java API中標注為線程安全的類,大多都不是絕對線程安全的,
- 3. 相對線程安全,通常意義所講的線程安全,對象的單獨操作是線程安全的。
- 大部分線程安全類
- 4. 線程兼容,對象本身并不是線程安全的,通過使用同步手段,達到線程安全。
- 5. 線程對立,無論是否采取同步措施,都無法在并發中的代碼。e.g. suspend/resume()
- 線程安全的實現方法(虛擬機)
- 1. 互斥同步(阻塞同步)
- 臨界區、互斥量、信號量是主要互斥實現方式。
- synchronized
- 會在同步塊前后分別形成monitorenter和monitorexit兩個字節碼指令。
- 需要一個reference類型參數指明要鎖定和解鎖的對象
- 指定對象,對應對象實例,Class對象
- monitorenter鎖計數器加1,exit減1,為0鎖釋放
- 同一線程可重入。虛擬機在未來性能改進中,更加偏向于原生的synchronized。
- 需要一個reference類型參數指明要鎖定和解鎖的對象
- 會在同步塊前后分別形成monitorenter和monitorexit兩個字節碼指令。
- 2. 非阻塞同步
- 隨著硬件指令集的發展,基于沖突檢測的樂觀并發策略。
- 先進行操作,產生了沖突,再采取補償措施(不斷重試)。
- 需要保證操作和沖突檢測具備原子性
- 必須從硬件角度保證一個從語義上看起來需要多次操作的行為只通過一條處理器指令就能完成。
- JDK1.5后,由sun.misc.Unsafe實現CAS操作,無法解決ABA問題,AtomicStampedReference過于雞肋。
- 隨著硬件指令集的發展,基于沖突檢測的樂觀并發策略。
- 3. 無同步方案
- 可重入代碼
- 一個方法,返回結果可預測。
- 線程本地存儲
- e.g. ThreadLocal, 經典web交互模型中,一個請求對應一個服務器線程(Thread-per-Request)
- 可重入代碼
- 1. 互斥同步(阻塞同步)
- 鎖優化
- 高效并發,是從jdk1.5到jdk1.6的一個重要改進,為了在線程之間更高效地共享數據,以及解決競爭問題,從而提高程序的執行效率。
- 自旋鎖與自適應自旋
- 忙循環(自旋)
- 掛起和恢復線程的操作都需要轉入內核態中完成
- 如果物理機上有一個以上處理器,可以讓后面請求鎖的那個線程,稍等一下,但不放棄處理器的執行時間,看看鎖會不會被快速釋放。
- JDK1.6中已默認開啟
- 缺陷
- 需要占用處理器時間,鎖占用時間長,會白白消耗處理器資源。
- JDK1.6之前,自旋次數的默認值是10次,超過后,則掛起線程,可通過-XX:PreBlockSpin更改
- JDK1.6+,引入自適應自旋鎖,時間不在固定,由上一次在同一個鎖上的自旋時間及鎖的擁有者狀態來決定。
- 鎖消除
- 虛擬機即時編譯器在運行時,對一些代碼上要求同步,但被檢測到不可能存在共享數據競爭(逃逸分析)的鎖進行消除。
- jdk1.5之前,String的+拼接使用StringBuffer(含有同步塊鎖),1.5+,改為StringBuilder
- 鎖粗化
- 虛擬機探測到對同一對象零碎加鎖,會擴大加鎖同步的范圍。
- 輕量級鎖
- 1.6中加入的新型鎖機制,傳統使用操作系統互斥量來實現的被稱為重量級鎖。
- HotSpot對象頭,與對象自身定義數據無關的額外存儲成本。
- Mark Word
- 指向方法區對象類型數據的指針
- 代碼進入同步塊后,如果同步對象沒被鎖定,虛擬機在當前線程的棧幀中建立Lock Record,用于存儲鎖對象目前的Mark Word,即Displaced Mark Word
- 然后,虛擬機使用CAS操作,嘗試將對象的Mark Word更新為指向Lock Record的指針
- 更新成功,則加鎖成功
- 更新失敗
- 對象的Mark Word是否指向當前線程的棧幀,如果是,則說明當前線程已擁有這個對象的鎖,直接進入同步塊
- 否,說明鎖對象被其他線程搶占,膨脹為重量級鎖(多個線程爭用同一個鎖)
- 解鎖也依賴于CAS
- 將對象當前的Mark Word和Displaced Mark Word替換回來,如果失敗,說明其他線程嘗試獲取該鎖,那么就要在釋放鎖的同時,喚醒被掛起的線程??
- 對于絕大部分的鎖,在整個同步周期內部是不存在競爭的。
- 在無競爭的情況下使用CAS操作去消除同步使用的互斥量。
- 如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,輕量級鎖會比傳統重量級鎖更慢。
- 偏向鎖
- 在無競爭的情況下,把整個同步都消除掉。
- 鎖偏向于第一個獲得它的線程,JDK1.6默認開啟,-XX:+UseBiasedLocking
- 使用CAS將獲取到鎖的線程ID記錄在Mark Word中,并標記為偏向鎖,持有偏向鎖的線程以后每次進入這個鎖相關的同步塊,都不再進行任何同步操作。
- 另一個線程嘗試獲取這個鎖時,偏向模式結束,撤銷偏向revoke bias,恢復到未鎖定或輕量級鎖。
- 效益權衡Trade Off
- 能夠寫出高伸縮性的并發程序是一門藝術。
- happens-before
總結
以上是生活随笔為你收集整理的《深入理解Java虚拟机(第2版)》-笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CSS框架解析
- 下一篇: 买卖股票的最佳时机 II Java (贪