JVM要點

本文轉載自:JVM 看這一篇就夠了

一、JVM概述

  • JVM:Java Virtual Machine,也就是Java虛擬機
  • 所謂虛擬機是指:通過軟件模擬的具有完整硬件系統功能的、運行在一個完全隔離環境中的計算機系統
  • JVM是通過軟件來模擬Java字節碼的指令集,是Java程序的運行環境

二、JVM主要功能

  • 通過 ClassLoader 尋找和裝載 class 文件
  • 解釋字節碼成為指令并執行,提供 class 文件的運行環境
  • 進行運行期間的內存分配和垃圾回收
  • 提供與硬件交互的平臺

三、虛擬機是Java平臺無關的保障

1

四、JVM規范作用及其核

4.1 JVM規范作用
  • Java 虛擬機規范為不同的硬件提供了一種編譯Java技術代碼的規范
  • 該規范使Java軟件獨立于平臺,因為編譯時針對作為虛擬機的“一般機器”而做
  • 這個“一般機器”可用軟件模擬并運行于各種現存的計算機系統,也可用硬件來實現
4.2 JVM規范定義的主要內容
  • 字節碼指令集
  • Class文件的格式
  • 數據類型和值
  • 運行時數據區
  • 棧幀
  • 特殊方法
  • 類庫
  • 異常
  • 虛擬機的啟動、加載、鏈接和初始化

六、Class字節碼解析

6.1 Class文件格式概述
  • Class文件是JVM的輸入,Java虛擬機規范中定義了Class文件的結構,Class文件是JVM實現平臺無關、技術無關的基礎
  • 無符號數:基本數據類型,以u1、u2、u4、u8來代表幾個字節的無符號數
  • 表:由多個無符號和其他表構成的符合數據類型,通常以 "_info"結尾
  • Class文件是一組以8字節為單位的字節流,各個數據項目按序緊湊排列
  • 對于占用空間大于8字節的數據項,按照高位在前的方式分割成多個8字節進行存儲
  • Class文件格式里面只有兩種類型:無符號數、表
6.2 Class文件的格式
  • javap工具生成非正式的 ”虛擬機匯編語言“,格式如下:

  • [[]…]] [comment]

  • 是指令操作碼在數組中的下標,該數組以字節形式來存儲當前方法的Java虛擬機代碼;也可以是相當于方法起始處的字節偏移量

  • 是指令的助記碼、是操作數、是行尾的注釋

6.3 Class文件格式說明
  • constant_pool_count:是從1開始的

  • 不同的常量類型,用tag來區分,它后面對應的 info 結構是不一樣的

  • L表示對象,[ 表示數組、V表示void

  • stack:方法執行時,操作棧的深度

  • Locals:局部變量所需的儲存空間,單位是slot

  • slot是虛擬機為局部變量分配內存所使用的最小單位

  • args_size:參數個數,為1的話,因實例方法默認會傳入this,locals也會預留一個slot來存放

七、ASM

7.1 ASM概述
  • ASM是一個Java字節碼操縱框架,它能被用來動態生成類或者增強既有類的功能
  • ASM可以直接產生二進制class文件,也可以在類被加載入虛擬機之前動態改變類行為,ASM從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能根據要求生成新類
  • 目前許多框架如 cglib、Hibernate、spring 都直接或間接地使用ASM操作字節碼
7.2 ASM編程模型
  • Core API:提供了基于事件形式的編程模型。該模型不需要一次性將整個類的結構讀取到內存中,因此這種方式更快,需要的內存更少,但這種編程方式難度較大
  • Tree API:提供了基于樹型的編程模型。該模型需要一次性將一個類的完整結構全部讀取到內存中,所以這種方法需要更多的內存,這種編程方式較簡單
7.3 ASM的Core API
  • ASM Core ApI 中操縱字節碼的功能基于 ClassVisitor 接口。這個接口中的每個方法對應了 class 文件中的每一項

  • ASM 提供了三個基于 ClassVisitor 接口的類來實現 class 文件的生成和轉換

  • ClassReader:ClassReader 解析一個類的 class 字節碼

  • ClassAdapter:ClassAdapter 是 ClassVisitor 的實現類,實現要變化的功能

  • ClassWriter:ClassWriter 也是 ClassVisitro 的實現類,可以用來輸出變化后的字節碼

  • ASM 給我們提供了 ASMifier 工具來幫助開發,可使用ASMifier 工具生成 ASM 結構來對比

八、類加載、連接和初始化

8.1 類加載和類加載器
  • 類被加載到 JVM 開始,到卸載出內存,整個生命周期如圖:
2
  • 加載:查找并加載類文件的二進制數據

  • 連接:就是將已經讀入內存的類的二進制數據合并到 JVM 運行時環境中去,包含以下步驟:

  • 驗證:確保被加載類的正確性

  • 準備:為類的 靜態變量 分配內存,并初始化

  • 解析:把常量池中的符號引用轉換成直接引用

  • 初始化:為類的靜態變量賦初始值

8.2 類加載要完成的功能
  • 通過類的全限定名來獲取該類的二進制字節流
  • 把二進制字節流轉化為方法區的運行時數據結構
  • 在堆上創建一個 java.lang.Class 對象,用來封裝類在方法區內的數據結構,并向外提供了訪問方法區內數據結構的接口
8.3 加載類的方式
  • 最常見的方式:本地文件系統中加載、從jar等歸檔文件中加載
  • 動態的方式:將 java 源文件動態編譯成 class
  • 其他方式:網絡下載、從專有數據庫中加載等等
8.4 類加載器
  • Java 虛擬機自帶的加載器包括以下幾種:
  • 啟動類加載器(BootstrapClassLoader)
  • 平臺類加載器(PlatformClassLoader) JDK8:擴展類加載器(ExtensionClassLoader)
  • 應用程序類加載器(AppClassLoader)
  • 用戶自定義的加載器:是 java.lang.ClassLoader 的子類,用戶可以定制類的加載方式;只不過自定義類加載器其加載的順序是在所有系統類加載器的最后
8.5 類加載器的關系
3

九、類加載器使用

9.1 類加載器說明
  • 啟動類加載器:用于加載啟動的基礎模塊類,比如:java.base、java.management、java.xml等
  • 平臺類加載器:用于加載一些平臺相關的模塊,比如:java.scripting、java.compiler *、java.corba *等
  • 應用程序類加載器:用于加載應用級別的模塊,比如:jak.compiler、jdk.jartool、jdk.jshell 等等;還加載 classpath 路徑中的所有類庫
  • JDK8:啟動類加載器:負責將<JAVA_HOME>/lib,或者 -Xbootclasspath 參數指定的路徑中的,且是虛擬機識別的類庫加載到內存中(按照名字識別,比如 rt.jar,對于不能識別的文件不予裝載)
  • JDK8:擴展類加載器:負責加載 <JRE_HOME>/lib/ext,或者 java.ext.dirs 系統變量所指定路徑中的所有類庫
  • JDK8:應用程序類加載器:負責加載 classpath 路徑中的所有類庫
  • Java 程序不能直接引用啟動類加載器,直接設置 classLoader 為 null,默認就使用啟動類加載器
  • 類加載器并不需要等到某個類“首次主動使用”的時候才加載它,JVM規范允許類加載器在預料到某個類將要被使用的時候就預先加載它
  • 如果在加載的時候 .class 文件缺失,會在該類首次主動使用時報告 LinkageError 錯誤,如果一直沒有被使用,就不會報錯
9.2 雙親委派模型
  • JVM中的 ClassLoader 通常采用雙親委派模型,要求除了啟動類加載器外,其余的類加載器都應該有自己的父級加載器。這里的父子關系是組合而不是繼承,工作過程如下:
  • 一個類加載器接收到類加載請求后,首先搜索它的內建加載器定義的所有“具名模塊”
  • 如果找到了合適的模塊定義,將會使用該加載器來加載
  • 如果 class 沒有在這些加載器定義的具名模塊中找到,那么將委托給父級加載器,直到啟動類加載器
  • 如果父級加載器反饋它不能完成加載請求,比如在它的搜索路徑下找不到這個類,那子類加載器才自己來加載
  • 在類路徑下找到的類將成為這些加載器的無名模塊
  • 雙親委派模型對于保證 Java 程序的穩定運作很重要,可以避免一個類被加載多次
  • 實現雙親委派的代碼在 java.lang.ClassLoader 的 loadClass() 方法中,如果自定義類加載器的話,推薦覆蓋實現 findClass() 方法
  • 如果有一個類加載器能加載某個類,稱為 定義類加載器,所有能成功返回該類的 Class 的類加載器 都被稱為初始類加載器
9.3 雙親委派模型的說明
  • 雙親委派模型對于保證 Java 程序的穩定運作很重要
  • 實現雙親委派的代碼 java.lang.ClassLoader 的 loadClass() 方法中,如果自定義類加載器的話,推薦覆蓋實現 findClass() 方法
  • 如果有一個類加載器能加載某個類,稱為 定義類加載器,所有能成功返回該類的 Class 的類加載器 都被稱為 初始化加載器
  • 如果沒有指定父加載器,默認就是啟動類加載器
  • 每個類加載器都有自己的命名空間,命名空間由該類加載器及其所有父加載器所加載的類構成,不同的命名空間,可以出現類的全路徑名 相同的情況
  • 運行時包由同一個類加載器的類構成,決定兩個類是否屬于同一個運行時包,不僅要看全路徑名是否一樣,還要看定義類加載器是否相同。只有屬于同一個運行時包的類才能實現相互包內可見
9.4 破壞雙親委派模型
  • 雙親委派模型有一個問題:父加載器無法向下識別子加載器加載的資源
  • 為了解決這個問題,引入了線程上下文類加載器,可以通過 Thread 的 setContextClassLoader() 進行設置
  • 實現熱部署時,比如 OSGI 的模塊化熱部署,它的類加載器就不再是嚴格按照雙親委派模型,很多可能就在平級的類加載器中執行了
9.5 雙親委派加載順序

Java的雙親委派機制是Java類加載器的一種工作模式。它是為解決類加載沖突和安全性問題而設計的。

當一個類需要被加載時,Java虛擬機會按照特定的順序嘗試加載這個類。首先,它會將這個請求交給最頂層的類加載器——啟動類加載器(Bootstrap Classloader)。如果啟動類加載器無法完成加載任務(因為啟動類加載器只加載核心類庫),那么它會請求它的父類加載器(根據雙親委派模型,通常是擴展類加載器)來加載該類。如果父類加載器仍然無法加載這個類,那么它會再請求它的父類加載器的父類加載器,直到達到頂層的啟動類加載器。

這種層層委派的機制能夠保證在一個Java應用程序中,同一個類只會被加載一次,并且由同一個類加載器加載。這樣可以避免不同的類加載器對同一個類進行加載,造成類的冗余或者不一致。同時,雙親委派機制還可以有效地提高類的安全性,防止惡意代碼替換核心類庫。

總結起來,雙親委派機制的步驟如下:

  1. 當一個類需要被加載時,首先詢問最頂層的啟動類加載器。
  2. 如果啟動類加載器找不到該類,請求父類加載器加載。
  3. 父類加載器再將加載請求往上委派,直到找到能加載該類的加載器或者無法再往上委派為止。
  4. 如果所有的父類加載器都無法加載該類,則由最底層的加載器嘗試加載。

通過雙親委派機制,Java保證了類的唯一性和安全性。

十、類連接和初始化

10.1 類連接主要驗證的內容
  • 類文件結構檢查:按照 JVM 規范規定的類文件結構進行
  • 元數據驗證:對字節碼描述的信息進行語義分析,保證其符合 Java 語言規范要求
  • 字節碼驗證:通過對數據流和控制流進行分析,確保程序語義是合法和符合邏輯的。這里主要對方法體進行校驗
  • 符號引用驗證:對類自身以外的信息,也就是常量池中的各種符號引用,進行匹配校驗
10.2 類連接中的準備
  • 為類的 靜態變量 分配內存,并初始化
10.3 類連接中的解析
  • 解析就是把常量中的符號引用轉換成直接引用的過程,包括:符號引用:以一組無歧義(唯一)的符號來描述所引用的目標,與虛擬機的失效無關
  • 直接引用:直接執行目標的指針、相對偏移量、或是能間接定位到目標的句柄,是和虛擬機實現相關的
  • 主要針對:類、接口、字段、類方法、接口方法、方法類型、方法句柄、調用點限定符
10.4 類的初始化
  • 類的初始化就是為類的靜態變量賦初始值,或者說是執行類構造器 方法的過程
  • 初始化一個類的時候,并不會先初始化它實現的接口
  • 初始化一個接口的時候,并不會先初始化它的父接口
  • 只有當程序首次使用接口里面的變量或者是調用接口方法的時候,才導致接口初始化
  • 如果類還沒有加載和連接,就先加載和連接
  • 如果類存在父類,且父類沒有初始化,就先初始化父類
  • 如果類中存在初始化語句,就依次執行這些初始化語句
  • 如果是接口的話:
  • 調用 Classloader 類的 loadClass 方法類裝載一個類,并不會初始化這個類,不是對類的主動使用

十一、類的主動初始化

11.1 類的初始化時機
  • Java 程序對類的使用方式分成:主動使用和被動使用,JVM 必須在每個類或接口 ”首次主動使用“ 時才初始化它們;被動使用類不會導致類的初始化,主動使用的情況:
  • 創建類實例
  • 訪問某個類或接口的靜態變量
  • 調用類的靜態方法
  • 反射某個類
  • 初始化某個類的子類,而父類還沒有初始化
  • JVM 啟動的時候運行的主類
  • 定義了 default 方法的接口,當接口實現類初始化時
11.2 類的卸載
  • 當代表一個類的 Class 對象不再被引用,那么 Class 對象的生命周期就結束了,對應的在方法區中的數據也會被卸載
  • JVM 自帶的類加載器裝載的類,是不會卸載的,由用戶自定義的類加載器的加載的類是可以卸載的
11.3 運行時數據區
  • PC 寄存器、Java虛擬機棧、Java堆、方法區、運行時常量池、本地方法棧等
11.4 PC 寄存器
  • 每個線程都擁有一個PC寄存器,是線程私有的,用來存儲指向下一條指令的地址
  • 在創建線程的時候,創建相應的PC寄存器
  • 執行本地方法時,PC寄存器的值為 undefined
  • 是一塊比較小的內存空間,是唯一一個在JVM規范中沒有規定 OutOfMemoryError 的內存區域
11.4 Java棧
  • 棧由一系列幀(棧幀)(Frame)組成(因此Java棧也叫做幀棧),是線程私有的
  • 棧幀用來保存一個方法的局部變量、操作數棧(Java沒有寄存器,所有參數傳遞使用操作數棧)、常量池指針、動態鏈接、方法返回等
  • 每一次方法調用創建一個幀,并壓棧,退出方法的時候,修改棧頂指針就可以把棧幀中的內容銷毀
  • 局部變量表存放了編譯期可知的各種基本數據類型和引用類型,每個 slot 存放32位的數據,long、double、占兩個槽位
  • 棧的優點:存取速度比堆塊,僅次于寄存器
  • 棧的缺點:存在棧中的數據大小、生存區是在編譯器決定的,缺乏靈活性
11.4 Java堆
  • 用來存放應用系統創建的對象和數組,所有線程共享 Java 堆
  • GC主要管理堆空間,對分代GC來說,堆也是分代的
  • 堆的優點:運行期動態分配內存大小,自動進行垃圾回收;
  • 堆的缺點:效率相對較慢
11.5 方法區
  • 方法區是線程共享的,通常用來保存裝載的類的結構信息
  • 通常和元空間關聯在一起,但具體的跟JVM實現和版本有關
  • JVM規范把方法區描述為堆的一個邏輯部分,但它有一個別名稱為 Non-heap(非堆),應是為了與 Java 堆分開
11.6 運行時常量池
  • 是Class文件中每個類或接口的常量池表,在運行期間的表示形式,通常包括:類的版、字段、方法、接口等信息
  • 在方法區中分配
  • 通常在加載類和接口到JVM后,就創建相應的運行時常量池
11.7 本地方法棧
  • 在 JVM 中用來支持 native 方法執行的棧就是本地方法棧

十二、Java堆內存模型和分配

12.1 Java堆內存概述
  • Java 堆用來存放應用系統創建的對象和數組,所有線程共享Java堆
  • Java堆是在運行期動態分配內存大小,自動進行垃圾回收
  • Java垃圾回收(GC)主要就是回收堆內存,對分代GC來說,堆也是分代的
12.2 Java堆的結構
4
  • 新生代用來存放新分配的對象;新生代中經過垃圾回收,沒有回收掉的對象,被復制到老年代
  • 老年代存儲對象比新生代存儲對象的年齡大得多
  • 老年代存儲一些大對象
  • 整個堆大小 = 新生代 + 老年代
  • 新生代 = Eden + 存活區
  • 從前的持久代,用來存放Class、Method 等元信息的區域,從 JDK8 開始去掉了,取而代之的是元空間(MetaSpace),元空間并不在虛擬機里面,而是直接使用本地內存
12.3 對象內存布局
  • 對象在內存中儲存的布局(這里以Hotspot虛擬機為例來說明),分為:對象頭、實例數據和對齊填充
  • 對象頭,包含兩部分:
    1、Mark Word:存儲對象自身的運行數據,如:HashCode、GC分代年齡,鎖狀態標志等
    2、類型指針:對象指向它的類元數據的指針
  • 實例數據:真正存放對象實例數據的地方
  • 對齊填充:這部分不一定存在,也沒有什么特別含義,僅僅是占位符。因為 HotSpot 要求對象起始地址都是8字節的整數倍,如果不是,就對齊
12.4 對象的訪問定位
  • 使用句柄:Java堆中會劃分出一塊內存來作為句柄池,reference 中存儲句柄的地址,句柄中存儲對象的實例數據和類元數據的地址,如圖
5
  • 使用指針:Java堆中會存放訪問類元數據的地址,reference存儲的就直接是對象的地址,如圖:
6

十三、Trace跟蹤和Java堆的參數配置

13.1 Trace跟蹤參數
  • 可以打印GC的簡要信息:-Xlog:gc
  • 打印GC詳細信息:-Xlog:gc*
  • 指定GC log的位置,以文件輸出:-Xlog:gc:garbage-collection.log
  • 每一次GC后,都打印堆信息:-xlog:gc+heap = debug
13.2 GC 日志格式
  • GC發生的時間,也就是 JVM 從啟動以來經過的秒數
  • 日志級別信息,和日志類型標記
  • GC 識別號
  • GC 的類型和說明 GC 的原因
  • 容量:GC 前容量 -> GC后容量(該區域總容量)
  • GC 持續時間,單位秒。有的收集器會有詳細的描述,比如:user表示應用程序消耗的時間,sys表示系統內核消耗的時間,real 表示操作從開始到結束的時間
13.3 Java堆的參數
  • -Xms:初始化堆大小,默認物理內存的 1/64
  • -Xmx:最大堆大小,默認物理內存的 1/4
  • -Xmn:新生代大小,默認整個堆的 3/8
  • -XX:+HeapDumpOnOutOfMemoryError:OOM時導出堆到文件
  • -XX:+HeapDumpPath:導出 OOM 的路徑
  • -XX:NewRatio:老年代與新生代的比值,如果 xms=xmx,且設置了 xmn 的情況,該參數不用設置
  • -XX:SurvivorRatio:Eden區和Survivor區的大小比值,設置為8,則兩個 Survivor 區與一個Eden區的比值為 2:8,一個 Survivor 占整個新生的 1/10
  • -XX:OnOutOfMemoryError:在OOM時,執行一個腳本
  • -Xss:通常只有幾百k,決定了函數調用的深度
13.4 元空間的參數
  • -XX:MetaspaceSize:初始空間大小
  • -XX:MaxMetaspaceSize:最大空間,默認是沒有限制的
  • -XX:MinMetaspaceFreeRatio:在GC之后,最小的Metaspace 剩余空間容量的百分比
  • -XX:MaxMetaspaceFreeRatio:在GC之后,最大的Metaspace剩余空間容量的百分比
13.5 字節碼執行引擎
  • JVM 的字節碼執行引擎,功能基本就是輸入字節碼文件,然后對字節碼進行解析并處理,最后輸出執行的結果
  • 實現方式可能有通過解釋器直接解釋執行字節碼,或者通過即時編譯器產生本地代碼,也就是編譯執行,當然也可能兩者都有
13.6 棧幀
  • 棧幀是用于支持JVM進行方法調用和方法執行的數據結構
  • 棧幀隨著方法調用而創建,隨著方法結束而銷毀
  • 棧幀里面存儲了方法的局部變量表、操作數棧、動態鏈接、方法返回地址等信息
13.7 局部變量表
  • 局部變量表:用來存放方法參數和方法內部定義的局部變量的存儲空間
  • 以變量槽 slot 為單位,目前一個 slot 存放32位以內的數據類型
  • 對于64位的數據占2個slot
  • 對于實例方法,第0位 slot 存放的是 this,然后從1到n,依次分配給參數列表
  • 然后根據方法體內部定義的變量順序和作用域來分配 slot
  • slot 是復用的,以節省棧幀的空間,這種設計可能會影響系統的垃圾收集行為
13.7 操作數棧
  • 操作數棧:用來存放方法運行期間,各個指令操作的數據
  • 操作數棧中元素的數據類型必須和字節碼指令的順序嚴格匹配
  • 虛擬機在實現棧幀的時候可能會做一些優化,讓兩個棧幀出現部分重疊區域,以存放公用的數據
13.8 動態鏈接
  • 動態鏈接:每一個棧幀持有一個指向運行時常量池中該棧幀所屬方法的引用,以支持方法調用過程的動態鏈接
  • 靜態解析:類加載的時候,符號引用就轉化為直接引用
  • 動態鏈接:運行期間轉化為直接引用
13.9 方法返回地址
  • 方法返回地址:方法執行后返回的地址
13.10 方法調用
  • 方法調用:就是確定具體調用哪一個方法,并不涉及方法內部的執行過程
  • 部分方法是直接在類加載的解析階段,就確定了直接引用關系
  • 但是對于實例方法,也稱虛方法,因為多重和多態,需要運行期動態分派
13.11 分派
  • 靜態分派:所有依賴靜態類型來定位方法執行版本的分派方式,比如:重載方法
  • 動態分派:根據運行期的實際類型來定位方法執行版本的分派方式,比如:覆蓋方法
  • 單分派和多分派:就是按照分派思考的維度,多于一個的就算多分配,只有一個的稱為單分派

十四、垃圾回收

14.1 垃圾回收概述
  • 什么是垃圾:簡單說就是內存中已經不再被使用到的內存空間就是垃圾

  • 垃圾回收算法:

  • 可作為GC Roots的對象包括:虛擬機棧(棧幀局部變量)中引用的對象、方法區類靜態屬性引用的對象、方法區中常量引用的對象、本地方法棧中JNI引用的對象

  • HotSpot 使用了一組叫做 OopMap 的數據結構達到準確式GC的目的

  • 在OopMap的協助下,JVM可以很快的做完GC Roots 枚舉。但是JVM并沒有為每一條指令生成一個OopMap

  • 記錄OopMap 的這些“特定位置”被稱為安全點,即當前線程執行到安全點后才允許暫停進行GC

  • 如果一段代碼中,對象引用關系不會發生變化,這個區域中任何地方開始GC都是安全的,那么這個區域稱為安全區域

  • 優點:失效簡單、效率高

  • 缺點:不能解決對象之間循環引用的問題

  • 引用計數法:給對象添加一個引用計數器,有訪問就加1,引用失效就減1

  • 根搜索算法(可達性分析法):從根(GC Roots)節點向下搜索對象節點,搜索走過的路徑稱為引用鏈,當一個對象到根之間沒有連通的話,則該對象不可用

14.2 跨代引用
  • 跨代引用:也就是一個代中的對象引用另一個代中的對象
  • 跨代引用假說:跨代引用相對于同代引用來說只是極少數
  • 隱含推論:存在相互引用關系的兩個對象,是應該傾向于同時生存或同時消亡的
14.3 記憶集
  • 記憶集(Remembered Set):一種用于記錄從非收集區域指向收集區域的指針集合的抽象數據結構
  • 字長精度:每個記錄精確到一個機器字長,該子包含跨代指針
  • 對象精度:每個記錄精確到一個對象,該對象里有字段含有跨代指針
  • 卡精度:每個記錄精確到一塊內存區域,該區域內有對象含有跨代指針
  • 卡表(Card Table):是記憶集的一種具體實現,定義了記憶集的記錄精度和與堆內存的映射關系等
  • 卡表的每個元素都對應著其標識的內存區域中一塊特定大小的內存塊,這個內存塊稱為 卡頁(Card Page)
14.4 寫屏障
  • 寫屏障可以看成是JVM對 ”引用類型字段賦值“ 這個動作的AOP
  • 通過寫屏障來實現當對象狀態改變后,維護卡表狀態
14.5 判斷是否垃圾的步驟
  • 跟搜索算法判斷不可用
  • 看是否有必要執行 finalize 方法
  • 兩個步驟走完后對象仍然沒有人使用,那就屬于垃圾
14.6 GC 類型
  • MinorGC / YoungGC:發生在新生代的收集動作
  • MajorGC / OldGC:發生在老年代的GC,目前只有CMS收集器會有單獨收集老年代的行為
  • MixedGC:收集整個新生代以及部分老年代,目前只有G1收集器會有這種行為
  • FullGC:收集整個Java堆和方法區的GC
14.7 Stop-The-World
  • STW是Java中一種全局暫停的現象,多半由于GC引起。所謂全局停頓,就是所有Java代碼停止運行,native代碼可以執行,但不能和JVM交互
  • 其危害是長時間服務停止,沒有響應;對于HA系統,可能引起主備切換,嚴重危害生產環境
14.8 垃圾收集類型
  • 串行收集:GC單線程內存回收、會暫停所有的用戶線程,如:Serial
  • 并行收集:多個GC線程并發工作,此時用戶線程是暫停的,如:Parallel
  • 并發收集:用戶線程和GC線程同時執行(不一定是并行,可能交替執行),不需要停頓用戶線程,如:CMS
14.9 判斷類無用的條件
  • JVM 中該類的所有實例都已經被回收
  • 加載該類的 ClassLoader 已經被回收
  • 沒有任何地方引用該類的 Class 對象
  • 無法在任何地方通過反射訪問這個類
14.10 垃圾回收算法
14.10.1 標記清除算法
  • 標記清除算法(Mark-Sweep):分為標記和清除兩個階段,先標記出要回收的對象,然后統一回收這些對象

  • 在這里插入圖片描述

  • 優點:簡單

  • 缺點:

  • 效率不高,標記和清除的效率都不高

  • 產生大量不連續的內存碎片,從而導致在分配大對象時觸發GC

14.10.2 復制算法
  • 復制算法(Copying):把內存分成兩塊完全相同的區域,每次使用其中一塊,當一塊使用完了,就把這塊上還存活的對象拷貝到另外一塊,然后把這塊清除掉
  • 在這里插入圖片描述
  • 優點:實現簡單,運行高效,不用考慮內存碎片問題
  • 缺點:內存有些浪費
  • JVM實際實現中,是將內存分為一塊較大的Eden區和兩塊較小的 Survivor 空間,每次使用Eden和一塊 Survivor,回收時,把存活的對象復制到另一塊 Survivor
  • HotSpot 默認的 Eden 和 Survivor 比是 8:1,也就是每次能用 90% 的新生代空間
  • 如果 Survivor 空間不夠,就要依賴老年代進行分配擔保,把放不下的對象直接進入老年代分配擔保:當新生代進行垃圾回收后,新生代的存活區放置不下,那么需要把這些對象放置到老年代去的策略,也就是老年代為新生代的GC做空間分配擔保,步驟如下:
  • 在發生 MinorGC 前,JVM會檢查老年代的最大可用的連續空間,是否大于新生代所有對象的總空間,如果大于,可以確保 MinorGC 是安全的
  • 如果小于,那么JVM會檢查是否設置了允許擔保失敗,如果允許,則繼續檢查老年代最大可用的連續空間,是否大于歷次晉升到老年代對象的平均大小
  • 如果大于,則嘗試進行一次 MinorGC
  • 如果不大于,則改做一次 Full GC
14.10.3 標記整理算法
  • 標記整理算法(Mark-Compact):由于復制算法在存活對象比較多的時候,效率較低,且有空間浪費,因此老年代一般不會選用復制算法,老年代多選用標記整理算法
  • 標記過程跟標記清除算法一樣,但后續不是直接清除可回收對象,而是讓所有存活對象都向一端移動,然后直接清除邊界以外的內存
  • 在這里插入圖片描述

垃圾收集器

  • 串行收集器、并行收集器、新生代Parallel、Scavenge收集器、CMS、G1
  • 在這里插入圖片描述

串行收集器

  • Serial(串行)收集器 / Serial Old 收集器,是一個單線程的收集器,在垃圾收集時,會 Stop-the-World
  • 在這里插入圖片描述
  • 優點:簡單,對于單cpu,由于沒有多線程的交互開銷,可能更高效,是默認的 Client 模式下的新生代收集器
  • 使用 -XX:+UseSerialGC 來開啟,會使用:Serial + SerialOld 的收集器組合
  • 新生代使用復制算法,老年代使用標記-整理算法

并行收集器****ParNew收集器

  • ParNew(并行)收集器:使用多線程進行垃圾回收,在垃圾收集時,會Stop-the-World
  • 在這里插入圖片描述
  • 在并發能力好的 CPU 環境里,它停頓的時間要比串行收集器短;但對于單 CPU 或并發能力較弱的CPU,由于多線程的交互開銷,可能比串行回收器更差
  • 是 Server 模式下首選的新生代收集器,且能和 CMS 收集器配合使用
  • 不再使用 -XX:+UseParNewGC來單獨開啟
  • -XX:ParallelGCThreads:指定線程數,最好與 cpu 數量一致

新生代Parallel Scavenge 收集器

  • 新生代 Parallel Scavenge 收集器 / Parallel Old 收集器:是一個應用于新生代的,使用復制算法的、并行的收集器
  • 與 ParNew 很類似,但更關注吞吐量,能最高效率的利用 CPU,適合運行后臺應用
  • 在這里插入圖片描述
  • 使用 -XX:+UseParallelGC 來開啟
  • 使用 -XX:+UseParallelOldGC 來開啟老年代使用 ParallelOld收集器,使用 Parallel Scavenge + Parallel Old 的收集器組合
  • -XX:MaxGCPauseMillis:設置GC 的最大停頓時間
  • 新生代使用復制算法,老年代使用標記-整理算法

CMS收集器

  • CMS(Concurrent Mark and Sweep 并發標記清除)收集器分為:初始標記:只標記GC Roots 能直接關聯到的對象;并發標記:進行GC Roots Tracing 的過程

  • 重新標記:修正并發標記期間,因程序運行導致標記發生變化的那一部分對象

  • 并發清除:并發回收垃圾對象

  • 在這里插入圖片描述

  • 在初始化標記和重新標記兩個階段還是會發生 Stop-the-World

  • 使用標記清除算法,多線程并發收集的垃圾收集器

  • 最后的重置線程,指的是清空跟收集相關的數據并重置,為下次收集做準備

  • 優點:低停頓,并發執行

  • 缺點:

  • 并發執行,對 CPU 資源壓力大

  • 無法處理 在處理過程中 產生的垃圾(浮動垃圾),可能導致 FullGC

  • 采用的標記清除算法會導致大量碎片,從而在分配大對象可能觸發 FullGC

  • 開啟:-XX:UseConcMarkSweepGC:使用 ParNew + CMS + Serial Old 的收集器組合,Serial Old 將作為 CMS 出錯的后備收集器

  • -XX:CMSInitiatingOccupancyFraction:設置 CMS 收集器在老年代空間被使用多少后觸發回收,默認 80%

G1收集器

  • G1(Garbage-First)收集器:是一款面向服務應用的收集器,與其他收集器相比,具有以下特點:

  • G1 把內存劃分成多個獨立的區域(Region)

  • G1 仍采用分代思想,保留了新生代和老年代,但它們不再是物理隔離的,而是一部分Region的集合,且不需要 Region 是連續的

  • 在這里插入圖片描述

  • G1 能充分利用多 CPU 、多核環境硬件優勢,盡量縮短 STW

  • G1 整體上采用標記-整理算法,局部是通過復制算法,不會產生內存碎片

  • G1 的停頓可預測,能明確指定在一個時間段內,消耗在垃圾收集上的時間不能超過多長時間

  • G1 跟蹤各個 Region 里面垃圾堆的價值大小,在后臺維護一個優先列表,每次根據允許的時間來回收價值最大的區域,從而保證在有限時間內的高效收集

  • 垃圾收集:

  • 初始標記:只標記GC Roots 能直接關聯到的對象

  • 并發標記:進行 GC Roots Tracing 的過程

  • 最終標記:修正并發標記期間,因程序運行導致標記發生變化的那一部分對象

  • 篩選回收:根據時間來進行價值最大化的回收

  • 使用和配置G1:-XX:+UseG1GC:開啟G1,默認就是G1

  • -XX:MaxGCPauseMillis = n :最大GC停頓時間,這是個軟目標,JVM將盡可能(但不保證)停頓小于這個時間

  • -XX:InitiatingHeapOccupancyPercent = n:堆占用了多少的時候就觸發GC,默認為45

  • -XX:NewRatio = n:默認為2

  • -XX:SurvivorRatio = n:默認為8

  • -XX:MaxTenuringThreshold = n:新生代到老年代歲數,默認是15

  • -XX:ParallelGCThreads = n:并行GC的線程數,默認值會根據平臺不同而不同

  • -XX:ConcGCThreads = n:并發 GC 使用的線程數

  • -XX:G1ReservePercent = n:設置作為空閑空間的預留內存百分比,以降低目標空間溢出的風險,默認值是 10%

  • -XX:G1HeapRegionSize = n:設置的 G1 區域的大小。值是2的冪,范圍是1MB到32MB,目標是根據最小的Java堆大小劃分出約2048個區域

14.11 ZGC收集器(了解)
  • ZGC收集器:JDK11加入的具有實驗性質的低延遲收集器

  • ZGC的設計目標是:支持TB級內存容量,暫停時間低(<10ms),對整個程序吞吐量的影響小于15%

  • ZGC里面的新技術:著色指針 和 讀屏障

  • GC性能指標:

  • 吞吐量 = 應用代碼執行的時間 / 運行的總時間

  • GC負荷,與吞吐量相反,是 GC 時間 / 運行的總時間

  • 暫停時間,就是發生 Stop-the-World 的總時間

  • GC 頻率,就是GC在一個時間段發生的次數

  • 反應速度:就是從對象成為垃圾開始到被回收的時間

  • 交互式應用通常希望暫停時間越少越好

  • JVM內存配置原則:

  • 新生代盡可能設置大點,如果太小會導致:

  • 對于老年代,針對響應時間優先的應用:由于老年代通常采用并發收集器,因此其大小要綜合考慮并發量和并發持續時間等參數

  • 對于老年代,針對吞吐量優先的應用:通常設置較大的新生代和較小的老年代,這樣可以盡可能回收大部分短期對象,減少中期對象,而老年代盡量存放長期存活的對象

  • 依據對象的存活周期進行分類,對象優先在新生代分配,長時間存活的對象進入老年代

  • 根據不同代的特點,選取合適的收集算法:少量對象存活,適合復制算法;大量對象存活,適合標記清除或標記整理

  • 如果設置小了,可能會造成內存碎片,高回收頻率會導致應用暫停

  • 如果設置大了,會需要較長的回收時間

  • YGC 次數更加頻繁

  • 可能導致 YGC 后的對象進入老年代,如果此時老年代滿了,會觸發FGC

十五、高效并發

15.1 Java內存模型
  • JCP 定義了一種 Java 內存模型,以前是在 JVM 規范中,后來獨立出來成為JSR-133(Java內存模型和線程規范修訂)
  • 內存模型:在特定的操作協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象
  • Java 內存模型主要關注 JVM 中把變量值存儲到內存和從內存中取出變量值這樣的底層細節
  • 在這里插入圖片描述
  • 所有變量(共享的)都存儲在主內存中,每個線程都有自己的工作內存;工作內存中保存該線程使用到的變量的主內存副本拷貝
  • 線程對變量的所有操作(讀、寫)都應該在工作內存中完成
  • 不同線程不能相互訪問工作內存,交互數據要通過主內存

內存間的交互操作

  • Java內存模型規定了一些操作來實現內存間交互,JVM會保存它們是原子的
  • lock:鎖定,把變量標識為線程獨占,作用于主內存變量
  • unlock:解鎖,把鎖定的變量釋放,別的線程才能使用,作用于主內存變量
  • read:讀取,把變量從主內存讀取到工作內存
  • load:載入,把read讀取到的值放入工作內存的變量副本中
  • use:使用,把工作內存中一個變量的值傳遞給執行引擎
  • assign:賦值,把從執行引擎接收到的值賦給工作內存里面的變量
  • store:存儲,把工作內存中一個變量的值傳遞到主內存中
  • wirte:寫入,把 store 進來的數據存放如主內存的變量中
  • 在這里插入圖片描述
15.2 內存間的交互操作的規則
  • 不允許 read 和 load 、store 和 write 操作之一單獨出現,以上兩個操作必須按照順序執行,但不保證連續執行,也就是說,read 和 load 之間、store 與 write 之間是可插入其他指令的
  • 不允許一個線程丟棄它的最近的 assign 操作,即變量在工作內存中改變了之后必須把該變化同步回主內存
  • 不允許一個線程無原因地(沒有發生過任何 assign 操作)把數據從線程的工作內存同步回主內存中
  • 一個新的變量只能從主內存中 ”誕生“,不允許在工作內存中直接使用一個未被初始化的變量,也就是對一個變量實施 use 和 store 操作之前,必須先執行過了 assign 和 load 操作
  • 一個變量在同一個時刻只允許一條線程對其執行 lock 操作,但 lock 操作可以被同一條線程重復執行多次,多次執行lock后,只有執行相同次數的 unlock 操作,變量才會被解鎖
  • 如果對一個變量執行 lock 操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行 load 或 assign 操作初始化變量的值
  • 如果一個變量沒有被 lock 操作鎖定,則不允許對它執行 unlock 操作,也不能 unlock 一個被其他線程鎖定的變量
  • 對一個變量執行 unlock 操作之前,必須先把此變量同步回主內存(執行 store 和 write 操作)
15.3 volatile特性
15.3.1多線程中的可見性
  • 可見性:就是一個線程修改了變量,其他線程可以知道
  • 保證可見性的常見方法:volatile、synchronized、final(一旦初始化完成,其他線程就可見)
15.3.2 volatile
  • volatile 基本上是 JVM 提供的最輕量級的同步機制,用 volatile 修飾的變量,對所有的線程可見,即對 volatile 變量所做的寫操作能立即反映到其他線程中

  • 用 volatile 修飾的變量,在多線程環境下仍然是不安全的

  • volatile 修飾的變量,是禁止指令重排優化的

  • 適合使用 valatile 的場景

  • 運算結果不依賴變量的當前值

  • 確保只有一個線程修改變量的值

15.3.3 指令重排
  • 指令重排:指的是 JVM 為了優化,在條件允許的情況下,對指令進行一定的重新排列,直接運行當前能夠立即執行的后序指令,避開獲取下一條指令所需數據造成的等待

  • 線程內串行語義,不考慮多線程間的語義

  • 不是所有的指令都能重排,比如:

  • 寫后讀 a = 1;b = a;寫一個變量之后,再讀這個位置

  • 寫后寫 a = 1;a = 2;寫一個變量之后,再寫這個變量

  • 讀后寫 a = b;b = 1;讀一個變量之后,再寫這個變量

  • 以上語句不可重排,但是 a = 1;b = 2;是可以重排的

  • 程序順序原則:一個線程內保證語義的串行性

  • volatile規則:volatile 變量的寫,先發生于讀

  • 鎖規則:解鎖(unlock)必然發生在隨后的加鎖(lock)前

  • 傳遞性:A 先于 B,B 先于 C,那么 A 必然先于 C

  • 線程的 start 方法先于它的每一個動作

  • 線程的所有操作先于線程的終結

  • 線程中斷(interrupt())先于被中斷線程的代碼

  • 對象的構造函數執行結束先于 finalize() 方法

15.4 Java線程安全的處理方法
  • 不可變是線程安全的

  • 互斥同步(阻塞同步):synchronized、java.util.concurrent.ReentrantLock。目前這兩個方法性能已經差不多了,建議優先選用 synchronized,ReentrantLock 增加了如下特性:

  • 等待可中斷:當持有鎖的線程長時間不釋放鎖,正在等待的線程可以選擇放棄等待

  • 公平鎖:多個線程等待同一個鎖時,須嚴格按照申請鎖的時間順序來獲取鎖

  • 鎖綁定多個條件:一個 ReentrantLock 對象可以綁定多個 condition 對象,而 synchronized 是針對一個條件的,如果要多個,就得有多個鎖

  • 非阻塞同步:是一種基于沖突檢查的樂觀鎖策略,通常是先操作,如果沒有沖突,操作就成功了,有沖突再采取其他方式進行補償處理

  • 無同步方案:其實就是在多線程中,方法并不涉及共享數據,自然也就無需同步了

15.5 鎖優化
15.5.1 自旋鎖與自適應自旋
  • 自旋:如果線程可以很快獲得鎖,那么可以不再 OS 層掛起線程,而是讓線程做幾個忙循環,這就是自旋
  • 自適應自旋:自旋的時間不再固定,而是由前一次在同一個鎖上的自旋時間和鎖的擁有者狀態來決定
  • 如果鎖被占用時間很短,自旋成功,那么能節省線程掛起、以及切換時間,從而提升系統性能
  • 如果鎖被占用時間很長,自旋失敗,會白白浪費處理器資源,降低系統性能
15.5.2 鎖消除
  • 在編譯代碼的時候,檢測到根本不存在共享數據競爭,自然也就無需同步加鎖了;通過 -XX:+EliminateLocks 來開啟
  • 同時要使用 -XX:DoEscapeAnalysis 開啟逃逸分析逃逸分析:
  • 如果一個方法中定義的一個對象,可能被外部方法引用,稱為方法逃逸
  • 如果對象可能被其他外部線程訪問,稱為線程逃逸,比如賦值給類變量或者可以在其他線程中訪問的實例變量
15.5.3 鎖粗化
  • 通常我們都要求同步塊要小,但一系列連續的操作導致一個對象反復的加鎖和解鎖,這會導致不必要的性能損耗。這種情況建議把鎖同步的范圍加大到整個操作序列
15.5.4 輕量級鎖
  • 輕量級是相對于傳統鎖機制而言,本意是沒有多線程競爭的情況下,減少傳統鎖機制使用 OS 實現互斥所產生的性能損耗
  • 其實現原理很簡單,就是類似樂觀鎖的方式
  • 如果輕量級鎖失敗,表示存在競爭,升級為重量級鎖,導致性能下降
15.5.5 偏向鎖
  • 偏向鎖是在無競爭情況下,直接把整個同步消除了,連樂觀鎖都不用,從而提高性能;所謂的偏向,就是偏心,即鎖會偏向于當前已經占有鎖的線程
  • 只要沒有競爭,獲得偏向鎖的線程,在將來進入同步塊,也不需要做同步
  • 當有其他線程請求相同的鎖時,偏向模式結束
  • 如果程序中大多數鎖總是被多個線程訪問的時候,也就是競爭比較激烈,偏向鎖反而會降低性能
  • 使用 -XX:-UseBiasedLocking 來禁用偏向鎖,默認開啟
15.6 JVM 中獲取鎖的步驟
  • 會先嘗試偏向鎖;然后嘗試輕量級鎖
  • 再然后嘗試自旋鎖
  • 最后嘗試普通鎖,使用 OS 互斥量在操作系統層掛起
15.7 同步代碼的基本規則
  • 盡量減少持有鎖的時間
  • 盡量減少鎖的粒度
15.8 性能監控與故障處理工具
15.8.1 命令行工具
  • 命令行工具:jps、jinfo、jstack、jmap、jstat、jstatd、jcmd
  • 圖形化工具:jconsole、jmc、visualvm
  • 兩種連接方式:JMX、jstatd
15.8.2 JVM 檢測工具的作用
  • 對 jvm 運行期間的內部情況進行監控,比如:對 jvm 參數、CPU、內存、堆等信息的查看
  • 輔助進行性能調優
  • 輔助解決應用運行時的一些問題,比如:OutOfMemoryError、內存泄漏、線程死鎖、鎖爭用、Java進程消耗 CPU 過高 等等
15.8.3 jps
  • jps(JVM Process Status Tool):主要用來輸出 JVM 中運行的進程狀態信息,語法格式如下:jps [options] [hostid]
  • hostid 字符串的語法與 URI 的語法基本一致:[protocol:] [ [ // ] hostname] [ :port ] [/servername],如果不指定hostid,默認為當前主機或服務器
15.8.4 jinfo
  • 打印給定進程或核心或遠程調試服務器的配置信息。語法格式:jinfo [option] pid #指定進程號(pid)的進程
  • jinfo [ option ] <executable #指定核心文件
  • jinfo [option] [server-id@] #指定遠程調試服務器
15.8.5 jstack
  • jstack 主要用來查看某個 Java 進程內的線程堆棧信息。語法格式如下:jstack [option] pid
  • jstack [option] executable core
  • jstack [option] [server-id@] remorte-hostname-or-ip
15.8.6 jmap
  • jmap 用來查看堆內存使用情況,語法格式如下:jmap [option] pid
  • jmap [option] executable core
  • jmap [option] [server-id@] remote-hostname-or-ip
15.8.7 jstat
  • JVM 統計監測工具,查看各個區域內存和 GC 的情況
  • 語法格式如下:jstat [generalOption | outputOptions vmid [interval[s|ms] [count]]]
15.8.8 jstated
  • 虛擬機的 jstat 守護進行,主要用于監控 JVM 的創建與終止,并提供一個接口,以有序遠程監視工具附加到本地系統上運行的 JVM、
  • 語法格式:jstatd [ options ]
15.8.9 jcmd
  • JVM 診斷工具,將診斷命令請求發送到正在運行的額 Java 虛擬機,比如可以用來導出堆,查看 java 進程,導出線程信息,執行 GC 等
15.9 圖形化工具
15.9.1 jconsole
  • 一個用于監視 Java 虛擬機的符合 JMX的圖形工具。它可以監視本地和遠程 JVM,還可以監視和管理應用程序
15.9.2 jmc
  • jmc(JDK Mission Control)Java 任務控制(JMC)客戶端包括用于監視和管理 Java 應用程序的工具,而不是引入通常與這些類型的工具相關聯的性能開銷
15.9.3 VisualVM
  • 一個圖形工具,它提供有關在 Java 虛擬機中運行的基于 Java 技術的應用程序的詳細信息
  • Java VisualVM 提供內存和 CPU 分析,堆轉儲分析,內存泄漏檢測,訪問 MBean 和垃圾回收
15.10 遠程連接
  • JMX 連接可以查看:系統信息、CPU使用情況、線程多少、手動執行垃圾回收等比較偏于系統層面的信息
  • jstatd 連接方式可以提供:JVM 內存分布詳細信息、垃圾回收分布圖、線程詳細信息,甚至可以看到某個對象使用內存的大小
    s|ms] [count]]]
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容