《深入理解Java虛擬機器》-筆記舊版__Java
來源:互聯網
上載者:User
第2章 Java記憶體地區與記憶體溢出異常
2.1 概述 為什麼要瞭解記憶體管理。 正是因為Java程式員把記憶體控制的權利交給了Java虛擬機器,一旦出現記憶體流失和溢出方面的問題,如果不瞭解虛擬機器是怎樣使用記憶體的,那麼排查錯誤將會成為一項異常艱難的工作。 記憶體溢出和記憶體流失的區別。 http://blog.csdn.net/buutterfly/article/details/6617375 記憶體溢出 out of memory,是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就是記憶體溢出。 記憶體泄露memory leak,是指程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體泄露危害可以忽略,但記憶體泄露堆積後果很嚴重,無論多少記憶體,遲早會被佔光。 memory leak會最終會導致out of memory。
2.2 運行時資料區域 http://blog.csdn.net/u012152619/article/details/46968883 2.2.1 程式計數器 程式計數器是幹什麼的。 位元組碼解譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令, 分支、迴圈、跳轉、異常處理、線程恢複等基礎功能都需要依賴這個計數器來完成。 為了線程切換後能恢複到正確的執行位置,每條線程都需要有一個獨立的程式計數器, 各條線程之間計數器互不影響,隔離儲存區 (Isolated Storage),我們稱這類記憶體地區為“線程私人”的記憶體。 如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令地址。 簡單的回答:幾乎不佔有記憶體。用於取下一條執行的指令。 2.2.2 Java虛擬機器棧 棧也是線程私人的,它的生命週期與線程相同。 Java虛擬機器棧是幹什麼的。 它為Java方法服務。虛擬機器棧描述的是Java方法執行的記憶體模型: 每個方法在執行的同時都會建立一個棧幀(Stack Frame)用於儲存局部變數表、運算元棧、動態連結、方法出口等資訊。 每一個方法從調用到執行完成的過程,就對應著一個棧幀的虛擬機器棧中入棧到出棧的過程。 局部變數表存放了編譯期可知的各種基礎資料型別 (Elementary Data Type)(boolean、byte、char、short、int、float、long、double)、對象引用和returnAddress類型(指向了一條位元組碼指令的地址)。 局部變數表所需的記憶體空間在編譯期間完成分配。 簡單回答:每個線程執行每個方法的時候都會在棧中申請一個棧幀,每個棧幀包括局部變數區和運算元棧,用於存放此次方法調用過程中的臨時變數、參數和中間結果。 2.2.3 本地方法棧 本地方法棧(Native Method Stack)與虛擬機器棧所發揮的作用是非常相似的,他們之間的區別不過是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務。 簡單回答:用於支援native方法的執行,儲存了每個native方法調用的狀態。 Sun HotSpot把虛擬機器棧和本地方法棧合二為一。 2.2.4 Java堆 Java堆是被所有線程共用的一塊記憶體地區,在虛擬機器啟動的時候建立。此記憶體地區的唯一目的就是存放對象執行個體,幾乎所有的對象執行個體都在這裡分配記憶體。 Java堆是垃圾收集器管理的主要區域。 現在收集器基本都採用分代收集演算法,所以Java堆中可以細分為:新生代和老年代; 再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。 進一步劃分的目的是為了更好地回收記憶體,或者更快地分配記憶體。
在 Java 中,堆被劃分成兩個不同的地區:新生代 ( Young )、老年代 ( Old )。 新生代 ( Young ) = 1/3 的堆空間大小。老年代 ( Old ) = 2/3 的堆空間大小。 新生代 ( Young ) 又被劃分為三個地區:Eden、From Survivor、To Survivor。 Edem : from : to = 8 :1 : 1 這樣劃分的目的是為了使 JVM 能夠更好的管理堆記憶體中的對象,包括記憶體的分配以及回收。 JVM 每次只會使用 Eden 和其中的一塊 Survivor 地區來為物件服務,所以無論什麼時候,總是有一塊Survivor地區是空閑著的。 因此,新生代實際可用的記憶體空間為 9/10 ( 即90% )的新生代空間。 2.2.5 方法區 方法區在一個jvm執行個體的內部 方法區(Area Method)與Java堆一樣,是各個線程共用的記憶體地區,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的代碼等資料。 這地區的記憶體回收目標主要是針對常量池的回收和對類型的卸載。 2.2.6 運行時常量池 運行時常量池(Runtime Constant Pool)是方法區的一部分。用於存放編譯期產生的各種字面量和符號引用,這部分內容將在類載入後進入方法區的運行時常量池中存放。 2.2.7 直接記憶體
2.3 HotSpot虛擬機器對象探秘 主要講述虛擬機器記憶體中的資料,如何建立、如何布局以及如何訪問。 探討了HotSpot虛擬機器在Java堆中對象分配、布局和訪問的全過程。 2.3.1 對象的建立 對象所需記憶體的大小在類載入完成後便可完全確定,為對象分配空間的任務等同於把一塊確定大小的記憶體從Java堆中劃分出來。 對象建立的時候,需要考慮如何劃分可用空間。 記憶體配置完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值。這一步操作保證了對象的執行個體欄位在Java代碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料類型所對應的零值。 執行new指令之後會接著執行<init>方法,把對象按照程式員的意願進行初始化,這樣一個真正可用的對象才算完全產生出來。 2.3.2 對象記憶體布局 在HotSpot虛擬機器中,對象在記憶體中儲存的布局可以分為3塊地區:對象頭(Header)、執行個體資料(Instance Data)和對齊填充(Padding)。 2.3.3 對象的訪問定位 目前主流的訪問方式有使用控制代碼和直接指標兩種。
2.4 實戰OutOfMemoryError異常
2.4.1 Java堆溢出
2.4.2 虛擬機器棧和本地方法棧溢出
2.4.3 方法區和運行時常量池溢出
2.4.4 本機直接記憶體溢出
第3章 垃圾收集器與記憶體配置策略
3.1 概述 GC需要完成3件事: 1.哪些記憶體需要回收 2.什麼時候回收 3.如何回收
為什麼要去瞭解GC和記憶體配置呢。 當需要排查各種記憶體溢出、記憶體流失問題時,當垃圾收整合為系統達到更高並發量的瓶頸時, 我們就需要對這些“自動化”的技術實施必要的監控和調節。
記憶體回收針對的是Java堆和方法區
3.2 對象已死嗎 “死去”的對象才需要回收。如何確定一個對象是否“死去”。 3.2.1 引用計數演算法 演算法思路:給對象添加一個引用計數器,每當有一個地方引用它,計數器值就加1; 當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。 主流的Java虛擬機器裡面沒有選用引用計數器演算法來管理記憶體,其中最主要的原因是 它很難解決對象之間相互循環參考的問題。
3.2.2 可達性分析演算法 主流的商用程式語言,使用這種演算法。 演算法思路:通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是停用。
哪些對象可以作為GC Roots呢。 1.虛擬機器棧(棧幀中的本地變數表)中引用的對象 2.方法區中類靜態屬性引用的對象 3.方法區中常量引用的對象 4.本地方法棧中JNI(即一般說的Native方法)引用的對象 總結就是,方法運行時,方法中引用的對象;類的靜態變數引用的對象;類中常量引用的對象;Native方法中引用的對象。
3.2.3 再談引用
參考型別: 1.強引用(Strong Reference) 只要存在強引用,就不會回收。類似“Object obj = new Object()” 2.軟引用(Soft Reference) 3.弱引用(Weak Reference) 4.虛引用(Phantom Reference)
3.2.4 生存還是死亡 即使在可達性分析演算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經曆兩次標記過程:如果對象在進行可達性分析後發現沒有與GC Roots相串連的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finallize()方法,或者finalize()方法已經被虛擬機器調用過,虛擬機器將這兩種情況都視為“沒有必要執行”。
3.2.5 回收方法區 方法區(或者HotSpot虛擬機器中的永久代)的垃圾收集主要回收兩部分:廢棄常量和無用的類。 回收廢棄常量和回收Java堆中的對象非常類似。比如回收常量池中的字面量,字串“abc”,當前系統沒有任何一個String對象引用常量池中的“abc”常量,它就會被回收。常量池中的類(介面)、方法、欄位的符號引用也與此類似。
判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。“無用的類”要滿足下面3個條件: 1.該類所有的執行個體都被回收 2.載入該類的ClassLoader已經被回收 3.該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
3.3 垃圾收集演算法 介紹演算法的思想和發展過程
3.3.1 標記-清除演算法 演算法分“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。
演算法有兩個不足: 1.效率問題,標記和清除兩個過程的效率都不高 2.空間問題,標記清除之後會產生大量不連續的記憶體片段,空間片段太多可能會導致以後在程式運行過程中需要分配較大對象時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
3.3.2 複製演算法 用於回收新生代 為瞭解決效率問題,“複製”收集演算法出現。它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。 當這一塊的記憶體用完了,就將還存活著的對象複製到另外一塊上面,然後再把以使用過的記憶體空間一次清理掉。 代價是將記憶體縮小為了原來的一半。
3.3.3 標記-整理演算法 用於回收老年代 思路:分成“標記”、“整理”、“清除”3個步驟 1.首先標記出所有需要回收的對象 2.讓所有存活的對象都向一端移動 3.直接清理掉端邊界意外的記憶體
3.3.4 分代收集演算法 根據對象存活周期的不同將記憶體劃分為新生代和老生代。 在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製演算法, 只需要付出少量存活對象的複製成本就可以完成收集。 老年代中因為獨享存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或者“標記-整理”。
3.4 HotSpot的演算法實現
3.4.1 枚舉根節點 當執行系統停頓下來後,並不需要一個不漏地檢查完所有執行內容和全域的引用位置,虛擬機器應當是有辦法直接得知哪些地方存放著對象引用。
3.4.2 安全點 程式運行時並非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。 安全點的選定基本上是以程式“是否具有讓程式長時間執行的特徵”為標準選定的。“長時間執行”的最明顯特徵就是指令序列服用,例如方法調用、迴圈跳轉、異常跳轉等,所以具有這些功能的指令才會產生Sagepoint。
3.4.3 安全區域
3.5 垃圾收集器
3.6 記憶體配置與回收策略 Java自動記憶體管理就是自動化的解決兩個問題: 1.給對象分配記憶體 2.回收分配給對象的記憶體
3.6.1 對象優先在Eden分配 大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機器將發起一次MinorGC。
3.6.2 大對象直接進入老年代 虛擬機器提供了一個-XX:PretenureSizeThreshold參數,令大於這個設定值的對象直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複製(因為,新生代採用複製演算法收集記憶體)。
3.6.3 長期存活的對象將進入老年代 如果對象的Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設為1。對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(預設15歲),就將會被晉陞到老年代中。
3.6.4 動態對象年齡判斷 如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。
3.6.5 空間分配擔保
第6章 類檔案結構
6.1 概述 我們編寫的程式編譯成二進位本地機器碼(Native Code)
6.2 無關性的基石 Java虛擬機器不和包括Java在內的任何語言綁定,它只與“Class檔案”這種特定的二進位檔案格式所關聯,Class檔案中包含了Java虛擬機器指令集和符號表以及若干其他輔助資訊。
6.3 Class類檔案的結構 Class檔案格式採用一種類似於C語言結構體的偽結構來儲存資料,這種偽結構只有兩種資料類型:無符號數和表。 1.無符號數屬於基本的資料類型 2.表是有多個無符號數或者其他表作為資料項目構成的複合資料型別,所有表都習慣已“_info”結尾。 表用於描述有層次關係的複合結構的資料。
6.3.1 魔數與Class檔案的版本 每個Class檔案的頭4個位元組稱為魔數(Magic Number),它的唯一作用就是確定這個檔案是否為一個能被虛擬機器接收的Class檔案。 第5和第6位元組是次版本號碼(Minor Version) 第7和第8位元組是主要版本號(Major Version) 高版本的JDK能向下相容以前版本的Class檔案,不能向上相容。
6.3.2 常量池 緊接著主次版本號碼之後的常量池入口。 首先,是一項u2類型的資料,代表常量池榮祥計數值(constant_pool_count)。 常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References): 1.字面量:比較接近Java語言層面的常量概念,如文本字串、聲明為final的常量值等 2.符號引用:屬於編譯原理方面的概念,包括以下3類常量: a.類和介面的全限定名(Fully Qualified Name) b.欄位的名稱和描述符(Descriptor) c.方法的名稱和描述符 在Class檔案中不會儲存各個方法、欄位的最終記憶體布局資訊,因此這些欄位、方法的符號引用 不經過運行期轉換的話無法得到真正的記憶體入口地址,也就無法直接被虛擬機器使用。
6.3.3 訪問標誌 這個標誌用於識別一些類或者介面層次的訪問資訊,包括:這個Class是類還是介面;是否定義為public類型;是否定義為abstract類型;如果是類的話,是否被聲明為final等。
6.3.4 類索引、父類索引與介面索引集合 Class檔案中由這三項資料來確定這個類的繼承關係。 1.類索引:用於確定這個類的全限定名 2.父類索引:用於確定這個類的父類的全限定名 3.介面索引集合:用於描述這個類實現了哪些介面
6.3.5 欄位表集合 欄位(field)包括類級變數以及執行個體級變數,但不包括在方法內部聲明的局部變數。
6.3.6 方法表集合
6.3.7 屬性工作表集合
6.4 位元組碼指令簡介
6.5 公有設計和私人實現