摘記《深刻理解Java虛擬機:JVM高級特性與最佳實踐(第2版)》

第2章 Java內存區域與內存溢出異常

2.2 運行時數據區域

Java虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。根據《Java虛擬機規範(Java SE 7版)》的規定,Java虛擬機所管理的內存將會包括如下幾個運行時數據區域: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分代年齡信息等
  • 執行<init>方法,初始化對象。

2.3.2 對象的內存佈局

HotSpot VM中,對象在內存中的佈局:多線程

  • 對象頭(Header)架構

    1. Mark Word。存儲運行時數據;如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等
    2. 類型指針。即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。
  • 實例數據(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 生存仍是死亡

  1. 當對象被檢測到沒有與GC Root可達,則將會被第一次標記,若是對象沒有覆蓋finalize(),或者finalize()已經被調用過,則不會執行
  2. 對象進入F-Queue,稍後虛擬機自動創建Finalizer線程執行它,僅觸發
  3. 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 ParNew

  • Serial的多線程版本
  • 許多Server模式下首選的新生代收集器
  • 除了Serial收集器外,目前只有它能與CMS收集器配合工做
  • 使用-XX:+UseConcMarkSweepGC選項後的默認新生代收集器,也可使用-XX:+UseParNewGC選項來強制指定它。
  • 默認開啓的收集線程數與CPU的數量相同
  • 可使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
並行(Parallel):指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。

併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另外一個CPU上。

圖片描述

3.5.3 Parallel Scavenge

  • 是一個新生代收集器,使用複製算法,並行的多線程收集器
  • 關注的維度不一樣

    • CMS考慮停頓時間,適合交互多的程序;
    • Parallel Scavenge考慮吞吐量,適合高效利用CPU時間的後臺程序
吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)

虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

  • -XX:MaxGCPauseMillis控制最大垃圾收集停頓時間,參數是>0的毫秒數,若是停頓時間減少,吞吐量下降,收集次數增長。
  • -XX:GCTimeRatio直接設置吞吐量大小,大於0且小於100的整數,垃圾收集時間佔總時間的比率,至關因而吞吐量的倒數
  • -XX:+UseAdaptiveSizePolicy GC自適應調節策略,內存管理調優過程由虛擬機完成,這是與ParNew最大的區別

3.5.4 Serial Old

  • Serial的老年版本
  • 單線程,使用「標記-整理」算法
  • Client模式下的虛擬機使用

圖片描述

3.5.5 Parallel Old

  • Parallel 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 20

圖片描述

jinfo:Java配置信息工具

  • Configuration Info for Java
  • 實時地查看和調整虛擬機各項參數
jinfo[option]pid

# 查詢CMSInitiatingOccupancyFraction參數值
$ jinfo -flag CMSInitiatingOccupancyFraction 13435
-XX:CMSInitiatingOccupancyFraction=-1

jmap: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種狀況必須當即對類進行「初始化」(而加載、驗證、準備天然須要在此以前開始):

  1. 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。生成這4條指令的最多見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  2. 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。
  3. 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。
  4. 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  5. 當使用JDK 1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。

7.3 類加載過程

7.3.1 加載

  1. 經過一個類的全限定名來獲取定義此類的二進制字節流。
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  3. 在內存中生成一個表明這個類的java.lang.Class對象(並無明確規定是在Java堆中,對於HotSpot虛擬機而言,Class對象比較特殊,它雖然是對象,可是存放在方法區裏面),做爲方法區這個類的各類數據的訪問入口。

7.3.2 驗證

  • 目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。

驗證的4個階段:

  1. 文件格式驗證:驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。主要目的是保證輸入的字節流能正確地解析並存儲於方法區以內,格式上符合描述一個Java類型信息的要求。這階段的驗證是基於二進制字節流進行的,只有經過了這個階段的驗證後,字節流纔會進入內存的方法區中進行存儲,因此後面的3個驗證階段所有是基於方法區的存儲結構進行的,不會再直接操做字節流。
  2. 元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求
  3. 字節碼驗證:第三階段是整個驗證過程當中最複雜的一個階段,主要目的是經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。在第二階段對元數據信息中的數據類型作完校驗後,這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的事件
  4. 符號引用驗證:校驗發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動做將在鏈接的第三階段——解析階段中發生。符號引用驗證能夠看作是對類自身之外(常量池中的各類符號引用)的信息進行匹配性校驗

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類加載器
相關文章
相關標籤/搜索