JVM系统学习
文章目錄
- 為什么要學習JVM
- Java內存區域
- 運行時數據區域
- 方法區的變化
- 數據的遷移
- 元空間簡介
- 元數據
- 為什么移除永久代?
- 為什么要引入元空間
- 程序計數器
- Java 虛擬機棧
- 本地方法棧
- 堆
- 方法區
- 方法區和永久代的關系
- 常用參數
- 元空間
- 運行時常量池
- 直接內存
- HotSpot虛擬機對象探秘
- 對象的創建
- Step1:類加載檢查
- Step2:分配內存
- Step3:初始化零值
- Step4:設置對象頭
- Step5:執行 init 方法
- 對象的內存布局
- 對象頭
- 對象自身的運行時數據
- 類型指針
- 實例數據
- 對齊填充
- 對象的訪問定位
- String 類和常量池
- 字符串拼接:
- 小問題
- 8 種基本類型的包裝類和常量池
- 參考鏈接
- 垃圾收集器與內存分配策略
- 本章常見面試題
- 導論
- 概述
- JVM內存分配和回收基礎
- 對象優先在 eden 區分配
- 大對象直接進入老年代
- 長期存活的對象將進入老年代
- 動態對象年齡判定
- 空間分配擔保
- Full GC 的觸發條件
- 1. 調用 System.gc()
- 2. 老年代空間不足
- 3. 空間分配擔保失敗
- 4. JDK 1.7 及以前的永久代空間不足
- 5. Concurrent Mode Failure
- 對象已死?
- 引用計數法
- 可達性分析算法
- GC Roots
- 再談引用
- 強引用
- 軟引用
- 弱引用
- 應用
- WeakHashMap
- ReferenceQueue
- 虛引用
- PhantomReference
- 生存還是死亡?
- 回收方法區
- 如何判斷一個常量是廢棄常量
- 如何判斷一個類是無用的類
- OOM
- Java.lang.StackOverflowError
- Java.lang.OutOfMemoryError: Java heap space
- Java.lang.OutOfMemoryError: GC overhead limit exceeded
- Java.lang.OutOfMemoryError: Direct buffer memory
- Java.lang.OutOfMemoryError: unable to create new native thread
- 導致原因
- 解決辦法
- Java.lang.OutOfMemoryError: Metaspace
- 垃圾收集算法
- 分代收集理論
- 標記-清除算法
- 標記-復制算法
- 標記-整理算法
- HotSpot的算法細節實現
- 經典垃圾收集器
- 1.新生代的收集器包括
- 2.老年代的收集器包括
- 3.回收整個Java堆(新生代和老年代)
- Serial 收集器 Serial串行收集器-復制算法
- 總結
- 優勢:
- 劣勢:
- 使用場景
- ParNew 收集器 復制算法
- 小結
- 優勢:
- 劣勢:
- 使用場景
- Parallel Scavenge 收集器 (并行回收)收集器-復制算法
- 小結
- 有如下特點:
- 優勢:
- 劣勢:
- 使用場景
- Serial Old 收集器 標記整理算法
- 小結
- 特點
- 使用場景
- Parallel Old收集器 收集器-標記整理算法
- CMS 收集器 標記整理算法
- 小結
- 特點:
- 優勢:
- 劣勢:
- G1 收集器
- ZGC
- 查看當前使用的垃圾收集器
- 一些參數
- 配置垃圾收集器
- 如何選擇垃圾收集器
- 低延遲垃圾收集器
- Shenandoah收集器
- ZGC收集器
- 選擇合適的垃圾收集器
- Epsilon收集器
- JVM垃圾收集器總結
- JVM調優
- JVM參數類型
- 標配參數
- X參數
- XX參數
- Boolean類型
- KV設值類型
- jinfo舉例,如何查看當前運行程序的配置
- JVM常用配置參數
- -Xms 和 -Xmx
- -Xss
- -Xmn
- -XX:MetaspaceSize
- -XX:+PrintGCDetails
- Minor GC
- Full GC
- 設置參數案例
- -XX:SurvivorRatio
- -XX:NewRatio
- -XX:MaxTenuringThreshold
- 如何查看JVM系統默認值
- 類文件結構
- 概述
- Class 文件結構總結
- 2.1 魔數
- 2.2 Class 文件版本
- 2.3 常量池
- 2.4 訪問標志
- 2.5 當前類索引,父類索引與接口索引集合
- 2.6 字段表集合
- 2.7 方法表集合
- 2.8 屬性表集合
- 參考鏈接
- 虛擬機類加載機制
- 類的生命周期
- 類加載過程
- 加載
- 驗證
- 準備
- 解析
- 初始化
- 主動引用
- 被動引用
- 卸載
- **參考鏈接**
- 類加載器
- 雙親委派模型
- 雙親委派模型實現源碼分析
- 雙親委派模型的好處
- 如果我們不想用雙親委派模型怎么辦?
- 自定義類加載器
- tomcat 為什么打破雙親委派模型
- 參考鏈接
- JVM調優
- 概述
- 為什么調優
- 性能優化的步驟
- 性能評價指標
- 性能監控及診斷工具
- jps
- 基本語法
- jstat
- 基本語法
- 判斷內存泄露
- jinfo
- 基本語法
- jmap
- 導出內存映像文件
- 顯示堆內存相關信息
- jat
- 基本語法
- Jstack(JVM Stack Trace)
- jcmd
- jstatd
- 圖形化工具
- Jconsole
- Visual VM
- MAT
- tomcat
- JProfile
- Arthas
- 命令
- Java Mission Control
- Flame Graphs
- Tprofiler
- 內存泄露
- 靜態集合類
- 單例模式
- 內部類持有外部類
- 各種連接
- 變量不合理的作用域
- 改變哈希值
- 緩存泄漏
- 監聽器和回調
- JVM運行時參數
- 分析日志
- Minor GC日志解析
- Full GC日志分析
- GC Easy
- GCViewer
- 性能優化
為什么要學習JVM
-
面試的需要(BATJ、TMD,PKQ等面試都愛問)
-
中高級程序員必備技能
- 項目管理、調優的需求
-
追求極客的精神
- 比如:垃圾回收算法、JIT(及時編譯器)、底層原理
Java內存區域
運行時數據區域
Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分為若干個不同的數據區域。JDK 1.8和之前的版本略有不用,見下圖:
方法區的變化
JDK1.8與JDK1.7最大的區別是:JDK1.8將永久代取消,取而代之的是元空間,在JDK1.8中方法區是由元空間來實現,所以原來屬于方法區的運行時常量池就屬于元空間了。
??元空間屬于本地內存,所以元空間的大小僅受本地內存限制,但是可以通過-XX:MaxMetaspaceSize進行增長上限的最大值設置,默認值為4G,元空間的初始空間大小可以通過-XX:MetaspaceSize進行設置,默認值為20.8M,還有一些其他參數可以進行設置,元空間大小會自動進行調整。
數據的遷移
在JDK1.7之前運行時常量池,字符串常量池,靜態域等存放在方法區, 運行時常量池邏輯包含字符串常量池,此時hotspot虛擬機對方法區的實現為永久代。
??在JDK1.7中字符串常量池和靜態域被從方法區(永久代)拿到了堆中(在堆中另開辟了一塊空間),這里沒有提到運行時常量池,也就是說字符串常量池被單獨拿到堆,運行時常量池剩下的東西還在方法區,也就是hotspot中的永久代。
??在JDK1.8 hotspot移除了永久代,用元空間(Metaspace)取而代之, 這時候字符串常量池還在堆,運行時常量池還在方法區,只不過方法區的實現從永久代變成了元空間(Metaspace)。
元空間簡介
用于存儲已被虛擬機加載的類元數據,符號引用,即包括運行時常量池。元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代之間最大的區別在于:元空間并不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過以下參數來指定元空間的大小:-XX:MetaspaceSize。
元數據
元數據(Meta Date),關于數據的數據或者叫做用來描述數據的數據或者叫做信息的信息。
??這些定義都很是抽象,我們可以把元數據簡單的理解成,最小的數據單位。元數據可以為數據說明其元素或屬性(名稱、大小、數據類型、等),或其結構(長度、字段、數據列),或其相關數據(位于何處、如何聯系、擁有者)
為什么移除永久代?
由于永久代使用jvm內存經常不夠用或發生內存泄露,引發惱人的OutOfMemoryError異常(在Java Web開發中非常常見)。
移除永久代可以促進HotSpot JVM與JRockit VM兩種虛擬機的融合,因為JRockit沒有永久代。
對永久代進行調優是很困難的。永久代中的元數據可能會隨著每一次Full GC發生而進行移動。
為什么要引入元空間
字符串常量存在永久代中,容易出現性能問題和內存溢出。
類及方法的信息等比較難確定其大小,因此對于永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出。
永久代會為 GC 帶來不必要的復雜度,并且回收效率偏低。
Oracle 可能會將HotSpot與Jrockit兩種虛擬機合二為一。
線程私有的:程序計數器、虛擬機棧、本地方法棧
線程共享的:堆、方法區、直接內存
程序計數器
程序計數器是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等功能都需要依賴這個計數器來完成。
另外,為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各線程之間計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存。
從上面的介紹中我們知道程序計數器主要有兩個作用:
如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是本地(Native)方法,這個計數器值則為空。
注意:程序計數器是唯一一個不會出現 OutOfMemoryError 的內存區域,它的生命周期隨著線程的創建而創建,隨著線程的結束而死亡。
Java 虛擬機棧
與程序計數器一樣,Java 虛擬機棧也是線程私有的,它的生命周期和線程相同,描述的是 Java 方法執行的內存模型,每次方法調用的數據都是通過棧傳遞的。
虛擬機棧描述的是Java方法執行的線程內存模型:每個方法執行的時候,Java虛擬機都會同步創建一個棧幀用于存儲局部變量表、操作數棧、動態鏈接、方法出口信息。每一個方法被調用直至執行完畢的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
局部變量表主要存放了編譯器可知的各種數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它不同于對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)和**returnAddress類型(**指向了一條字節碼指令的地址)。
這些數據類型在局部變量表中的存儲空間以局部變量槽(Slot)來表示,其中64位長度的long和double類型的數據會占用兩個變量槽,其余類型只占用一個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在棧幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。("大小"指的是變量槽的數量)
Java 虛擬機棧會出現兩種錯誤:StackOverFlowError 和 OutOfMemoryError。
- StackOverFlowError: 若 Java 虛擬機棧的內存大小不允許動態擴展,那么當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,就拋出 StackOverFlowError 錯誤。
- OutOfMemoryError: 若 Java 虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出 OutOfMemoryError 錯誤。
擴展:那么方法/函數如何調用?
Java ??捎妙惐葦祿Y構中棧,Java 棧中保存的主要內容是棧幀,每一次函數調用都會有一個對應的棧幀被壓入 Java 棧,每一個函數調用結束后,都會有一個棧幀被彈出。
Java 方法有兩種返回方式:
不管哪種返回方式都會導致棧幀被彈出。
本地方法棧
和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧為虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。
方法執行完畢后相應的棧幀也會出棧并釋放內存空間,也會出現 StackOverFlowError 和 OutOfMemoryError 兩種錯誤。
堆
Java 堆是Java 虛擬機所管理的內存中最大的一塊,也是所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這里分配內存。
Java 堆是垃圾收集器管理的主要區域,因此也被稱作GC 堆(Garbage Collected Heap).從垃圾回收的角度,由于現在收集器基本都采用分代垃圾收集算法,所以 Java 堆還可以細分為:新生代和老年代:再細致一點有:Eden 空間、From Survivor、To Survivor 空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。
在 JDK 7 版本及JDK 7 版本之前,堆內存被通常被分為下面三部分:
-
新生代內存(Young Generation)
-
老生代(Old Generation)
-
永生代(Permanent Generation)
JDK 8 版本之后方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。
上圖所示的 Eden 區、兩個 Survivor 區都屬于新生代(為了區分,這兩個 Survivor 區域按照順序被命名為 s1 和 s2),中間一層屬于老年代。
大部分情況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收后,如果對象還存活,則會進入 s1 或者 s2,并且對象的年齡還會加 1(Eden 區->Survivor 區后對象的初始年齡變為 1),當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。
新生代到老年代的閾值:
Hotspot遍歷所有對象時,按照年齡從小到大對其所占用的大小進行累積,當累積的某個年齡大小超過了survivor區的一半時,取這個年齡和MaxTenuringThreshold中更小的一個值,作為新的晉升年齡閾值。
堆這里最容易出現的就是 OutOfMemoryError 錯誤,并且出現這種錯誤之后的表現形式還會有幾種,比如:
方法區
方法區與 Java 堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。雖然 Java 虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。方法區也被稱為永久代。
方法區和永久代的關系
《Java 虛擬機規范》只是規定了有方法區這么個概念和它的作用,并沒有規定如何去實現它。那么,在不同的 JVM 上方法區的實現肯定是不同的了。方法區和永久代的關系很像 Java 中接口和類的關系,類實現了接口,而永久代就是 HotSpot 虛擬機對虛擬機規范中方法區的一種實現方式。 也就是說,永久代是 HotSpot 的概念,方法區是 Java 虛擬機規范中的定義,是一種規范,而永久代是一種實現,一個是標準一個是實現,其他的虛擬機實現并沒有永久代這一說法。
常用參數
JDK 1.8 之前永久代還沒被徹底移除的時候通常通過下面這些參數來調節方法區大小
-XX:PermSize=N //方法區 (永久代) 初始大小 -XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGen相對而言,垃圾收集行為在這個區域是比較少出現的,但并非數據進入方法區后就“永久存在”了。這區域的內存回收目標主要是針對常量池的回收和對類型的卸載,一般來說這個區域的回收效果比較好。
JDK 1.8 的時候,方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。
下面是一些常用參數:
-XX:MetaspaceSize=N //設置 Metaspace 的初始(和最小大小) -XX:MaxMetaspaceSize=N //設置 Metaspace 的最大大小與永久代很大的不同就是,如果不指定大小的話,隨著更多類的創建,虛擬機會耗盡所有可用的系統內存。
元空間
JDK 1.8之后使用元空間(存放在內存里)替代永久代,為什么要這樣做?
整個永久代有一個 JVM 本身設置固定大小上限,無法進行調整,而元空間使用的是直接內存,受本機可用內存的限制,雖然元空間仍舊可能溢出,但是比原來出現的幾率會更小。
當你元空間溢出時會得到如下錯誤: java.lang.OutOfMemoryError: MetaSpace
你可以使用 -XX:MaxMetaspaceSize 標志設置最大元空間大小,默認值為 unlimited,這意味著它只受系統內存的限制。-XX:MetaspaceSize 調整標志定義元空間的初始大小如果未指定此標志,則 Metaspace 將根據運行時的應用程序需求動態地重新調整大小。
元空間里面存放的是類的元數據,這樣加載多少類的元數據就不由 MaxPermSize 控制了, 而由系統的實際可用空間來控制,這樣能加載的類就更多了。
運行時常量池
運行時常量池是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池信息(用于存放編譯期生成的各種字面量和符號引用)
既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出 OutOfMemoryError 錯誤。
**JDK1.7 及之后版本的 JVM 已經將字符串常量池從方法區中移了出來,在 Java 堆(Heap)中開辟了一塊區域存放字符串常量池。**但是運行時常量池的其他內容還在方法區,也就是在元空間里面。
直接內存
直接內存并不是虛擬機運行時數據區的一部分,也不是虛擬機規范中定義的內存區域,但是這部分內存也被頻繁地使用。而且也可能導致 OutOfMemoryError 錯誤出現。
JDK1.4 中新加入的 NIO(New Input/Output) 類,引入了一種基于通道(Channel) 與緩存區(Buffer) 的 I/O 方式,它可以直接使用 Native 函數庫直接分配堆外內存,然后通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內存的引用進行操作。這樣就能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆之間來回復制數據。
本機直接內存的分配不會受到 Java 堆的限制,但是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。
HotSpot虛擬機對象探秘
對象的創建
通過上面的介紹我們大概知道了虛擬機的內存情況,下面我們來詳細的了解一下 HotSpot 虛擬機在 Java 堆中對象分配、布局和訪問的全過程。
Step1:類加載檢查
虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,并且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
Step2:分配內存
在類加載檢查通過后,接下來虛擬機將為新生對象分配內存。對象所需的內存大小在類加載完成后便可確定,為對象分配空間的任務等同于把一塊確定大小的內存從 Java 堆中劃分出來。分配方式有 “指針碰撞” 和 “空閑列表” 兩種,選擇那種分配方式由 Java 堆是否規整決定,而 Java 堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
內存分配的兩種方式:
選擇以上兩種方式中的哪一種,取決于 Java 堆內存是否規整。而 Java 堆內存是否規整,取決于 GC 收集器的算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,復制算法內存也是規整的。
內存分配并發問題
在創建對象的時候有一個很重要的問題,就是線程安全,因為在實際開發過程中,創建對象是很頻繁的事情,作為虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機采用兩種方式來保證線程安全:
- CAS+失敗重試: CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性。
- **本地線程分配緩沖(TLAB):**為每一個線程預先在 Eden 區分配一塊兒內存,稱為TLAB;JVM 在給線程中的對象分配內存時,首先在 TLAB 分配,當對象大于 TLAB 中的剩余內存或 TLAB 的內存已用盡時,再采用上述的 CAS 進行內存分配。
Step3:初始化零值
內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
Step4:設置對象頭
初始化零值完成之后,虛擬機要對 對象進行必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。
Step5:執行 init 方法
在上面工作都完成之后,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建才剛開始。<init>方法還沒有執行,所有的字段都還為零。所以一般來說,執行 new 指令之后會接著執行<init>方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產生出來。(<init>方法即初始化方法或者稱為構造方法)
對象的內存布局
在 Hotspot 虛擬機中,對象在內存中的布局可以分為 3 塊區域:對象頭、實例數據和對齊填充。
對象頭
Hotspot 虛擬機的對象頭包括兩部分信息,第一部分用于存儲對象自身的運行時數據,另一部分是類型指針。
對象自身的運行時數據
包括哈希碼、GC 分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID和偏向時間戳等,這部分數據的長度在32位和64位的虛擬機中分別為32bit和64bit,稱為**“Mark Word”**。
對象需要存儲的運行時數據很多,其實已經超過64bit,因對象頭里的信息是與對象自身定義無關的額外存儲成本,所以Mark Word被設計成一個動態數據結構,根據對象的狀態復用自己的存儲空間。
在32位的HotSpot虛擬機中,Mark Word存儲內容如下:
如:在對象未被鎖時,Mark Word的25bit存儲對象的hashcode,4bit存儲分代年齡,1bit表示是否偏向鎖,2bit用于存儲鎖標志位。不用的鎖狀態用不同的鎖標志位表示。
可以看到,分代年齡最大為15,因為只用4bit存儲。
類型指針
類型指針是對象指向它的類型元數據的指針,Java虛擬機通過這個指針來確定該對象是哪個類的實例。并不是所有的虛擬機實現都必須在對象數據上保留類型指針,即查找對象的元數據信息不一定要經過對象本身。
如果對象是一個Java數組,那在對象頭上還必須有一塊用于記錄數組長度的數據,因為虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是如果數組的長度是不確定的,將無法通過元數據中的信息推斷出數組的大小。
實例數據
實例數據部分是對象真正存儲的有效信息,即我們定義的各種類型的字段內容,無論是從父類繼承下來的,還是子類中定義的,都必須記錄下來。
HotSpot虛擬機默認的分配順序為longs/doubles、ints、shorts/chars、bytes/booleans、oops,從以上默認的分配策略中可以看到,相同寬度的字段總是被分配到一起存放,在滿足這個前提條件下,在父類中定義的變量會出現在子類之前。
對齊填充
對齊填充部分不是必然存在的,也沒有什么特別的含義,僅僅起占位作用。 因為 Hotspot 虛擬機的自動內存管理系統要求對象起始地址必須是 8 字節的整數倍,換句話說就是對象的大小必須是 8 字節的整數倍。而對象頭部分正好是 8 字節的倍數(1 倍或 2 倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。
對象的訪問定位
建立對象就是為了使用對象,我們的 Java 程序通過棧上的 reference 數據來操作堆上的具體對象。對象的訪問方式由虛擬機實現而定,目前主流的訪問方式有①使用句柄和②直接指針兩種:
句柄: 如果使用句柄的話,那么 Java 堆中將會劃分出一塊內存來作為句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息;
直接指針: 如果使用直接指針訪問,那么 Java 堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,而 reference 中存儲的直接就是對象的地址。
這兩種對象訪問方式各有優勢。使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 本身不需要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。
String 類和常量池
String對象的兩種創建方式:
String str1 = "abcd";//先檢查字符串常量池中有沒有"abcd",如果字符串常量池中沒有,則創建一個,然后 str1 指向字符串常量池中的對象,如果有,則直接將 str1 指向"abcd""; String str2 = new String("abcd");//堆中創建一個新的對象 String str3 = new String("abcd");//堆中創建一個新的對象 System.out.println(str1==str2);//false System.out.println(str2==str3);//false因為str1是字面量,字面量是存儲在常量池的,通過new創建的是一個對象,對象是存儲在java堆的。
這兩種不同的創建方法是有差別的。
- 第一種方式是在常量池中拿對象;
- 第二種方式是直接在堆內存空間創建一個新的對象。
記住一點:只要使用 new 方法,便需要創建新的對象。
String 類型的常量池比較特殊。它的主要使用方法有兩種:
- 直接使用雙引號聲明出來的 String 對象會直接存儲在常量池中。
- 如果不是用雙引號聲明的 String 對象,可以使用 String 提供的 intern 方法。String.intern() 是一個 Native 方法,它的作用是:如果運行時常量池中已經包含一個等于此 String 對象內容的字符串,則返回常量池中該字符串的引用;如果沒有,JDK1.7之前(不包含1.7)的處理方式是在常量池中創建與此 String 內容相同的字符串(就是在常量池存儲一個副本),并返回常量池中創建的字符串的引用,JDK1.7以及之后的處理方式是在常量池中存儲此字符串的引用,并返回該引用。注意:JDK 1.6及以后存儲的是字符串的一個副本,JDK 1.7及其以后存儲的是字符串的引用,存儲內容變了。見幾張圖輕松理解String.intern()
把第二行和第三行互換順序,依然是同樣的結果。
字符串拼接:
String str1 = "str"; String str2 = "ing";String str3 = "str" + "ing";//常量池中的對象 String str4 = str1 + str2; //在堆上創建的新的對象 String str5 = "string";//常量池中的對象 System.out.println(str3 == str4);//false System.out.println(str3 == str5);//true System.out.println(str4 == str5);//false盡量避免多個字符串拼接,因為這樣會重新創建對象。如果需要改變字符串的話,可以使用 StringBuilder 或者 StringBuffer。
常量字符串的“+”操作,編譯階段直接會合成為一個字符串。如string str=”JA”+”VA”,在編譯階段會直接合并成語句String str=”JAVA”,于是會去常量池中查找是否存在”JAVA”,從而進行創建或引用。
對于final字段,編譯期直接進行了常量替換(而對于非final字段則是在運行期進行賦值處理的)。
final String str1=”ja”; final String str2=”va”; String str3=str1+str2;在編譯時,直接替換成了String str3=”ja”+”va”,根據第三條規則,再次替換成String str3=”JAVA”
常量字符串和變量拼接時(如:String str3=baseStr + “01”;)會調用stringBuilder.append()在堆上創建新的對象。
小問題
String s1 = new String(“abc”);這句話創建了幾個字符串對象?
將創建 1 或 2 個字符串。如果池中已存在字符串常量“abc”,則只會在堆空間創建一個字符串常量“abc”。如果池中沒有字符串常量“abc”,那么它將首先在池中創建,然后在堆空間中創建,因此將創建總共 2 個字符串對象。
驗證:
String s1 = new String("abc");// 堆內存的地址值 String s2 = "abc"; System.out.println(s1 == s2);// 輸出 false,因為一個是堆內存,一個是常量池的內存,故兩者是不同的。 System.out.println(s1.equals(s2));// 輸出 true8 種基本類型的包裝類和常量池
Java 基本類型的包裝類的大部分都實現了常量池技術,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 種包裝類默認創建了數值[-128,127] 的相應類型的緩存數據,Character創建了數值在[0,127]范圍的緩存數據,Boolean 直接返回True Or False。如果超出對應范圍仍然會去創建新的對象。 為啥把緩存設置為[-128,127]區間?(參見issue/461)性能和資源之間的權衡。
public static Boolean valueOf(boolean b) {return (b ? TRUE : FALSE); }private static class CharacterCache { private CharacterCache(){}static final Character cache[] = new Character[127 + 1]; static { for (int i = 0; i < cache.length; i++) cache[i] = new Character((char)i); } }兩種浮點數類型的包裝類 Float,Double 并沒有實現常量池技術。
Integer i1 = 33; Integer i2 = 33; System.out.println(i1 == i2);// 輸出 true Integer i11 = 333; Integer i22 = 333; System.out.println(i11 == i22);// 輸出 false Double i3 = 1.2; Double i4 = 1.2; System.out.println(i3 == i4);// 輸出 falseInteger 緩存源代碼:
/** *此方法將始終緩存-128 到 127(包括端點)范圍內的值,并可以緩存此范圍之外的其他值。 */ public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)return IntegerCache.cache[i + (-IntegerCache.low)];return new Integer(i); }應用場景:
Integer 比較更豐富的一個例子:
Integer i1 = 40; Integer i2 = 40; Integer i3 = 0; Integer i4 = new Integer(40); Integer i5 = new Integer(40); Integer i6 = new Integer(0);System.out.println("i1=i2 " + (i1 == i2)); // true System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); // true System.out.println("i1=i4 " + (i1 == i4)); // false System.out.println("i4=i5 " + (i4 == i5)); // false System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); // true System.out.println("40=i5+i6 " + (40 == i5 + i6)); // true解釋:
語句 i4 == i5 + i6,因為+這個操作符不適用于 Integer 對象,首先 i5 和 i6 進行自動拆箱操作,進行數值相加,即 i4 == 40。然后 Integer 對象無法與數值進行直接比較,所以 i4 自動拆箱轉為 int 值 40,最終這條語句轉為 40 == 40 進行數值比較。
參考鏈接
- 《深入理解 Java 虛擬機:JVM 高級特性與最佳實踐(第三版》
- Guide哥的筆記
- 幾張圖輕松理解String.intern()
垃圾收集器與內存分配策略
本章常見面試題
- 如何判斷對象是否死亡(兩種方法)。
- 簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)。
- 如何判斷一個常量是廢棄常量
- 如何判斷一個類是無用的類
- 垃圾收集有哪些算法,各自的特點?
- HotSpot 為什么要分為新生代和老年代?
- 常見的垃圾回收器有哪些?
- 介紹一下 CMS,G1 收集器。
- Minor Gc 和 Full GC 有什么不同呢?
導論
當需要排查各種內存溢出問題、當垃圾收集成為系統達到更高并發的瓶頸時,我們就需要對這些“自動化”的技術實施必要的監控和調節。
概述
Java內存區域中程序計數器、虛擬機棧、本地方法棧3個區域隨著線程而生,隨著線程而滅。每一個棧幀中分配多少內存基本上是在類結構確定下來就已知的,這幾個區域的內存分配和回收都具備確定性,不需要過多考慮回收的問題,當方法結束或者線程結束時,內存自然就跟著回收了。
Java堆和方法區這兩個內存區域則有很顯著的不確定性,一個接口的多個實現類需要的內存可能會不一樣,一個方法所執行的不同條件分支所需要的內存也可能不一樣,只有處于運行期間,我們才能知道程序究竟會創建哪些對象,創建多少個對象,這部分內存分配和回收是動態的。垃圾收集器正是關注這部分內存。
JVM內存分配和回收基礎
Java 的自動內存管理主要是針對對象內存的回收和對象內存的分配。同時,Java 自動內存管理最核心的功能是 堆 內存中對象的分配與回收。
Java 堆是垃圾收集器管理的主要區域,因此也被稱作GC 堆(Garbage Collected Heap).從垃圾回收的角度,由于現在收集器基本都采用分代垃圾收集算法,所以 Java 堆還可以細分為:新生代和老年代:再細致一點有:Eden 空間、From Survivor、To Survivor 空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。
一些術語:
- 部分收集(Partial GC):指目標不是完整收集整個Java堆的垃圾,而且對部分區域收集??杉毞譃?#xff1a;
- 新生代收集(Minor GC/Young GC)
- 老年代收集(Major GC/Old GC)
- 混合收集(Mixed GC):收集整個新生代以及部分老年代
- 整堆收集(Full GC):收集整個Java堆和方法區的垃圾
堆空間的基本結構:
上圖所示的 eden 區、s0(“From”) 區、s1(“To”) 區都屬于新生代,tentired 區屬于老年代。大部分情況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收后,如果對象還存活,則會進入 s1(“To”),并且對象的年齡還會加 1(Eden 區->Survivor 區后對象的初始年齡變為 1),當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。經過這次GC后,Eden區和"From"區已經被清空。這個時候,“From"和"To"會交換他們的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To”。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重復這樣的過程,直到“To”區被填滿,"To"區被填滿之后,會將所有對象移動到老年代中。
對象優先在 eden 區分配
目前主流的垃圾收集器都會采用分代回收算法,因此需要將堆內存分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。
大多數情況下,對象在新生代中 eden 區分配。當 eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC,如果當Minor GC后eden區依然沒有足夠的空間進行分配,只好通過分配擔保機制 把新生代的對象提前轉移到老年代中去;如果執行分配擔保機制后eden區依然存不下該對象,則再老年代分配;如果能存在,則繼續在eden區分配。
Minor GC 和 Full GC 有什么不同呢?
- 新生代 GC(Minor GC):指發生新生代的的垃圾收集動作,Minor GC 非常頻繁,回收速度一般也比較快。
- 老年代 GC(Major GC):指發生在老年代的 GC,出現了 Major GC 經常會伴隨至少一次的 Minor GC(并非絕對),Major GC 的速度一般會比 Minor GC 的慢 10 倍以上。
大對象直接進入老年代
大對象就是需要大量連續內存空間的對象(比如:字符串、數組)。
為什么要這樣呢?
為了避免為大對象分配內存時由于分配擔保機制帶來的復制而降低效率。
長期存活的對象將進入老年代
既然虛擬機采用了分代收集的思想來管理內存,那么內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代中。為了做到這一點,虛擬機給每個對象一個對象年齡(Age)計數器。
如果對象在 Eden 出生并經過第一次 Minor GC 后仍然能夠存活,并且能被 Survivor 容納的話,將被移動到 Survivor 空間中,并將對象年齡設為 1.對象在 Survivor 中每熬過一次 MinorGC,年齡就增加 1 歲,當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。
動態對象年齡判定
虛擬機并不是永遠要求對象的年齡必須達到 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 中相同年齡所有對象大小的總和大于 Survivor 空間的一半,則年齡大于或等于該年齡的對象可以直接進入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。
空間分配擔保
在發生 Minor GC 之前,虛擬機先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果條件成立的話,那么 Minor GC 可以確認是安全的。
如果不成立的話虛擬機會查看 HandlePromotionFailure 的值是否允許擔保失敗,如果允許那么就會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允許冒險,那么就要進行一次 Full GC。
Full GC 的觸發條件
對于 Minor GC,其觸發條件非常簡單,當 Eden 空間滿時,就將觸發一次 Minor GC。而 Full GC 則相對復雜,有以下條件:
1. 調用 System.gc()
只是建議虛擬機執行 Full GC,但是虛擬機不一定真正去執行。不建議使用這種方式,而是讓虛擬機管理內存。
2. 老年代空間不足
老年代空間不足的常見場景為前文所講的大對象直接進入老年代、長期存活的對象進入老年代等。
為了避免以上原因引起的 Full GC,應當盡量不要創建過大的對象以及數組。除此之外,可以通過 -Xmn 虛擬機參數調大新生代的大小,讓對象盡量在新生代被回收掉,不進入老年代。還可以通過 -XX:MaxTenuringThreshold 調大對象進入老年代的年齡,讓對象在新生代多存活一段時間。
3. 空間分配擔保失敗
使用復制算法的 Minor GC 需要老年代的內存空間作擔保,如果擔保失敗會執行一次 Full GC。具體內容請參考上面的內容。
4. JDK 1.7 及以前的永久代空間不足
在 JDK 1.7 及以前,HotSpot 虛擬機中的方法區是用永久代實現的,永久代中存放的為一些 Class 的信息、常量、靜態變量等數據。
當系統中要加載的類、反射的類和調用的方法較多時,永久代可能會被占滿,在未配置為采用 CMS GC 的情況下也會執行 Full GC。如果經過 Full GC 仍然回收不了,那么虛擬機會拋出 java.lang.OutOfMemoryError。
為避免以上原因引起的 Full GC,可采用的方法為增大永久代空間或轉為使用 CMS GC。
5. Concurrent Mode Failure
執行 CMS GC 的過程中同時有對象要放入老年代,而此時老年代空間不足(可能是 GC 過程中浮動垃圾過多導致暫時性的空間不足),便會報 Concurrent Mode Failure 錯誤,并觸發 Full GC。
對象已死?
堆中幾乎放著所有的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象)。
下面介紹判斷一個對象是否死亡的兩種方法。
引用計數法
給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加 1;當引用失效,計數器就減 1;任何時候計數器為 0 的對象就是不可能再被使用的。
這個方法實現簡單,效率高,但是目前主流的虛擬機中并沒有選擇這個算法來管理內存,其最主要的原因是它很難解決對象之間相互循環引用的問題。 所謂對象之間的相互引用問題,如下面代碼所示:除了對象 objA 和 objB 相互引用著對方之外,這兩個對象之間再無任何引用。但是他們因為互相引用對方,導致它們的引用計數器都不為 0,于是引用計數算法無法通知 GC 回收器回收他們。
public class ReferenceCountingGc {Object instance = null;public static void main(String[] args) {ReferenceCountingGc objA = new ReferenceCountingGc();ReferenceCountingGc objB = new ReferenceCountingGc();objA.instance = objB;objB.instance = objA;objA = null;objB = null;} }可達性分析算法
這個算法的基本思想就是通過一系列的稱為 “GC Roots” 的對象作為起點,從這些節點開始向下搜索,節點所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的。
GC Roots
在Java技術體系中,固定可作為GC Roots的對象包括以下幾種:
- 在虛擬機棧中引用的對象,比如各個線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。
- 在方法區中類靜態屬性引用的對象,比如Java類的引用類型靜態變量。
- 在方法區中常量引用的對象,比如字符串常量池里的引用。
- 在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
- Java虛擬機內部的引用,如基本類型對應的Class對象,一些常駐的異常對象(比如NullPointException)等,還有系統類加載器。
- 所有被同步鎖(synchronized關鍵字)所持有的對象。
再談引用
無論是通過引用計數法判斷對象引用數量,還是通過可達性分析法判斷對象的引用鏈是否可達,判定對象的存活都與“引用”有關。
JDK1.2 之前,Java 中引用的定義很傳統:如果 reference 類型的數據存儲的數值代表的是另一塊內存的起始地址,就稱這塊內存代表一個引用。
JDK1.2 以后,Java 對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)。
強引用
以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當內存空間不足,Java 虛擬機寧愿拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。
軟引用
如果一個對象只具有軟引用,那就類似于可有可無的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,JAVA 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。
弱引用
如果一個對象只具有弱引用,那就類似于可有可無的生活用品。弱引用與軟引用的區別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由于垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java 虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。
應用
假如一個應用需要讀取大量的本地圖片,如果每次讀取圖片都從硬盤讀取則會嚴重影響性能,如果一次性全部加裝到內存中又可能造成內存溢出。這時可以用軟引用或者弱引用解決這個問題。
Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();WeakHashMap
import java.util.WeakHashMap;/*** @author wardseptember* @create 2020-09-23 13:39*/ public class WeakHashMapDemo {public static void main(String[] args) {myWeakHashMap();}private static void myWeakHashMap() {WeakHashMap<Integer, String> map = new WeakHashMap<>();Integer key = new Integer(1);String value = "https://wardseptember.gitee.io";map.put(key, value);System.out.println(map);key = null;// 手動觸發一次GCSystem.gc();System.out.println(map);} }ReferenceQueue
import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference;/*** @author wardseptember* @create 2020-09-23 13:50*/ public class WeakReferenceDemo {public static void main(String[] args) {Object o1 = new Object();ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);System.out.println(o1);System.out.println(weakReference.get());System.out.println(referenceQueue.poll());System.out.println("==============");o1 = null;System.gc();try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}// o1被回收前,會給referenceQueue發送一個通知System.out.println(o1);System.out.println(weakReference.get());System.out.println(referenceQueue.poll());} }虛引用
"虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。
虛引用主要用來跟蹤對象被垃圾回收的活動。
為一個對象設置虛引用關聯的唯一目的只是為了能在這個對象被收集器回收時收到一個系統通知。
虛引用與軟引用和弱引用的一個區別在于: 虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。
PhantomReference的get方法總是返回null。
特別注意,在程序設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因為軟引用可以加速 JVM 對垃圾內存的回收速度,可以維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生。
PhantomReference
import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference;/*** @author wardseptember* @create 2020-09-23 13:50*/ public class PhantomReferenceDemo {public static void main(String[] args) {Object o1 = new Object();ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();PhantomReference<Object> phantomReference = new PhantomReference<>(o1, referenceQueue);System.out.println(o1);System.out.println(phantomReference.get());System.out.println(referenceQueue.poll());System.out.println("==============");o1 = null;System.gc();try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}// o1被回收前,會給referenceQueue發送一個通知System.out.println(o1);System.out.println(phantomReference.get());System.out.println(referenceQueue.poll());} }生存還是死亡?
即使在可達性分析法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑階段”,要真正宣告一個對象死亡,至少要經歷兩次標記過程;如果對象在進行可達性分析后被判斷為不可達對象,那它將會被第一次標記,隨后進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。對象沒有覆蓋 finalize 方法,或 finalize 方法已經被虛擬機調用過時,虛擬機將這兩種情況視為沒有必要執行。
被判定為需要執行finalize()方法的對象將會被放在一個隊列中進行第二次標記,除非這個對象與引用鏈上的任何一個對象建立關聯,否則就會被真的回收。finalize()方法是對象逃脫回收的最后一次機會。自救只能進行一次,如果回收的對象之前調用了 finalize() 方法自救,后面回收時不會再調用該方法。
finalize()類似 C++ 的析構函數,用于關閉外部資源。但是 try-finally 等方式可以做得更好,并且該方法運行代價很高,不確定性大,無法保證各個對象的調用順序,因此最好不要使用。
回收方法區
方法區的垃圾收集主要回收廢棄的常量和不再使用的類型。
如何判斷一個常量是廢棄常量
假如在常量池中存在數值“333” ,如果當前沒有任何 int 引用該常量的話,就說明常量 “333" 就是廢棄常量,如果這時發生內存回收的話而且有必要的話,”333" 就會被系統清理出常量池。
如何判斷一個類是無用的類
- 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類及其任何派生子類的實例。
- 加載該類的 ClassLoader 已經被回收。
- 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機可以對滿足上述 3 個條件的無用類進行回收,這里說的僅僅是“可以”,而并不是和對象一樣不使用了就會必然被回收。
OOM
Java.lang.StackOverflowError
棧溢出,棧里面存儲的是方法的調用,上面有講。
Java.lang.OutOfMemoryError: Java heap space
對象太多,堆溢出。
Java.lang.OutOfMemoryError: GC overhead limit exceeded
GC回收時間過長時會拋出OutOfMemroyError。過長的定義是,超過98%的時間用來做GC并且回收了不到2%的堆內存,連續多次GC都只回收了不到2%的內存的極端情況下才會拋出。CPU使用率一直是100%,而GC卻沒有任何成果。
Java.lang.OutOfMemoryError: Direct buffer memory
ByteBuffer.allocateDirect(capability)分配操作系統本地內存,不屬于GC管轄范圍。分配太多,可能導致 Direct buffer memory
Java.lang.OutOfMemoryError: unable to create new native thread
創建太多線程。
導致原因
- 一個應用進程創建了多個線程,超過系統承載極限。
- 服務器不允許你的應該程序創建這么多線程,linux系統默認允許單個進程可以創建的線程數是1024個。
解決辦法
- 想辦法降低你應用程序創建線程的數量,分析應用是否真的需要創建這么多線程,如果不是,改代碼將線程數降到最低。
- 對于有的應用,確實需要創建很多線程,遠超過linux系統的默認1024個線程的限制,可以通過修改linux服務器配置,擴大linux默認限制
Java.lang.OutOfMemoryError: Metaspace
Metaspace是方法區在HotSpot中的實現,使用的是本地內存,主要存儲虛擬機加載的類信息、常量池、靜態變量、即時編譯后的代碼等。
如果類太多,就報Java.lang.OutOfMemoryError: Metaspace.
垃圾收集算法
從如何判定對象消亡的角度出發,垃圾收集算法可以劃分為“引用計數式垃圾收集”和“追蹤式垃圾收集“。本文全部屬于追蹤式垃圾收集范疇。
分代收集理論
當前虛擬機的垃圾收集都采用分代收集算法,這種算法沒有什么新的思想,只是根據對象存活周期的不同將內存分為幾塊。一般將 java 堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。
比如在新生代中,每次收集都會有大量對象死去,所以可以選擇復制算法,只需要付出少量對象的復制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇“標記-清除”或“標記-整理”算法進行垃圾收集。
標記-清除算法
該算法分為“標記”和“清除”階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。它是最基礎的收集算法,后續的算法都是對其不足進行改進得到。這種垃圾收集算法會帶來兩個明顯的問題:
- 執行效率不穩定。如果Java堆中包含大量需要被回收的對象,這是必須進行大量的標記和清除動作,導致回收效率降低。
- 內存空間碎片化。標記、清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致當以后程序運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
標記-復制算法
為了解決標記-清除算法中的效率問題,“復制”收集算法出現了。它可以將內存分為大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完后,就將還存活的對象復制到另一塊去,然后再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。
缺點:
- 可用內存縮小為原來的一半,空間浪費明顯。
- 當對象存活率高時,復制效率低。
標記-整理算法
根據老年代的特點提出的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一端移動,然后直接清理掉端邊界以外的內存。
缺點:
- 如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區域,這種移動操作必須全程暫停用戶應用程序才能進行。
HotSpot的算法細節實現
此節暫時省略。
經典垃圾收集器
1.新生代的收集器包括
Serial
PraNew
Parallel Scavenge
2.老年代的收集器包括
Serial Old
Parallel Old
CMS
3.回收整個Java堆(新生代和老年代)
G1收集器
以上是 HotSpot 虛擬機中的 7 個垃圾收集器,連線表示垃圾收集器可以配合使用;圖中收集器所處的區域,則表示它是屬于新生代收集器或者是老年代收集器。
- 單線程與多線程:單線程指的是垃圾收集器只使用一個線程,而多線程使用多個線程;
- 串行與并行:串行指的是垃圾收集器與用戶程序交替執行,這意味著在執行垃圾收集的時候需要停頓用戶程序;并行指的是垃圾收集器和用戶程序同時執行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式執行。
Serial 收集器 Serial串行收集器-復制算法
Serial(串行)收集器收集器是最基本、歷史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單線程收集器了。它的 “單線程” 的意義不僅僅意味著它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( “Stop The World” ),直到它收集結束。
總結
Serial有如下特點:
針對新生代;
采用復制算法;
單線程收集;
進行垃圾收集時,必須暫停所有工作線程,直到完成;
優勢:
簡單高效,由于采用的是單線程的方法,因此與其他類型的收集器相比,對單個cpu來說沒有了上下文之間的的切換,效率比較高。
劣勢:
會在用戶不知道的情況下停止所有工作線程。
使用場景
Client 模式(桌面應用)
在用戶的桌面應用場景中,可用內存一般不大,可以在較短時間內完成垃圾收集,只要不頻繁發生,這是可以接受的
單核服務器
對于限定單個CPU的環境來說,Serial收集器沒有線程切換開銷,可以獲得最高的單線程收集效率
參數設置
-XX:+UseSerialGC:添加該參數來顯式的使用串行垃圾收集器
新生代采用復制算法,老年代采用標記-整理算法。
它的優點是簡單高效,在單個 CPU 環境下,由于沒有線程交互的開銷,因此擁有最高的單線程收集效率。
它是 Client 場景下的默認新生代收集器,因為在該場景下內存一般來說不會很大。它收集一兩百兆垃圾的停頓時間可以控制在一百多毫秒以內,只要不是太頻繁,這點停頓時間是可以接受的。
ParNew 收集器 復制算法
它是 Serial 收集器的多線程版本。
它是 Server 場景下默認的新生代收集器,除了性能原因外,主要是因為除了 Serial 收集器,只有它能與 CMS 收集器配合使用。
自JDK 9開始,ParNew加CMS收集器的組合就不再是官方推薦的服務端模式下的收集器解決方案,官方希望G1(Garbage First)能完全取代它。
并行和并發概念補充:
- 并行(Parallel) :并行描述的是多條垃圾收集器線程之間的關系,指同一時間多條垃圾收集線程在協同工作,但此時用戶線程仍然處于等待狀態。
- 并發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是并行,可能會交替執行),用戶程序在繼續運行,而垃圾收集器運行在另一個 CPU 上。
小結
優勢:
多線程版本的Serial,可以更加有效的利用系統資源
劣勢:
同Serial,會在用戶不知道的情況下停止所有工作線程
使用場景
Server模式下使用,亮點是除Serial外,目前只有它能與CMS收集器配合工作,是一個非常重要的垃圾回收器。
參數設置
-XX:+UseConcMarkSweepGC:指定使用CMS后,會默認使用ParNew作為新生代收集器;
-XX:+UseParNewGC:強制指定使用ParNew;
-XX:ParallelGCThreads:指定垃圾收集的線程數量,ParNew默認開啟的收集線程與CPU的數量相同;
Parallel Scavenge 收集器 (并行回收)收集器-復制算法
Parallel Scavenge 收集器也是使用復制算法的多線程收集器,它看上去幾乎和ParNew都一樣。 那么它有什么特別之處呢?
-XX:+UseParallelGC 使用 Parallel 收集器+ 老年代串行-XX:+UseParallelOldGC使用 Parallel 收集器+ 老年代并行CMS 等垃圾收集器的目標是盡可能縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge的目標是達到一個可控制的吞吐量,因此它被稱為“吞吐量優先”收集器。這里的吞吐量指 CPU 用于運行用戶程序的時間占總時間的比值。
停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗。而高吞吐量則可以高效率地利用 CPU 時間,盡快完成程序的運算任務,適合在后臺運算而不需要太多交互的任務。
縮短停頓時間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,導致吞吐量下降。
可以通過一個開關參數打開 GC 自適應的調節策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 區的比例、晉升老年代對象年齡等細節參數了。虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。
Parallel Scavenge 收集器提供了很多參數供用戶找到最合適的停頓時間或最大吞吐量,如果對于收集器運作不太了解的話,手工優化存在困難的話可以選擇把內存管理優化交給虛擬機去完成也是一個不錯的選擇。
小結
有如下特點:
新生代收集器;
采用復制算法;
多線程收集;
關注點與其他收集器不同:
CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間;
而Parallel Scavenge收集器的目標則是達一個可控制的吞吐量;
優勢:
追求高吞吐量,高效利用CPU,是吞吐量優先,且能進行精確控制。
劣勢:
應該說是特點,追求高吞吐量必然要犧牲一些其他方面的優勢,不能做到既,又。ParNew收集器關注點在于盡可能的縮短垃圾收集時用戶線程的停頓時間,原本10s收集一次, 每次停頓100ms, 設置完參數之后可能變成5s收集一次, 每次停頓70ms. 停頓時間變短, 但收集次數變多。
使用場景
根據相關特性,我們很容易想到它的使用場景,即:當應用程序運行在具有多個CPU上,對暫停時間沒有特別高的要求時,程序主要在后臺進行計算,而不需要與用戶進行太多交互等就特別適合ParNew收集器。
例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序等
參數設置
-XX:MaxGCPauseMillis:控制最大垃圾收集停頓時間,大于0的毫秒數;
-XX:GCTimeRatio:設置垃圾收集時間占總時間的比率,0<n<100的整數;
Serial Old 收集器 標記整理算法
Serial Old收集器是Serial 收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在 JDK1.5 以及以前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作為 CMS 收集器發生失敗時的后備方案。
小結
特點
Serial Old是Serial收集器的老年代版本,同樣是一個單線程收集器,使用標記-整理算法。
有如下特點:
針對老年代;
采用"標記-整理"算法(還有壓縮,Mark-Sweep-Compact);
單線程收集;
優劣勢基本和Serial無異,它是和Serial收集器配合使用的老年代收集器。
使用場景
Client模式;
單核服務器;
與Parallel Scavenge收集器搭配;
作為CMS收集器的后備方案,在并發收集發生Concurrent Mode Failure時使用
Parallel Old收集器 收集器-標記整理算法
Parallel Scavenge 收集器的老年代版本。使用多線程和“標記-整理”算法。在注重吞吐量以及 CPU 資源的場合,都可以優先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器。
特點:
針對老年代;
采用"標記-整理"算法;
多線程收集;
優劣勢參考Parallel Scavenge收集器。
使用場景
JDK1.6及之后用來代替老年代的Serial Old收集器;
特別是在Server模式,多CPU的情況下;
這樣在注重吞吐量以及CPU資源敏感的場景,就有了Parallel Scavenge(新生代)加Parallel Old(老年代)收集器的"給力"應用組合;
參數設置
-XX:+UseParallelOldGC:指定使用Parallel Old收集器
CMS 收集器 標記整理算法
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它非常符合在注重用戶體驗的應用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛擬機第一款真正意義上的并發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。
從名字中的Mark Sweep這兩個詞可以看出,CMS 收集器是一種 “標記-清除”算法實現的,它的運作過程相比于前面幾種垃圾收集器來說更加復雜一些。整個過程分為四個步驟:
- 初始標記: 暫停所有的其他線程,并記錄下直接與 root 相連的對象,速度很快 ;
- 并發標記: 進行 GC Roots Tracing 的過程,它在整個回收過程中耗時最長,不需要停頓。
- 重新標記: 重新標記階段就是為了修正并發標記期間因為用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比并發標記階段時間短
- 并發清除: 開啟用戶線程,同時 GC 線程開始對未標記的區域做清掃
從它的名字就可以看出它是一款優秀的垃圾收集器,主要優點:并發收集、低停頓。但是它有下面三個明顯的缺點:
- 吞吐量低:低停頓時間是以犧牲吞吐量為代價的,導致 CPU 利用率不夠高。它會因為占用一部分線程而導致應用程序變慢,降低總吞吐量。
- **無法處理浮動垃圾;**浮動垃圾是指并發標記和并發清除階段,由于用戶線程繼續運行而產生的垃圾,這部分垃圾只能到下一次 GC 時才能進行回收。由于用戶線程還在繼續運行,因此需要預留出一部分內存提供給用戶線程使用,意味著 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。如果預留的內存無法滿足程序分配新對象的需要,就會出現 一次“并發失敗”(Concurrent Mode Failure),這時虛擬機將臨時啟用 Serial Old 來替代 CMS收集老年代的垃圾。
- **它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。**往往出現老年代空間有剩余,但無法找到足夠大連續空間來分配當前對象,不得不提前觸發一次 Full GC。
小結
特點:
針對老年代;
基于"標記-清除"算法(不進行壓縮操作,產生內存碎片);
以獲取最短回收停頓時間為目標;
并發收集、低停頓;
需要更多的內存(看后面的缺點);
優勢:
停頓時間短;
吞吐量大;
并發收集
劣勢:
對CPU資源非常敏感
無法收集浮動垃圾
容易產生大量內存碎片
使用場景
與用戶交互較多的場景;
希望系統停頓時間最短,注重服務的響應速度;
以給用戶帶來較好的體驗;
如常見WEB、B/S系統的服務器上的應用。
參數設置
-XX:+UseConcMarkSweepGC:指定使用CMS收集器
G1 收集器
1.G1收集器-標記整理算法
JDK1.7后全新的回收器, 用于取代CMS收集器。
G1 (Garbage-First) 是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特征.
堆被分為新生代和老年代,其它收集器進行收集的范圍都是整個新生代或者老年代,而 G1 可以直接對新生代和老年代一起回收。
G1 把堆劃分成多個大小相等的獨立區域(Region),新生代和老年代不再物理隔離。
通過引入 Region 的概念,從而將原來的一整塊內存空間劃分成多個的小空間,使得每個小空間可以單獨進行垃圾回收。這種劃分方法帶來了很大的靈活性,使得可預測的停頓時間模型成為可能。通過記錄每個 Region 垃圾回收時間以及回收所獲得的空間(這兩個值是通過過去回收的經驗獲得),并維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的 Region。
每個 Region 都有一個 Remembered Set,用來記錄該 Region 對象的引用對象所在的 Region。通過使用 Remembered Set,在做可達性分析的時候就可以避免全堆掃描。
如果不計算維護 Remembered Set 的操作,G1 收集器的運作大致可劃分為以下幾個步驟:
- 初始標記:僅僅是標記一下GC Roots能直接關聯到的對象,并且修改TAMS指針的值,讓下一階段用戶線程并發運行時,能正確地在可用的Region中分配新對象。這個階段需要停頓,但耗時很短,并且是借用進行Minor GC的時候同步完成的,所以G1在這階段實際上沒有額外的停頓。每一個Region設計了兩個名為TAMS(Top at Mark Start)的指針,把Region中的一部分空間劃分出來用于并發回收過程中的新對象分配,并發回收時新分配的對象地址都必須在這兩個指針位置上。
- 并發標記:從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序并發執行。當對象圖掃描完成以后,還要重新處理原始快照(SATB)記錄下的在并發時有引用變動的對象。這是整個G1過程中,唯一不需要用戶線程停頓的算法。
- 最終標記:為了修正在并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 里面,最終標記階段需要把 Remembered Set Logs 的數據合并到 Remembered Set 中。這階段需要停頓線程,但是可并行執行。
- 篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來制定回收計劃。此階段其實也可以做到與用戶程序一起并發執行,但是因為只回收一部分 Region,時間是用戶可控制的,而且停頓用戶線程將大幅度提高收集效率。
G1被視為 JDK1.7 中 HotSpot 虛擬機的一個重要進化特征。它具備一下特點:
- 空間整合:與 CMS 的“標記–清理”算法不同,G1 從整體來看是基于“標記整理”算法實現的收集器;從局部上來看是基于“復制”算法實現的;運行期間不會產生內存空間碎片。
- 可預測的停頓:這是 G1 相對于 CMS 的另一個大優勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內。
- 區域化內存劃片,整體編為一系列不連續的內存區域,避免了全內存區的GC操作。核心思想是將整個堆內存區域分成大小相同的子區域,在JVM啟動時自動設置這些子區域的大小。
- 在堆的使用上,G1并不要求對象的存儲一定是物理上連續的,只要邏輯上連續即可,每個分區也不會固定地為某個代服務,可以按需在新生代和老年代切換。啟動時可以通過參數-XX:G1HeapRegionSize=n指定分區大小(1MB32MB,且必須是2的冪),默認將整堆劃分為2048個分區。大小范圍在1MB32MB,最多能設置2048個區域,也即最大能夠支持的最大內存為:32MB * 2048 = 65536MB = 64內存。
- 在G1中,還有一種特效的區域,叫Humongous區域。如果一個對象占用的空間超過了分區容量50%以上,G1收集器就認為這是一個巨型對象。這些巨型對象默認直接會被分配在老年代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1劃分了一個H區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那么G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC。
缺點:
- G1為了垃圾收集產生的內存占用和程序運行時的額外執行負載比CMS要高。
特點:
并行與并發
分代收集,收集范圍包括新生代和老年代
結合多種垃圾收集算法,空間整合,不產生碎片
可預測的停頓:低停頓的同時實現高吞吐量
面向服務端應用,將來替換CMS
優勢:
能充分利用多CPU、多核環境下的硬件優勢;
能獨立管理整個GC堆(新生代和老年代),而不需要與其他收集器搭配;
不會產生內存碎片,有利于長時間運行;
除了追求低停頓處,還能建立可預測的停頓時間模型;
G1收集器是當今收集器技術發展的最前沿成果。
劣勢:
G1 需要記憶集 (具體來說是卡表)來記錄新生代和老年代之間的引用關系,這種數據結構在 G1 中需要占用大量的內存,可能達到整個堆內存容量的 20% 甚至更多。而且 G1 中維護記憶集的成本較高,帶來了更高的執行負載,影響效率。
按照《深入理解Java虛擬機》作者的說法,CMS 在小內存應用上的表現要優于 G1,而大內存應用上 G1 更有優勢,大小內存的界限是6GB到8GB。
所以,盡管是最前沿的成果,也不是完美無缺的。
使用場景
個人以為G1已經基本全面壓制cms、parallel等回收器,缺點見上面的劣勢。但如果不是追求極致的性能,基本可以無腦G1
參數設置
-XX:+UseG1GC:指定使用G1收集器;
-XX:InitiatingHeapOccupancyPercent:當整個Java堆的占用率達到參數值時,開始并發標記階段;默認為45;
-XX:MaxGCPauseMillis:為G1設置暫停時間目標,默認值為200毫秒;
-XX:G1HeapRegionSize:設置每個Region大小,范圍1MB到32MB;目標是在最小Java堆時可以擁有約2048個Region;
ZGC
- ZGC
查看當前使用的垃圾收集器
java -XX:+PrintCommandLineFlags -version一些參數
DefNew -----> Default New Generation Tenured -----> Old ParNew -----> Parallel New Generation PsYoungGen -----> Parallel Scavenge ParOldGen -----> Parallel Old Generation配置垃圾收集器
使用串行垃圾收集器,老年代會自動使用Serial Old收集器:
java +XX:+UseSerialGC啟用ParNew收集器,老年代會使用Serial Old收集器:
java -XX:+UseParNewGC啟用Parallel Scavenge收集器,老年代自動使用Parallel Old收集器:
java -XX:+UseParallelGC # 或者下面這句 java -XX:+UseParallelOldGCjava -XX:ParallelGCThreads=8 # 表示啟動8個GC線程啟用Concurrent Mark Sweep(CMS)收集器,新生代自動使用ParNew:
java -XX:+UseConcMarkSweepGC開啟該參數后,新生代使用ParNew,老年代使用CMS,如果CMS出錯,將使用Serial Old代理CMS收集垃圾。
啟用G1收集器:
java -XX:+UseG1GC如何選擇垃圾收集器
-
單CPU或者小內存,單機程序
-XX:+UseSerialGC -
多CPU,需要大量吞吐量,如后臺計算型
-XX:+UseParallelGC # 或者 -XX:+UseParallelOldGC -
多CPU,追求低停頓時間,需要快速響應,如互聯網應用
-XX:UseConcMarkSweepGC # 或者 -XX:+ParNewGC
低延遲垃圾收集器
衡量垃圾收集器的三項重要指標:
- 內存占用
- 吞吐量
- 延遲
隨著計算機硬件的發展、性能的提升,延遲的重要性日益凸顯。
Shenandoah和ZGC垃圾收集器,幾乎整個工作過程全部都是并發的,只有初始標記、最終標記這些階段有短暫的停頓,這部分停頓時間基本上是固定的,與堆的容量、堆中對象的數量沒有正比例關系,停頓時間不超過10毫秒。
Shenandoah收集器
以后更新
ZGC收集器
以后更新
選擇合適的垃圾收集器
Epsilon收集器
Epsilon收集器是一個不能進行垃圾收集的垃圾收集器。一個垃圾收集器除了垃圾收集這個本職工作之外,它還要負責堆的管理與布局、對象的分配、與解釋器的協作、與編譯器的協作、與監控子系統協作等職責。Epsilon收集器主要負責這部分工作。
JVM垃圾收集器總結
本文主要介紹了JVM中的垃圾回收器,主要包括串行回收器、并行回收器以及CMS回收器、G1回收器。他們各自都有優缺點,通常來說你需要根據你的業務,進行基于垃圾回收器的性能測試,然后再做選擇。下面給出配置回收器時,經常使用的參數:
-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParallelGC :新生代使用并行回收收集器,更加關注吞吐量
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:ParallelGCThreads:設置用于垃圾回收的線程數
-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:ParallelCMSThreads:設定CMS的線程數量
-XX:+UseG1GC:啟用G1垃圾回收器
JVM調優
JVM參數類型
標配參數
java -versionjava -helpjava -showversionX參數
解釋執行
java -Xint第一次使用就編譯成本地代碼
java -Xcomp混合模式
java -XmixedXX參數
Boolean類型
-XX:+ 或者 -某個屬性值 + 表示開啟 - 表示關閉查看正在運行的java程序:
jps -l查看某個java程序的某個jvm參數是否開啟:
jinfo -flag jvm參數 進程號例如:
jinfo -flag PrintGCDetails 進程號 # 查看這個java程序是否開始GC日志查看某個java程序的所有jvm參數:
jinfo -flags 進程號是否使用串行垃圾回收器:
-XX:-UseSerialGC +XX:+UseSerialGCKV設值類型
-XX:屬性key=屬性值value使用例子:
-XX:MetaspaceSize=128m # 設置元空間的大小 -XX:MaxTenuringThreshold=15 # 年輕代升老年代的閾值-Xms等價于-XX:InitialHeapSize;-Xmx等價于-XX:MaxHeapSize
jinfo舉例,如何查看當前運行程序的配置
查看jvm初始化參數:
java -XX:+PrintFlagsInitial查看jvm修改更新后的參數:
java -XX:PrintFlagsFinal":= "代表UseParallelGC被修改過,沒有被修改就是“=”
查看參數:
java -XX:+PrintCommandLineFlags -versionJVM常用配置參數
-Xms 和 -Xmx
-Xms和-Xmx設置為一樣大小,默認為物理內存的四分之一
-Xss
設置單個線程棧的大小,一般默認為512k~1024k
-Xss等價于-XX:ThreadStackSize
-Xmn
設置新生代的大小
-XX:MetaspaceSize
設置元空間大小
-XX:+PrintGCDetails
輸出GC的詳細信息
Minor GC
Full GC
設置參數案例
-Xms128m -Xms4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC-XX:SurvivorRatio
新生代默認比例:
Eden :from survivor : to survivo = 8 : 1 : 1
jvm默認新生代參數:
-XX:SurvivorRatio=8 # 這就是上面的8:1:1-XX:NewRatio
配置新生代和老年代在堆結構中的占比。
默認-XX:NewRatio=2,即新生代 : 老年代=1 : 2
-XX:MaxTenuringThreshold
設置新生代升老年代的閾值,默認是15,最大也是15,因為只用4bit存儲這個值。
如何查看JVM系統默認值
類文件結構
概述
在 Java 中,JVM 可以理解的代碼就叫做字節碼(即擴展名為 .class 的文件),它不面向任何特定的處理器,只面向虛擬機。Java 語言通過字節碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。所以 Java 程序運行時比較高效,而且,由于字節碼并不針對一種特定的機器,因此,Java 程序無須重新編譯便可在多種不同操作系統的計算機上運行。
Clojure(Lisp 語言的一種方言)、Groovy、Scala 等語言都是運行在 Java 虛擬機之上。下圖展示了不同的語言被不同的編譯器編譯成.class文件最終運行在 Java 虛擬機之上。.class文件的二進制格式可以使用 WinHex 查看。
可以說.class文件是不同的語言在 Java 虛擬機之間的重要橋梁,同時也是支持 Java 跨平臺很重要的一個原因。
Class 文件結構總結
根據 Java 虛擬機規范,類文件由單個 ClassFile 結構組成:
ClassFile {u4 magic; //Class 文件的標志u2 minor_version;//Class 的小版本號u2 major_version;//Class 的大版本號u2 constant_pool_count;//常量池的數量cp_info constant_pool[constant_pool_count-1];//常量池u2 access_flags;//Class 的訪問標記u2 this_class;//當前類u2 super_class;//父類u2 interfaces_count;//接口u2 interfaces[interfaces_count];//一個類可以實現多個接口u2 fields_count;//Class 文件的字段屬性field_info fields[fields_count];//一個類會可以有個字段u2 methods_count;//Class 文件的方法數量method_info methods[methods_count];//一個類可以有個多個方法u2 attributes_count;//此類的屬性表中的屬性數attribute_info attributes[attributes_count];//屬性表集合 }下面詳細介紹一下 Class 文件結構涉及到的一些組件。
Class文件字節碼結構組織示意圖 (之前在網上保存的,非常不錯,原出處不明):
2.1 魔數
u4 magic; //Class 文件的標志每個 Class 文件的頭四個字節稱為魔數(Magic Number),它的唯一作用是確定這個文件是否為一個能被虛擬機接收的 Class 文件。
程序設計者很多時候都喜歡用一些特殊的數字表示固定的文件類型或者其它特殊的含義。
2.2 Class 文件版本
u2 minor_version;//Class 的小版本號u2 major_version;//Class 的大版本號緊接著魔數的四個字節存儲的是 Class 文件的版本號:第五和第六是次版本號,第七和第八是主版本號。
高版本的 Java 虛擬機可以執行低版本編譯器生成的 Class 文件,但是低版本的 Java 虛擬機不能執行高版本編譯器生成的 Class 文件。所以,我們在實際開發的時候要確保開發的的 JDK 版本和生產環境的 JDK 版本保持一致。
2.3 常量池
u2 constant_pool_count;//常量池的數量cp_info constant_pool[constant_pool_count-1];//常量池緊接著主次版本號之后的是常量池,常量池的數量是 constant_pool_count-1(常量池計數器是從1開始計數的,將第0項常量空出來是有特殊考慮的,索引值為0代表“不引用任何一個常量池項”)。
常量池主要存放兩大常量:字面量和符號引用。字面量比較接近于 Java 語言層面的的常量概念,如文本字符串、聲明為 final 的常量值等。而符號引用則屬于編譯原理方面的概念。包括下面三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
常量池中每一項常量都是一個表,這14種表有一個共同的特點:開始的第一位是一個 u1 類型的標志位 -tag 來標識常量的類型,代表當前這個常量屬于哪種常量類型.
| CONSTANT_utf8_info | 1 | UTF-8編碼的字符串 |
| CONSTANT_Integer_info | 3 | 整形字面量 |
| CONSTANT_Float_info | 4 | 浮點型字面量 |
| CONSTANT_Long_info | 5 | 長整型字面量 |
| CONSTANT_Double_info | 6 | 雙精度浮點型字面量 |
| CONSTANT_Class_info | 7 | 類或接口的符號引用 |
| CONSTANT_String_info | 8 | 字符串類型字面量 |
| CONSTANT_Fieldref_info | 9 | 字段的符號引用 |
| CONSTANT_Methodref_info | 10 | 類中方法的符號引用 |
| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符號引用 |
| CONSTANT_NameAndType_info | 12 | 字段或方法的符號引用 |
| CONSTANT_MothodType_info | 16 | 標志方法類型 |
| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
| CONSTANT_InvokeDynamic_info | 18 | 表示一個動態方法調用點 |
.class 文件可以通過javap -v class類名 指令來看一下其常量池中的信息(javap -v class類名-> temp.txt :將結果輸出到 temp.txt 文件)。
2.4 訪問標志
在常量池結束之后,緊接著的兩個字節代表訪問標志,這個標志用于識別一些類或者接口層次的訪問信息,包括:這個 Class 是類還是接口,是否為 public 或者 abstract 類型,如果是類的話是否聲明為 final 等等。
類訪問和屬性修飾符:
我們定義了一個 Employee 類
package top.snailclimb.bean; public class Employee {... }通過javap -v class類名 指令來看一下類的訪問標志。
2.5 當前類索引,父類索引與接口索引集合
u2 this_class;//當前類u2 super_class;//父類u2 interfaces_count;//接口u2 interfaces[interfaces_count];//一個類可以實現多個接口類索引用于確定這個類的全限定名,父類索引用于確定這個類的父類的全限定名,由于 Java 語言的單繼承,所以父類索引只有一個,除了 java.lang.Object 之外,所有的 java 類都有父類,因此除了 java.lang.Object 外,所有 Java 類的父類索引都不為 0。
接口索引集合用來描述這個類實現了那些接口,這些被實現的接口將按implents(如果這個類本身是接口的話則是extends) 后的接口順序從左到右排列在接口索引集合中。
2.6 字段表集合
u2 fields_count;//Class 文件的字段的個數field_info fields[fields_count];//一個類會可以有個字段字段表(field info)用于描述接口或類中聲明的變量。字段包括類級變量以及實例變量,但不包括在方法內部聲明的局部變量。
field info(字段表) 的結構:
- access_flags: 字段的作用域(public ,private,protected修飾符),是實例變量還是類變量(static修飾符),可否被序列化(transient 修飾符),可變性(final),可見性(volatile 修飾符,是否強制從主內存讀寫)。
- name_index: 對常量池的引用,表示的字段的名稱;
- descriptor_index: 對常量池的引用,表示字段和方法的描述符;
- attributes_count: 一個字段還會擁有一些額外的屬性,attributes_count 存放屬性的個數;
- attributes[attributes_count]: 存放具體屬性具體內容。
上述這些信息中,各個修飾符都是布爾值,要么有某個修飾符,要么沒有,很適合使用標志位來表示。而字段叫什么名字、字段被定義為什么數據類型這些都是無法固定的,只能引用常量池中常量來描述。
字段的 access_flags 的取值:
2.7 方法表集合
u2 methods_count;//Class 文件的方法的數量method_info methods[methods_count];//一個類可以有個多個方法methods_count 表示方法的數量,而 method_info 表示的方法表。
Class 文件存儲格式中對方法的描述與對字段的描述幾乎采用了完全一致的方式。方法表的結構如同字段表一樣,依次包括了訪問標志、名稱索引、描述符索引、屬性表集合幾項。
method_info(方法表的) 結構:
方法表的 access_flag 取值:
注意:因為volatile修飾符和transient修飾符不可以修飾方法,所以方法表的訪問標志中沒有這兩個對應的標志,但是增加了synchronized、native、abstract等關鍵字修飾方法,所以也就多了這些關鍵字對應的標志。
2.8 屬性表集合
u2 attributes_count;//此類的屬性表中的屬性數attribute_info attributes[attributes_count];//屬性表集合在 Class 文件,字段表,方法表中都可以攜帶自己的屬性表集合,以用于描述某些場景專有的信息。與 Class 文件中其它的數據項目要求的順序、長度和內容不同,屬性表集合的限制稍微寬松一些,不再要求各個屬性表具有嚴格的順序,并且只要不與已有的屬性名重復,任何人實現的編譯器都可以向屬性表中寫 入自己定義的屬性信息,Java 虛擬機運行時會忽略掉它不認識的屬性。
參考鏈接
- JavaGuide
虛擬機類加載機制
類的生命周期
一個類的完整生命周期如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-PR2UnqcU-1624851354188)(http://wardseptember.top/20200818171655.png)]
類加載過程
Class 文件需要加載到虛擬機中之后才能運行和使用,那么虛擬機是如何加載這些 Class 文件呢?
系統加載 Class 類型的文件主要三步:加載->連接->初始化。連接過程又可分為三步:驗證->準備->解析。
加載
class文件的加載時機:
- 1 遇到 new、getstatic、putstatic、或invokestatic這四條字節碼指令 2 使用
java.lang.reflect 包的方法對類進行反射調用的時候 3 初始化類時,父類沒有被初始化,先初始化父類
4 虛擬機啟動時,用戶指定的主類(包含main()的那個類)
5 當使用JDK1.7動態語言支持的時,如果一個java.lang.invoke.MethodHandle
實例最后解析的結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄鎖對應的類沒有進行過初始化時
關于序號1的詳細解釋:
使用 new 關鍵字實例化對象時
讀取類的靜態變量時(被 final修飾,已在編譯期把結果放入常量池的靜態字段除外)
設置類的靜態變量時
調用一個類的靜態方法時
注意: newarray指令觸發的只是數組類型本身的初始化,而不會導致其相關類型的初始化,比如,new String[]只會直接觸發 String[]類的初始化,也就是觸發對類[Ljava.lang.String的初始化,而直接不會觸發String類的初始化。
生成這四條指令最常見的Java代碼場景是:
對于這5種會觸發類進行初始化的場景,虛擬機規范中使用了一個很強烈的限定語:“有且只有”,這5種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱為 被動引用。
需要特別指出的是,類的實例化和類的初始化是兩個完全不同的概念:
類的實例化是指創建一個類的實例(對象)的過程;
類的初始化是指為類各個成員賦初始值的過程,是類生命周期中的一個階段;
被動引用的三個場景:
通過子類引用父類的靜態字段,不會導致子類初始化
public class Test1 {
static {System.out.println("Init Superclass!!!"); }public static void main(String[] args) {int x = Son.count; }}
class Father extends Test1{
static int count = 1;
static {
System.out.println(“Init father!!!”);
}
}
class Son extends Father{
static {
System.out.println(“Init son!!!”);
}
}
輸出:
Init Superclass!!!
Init father!!!
1
2
對于靜態字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。至于是否要觸發子類的加載和驗證,在虛擬機中并未明確規定,這點取決于虛擬機的具體實現。對于Sun HotSpot虛擬機來說,可通過-XX:+TraceClassLoading參數觀察到此操作會導致子類的加載。
上面的案例中,由于count字段是在Father類中定義的,因此該類會被初始化,此外,在初始化類Father的時候,虛擬機發現其父類Test1 還沒被初始化,因此虛擬機將先初始化其父類Test1 ,然后初始化子類Father,而Son始終不會被初始化;
通過數組定義來引用類,不會觸發此類的初始化
public class Test2 {
public static void main(String[] args) {M[] m = new M[8]; }}
class M{
static {
System.out.println(“Init M!!!”);
}
}
運行之后我們會發現沒有輸出 “Init M!!!”,說明沒有觸發類的初始化階段
常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
public class Test3 {
public static void main(String[] args) {System.out.println(ConstClass.COUNT); }}
class ConstClass{
static final int COUNT = 1;
static{
System.out.println(“Init ConstClass!!!”);
}
}
上面代碼運行后也沒有輸出 Init ConstClass!!!,這是因為雖然在Java源碼中引用了ConstClass 類中的常量COUNT ,但其實在編譯階段通過常量傳播優化,已經將常量的值 "1"存儲到Test3 常量池中了,對常量ConstClass.COUNT的引用實際都被轉化為Test3 類對自身常量池的引用了,也就是說,實際上Test3 的Class文件之中并沒有ConstClass類的符號引用入口,這兩個類在編譯為Class文件之后就不存在關系
類加載過程的第一步,主要完成下面3件事情:
虛擬機規范多上面這3點并不具體,因此是非常靈活的。比如:“通過全類名獲取定義此類的二進制字節流” 并沒有指明具體從哪里獲取、怎樣獲取。比如:比較常見的就是從 ZIP 包中讀取(日后出現的JAR、EAR、WAR格式的基礎)、其他文件生成(典型應用就是JSP)等等。
一個非數組類的加載階段(加載階段獲取類的二進制字節流的動作)是可控性最強的階段,這一步我們可以去完成還可以自定義類加載器去控制字節流的獲取方式(重寫一個類加載器的 loadClass() 方法)。數組類型不通過類加載器創建,它由 Java 虛擬機直接創建。
類加載器、雙親委派模型也是非常重要的知識點,這部分內容會在后面的文章中單獨介紹到。
加載階段和連接階段的部分內容是交叉進行的,加載階段尚未結束,連接階段可能就已經開始了。
驗證
準備
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些內存都將在方法區中分配。對于該階段有以下幾點需要注意:
基本數據類型的零值:
解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符7類符號引用進行。
符號引用就是一組符號來描述目標,可以是任何字面量。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。在程序實際運行時,只有符號引用是不夠的,舉個例子:在程序執行方法時,系統需要明確知道這個方法所在的位置。Java 虛擬機為每個類都準備了一張方法表來存放類中所有的方法。當需要調用一個類的方法的時候,只要知道這個方法在方發表中的偏移量就可以直接調用該方法了。通過解析操作符號引用就可以直接轉變為目標方法在類中方法表的位置,從而使得方法可以被調用。
綜上,解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,也就是得到類或者字段、方法在內存中的指針或者偏移量。
初始化
初始化是類加載的最后一步,也是真正執行類中定義的 Java 程序代碼(字節碼),初始化階段是執行類構造器 <clinit> ()方法的過程。
對于<clinit>() 方法的調用,虛擬機會自己確保其在多線程環境中的安全性。因為 <clinit>() 方法是帶鎖線程安全,所以在多線程環境下進行類初始化的話可能會引起死鎖,并且這種死鎖很難被發現。
主動引用
對于初始化階段,虛擬機嚴格規范了有且只有5種情況下,必須對類進行初始化(只有主動去使用類才會初始化類):
- 當jvm執行new指令時會初始化類。即當程序創建一個類的實例對象。
- 當jvm執行getstatic指令時會初始化類。即程序訪問類的靜態變量(不是靜態常量,常量會被加載到運行時常量池)。
- 當jvm執行putstatic指令時會初始化類。即程序給類的靜態變量賦值。
- 當jvm執行invokestatic指令時會初始化類。即程序調用類的靜態方法。
被動引用
以上6 種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用。被動引用的常見例子包括:
- 通過子類引用父類的靜態字段,不會導致子類初始化。
- 通過數組定義來引用類,不會觸發此類的初始化。該過程會對數組類進行初始化,數組類是一個由虛擬機自動生成的、直接繼承自 Object 的子類,其中包含了數組的屬性和方法。
- 常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
卸載
卸載類即該類的Class對象被GC。
卸載類需要滿足3個要求:
所以,在JVM生命周期類,由jvm自帶的類加載器加載的類是不會被卸載的。但是由我們自定義的類加載器加載的類是可能被卸載的。
只要想通一點就好了,jdk自帶的BootstrapClassLoader,PlatformClassLoader,AppClassLoader負責加載jdk提供的類,所以它們(類加載器的實例)肯定不會被回收。而我們自定義的類加載器的實例是可以被回收的,所以使用我們自定義加載器加載的類是可以被卸載掉的。
參考鏈接
- JavaGuide
類加載器
所有的類都由類加載器加載,加載的作用就是將 .class文件加載到內存。
JVM 中內置了三個重要的 ClassLoader,除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自java.lang.ClassLoader:
雙親委派模型
每一個類都有一個對應它的類加載器。系統中的 ClassLoder 在協同工作的時候會默認使用 雙親委派模型 。即在類加載的時候,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則才會嘗試加載。加載的時候,首先會把該請求委派該父類加載器的 loadClass() 處理,因此所有的請求最終都應該傳送到頂層的啟動類加載器 BootstrapClassLoader 中。當父類加載器無法處理時,才由自己來處理。當父類加載器為null時,會使用啟動類加載器 BootstrapClassLoader 作為父類加載器。
每個類加載都有一個父類加載器,我們通過下面的程序來驗證。
public class ClassLoaderDemo {public static void main(String[] args) {System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());} }Output
ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2 The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@1b6d3586 The GrandParent of ClassLodarDemo's ClassLoader is nullAppClassLoader的父類加載器為ExtClassLoader ExtClassLoader的父類加載器為null,null并不代表ExtClassLoader沒有父類加載器,而是 BootstrapClassLoader 。
其實這個雙親翻譯的容易讓別人誤解,我們一般理解的雙親都是父母,這里的雙親更多地表達的是“父母這一輩”的人而已,并不是說真的有一個 Mother ClassLoader 和一個 Father ClassLoader 。另外,類加載器之間的“父子”關系也不是通過繼承來體現的,是由“優先級”來決定。官方API文檔對這部分的描述如下:
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.
雙親委派模型實現源碼分析
雙親委派模型的實現代碼非常簡單,邏輯非常清晰,都集中在 java.lang.ClassLoader 的 loadClass() 中,相關代碼如下所示。
private final ClassLoader parent; protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// 首先,檢查請求的類是否已經被加載過Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {//父加載器不為空,調用父加載器loadClass()方法處理c = parent.loadClass(name, false);} else {//父加載器為空,使用啟動類加載器 BootstrapClassLoader 加載c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//拋出異常說明父類加載器無法完成加載請求}if (c == null) {long t1 = System.nanoTime();//自己嘗試加載c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}雙親委派模型的好處
雙親委派模型保證了Java程序的穩定運行,可以避免類的重復加載(JVM 區分不同類的方式不僅僅根據類名,相同的類文件被不同的類加載器加載產生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改。如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現一些問題,比如我們編寫一個稱為 java.lang.Object 類的話,那么程序運行的時候,系統就會出現多個不同的 Object 類。
- 避免類的重復加載
- 打破雙親委派模型,實現類之間的隔離,還可以實現熱部署
如果我們不想用雙親委派模型怎么辦?
自定義加載器的話,需要繼承 ClassLoader 。如果我們不想打破雙親委派模型,就重寫 ClassLoader 類中的 findClass() 方法即可,無法被父類加載器加載的類最終會通過這個方法被加載。但是,如果想打破雙親委派模型則需要重寫 loadClass() 方法
自定義類加載器
除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自java.lang.ClassLoader。如果我們要自定義自己的類加載器,很明顯需要繼承 ClassLoader。
tomcat 為什么打破雙親委派模型
我們思考一下:Tomcat是個web容器, 那么它要解決什么問題:
參考鏈接
- JavaGuide
JVM調優
概述
為什么調優
- 防止出現OOM
- 解決OOM
- 減少Full GC出現的頻率
性能優化的步驟
性能監控
- GC 頻繁
- cpu load 過高
- OOM
- 內存泄露
- 死鎖
- 程序響應時間較長
性能優化
- 打印GC日志,通過GCviewer或者http://gceasy.io 來分析日志信息
- 靈活運用命令行工具,jstack、jmap、jinfo等
- dump出堆文件,使用內存分析工具分析文件
- 使用阿里Arthas,或jconsole、JVisualVM來實時查看JVM狀態
- jstack查看堆棧信息
性能調優
- 適當增加內存,根據業務背景選擇垃圾收集器
- 優化代碼,控制內存使用
- 增加機器,分散節點壓力
- 合理設置線程池線程數量
- 使用中間件提高程序效率,比如緩存、消息隊列
性能評價指標
停頓時間
提交請求和返回該請求的響應之間使用的時間,一般比較關注平均響應時間。
常用操作的響應時間列表:
在垃圾回收環節中:
暫停時間:執行垃圾收集時,程序的工作線程被暫停的時間。
吞吐量
對單位時間內完成的工作量的量度。
在GC中,吞吐量指運行用戶代碼的時間占總運行時間的比例,吞吐量為1-1/(1+n)。-XX:GCTimeRatio=n
并發數
同一時刻,對服務器有實際交互的請求數
內存占用
Java堆區所占的內存大小
性能監控及診斷工具
jps
Jps(Java Process Status)可以顯示指定系統內所有的HotSpot虛擬機內正在運行的進程。
基本語法
Jps [options] [hostid]
options參數如下圖:
jstat
jstat(JVM Statistics Monitoring Tool)查看JVM統計信息
基本語法
jstat -<option> [-t] [-h<lines] <vmid> [<interval [<count>]]
Jstat -h 查看命令相關參數
- -h3表示每輸出三行打印一遍表頭
- vmid指定進程id
- interval是查看間隔,單位毫秒
- count是輸出多少次
- -t可以再輸出信息前加一個Timestamp列,顯示程序的運行時間,單位是秒
判斷內存泄露
jinfo
Jinfo(Configuration Info for Java) 查看虛擬機配置參數信息,也可用于調整虛擬機的配置參數。
基本語法
Jinfo [options] pid
jmap
Jmap(JVM Memory Map):一方面是獲取dump文件(堆轉儲快照文件,二進制文件),它還可以獲取目標Java進程的內存相關信息,包括Java堆各區域的使用情況、堆中對象的統計信息、類加載信息等。
導出內存映像文件
顯示堆內存相關信息
- Jmap -heap pid
- Jmap -histo pid
- Jmap -permstat pid 查看系統的ClassLoader信息
- Jmap -finalizerinfo 查看堆積在finalizer隊列中的對象
jat
Jhat(JVM Heap Analysis Tool)是JDK自帶的堆分析工具
基本語法
jhat [option] [dumpfile]
Jstack(JVM Stack Trace)
Jstack:用于生成虛擬機指定進程當前時刻的線程快照(虛擬機堆棧跟蹤)。線程快照就是當前虛擬機內指定進程的每一條線程正在執行的方法堆棧的集合。
jcmd
jstatd
圖形化工具
Jconsole
Visual VM
主要功能
MAT
tomcat
JProfile
Arthas
命令
- 官網教程
Java Mission Control
Flame Graphs
Tprofiler
內存泄露
靜態集合類
單例模式
內部類持有外部類
各種連接
變量不合理的作用域
改變哈希值
緩存泄漏
監聽器和回調
JVM運行時參數
java -X
分析日志
Minor GC日志解析
Full GC日志分析
GC Easy
GCViewer
性能優化
總結
- 上一篇: 好用的图片标注工具
- 下一篇: Linux课堂练习2