主頁 > 後端開發 > JVM | 第1部分:自動記憶體管理與性能調優《深入理解 Java 虛擬機》

JVM | 第1部分:自動記憶體管理與性能調優《深入理解 Java 虛擬機》

2022-03-25 06:26:40 後端開發

目錄
  • 前言
  • 1. 自動記憶體管理
    • 1.1 JVM運行時資料區
    • 1.2 Java 記憶體結構
    • 1.3 HotSpot 虛擬機創建物件
    • 1.4 HotSpot 虛擬機的物件記憶體布局
    • 1.5 訪問物件
  • 2. 垃圾回收與記憶體分配
    • 2.1 判斷物件是否存活
    • 2.2 分代與記憶體分配、回收策略
    • 2.3 垃圾回收演算法(GC 的演算法)
    • 2.4 HotSpot 的演算法實作
    • 2.5 垃圾收集器
  • 3. JVM 引數配置
    • 3.1 JVM 記憶體引數簡述
    • 3.2 JVM 的 GC 收集器設定
  • 4. JVM 性能調優案例分析
    • 4.1 大記憶體硬體上的應用程式部署策略
    • 4.2 集群間同步導致的記憶體溢位
    • 4.3 堆外記憶體導致的溢位錯誤
    • 4.4 外部命令導致系統緩慢
    • 4.5 服務器虛擬機行程崩潰
    • 4.6 不恰當資料結構導致記憶體占用過大
    • 4.7 由 Windows 虛擬記憶體導致的長時間停頓
    • 4.8 由安全點導致長時間停頓
    • 4.9 調優總結
  • 最后


前言

參考資料
《深入理解 Java 虛擬機 - JVM 高級特性與最佳實踐》

第1部分主題為自動記憶體管理,以此延伸出 Java 記憶體區域與記憶體溢位、垃圾收集器與記憶體分配策略、引數配置與性能調優等相關內容;

第2部分主題為虛擬機執行子系統,以此延伸出 class 類檔案結構、虛擬機類加載機制、虛擬機位元組碼執行引擎等相關內容;

第3部分主題為程式編譯與代碼優化,以此延伸出程式前后端編譯優化、前端易用性優化、后端性能優化等相關內容;

第4部分主題為高效并發,以此延伸出 Java 記憶體模型、執行緒與協程、執行緒安全與鎖優化等相關內容;

本系列學習筆記可看做《深入理解 Java 虛擬機 - JVM 高級特性與最佳實踐》書籍的縮減版與總結版,想要了解細節請見紙質版書籍;


1. 自動記憶體管理

1.1 JVM運行時資料區

JVM運行時資料區

  • 執行緒共享資料區
    • 方法區(Non-Heap 非堆):存盤已被 Java 虛擬機加載的類資訊常量靜態變數即時編譯器編譯后的代碼等資料,當方法區無法滿足記憶體分配需求時,拋出 OutOfMemoryError 例外;
      • 運行時常量池:存放編譯期生成的各種字面量和符號參考;
    • Java 堆(Java Heap):記憶體中最大的一塊,存放物件實體和陣列,是垃圾收集器管理的主要區域,可能劃分出多個執行緒私有的分配緩沖區,目的是為了更好的回收記憶體,或者更快的分配記憶體,可以處于物理上不連續的記憶體空間中,沒有記憶體可以完成實體分配,并且堆也無法再擴展時,將會拋出 OutOfMemoryError 例外;
  • 執行緒獨立資料區
    • 程式計數器(Program Counter Register 執行緒計數器):執行緒正在執行 Java 方法時,保存虛擬機位元組碼指令的地址,否則 Undefined,Java 虛擬機的多執行緒是通過執行緒輪流切換并分配處理器執行時間的方式來實作的,每條執行緒都有一個獨立的程式計數器,各個執行緒之間計數器互不影響,獨立存盤,是虛擬機中唯一沒有規定 OutOfMemoryError 情況的區域;
    • 本地方法堆疊(Native Method Stack):為虛擬機使用到的 Native 方法服務,原始碼是 C 和 C++;
    • Java 虛擬機堆疊(Java Virtual Machine Stacks):生命周期和執行緒一致,存盤 Java 方法執行的記憶體模型:每個方法在執行的同時都會創建一個堆疊幀(Stack Frame)用于存盤區域變數表運算元堆疊動態鏈接方法出口等資訊;
      • 區域變數表:存放方法引數和方法內定義的區域變數,存放編譯期可知的各種基本型別(boolean、byte、char、short、int、float、long、double)、物件參考(reference 型別,能找到物件在 Java 堆中的資料存放的起始地址索引,與物件所屬資料型別在方法區中存盤的型別資訊)、returnAddress型別(指向了一條位元組碼指令的地址),執行緒安全,通過索引定位方式使用區域變數表,容量以變數槽(slot)為最小單位;
        • slot 可以復用,但可能會導致 GC 問題:大物件復用時會作為 GC Roots的一部分,當它的其中一個區域變數超過作用域時,理應回收大物件,但由于 slot 復用保持著大物件的參考,導致 GC 無法回收;
      • 運算元堆疊:運算元堆疊是用來操作的,堆疊中的資料元素必須與位元組碼指令的序列嚴格匹配,在概念模型中,兩個堆疊幀是相互獨立的;在實際實作中,上下兩個堆疊幀可能會出現一部分重疊,以實作資料共享;
      • 動態鏈接:鏈接到別的方法中去,用來存盤鏈接的地方,動態體現在:在每一次運行期間轉化為直接參考,而不是第一次類加載階段(靜態決議);
      • 方法出口:有兩種出口:
        • 正常 return:方法呼叫者的程式計數器的值可以作為回傳地址;
        • 不正常拋出例外:需要通過例外處理表來確定出口;
      • 附加資訊:虛擬機規范允許具體的虛擬機實作增加一些規范里沒有描述的資訊到堆疊幀中,例如與除錯相關的資訊,由虛擬機自行實作;

幀堆疊

1.2 Java 記憶體結構

Java 記憶體結構

  • 直接記憶體(Direct Memory):非虛擬機運行時資料區的部分,不是 Java 虛擬機規范中定義的記憶體區域;
    • 應用:JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的 I/O 方式,可以使用 Native 函式庫直接分配堆外記憶體,然后使用一個存盤在 Java 堆中的 DirectByteBuffer 物件作為這塊記憶體的參考進行操作,避免了在 Java 堆和 Native(本地)堆中來回復制資料;
    • 記憶體區域總和大于物理記憶體限制從而導致動態擴展時出現 OutOfMemoryError 例外;
  • 直接記憶體與堆記憶體的區別
    • 直接記憶體:ByteBuffer.allocateDirect();
    • 非直接記憶體:ByteBuffer.allocate();
    • 直接記憶體申請空間耗費高性能,堆記憶體申請空間耗費比較低;
    • 直接記憶體的 IO 讀寫的性能優于堆記憶體,在多次讀寫操作的情況相差非常明顯;
  • JVM 位元組碼執行引擎:核心組件,負責執行虛擬機的位元組碼;
  • 垃圾收集系統:垃圾收集系統是 Java 的核心,垃圾指沒有參考指向的記憶體物件;

1.3 HotSpot 虛擬機創建物件

  • 1. 判斷是否加載:遇到 new 指令時,首先檢查這個指令的引數是否能在常量池中定位到一個類的符號參考,并且檢查這個符號參考代表的類是否已經被加載、決議和初始化過,如果沒有,執行相應的類加載;
  • 2. 分配記憶體:類加載檢查通過之后,為新物件分配記憶體(在堆里,記憶體大小在類加載完成后便可確認),在堆的空閑記憶體中劃分一塊區域(有兩種:‘指標碰撞’——serial、ParNew 演算法;‘空閑串列’——CMS 演算法);
    • 這里可能會有并發執行緒安全問題,多個個執行緒同時分配同一塊記憶體,兩種解決方法:對分配記憶體的動作進行同步處理(采用 CAS 配上失敗重試保證原子操作),或者采用:根據執行緒不同劃分不同的記憶體緩沖區執行記憶體分配操作;
  • 3. 初始化值:記憶體空間分配完成后會初始化為 0(不包括物件頭),然后填充物件頭(哪個類的實體、何找到類的元資料資訊、哈希碼、GC 分代年齡等);
  • 4. 執行 init 方法:賦實際值,程式員可控;

1.4 HotSpot 虛擬機的物件記憶體布局

  • 物件頭(Header):包含兩部分:
    • 用于存盤物件自身的運行時資料:哈希碼、GC 分代年齡、鎖狀態標志、執行緒持有的鎖、偏向執行緒 ID、偏向時間戳等;
    • 型別指標:物件指向它的類的元資料指標,確定是哪個類的實體;
    • 陣列在物件頭中還必須有一塊用于記錄陣列長度的資料(普通物件可以通過元資料確定大小);
  • 實體資料(Instance Data):程式代碼中所定義的各種型別的欄位內容(包含父類繼承下來的和子類中定義的)
  • 對齊填充(Padding):不是必然需要,主要是占位,保證物件大小是某個位元組的整數倍;

1.5 訪問物件

  • 通過堆疊上的 reference 資料(在 Java 堆中)來操作堆上的具體物件:
    • reference 存盤的是句柄地址:好處是在物件移動(GC)時只改變實體資料指標地址;
    • reference 中直接存盤物件地址:好處是速度快(只需一次指標尋址);

通過句柄訪問物件
通過直接指標訪問物件


2. 垃圾回收與記憶體分配

垃圾回識訓制的缺點:是否執行,什么時候執行卻是不可知的;

2.1 判斷物件是否存活

  • 參考計數法
    • 如果一個物件沒有被任何參考指向,則可視之為垃圾;
    • 主流的Java虛擬機里面都沒有選用參考計數演算法來管理記憶體;
    • 缺點:不能解決回圈參考問題;
  • 可達性分析法:(主流)
    • 從 GC Roots 開始向下搜索,搜索所走過的路徑為參考鏈,當一個物件到 GC Roots 沒用任何參考鏈時,則證明此物件是不可用的,表示可以回收,實際上一個物件的真正死亡至少要經歷兩次標記程序;
    • GC Roots 的物件:
      • 虛擬機堆疊(堆疊幀中的本地變數表)中參考的物件;
      • 方法區中類靜態屬于參考的物件
      • 方法區中常量參考的物件
      • 本地方法堆疊中 JNI(即一般說的 Native方法)參考的物件;
    • 目前主流的虛擬機都是采用的演算法;
  • 物件的四種參考:(JDK 1.2 之后,參考概念進行了擴充)
    • 強參考:類似 new 關鍵字創建的參考,只要強參考在就不回收;
    • 軟參考:SoftReference 類實作,發生記憶體溢位例外之前,會把這些物件列進回收范圍;
    • 弱參考:WeakReference 類實作,在垃圾收集器作業時,無論記憶體是否足夠都會回收;
    • 虛參考:PhantomReference 類實作,無法訪問實體,唯一目的是在這個物件被收集器回收時收到一個系統通知;

2.2 分代與記憶體分配、回收策略

  • 相關代碼:
    • 手動回收垃圾:System.gc()
    • 執行 GC 操作呼叫:Object.finalize()
  • 分代
    • 方法區永久代,不容易回收,主要回收廢棄的常量(沒有該常量的參考)和無用的類(所有實體已回收、該類的 ClassLoader 已回收、無法通過反射訪問);
    • Java 堆:新生代 + 老年代,默認新生代與老年代的比例的值為 1:2;
      • 老年代(2/3):物件存活率高、沒有額外空間對它進行分配擔保,“標記-清理”演算法或者“標記-整理”演算法,大物件、長期存活物件分配在老年代;
      • 新生代(1/3):Eden + From Survivor + To Survivor,默認的 Edem : From Survivor : To Survivor = 8 : 1 : 1,JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來為物件服務,剩余的存放回收后存活的物件(與復制演算法有關);
        • Eden(4/15):資料會首先分配到 Eden 區,Eden沒有足夠空間的時候就會觸發 JVM 發起一次 Minor GC,存活則進入 Survivor
        • From Survivor(1/30) 和 To Survivor(1/30):物件每熬過一次 Minor GC 還存活則年齡加1,當年齡達到(默認為15)時晉升到老年代
  • 幾種分代 GC:
    • Minor GC:新生代 GC,執行頻繁,回收速度快;
      • 觸發條件:Eden 區滿,
    • Major GC:老年代 GC,通常會連著 Minor GC 一起執行,速度慢;
      • 觸發條件:晉升老年代物件大小 > 老年代剩余空間,Minor GC 后存活物件大小 > 老年代剩余空間,永久代空間不足,執行 System.gc(),CMS GC例外,堆記憶體分配很大物件,
    • Full GC:清理整個堆空間,包括新生代和老年代,Full GC 相對于Minor GC來說,停止用戶執行緒的 STW(stop the world)時間過長,應盡量避免;
      • 觸發條件:System.gc() 方法呼叫,晉升老年代物件大小 > 老年代剩余空間,Metaspace區記憶體達到閾值(JDK8 引入,使用本地記憶體),堆中產生大物件超過閾值,老年代連續空間不足,
  • 動態物件年齡判定:在 Survivor 空間中相同年齡 x 的物件總大小 > Survivor 空間的一半時,年齡大于等于 x 的物件將直接進入老年代;(HotSpot 虛擬機)
  • 空間分配擔保
    • JDK 6 Update 24 之前:老年代可用連續空間大小 < 新生代物件總大小 時,查看相關引數判斷是否允許擔保失敗,允許則判斷是否:年代可用連續空間大小 > 歷次晉升老年代物件平均大小,成立則進行 Major GC(有風險);不成立說明老年代可用連續空間很少,進行 Full GC,或者不允許擔保失敗也會進行 Full GC;
    • JDK 6 Update 24 之后:老年代可用連續空間大小 > 新生代物件總大小老年代可用連續空間大小 > 次晉升老年代物件平均大小 時,進行 Major GC,反之進行 Full GC;

2.3 垃圾回收演算法(GC 的演算法)

  • 參考計數演算法(Reference counting)
    • 每個物件在創建的時候,就給這個物件系結一個計數器,每當有一個參考指向該物件時,計數器加一,每當有一個指向它的參考被洗掉時,計數器減一,計數器為0就代表該物件死亡,這時就應該對這個物件進行垃圾回收操作;
    • 主流的 Java 虛擬機里面都沒有選用參考計數演算法來回收垃圾;
  • 標記–清除演算法(Mark-Sweep)
    • 分為兩個階段,一個是標記階段,這個階段內,為每個物件更新標記位,檢查物件是否死亡,第二個階段是清除階段,該階段對死亡的物件進行清除,執行 GC 操作;
    • 優點:必要時才回收,解決回圈參考的問題;
    • 缺點:回收時,應用需要掛起,效率不高,會造成記憶體碎片;
    • 應用:老年代(生命周期比較長);
  • 標記–整理演算法
    • 在第二個清除階段,該演算法并沒有直接對死亡的物件進行清理,而是將所有存活的物件整理一下,放到另一處空間,然后把剩下的所有物件全部清除;
    • 優點:解決記憶體碎片問題;
    • 缺點:由于移動了可用物件,需要去更新參考;
    • 應用:老年代(生命周期比較長);
  • 復制演算法
    • 把空間分成兩塊,每次只對其中一塊進行 GC,當這塊記憶體使用完時,就將還存活的物件復制到另一塊上面,回圈下去,實際分為一塊 Eden 和兩塊 Survivor;
    • 優點:存活物件不多時性能高,解決記憶體碎片和參考更新問題;
    • 缺點:記憶體浪費,存活物件數量大時性能差;
    • 應用:新生代(當回收時,將 Eden 和 Survivor 中還存活的物件一次性復制到另一塊 Survivor 上,最后清理 Eden 和 Survivor 空間);
  • 分代演算法:(次要)
    • 針對不同代使用不同的 GC 演算法;

2.4 HotSpot 的演算法實作

2.5 垃圾收集器

垃圾回收演算法是記憶體回收的理論,垃圾回收器是記憶體回收的實踐;

垃圾回收器

  • 上圖說明:如果兩個收集器之間存在連線說明他們之間可以搭配使用;
  • 垃圾收集器
    • 是垃圾回收演算法的具體實作,不同版本的 JVM 所提供的垃圾收集器可能會有很在差別;
  • JDK8 的垃圾收集器:
    • Serial:Client 模式下默認,一個單執行緒收集器,只會使用一個 CPU 或者執行緒去完成垃圾收集作業,而且在它進行垃圾收集時,必須暫停其他所有的作業執行緒,直到它收集結束,單執行緒收集高效;
      • 作業區域:新生帶;
      • 回收演算法:復制演算法;
      • 作業執行緒:單執行緒;
      • 執行緒并行:不支持;
    • ParNew:可看做 Serial 的多執行緒版本,Server 模式下首選, 可搭配 CMS 的新生代收集器;
      • 作業區域:新生帶;
      • 回收演算法:復制演算法;
      • 作業執行緒:多執行緒;
      • 執行緒并行:不支持;
    • Parallel Scavenge:目標是達到可控制的吞吐量(即:減少垃圾收集時間),吞吐量 Throughput = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間);
      • 作業區域:新生帶;
      • 回收演算法:復制演算法;
      • 作業執行緒:多執行緒;
      • 執行緒并行:不支持;
    • Serial Old:Serial 老年代版本,Client 模式下的虛擬機使用;
      • 作業區域:老年帶;
      • 回收演算法:標記-整理演算法;
      • 作業執行緒:單執行緒;
      • 執行緒并行:不支持;
    • Parallnel old:Parallel Scavenge 老年代版本,吞吐量優先;
      • 作業區域:老年帶;
      • 回收演算法:標記-整理演算法;
      • 作業執行緒:多執行緒;
      • 執行緒并行:不支持;
    • CMS:一種以獲取最短回收停頓時間為目標的收集器,適用于互聯網站或者 B/S 系統的服務端上,并發收集、低停頓,與用戶執行緒可以同時作業;
      • 作業區域:老年帶;
      • 回收演算法:標記-清除演算法(記憶體碎片);
      • 作業執行緒:多執行緒;
      • 執行緒并行:支持;
      • 缺點:對 CPU 資源敏感,無法收集浮動垃圾(Concurrent Mode Failure),記憶體碎片;
      • 運作步驟:初始標記(標記 GC Roots 能直接關聯到的物件)、并發標記(進行 GC Roots Tracing)、重新標記;
    • G1:最前沿成果之一,面向服務端應用的垃圾收集器,可看做 CM的終極改進版,JDK1.9 默認垃圾收集器,能充分利用多CPU、多核環境下的硬體優勢,可以并行來縮短(Stop The World)停頓時間,能獨立管理整個 GC 堆,采用不同方式處理不同時期的物件,
      • 作業區域:新生帶 + 老年帶;
      • 回收演算法:標記-整理 + 復制演算法;
      • 作業執行緒:多執行緒;
      • 執行緒并行:支持;
      • 運作步驟:初始標記(標記 GC Roots 能直接關聯到的物件)、并發標記(進行 GC Roots Tracing)、重新標記;

3. JVM 引數配置

3.1 JVM 記憶體引數簡述

  • 常用:
    • -Xms:初始堆大小,JVM 啟動的時候,給定堆空間大小;
    • -Xmx:最大堆大小,JVM 運行程序中,如果初始堆空間不足的時候,最大可以擴展到多少;
    • -Xmn:設定堆中年輕代大小,整個堆大小=年輕代大小+年老代大小+持久代大小;
    • -XX:NewSize=n 設定年輕代初始化大小大小;
    • -XX:MaxNewSize=n 設定年輕代最大值;
    • -XX:NewRatio=n 設定年輕代和年老代的比值,如: -XX:NewRatio=3,表示年輕代與年老代比值為 1:3,年輕代占整個年輕代+年老代和的 1/4 ;
    • -XX:SurvivorRatio=n 年輕代中 Eden 區與兩個 Survivor 區的比值,注意 Survivor 區有兩個,8表示兩個Survivor :eden=2:8 ,即一個Survivor占年輕代的1/10,默認就為8;
    • -Xss:設定每個執行緒的堆疊大小,JDK5后每個執行緒 Java 堆疊大小為 1M,以前每個執行緒堆疊大小為 256K;
    • -XX:ThreadStackSize=n 執行緒堆疊大小;
    • -XX:PermSize=n 設定持久代初始值;
    • -XX:MaxPermSize=n 設定持久代大小;
    • -XX:MaxTenuringThreshold=n 設定年輕帶垃圾物件最大年齡,如果設定為 0 的話,則年輕代物件不經過 Survivor 區,直接進入年老代;
  • 不常用:
    • -XX:LargePageSizeInBytes=n 設定堆記憶體的記憶體頁大小;
    • -XX:+UseFastAccessorMethods 優化原始型別的 getter 方法性能;
    • -XX:+DisableExplicitGC 禁止在運行期顯式地呼叫 System.gc(),默認啟用;
    • -XX:+AggressiveOpts 是否啟用JVM開發團隊最新的調優成果,例如編譯優化,偏向鎖,并行年老代收集等,jdk6 之后默認啟動;
    • -XX:+UseBiasedLocking 是否啟用偏向鎖,JDK6 默認啟用;
    • -Xnoclassgc 是否禁用垃圾回收;
    • -XX:+UseThreadPriorities 使用本地執行緒的優先級,默認啟用;

3.2 JVM 的 GC 收集器設定

  • -XX:+UseSerialGC:設定串行收集器,年輕帶收集器;
  • -XX:+UseParNewGC:設定年輕代為并行收集,可與 CMS 收集同時使用,JDK5.0 以上,JVM 會根據系統配置自行設定,所以無需再設定此值;
  • -XX:+UseParallelGC:設定并行收集器,目標是目標是達到可控制的吞吐量;
  • -XX:+UseParallelOldGC:設定并行年老代收集器,JDK6.0 支持對年老代并行收集;
  • -XX:+UseConcMarkSweepGC:設定年老代并發收集器;
  • -XX:+UseG1GC:設定 G1 收集器,JDK1.9 默認垃圾收集器;

4. JVM 性能調優案例分析

調優目的:GC 的時間足夠的小、GC 的次數足夠的少、發生 Full GC 的周期足夠的長;
問題原因:Full GC 的停止用戶執行緒的 STW 時間過長,應盡量避免;
Full GC 觸發條件:主要是兩個:老年代記憶體過小、老年代連續記憶體過小;
控制 Full GC 頻率的關鍵:保障老年代空間的穩定,大多數物件的生存時間不應當太長,尤其是不能有成批量的、長生存時間的大物件產生;

4.1 大記憶體硬體上的應用程式部署策略

  • 場景簡述:原來有 16GB 物理記憶體(堆記憶體有 4GB),升級硬體配置后控制堆記憶體為 12GB,結構出現不定期長時間失去回應的問題;
  • 場景特點:用戶互動性強、對停頓時間敏感、記憶體較大、Java堆較大;
  • 問題原因:記憶體出現很多由檔案序列化產生的大物件,大物件大多在分配時就直接進入了老年代,Minor GC 清理不掉,最終導致導致老年代記憶體過小,經常發生 Full GC;
  • 解決思路:通過減少單個行程的記憶體,減低老年代記憶體,使檔案序列化物件不易進入老年代,在 Minor GC 時就被清理;
  • 實際方案:目前單體應用在較大記憶體的硬體上主要的部署方式有兩種:
    • 方案一:通過一個單獨的 Java 虛擬機實體來管理大量的 Java 堆記憶體,具體來說:
      • 1. 使用 Shenandoah、ZGC 這些明確以控制延遲為目標的垃圾收集器;
      • 2. 在把 Full GC 頻率控制得足夠低的情況下(老年代的相對穩定),使用 Parallel Scavenge/Old 收集器,并且給 Java 虛擬機分配較大的堆記憶體;
    • 方案二:使用多個 Java 虛擬機,建立邏輯集群來利用硬體資源,具體來說:
      • 1. 在一臺物理機器上啟動多個應用服務器行程,為每個服務器行程分配不同埠,然后在前端搭建一個負載均衡器,以反向代理的方式來分配訪問請求;
      • 2. 使用無 Session 復制的親合式集群,即:均衡器按一定的規則演算法(譬如根據 Session ID 分配)將一個固定的用戶請求永遠分配到一個固定的集群節點進行處理;(一致 hash 演算法的思想);
  • 調優程序
    • 1. 發現問題:監控服務器運行狀況 -> 發現網站失去回應是由垃圾收集停頓所導致的;
    • 2. 分析解決
  • 經驗之談
    • 1. 計劃使用單個 Java 虛擬機實體來管理大記憶體,可能遇到的問題:
      • 回收大塊堆記憶體而導致的長時間停頓(G1 收集器緩解問題,ZGC 和 Shenandoah 收集器徹底解決);
      • 大記憶體必須有 64 位 Java 虛擬機的支持,但由于壓縮指標、處理器快取行容量(Cache Line)等因素,64 位虛擬機的性能測驗結果普遍略低于相同版本的 32 位虛擬機;
      • 必須保證應用程式足夠穩定,因為這種大型單體應用要是發生了堆記憶體溢位,幾乎無法產生堆轉儲快照(要產生十幾GB乃至更大的快照檔案),出了問題可能必須應用 JMC 這種能夠在生產環境中進行的運維工具;
      • 相同的程式在 64 位虛擬機中消耗的記憶體一般比 32 位虛擬機要大,這是由于指標膨脹,以及資料型別對齊補白等因素導致的,可以開啟(默認即開啟)壓縮指標功能來緩解;
    • 2. 使用邏輯集群的方式來部署程式,可能遇到的問題:
      • 節點競爭全域的資源,最典型的就是磁盤競爭;
      • 很難最高效率地利用某些資源池,譬如連接池,一般都是在各個節點建立自己獨立的連接池,這樣有可能導致一些節點的連接池已經滿了,而另外一些節點仍有較多空余,盡管可以使用集中式的 JNDI 來解決,但這個方案有一定復雜性并且可能帶來額外的性能代價;
      • 如果使用 32 位 Java 虛擬機作為集群節點的話,各個節點仍然不可避免地受到 32 位的記憶體限制,在 32 位 Windows 平臺中每個行程只能使用 2GB 的記憶體,考慮到堆以外的記憶體開銷,堆最多一般只能開到 1.5GB,在某些 Linux 或 UNIX 系統(如 Solaris)中,可以提升到 3GB 乃至接近 4GB 的記憶體,但 32 位中仍然受最高 4GB(2 的 32 次冪)記憶體的限制;
      • 大量使用本地快取(如大量使用 HashMap 作為 K/V 快取)的應用,在邏輯集群中會造成較大的記憶體浪費,因為每個邏輯節點上都有一份快取,這時候可以考慮把本地快取改為集中式快取(如 4.6);

4.2 集群間同步導致的記憶體溢位

  • 場景簡述:采用親合式集群的 MIS 系統,為了實作部分資料在各個節點中共享,使用 JBossCache 構建了一個全域快取,結果不定期出現多次的記憶體溢位問題;
  • 場景特點:親合式集群、JBossCache 全域快取;
  • 問題原因:JBossCache 基于 JGroups 進行集群間的資料通信,JGroups 在收發資料包時會在記憶體構建 NAKACK 堆疊保證順序與重發,當網路不好時重發資料在記憶體中不斷堆積;
  • 解決思路:改進 JBossCache 的缺陷,改進 MIS 系統;
  • 實際方案:可以允許讀操作頻繁,不允許寫操作頻繁,避免大的網路同步開銷;
  • 調優程序
    • 1. 發現問題:添加 -XX:+HeapDumpOnOutOfMemoryError 引數 -> 運行一段時間發現存在大量 t.NAKACK 物件;
    • 2. 分析解決

4.3 堆外記憶體導致的溢位錯誤

  • 場景簡述:使用 CometD 1.1.1 作為服務端推送框架,服務器為 4GB 記憶體,運行 32 位
    Windows 作業系統,堆記憶體設定為 1.6GB,結果不定時拋出記憶體溢位例外;
  • 場景特點:32 位系統、小記憶體、大量的 NIO 操作
  • 問題原因:32 位 Windows 平臺中每個行程只能使用 2GB 的記憶體,其中 1.6GB 分配給了堆記憶體,0.4 GB 分配給了直接記憶體,CometD 1.1.1 框架,有大量的 NIO 操作,NIO 會使用 Native 函式庫直接分配堆外記憶體,最終導致直接記憶體溢位;
  • 解決思路:注意占用較多記憶體的區域:調整直接記憶體、執行緒堆疊、Socket 緩沖區大小,注意 JNI 代碼,選擇合適的虛擬機與垃圾收集器;
    • 1. 直接記憶體:通過 -XX:MaxDirectMemorySize 調整直接記憶體大小;
    • 2. 執行緒堆疊:通過 -Xss 調整執行緒堆大小;
    • 3. Socket快取:每個 Socket 連接都 Receive 和 Send 兩個快取區,控制 Socket 連接數;
    • 4. JNI代碼:JNI呼叫本地庫會使用 Native 函式庫直接分配堆外記憶體;
    • 5. 虛擬機和垃圾收集器:虛擬機、垃圾收集器的作業也是要消耗一定數量的記憶體的;
  • 調優程序
    • 1. 發現問題:首先查看日志 -> 在記憶體溢位后的系統日志中找到例外堆疊(OutOfMemoryError);
    • 2. 分析解決

4.4 外部命令導致系統緩慢

  • 場景簡述:在一臺四路處理器的 Solaris 10 作業系統上,處理每次用戶請求時都會執行一個外部 Shell 腳本獲取系統資訊,最后發現請求回應時間比較慢,并且系統中占用絕大多數處理器資源的程式并不是該應用本身;
  • 場景特點:Shell 腳本、創建行程耗費大量資源、“fork”系統;
  • 問題原因:執行 Shell 腳本是通過 Java 的 Runtime.getRuntime().exec() 方法來呼叫的,它首先復制一個和當前虛擬機擁有一樣環境變數的行程,再用這個新的行程去執行外部命令,最后再退出這個行程;
  • 解決思路:盡量減少創建行程的開銷;
  • 實際方案:去掉這個 Shell 腳本執行的陳述句,改為使用 Java 的 API 去獲取資訊;
  • 調優程序
    • 1. 發現問題:通過 Solaris 10 的 dtrace 腳本 -> 查看當前情況下哪些系統呼叫花費了最多的處理器資源;
    • 2. 定位問題:發現最消耗處理器資源的竟然是“fork”系統呼叫(用來創建行程);
    • 3. 分析問題:Shell腳本是通過 Java 的 Runtime.getRuntime().exec() 方法創建大量行程;
    • 4. 分析解決

4.5 服務器虛擬機行程崩潰

  • 場景簡述:MIS 系統在與一個 OA 門戶做了集成后,服務器運行期間頻繁出現集群節點的虛擬機行程自動關閉的現象;
  • 場景特點:遠程斷開連接例外、OA 門戶集成、異步呼叫;
  • 問題原因:MIS 系統作業流待辦事項變化時,使用異步呼叫 Web 服務,通知 OA 門戶,兩邊服務速度不對等,時間越長越多 Web 服務沒有呼叫,等待執行緒和 Socket 連接越多;
  • 解決思路:問題根源是異步呼叫導致執行緒過多,處理時間超過了設定的超時等待時間;可以從服務通信和超時等待兩方面優化;
  • 實際方案:將異步呼叫改為生產者/消費者模式的訊息佇列;
  • 調優程序
    • 1. 發現問題:首先查看日志 -> 發現報大量相同的 Socket 重連例外(java.net.SocketException: Connection reset);
    • 2. 分析解決

4.6 不恰當資料結構導致記憶體占用過大

  • 場景簡述:一個后臺 RPC 服務器,需要每 10 min 加載一個約 800MB 的 HashMap<Long,Long>Entry 型別的資料結構,在這段時間內執行 Minor GC 停頓較長時間;
  • 場景特點:Map資料結構、長停頓 Minor GC;
  • 問題原因:有兩方面,一來 800MB 的資料很快把 Eden 填滿引發垃圾收集,垃圾收集時這 800MB 資料重復復制到 Survivor 導致 Minor GC 時間長,二來 HashMap<Long,Long> 型別 key 和 value 共占 2*8=16 位元組,封裝成 Map.Entry 后多了 16 位元組物件頭、8 位元組 next 欄位和 4 位元組 int 型別的 hash 欄位,為了對其追加 4 位元組空白物件頭,還有 8 位元組對這個 Map.Entry 的參考,最后實際耗費的記憶體為 (Long(24byte)×2)+Entry(32byte)+HashMap Ref(8byte) = 88byte,空間效率為:16 位元組 / 88 位元組 = 18% 太低;
  • 解決思路:有兩方面的思路,一來可以將大物件盡早劃入老年代,二來可以優化資料結構;
  • 實際方案:將新生代空間減少或使用親合式集群將大記憶體劃進老年代(類似 4.1),除此之外還可以將 Survivor 空間去掉,讓新生代中存活的物件在第一次 Minor GC 后立即進入老年代,等到 Major GC 的時候再去清理它們,最根本的方法是優化資料結構;
    • 方案一:去掉 Survivor 空間,具體來說:
      • 1. 加入
        引數 -XX:SurvivorRatio=65536-XX:MaxTenuringThreshold=0
      • 2. 或者 -XX:+Always-Tenure
    • 方案二:優化資料結構,需要具體的業務背景;
  • 調優程序
    • 1. 發現問題:首先查看日志 -> 發現在每 10min 里,Minor GC 會造成 500ms 停頓;
    • 2. 分析解決

4.7 由 Windows 虛擬記憶體導致的長時間停頓

  • 場景簡述:GUI 程式使用記憶體較小,在最小化時,偶爾會出現長時間完全無日志輸出,程式處于停頓狀態,查看記憶體發現在最小化時占用記憶體大幅減小,但虛擬了留下來沒有變化;
  • 場景特點:GUI 程式、虛擬記憶體、應用最小化;
  • 問題原因:GUI 程式在應用最小化時,會將作業記憶體交換到磁盤頁面檔案中(修剪),在進行垃圾回收前需要恢復作業頁面檔案導致停頓,進而導致從準備開始垃圾收集,到真正開始之間所消耗的時間較長;
  • 解決思路:由于 GUI 程式使用記憶體較小,不對其修剪,修剪的好處是記憶體可用于其他應用程式,缺點是在恢復作業集記憶體時會有延遲;
  • 實際方案:在應用程式最小化后阻止 JVM 對其進行修剪,具體來說:
    • 1. -Dsun.awt.keepWorkingSetOnMinimize=true
  • 調優程序
    • 1. 定位停頓問題:加入引數 -XX:+PrintGCApplicationStoppedTime-XX:+PrintGCDate-Stamps-Xloggc:gclog.log -> 確認了停頓確實是由垃圾收集導致;
    • 2. 定位停頓日志:添加 -XX:+PrintReferenceGC 引數,找到長時間停頓的具體日志資訊 -> 發現從準備開始收集,到真正開始收集之間所消耗的時間卻占了絕大部分;
    • 3. 分析解決

4.8 由安全點導致長時間停頓

  • 場景簡述:一個使用 G1 收集器的離線 HBase 集群,有大量的 MapReduce 或 Spark 離線分析任務對其進行訪問,集群讀寫壓力較大,結果發現垃圾收集的停頓時間較長;
  • 場景特點:MapReduce 與 Spark 任務、垃圾收集時間短但空轉等待時間長、可數回圈;
  • 問題原因:HotSpot 虛擬機在認為回圈次數較少時,使用 int 型別或范圍更小
    的資料型別作為索引值,不進入安全點(具有讓程式長時間執行的特征),在 HBase 連接中有很多個Mapper / Reducer / Executer 執行緒,清理這些執行緒靠一個連接超時清理的回圈函式, HotSpot 判斷這個回圈函式為可數回圈,等待回圈全部跑完才能進入安全點,此時其他執行緒也必須一起等著,宏觀來看就是長時間停頓;
  • 解決思路:連接超時清理的回圈函式使用 int 索引因此被判斷為可數回圈,修改索引將其變為不可數回圈即可;
  • 實際方案:把回圈索引的資料型別從int改為long即可;
  • 調優程序
    • 1. 發現問題:首先查看日志 -> 發現垃圾收集停頓時間長,但實際垃圾回收時間短;
    • 2. 查看安全點日志:加入引數 -XX:+PrintSafepointStatistics-XX:PrintSafepointStatisticsCount=1 查看安全點日志 -> 發現虛擬機在等待所有用戶執行緒進入安全點時有執行緒很慢;
    • 3. 找到超時執行緒:添加 -XX: +SafepointTimeout-XX:SafepointTimeoutDelay=2000 兩個引數,使虛擬機在等到執行緒進入安全點的時間超過 2000 毫秒時就認定為超時 -> 輸出導致問題的執行緒名稱;
    • 4. 分析解決

4.9 調優總結

  • 在實際作業中,我們可以直接將初始的堆大小與最大堆大小相等,這樣的好處是可以減少程式運行時垃圾回收次數,從而提高效率;
  • 初始堆值和最大堆記憶體記憶體越大,吞吐量就越高,但是也要根據自己電腦(服務器)的實際記憶體來比較;
  • 最好使用并行收集器,因為并行收集器速度比串行吞吐量高,速度快,當然,服務器一定要是多執行緒的;
  • 設定堆記憶體新生代的比例和老年代的比例最好為 1:2 或者 1:3 ,默認的就是 1:2;
  • 減少 GC 對老年代的回收(老年代 GC 慢),設定新生代垃圾物件最大年齡,盡量不要有大量連續記憶體空間的 Java 物件,因為會直接到老年代,記憶體不夠就會執行 GC;
  • 默認的 JVM 堆大小是電腦實際記憶體的四分之一左右;

最后

新人制作,如有錯誤,歡迎指出,感激不盡!
歡迎關注公眾號,會分享一些更日常的東西!
如需轉載,請標注出處!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/449045.html

標籤:Java

上一篇:springboot html vue.js 前后分離代碼示例

下一篇:勁爆!Java 18 正式發布,默認 UTF-8,finalize 被棄用。。別再亂用了!

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more