后端学习 - JVM(上)内存与垃圾回收
JVM 架構圖
文章目錄
- 一 JVM 簡介
- 二 類加載子系統(tǒng):
- 1 作用
- 2 類的三個加載過程
- 3 類加載器的分類
- 4 雙親委派機制
- 5 兩個 class 對象為同一個類的必要條件
- 三 運行時數(shù)據(jù)區(qū):PC寄存器(Program Counter Register)
- 四 運行時數(shù)據(jù)區(qū):虛擬機棧
- 1 概述
- 2 ??赡艹霈F(xiàn)的異常
- 3 棧的存儲結構和運行原理
- 4 棧幀的組成
- 五 方法的調用
- 1 靜態(tài)鏈接與動態(tài)鏈接
- 2 早期綁定與晚期綁定
- 3 (非)虛方法
- 4 JVM 方法調用的指令
- 5 虛方法表
- 六 運行時數(shù)據(jù)區(qū):本地方法棧
- 七 運行時數(shù)據(jù)區(qū):堆
- 1 概述
- 2 堆的內存結構
- 3 YGC / Minor GC
- 4 Major GC & Full GC
- 5 內存分配策略
- 6 線程分配緩沖區(qū)(Thread Local Allocation Buffer, TLAB)
- 7 逃逸分析與優(yōu)化
- 8 對象的內存分配流程
- 八 運行時數(shù)據(jù)區(qū):方法區(qū)(永久代 / 元空間)
- 1 概述
- 2 方法區(qū)與堆棧的交互
- 3 方法區(qū)的內部結構
- 4 方法區(qū)的發(fā)展:為什么需要元空間
- 5 方法區(qū)的垃圾回收
- 九 對象的實例化、內存布局、訪問定位
- 1 對象創(chuàng)建的方法和步驟
- 2 !!對象的內存布局
- 十 String Table
- 1 String 的基本特性
- 2 String 拼接
- 3 intern()
- 4 創(chuàng)建了幾個對象?
- 5 兩道難題*
- 十一 垃圾回收相關概念
- 1 什么是垃圾
- 2 內存溢出
- 3 內存泄漏
- 4 強引用:存在就不回收
- 5 軟引用:內存不足時回收
- 6 弱引用:發(fā)現(xiàn)即回收
- 7 虛引用:形同虛設,回收跟蹤
- 十二 垃圾回收算法
- 1 標記階段:可達性分析算法
- 2 finalize
- 3 判斷對象是否可回收的流程(至少兩次標記)
- 4 清除階段:標記-清除算法
- 5 清除階段:標記-復制算法
- 6 清除算法:標記-壓縮算法
- 7 三種清除算法的對比
- 8 分代收集算法
- 十三 垃圾回收器
- 1 主要性能指標
一 JVM 簡介
- JVM 本質上是二進制字節(jié)碼的運行環(huán)境,是運行在操作系統(tǒng)上的,與硬件沒有直接的交互
- Java 是跨平臺的語言:一次編寫,到處運行
- JVM 是跨語言的平臺:JVM 是面向字節(jié)碼文件的,只要符合 JVM 規(guī)范,JVM 不僅可以處理 Java 語言編譯的字節(jié)碼文件,還支持其它語言編譯的字節(jié)碼文件
二 類加載子系統(tǒng):
1 作用
- 負責加載文件開頭有特定的標識的 class 文件
- 只負責文件的加載,而不保證 class 文件可以運行(能否運行由執(zhí)行引擎決定)
- 加載的類信息存放在方法區(qū),除此之外,方法區(qū)還會存放運行時的常量池信息
- 在 class file -> JVM -> 元數(shù)據(jù)模板 的過程中作為“快遞員”的角色
2 類的三個加載過程
- 通過類的全限定名獲取定義此類的二進制字節(jié)流
- 將該字節(jié)流代表的靜態(tài)存儲結構,轉化為方法區(qū)的運行時數(shù)據(jù)結構
- 在內存中聲明一個 java.lang.Class 類型的對象,作為方法區(qū)的該類的各種數(shù)據(jù)的訪問入口
- 包含 驗證(verify)、準備(prepare)、解析(resolve) 三個階段
- 驗證階段:確保 class 文件符合虛擬機要求,保證被加載類的正確性
- 準備階段:為 類變量(static 修飾) 申請內存空間(不包含 final 修飾的 static 類變量,因為這些變量在編譯時分配了內存空間),并賦初始零值(final 修飾的 static 類變量為指定值);此外,該階段不會為 實例變量(通過 this 引用) 初始化,因為實例變量隨著對象分配到堆中,而類變量分配到方法區(qū)中
- 解析階段
- 該階段的任務是,執(zhí)行類構造器方法 <clinit>() 的過程,該方法是 javac 編譯器(前端編譯器)自動收集 類變量的賦值動作 和 靜態(tài)代碼塊的語句 合并得到的
- 子類的 <clinit>() 在父類的 <clinit>() 執(zhí)行后才能執(zhí)行
- <clinit>() 不同于類的構造器,在 JVM 視角下,類的構造器是 <init>() 方法
- 虛擬機保證 <clinit>() 在多線程下被同步加鎖,避免同一個類加載多次
3 類加載器的分類
- BootStrap ClassLoader(啟動類加載器):使用 C/C++ 實現(xiàn),沒有父類(上級,非繼承意義的父類)加載器,用于加載 Java 核心庫
- Extension ClassLoader(擴展類加載器):繼承自 ClassLoader 類,父類(上級,非繼承意義的父類)加載器為啟動類加載器
- AppClassLoader(應用類加載器):繼承自 ClassLoader 類,父類(上級,非繼承意義的父類)加載器為擴展類加載器,是程序默認的類加載器
- 用戶自定義類加載器
4 雙親委派機制
- 是 JVM 加載類的 class 文件的機制,避免類的重復加載,防止核心 API 被篡改
- 具體地,如果有人想替換系統(tǒng)級別的類:String.java,篡改它的實現(xiàn),在這種機制下這些系統(tǒng)的類已經(jīng)被 Bootstrap classLoader 加載過了(因為當一個類需要加載的時候,最先去嘗試加載的就是 BootstrapClassLoader),所以其他類加載器并沒有機會再去加載,從一定程度上防止了危險代碼的植入
- 具體地,如果一個類加載器收到了類加載請求,它不會直接執(zhí)行類的加載,而是將請求委托到上級的加載器,上級的加載器遞歸執(zhí)行該過程,最終請求到達啟動類加載器
- 如果上級加載器可以執(zhí)行指定類的加載,則過程結束;否則向下級傳遞該請求,直到類可以被加載
5 兩個 class 對象為同一個類的必要條件
- 全類名一致
- 加載類的 ClassLoader(指 ClassLoader 實例)相同,即:同一個 class 文件,被同一個 JVM 的不同 ClassLoader 實例加載,不能算作同一個類對象
三 運行時數(shù)據(jù)區(qū):PC寄存器(Program Counter Register)
- 用于存儲指向下一條指令的地址(執(zhí)行引擎負責讀取下一條指令)
- 線程私有,生命周期和線程保持一致
- 是 Java 內存中唯一一個沒有規(guī)定 OutOfMemoryError 的區(qū)域
- 使用PC寄存器存儲字節(jié)碼指令地址的作用,為什么要記錄當前線程的執(zhí)行地址?
一個PC寄存器記錄一個線程的字節(jié)碼指令地址,程序運行時,CPU 需要在各個線程間切換,切換到某個線程時需要還原它切換之前的現(xiàn)場,通過PC寄存器確定繼續(xù)執(zhí)行的位置
四 運行時數(shù)據(jù)區(qū):虛擬機棧
1 概述
- 主管 Java 程序的運行,保存方法的局部變量(8種基本數(shù)據(jù)類型+對象的引用地址)、部分結果,參與方法的調用和返回
- 棧是運行時的單位,解決程序運行的問題;堆是存儲的單位,解決數(shù)據(jù)存儲的問題
- 線程私有,生命周期和線程保持一致
2 棧可能出現(xiàn)的異常
- JVM 允許棧的容量為動態(tài)的,或是固定的
- 棧容量動態(tài)時,如果棧嘗試擴展并無法申請到足夠的內存,或是創(chuàng)建新線程時沒有足夠的內存創(chuàng)建對應的虛擬機棧,拋出 OutOfMemoryError
- 棧容量固定時,請求的容量超過指定容量時,拋出 StackOverflowError
3 棧的存儲結構和運行原理
- 棧的存儲格式是棧幀,棧幀是一個內存區(qū)塊,維護著方法執(zhí)行過程中的數(shù)據(jù)信息
- 棧幀和執(zhí)行的方法是一一對應的
- 在一個活動線程中,同一時刻只有棧頂?shù)臈腔顒拥?#xff08;即:一個線程同一時刻只能執(zhí)行一個方法),執(zhí)行引擎運行的所有字節(jié)碼指令只針對當前棧幀進行操作
- 不同線程中包含的棧幀不允許相互引用,即不能在某個棧幀中引用另外一個線程的棧幀
- 方法返回時(使用 return 指令 / 拋出未處理的異常),當前棧幀會將執(zhí)行結果傳遞給前一個棧幀,之后丟棄該棧幀,使得其下一個棧幀成為新的棧頂棧幀
4 棧幀的組成
| 局部變量表 | 存儲方法的參數(shù)、定義在方法體內的局部變量 |
| 操作數(shù)棧 | 保存計算過程的中間結果,同時作為計算過程中變量的臨時存儲空間 |
| 動態(tài)鏈接 | 將符號引用轉換為調用方法的直接引用 |
| 方法返回地址 | 存放調用該方法的PC寄存器的值,即調用該方法的指令的下一條指令的地址 |
| 附加信息 | … |
4.1 局部變量表
- 是一個數(shù)字數(shù)組,主要用于存儲方法的參數(shù)、定義在方法體內的局部變量
- 線程私有,所以不存在數(shù)據(jù)安全問題
- 所需容量大小在編譯時確定,方法運行時不會更改
- 最基本的存儲單元是 Slot
有關 Slot
- 局部變量表中的變量,是重要的垃圾回收根節(jié)點,只要被局部變量表直接或間接引用的對象都不會被回收
- 成員變量(包括靜態(tài)變量、實例變量)和局部變量的對比
| 靜態(tài)變量 | 在類加載的 Linking 階段申請內存空間并賦初始0值,在 Initialization 階段顯式賦值(靜態(tài)代碼塊賦值) |
| 實例變量 | 對象創(chuàng)建時,在堆中申請內存空間并賦初始0值 |
| 局部變量 | 無初始0值,使用前必須要顯式賦值 |
4.2 操作數(shù)棧
- 主要用于保存計算過程的中間結果,同時作為計算過程中變量的臨時存儲空間
- 方法剛開始執(zhí)行時,操作數(shù)棧被創(chuàng)建,在編譯時確定其最大深度:引用類型、byte、short、char…占用1個單位深度;long、double 占用兩個單位深度
- 不能通過索引訪問數(shù)據(jù),只能通過棧的 push / pop
- 如果方法具有返回值,則返回值會被壓入當前棧幀的操作數(shù)棧中,并更新PC寄存器為下一條需要執(zhí)行的字節(jié)碼指令
4.3 動態(tài)鏈接
- Java 源代碼被編譯成字節(jié)碼文件時,所有的變量和方法引用都作為符號引用保存在 class 文件的常量池(屬于方法區(qū))里,動態(tài)鏈接的作用是,將符號引用轉換為調用方法的直接引用
- 為了實現(xiàn)動態(tài)鏈接,每個棧幀都包含一個指向運行時常量池中的該棧幀所屬方法的引用
4.4 方法返回地址
- 方法的結束有兩種方式:正常退出;出現(xiàn)未處理的異常,非正常退出
- 正常退出的方法會給調用者返回值,而非正常退出的方法不會
- 無論通過哪種方式退出,方法退出后都要返回其被調用的位置
- 方法返回地址的作用是,在方法正常退出時,存放調用該方法的PC寄存器的值,即調用該方法的指令的下一條指令的地址
五 方法的調用
1 靜態(tài)鏈接與動態(tài)鏈接
- 此處的 “鏈接” 是將調用方法的符號引用轉換為直接引用的過程,針對的是方法調用
- 某種程度上,動態(tài)鏈接對應語言的多態(tài)特性
- 靜態(tài)鏈接:字節(jié)碼文件被裝載到 JVM 內部時,被調用的方法在編譯期間可知,且運行時保持不變。這種情況下,將調用方法的符號引用轉換為直接引用的過程,稱為靜態(tài)鏈接
- 動態(tài)鏈接:字節(jié)碼文件被裝載到 JVM 內部時,被調用的方法在編譯期間不可知,只有在運行時才能將方法調用符號引用轉換為直接引用,稱為動態(tài)鏈接
2 早期綁定與晚期綁定
- “綁定” 指的是字段、方法、類的符號引用轉換為直接引用的過程
- 早期綁定:對應靜態(tài)鏈接,在編譯時可以執(zhí)行引用的轉換
- 晚期綁定:對應動態(tài)鏈接,只能在運行時執(zhí)行引用的轉換
3 (非)虛方法
- 非虛方法:在編譯時可以確定具體的調用版本,且在運行時不變,則該方法為非虛方法
- 靜態(tài)方法、私有方法、final 方法、構造器方法、父類的方法 均為非虛方法,其它方法稱為虛方法
- 多態(tài)的前提是類的繼承或方法的重寫,所以不涉及到繼承和重寫的方法均為非虛方法
4 JVM 方法調用的指令
| invokestatic | 調用靜態(tài)方法 |
| invokespecial | 調用<init>方法、私有方法、父類方法 |
| invokevirtual | 調用虛方法(包括 final 修飾的方法) |
| invokeinterface | 調用接口方法 |
| invokedynamic | 動態(tài)解析并執(zhí)行需要調用的方法 |
- invokestatic 調用的方法, invokespecial 調用的方法,invokevirtual 調用的 final 方法,為非虛方法
- invokedynamic 是 Java8 中 lambda 表達式引入的新指令
5 虛方法表
- JVM 在每個類的方法區(qū)建立虛方法表(非虛方法不會在此出現(xiàn)),表中存放的是各個方法的實際入口,以提高動態(tài)鏈接情況下的查找性能
- 使用舉例:
六 運行時數(shù)據(jù)區(qū):本地方法棧
- 本地方法:Java 調用的非 Java 語言實現(xiàn)的方法
- 本地方法不是抽象方法,有具體實現(xiàn),但非 Java 語言,所以 native 不能與 abstract 共同使用
- 虛擬機棧用于管理 Java 方法的調用,本地方法棧用于管理本地方法的調用
- 本地方法棧的容量可以設置為可變,也可以設置為固定
七 運行時數(shù)據(jù)區(qū):堆
1 概述
- 一個 JVM 實例只存在一個堆空間,在 JVM 啟動時堆的大小已確定
- 堆在物理內存上可以不連續(xù),但在邏輯上被視為連續(xù)的
- 除了 TLAB(Thread Local Allocation Buffer)區(qū)域,所有的線程共享堆內存
- 堆是 GC(Garbage Collection) 執(zhí)行垃圾回收的重點區(qū)域。方法結束后,堆中的對象不會被立刻回收,而是在垃圾回收時被移除
- 所有的 對象實例 和 數(shù)組,在運行時都在堆上分配,而不是在棧上(虛擬機棧的棧幀保存的是對象實例和數(shù)組的引用)
2 堆的內存結構
- 新生代(伊甸園區(qū)、幸存者1區(qū)、幸存者2區(qū);默認比例為8:1:1)、老年代
- 永久代 / 元空間是 Hotspot JVM 對于方法區(qū)的具體實現(xiàn),不屬于堆
- 幾乎所有對象都是在伊甸園區(qū)被創(chuàng)建的
- 分代的唯一理由是優(yōu)化 GC 性能
3 YGC / Minor GC
- 是新生代的垃圾回收機制
- 相較于 Major GC、Full GC,執(zhí)行更頻繁,所需時間更短
- 執(zhí)行流程:
- 當伊甸園區(qū)滿的時候觸發(fā)(幸存者區(qū)滿時不會觸發(fā))
- 對新生代(包括伊甸園區(qū)和幸存者區(qū))執(zhí)行垃圾回收。對于沒有回收的實例,將伊甸園區(qū)的實例轉移到幸存者區(qū),在幸存者區(qū)的實例從 from 區(qū)轉移到 to 區(qū)
- 對于幸存者區(qū)的實例,轉移次數(shù)超過設定值時,實例從幸存者區(qū)轉移到老年代,轉移到老年代的實例不再參與 Minor GC
4 Major GC & Full GC
- Major GC 針對老年代進行垃圾回收,如果進行之后內存仍不足,則 OOM
- Full GC 針對新生代、老年代、永久代進行垃圾回收,所需時間最長,通過調優(yōu)盡量避免
5 內存分配策略
- 對象優(yōu)先分配到伊甸園區(qū)
- 大對象直接分配到老年代
- 長期存活的對象分配到老年代
- 動態(tài)對象年齡判斷:幸存者區(qū)中,年齡相同的對象如果占幸存者區(qū)空間的一半以上,則大于等于該年齡的對象直接進入老年代,而不用到達閾值
6 線程分配緩沖區(qū)(Thread Local Allocation Buffer, TLAB)
- 在伊甸園區(qū),為每個線程分配一塊線程獨有的內存區(qū)域
- 多線程同時分配內存時,使用 TLAB 可以避免一系列線程安全問題,同時提升了內存分配吞吐量
- JVM 將 TLAB 作為內存分配的首選,無法在 TLAB 分配時,JVM 嘗試使用加鎖機制保證線程安全,并直接在伊甸園區(qū)分配內存
- 類的實例化過程:
7 逃逸分析與優(yōu)化
- 逃逸分析是 減少 Java 程序的同步負載 和 堆分配壓力的 跨函數(shù)全局流分析算法
- 如果經(jīng)過逃逸分析發(fā)現(xiàn),對象并沒有逃逸出方法的話,該對象可能被優(yōu)化為棧上分配而非堆上分配。這么做的好處是,無需對該對象進行垃圾回收,并減緩了堆的壓力
- 結論:能使用局部變量的,就不要在方法外定義
- 優(yōu)化方法與作用
| 棧上分配 | 如果對象沒有逃逸出方法,可能被優(yōu)化為棧上分配,避免了對其GC,減輕堆的壓力 |
| 同步省略 | 借助逃逸分析,判斷同步的代碼塊使用的鎖對象是否只能被一個線程訪問,如果是則取消代碼塊的同步 |
| 標量替換 | 將符合條件的對象“打散”,分配在棧上,避免對象的創(chuàng)建,因此不使用堆的內存 |
- 標量替換的實例
8 對象的內存分配流程
八 運行時數(shù)據(jù)區(qū):方法區(qū)(永久代 / 元空間)
1 概述
- 方法區(qū)用于存儲已被虛擬機加載的 類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等
- 類似于堆,在 JVM 啟動時創(chuàng)建,物理內存空間可以不連續(xù),容量可以設置為固定或可變
- JDK7 之前稱方法區(qū)為永久代,8 及之后稱為元空間。元空間最大的區(qū)別在于使用的是本地內存,而非 JVM 設置的內存
- 永久代 ≠ 方法區(qū),因為永久代僅僅是針對 Hotspot JVM 的概念,而方法區(qū)是 JVM 的概念
- 方法區(qū)的容量決定了系統(tǒng)可以保存多少個類(類的個數(shù)而非類的實例個數(shù)),如果超出容量則 OOM: PermGen Space(JDK8 及之后為OOM: MetaSpace)
2 方法區(qū)與堆棧的交互
- 方法區(qū)和堆是線程共享的,而虛擬機棧、本地方法棧、PC寄存器是線程私有的
- 容量滿時的異常類型不同:
-對象創(chuàng)建時,方法區(qū)與堆棧的交互:
- 程序執(zhí)行實例:字節(jié)碼指令存在于 class 文件
3 方法區(qū)的內部結構
-
類型信息
對于每個加載的類型(class, interface, enum, annotation),JVM存儲:
該類型的完整有效名稱、該類型的直接父類的有效名稱、該類型的直接接口的有效名稱有序列表、該類型的修飾符 -
域信息
按 域聲明順序 存儲:
域名稱、域類型、域修飾符 -
方法信息
按 方法聲明順序 存儲:
方法聲明順序、方法的返回值類型、方法參數(shù)的數(shù)量和類型(按聲明順序)、方法的修飾符、方法的字節(jié)碼(方法名和方法體)、操作數(shù)棧大小、局部變量表大小、異常表 -
非 final 的類變量
即有 static 無 final 修飾的變量,類變量隨著類的加載而加載,和類數(shù)據(jù)屬于同一邏輯部分
被 final 修飾的類變量在編譯時完成加載 -
運行時常量池
是 字節(jié)碼文件常量池(可以看作一張表,存放索引到類名、方法名、參數(shù)類型、字面量等類型的映射,以及相互之間的調用關系) 經(jīng)過類加載后得到的結果,存放在方法區(qū)中
JVM 為每個已加載的類型維護一個運行時常量池,運行時常量池的每一項通過索引訪問
4 方法區(qū)的發(fā)展:為什么需要元空間
- 永久代的需要的空間難以估計,而元空間的容量僅受限于本地內存,不易產生 OOM
- 對永久代調優(yōu)比較困難
| 1.6及之前 | 有永久代 |
| 1.7 | 去永久代,運行時常量池中的字符串常量池、靜態(tài)變量移動到堆中,原因:永久代的回收效率很低,只有老年代或永久代空間不足時觸發(fā) Full GC 才會進行回收;而放在堆里能及時回收內存 |
| 1.8及之后 | 無永久代,運行時常量池中的字符串常量池、靜態(tài)變量仍在堆中,永久代的其余部分移動到本地內存中 |
5 方法區(qū)的垃圾回收
主要回收的內容:常量池中不再使用的常量,和不再使用的類型
- 常量池中不再使用的常量:類似于堆中實例的回收,一旦沒有被任何地方引用,就可以被回收
- 不再使用的類型:實現(xiàn)比較困難,需要滿足以下三個條件
該類的所有實例都被回收(包括派生子類)
該類的類加載器已被回收
該類對應的 java.lang.Class 對象在任何地方都沒有被引用,即無法通過反射訪問該類的方法
九 對象的實例化、內存布局、訪問定位
1 對象創(chuàng)建的方法和步驟
2 !!對象的內存布局
- 在 main 方法中創(chuàng)建了一個名為 cust 的對象,其內存布局如下圖所示
- 因為是靜態(tài)方法,所以局部變量表的首位不是 this
十 String Table
1 String 的基本特性
- 聲明為 final,不可繼承
- 實現(xiàn)了 Serializable 接口,可序列化;實現(xiàn)了 Comparable 接口,可以比較大小
- JDK8 及之前使用 char[] 存儲,JDK 9 之后使用 byte[],同時 StringBuffer 和 StringTable 也隨之更改
- 通過字面量的方式(而非 new)給一個字符串賦值,此時字符串聲明在字符串常量池中
- 字符串常量池不會存儲內容相同的字符串
- 具有不可變性,對字符串修改時,必須重新申請內存區(qū)域進行賦值,而不能在原內存空間中修改
- JDK6 屬于永久代 -> JDK7 及之后屬于堆空間(詳見運行時數(shù)據(jù)區(qū):方法區(qū))
2 String 拼接
-
常量和常量的拼接結果,仍然放在字符串常量池,原理是編譯期優(yōu)化
-
拼接過程中只要有一個是變量(如果聲明時被 final 修飾則不能視為“變量”,編譯期優(yōu)化),結果就在堆中,原理是變量拼接使用 StringBuilder,拼接后調用 toString(),類似于 new String()
-
區(qū)別:new String() 生成的字符串會在常量池中保存一個字符串對象的復制(對象而非地址的復制),而 toString() 不會
-
如果拼接的結果調用 intern(),則將拼接得到的字符串放入常量池(如果使用 equals() 判斷字符串已經(jīng)存在則無需放入),并返回字符串在常量池中的地址
3 intern()
- 某個字符串調用 intern() 方法,該方法會從字符串常量池中查詢當前字符串是否存在,若不存在則復制到字符串常量池中,并返回它在字符串常量池的地址
- 有關 intern() “復制” 的說明:JDK1.6 及之前復制的是對象,將字符串對象從堆復制一份放在永久代(此時字符串常量池、靜態(tài)變量仍在永久代中);1.7 及之后復制的是字符串對象的引用地址,將地址放入字符串常量池(此時的字符串常量池、靜態(tài)變量移動到了堆中)
- 注意以上僅針對 intern() “復制” ,new String(...) 只是單純地 創(chuàng)建兩個對象
- 調用任意字符串的 intern() 方法,返回結果指向的實例,和以常量形式出現(xiàn)的字符串實例完全相同
4 創(chuàng)建了幾個對象?
- new String() 生成的字符串會在常量池中保存一份 對象的復制,而 toString() 不會
- 以下對 JDK 6/7 均成立
5 兩道難題*
String s1 = new String("a"); // 不涉及到intern()的復制,只是單純創(chuàng)建兩個對象,無論6和7 s1.intern(); // 什么都沒做 String s2 = "a"; // 6:s2 ==(字符串常量池的實例 "a" 的地址) != s1的地址 // 7:s2 ==(字符串常量池中實例 "a" 的地址) != s1的地址 sout(s1 == s2); // 6/7/8 均返回 false String s3 = new String("a") + new String("a"); s3.intern(); // "aa"放入字符串常量池,是intern()放入的,而非 new String(...)放入,區(qū)別于上面 String s4 = "aa"; // 6:s4 ==(字符串常量池的實例 "aa" 的地址) != s3的地址 // 7:s4 ==(字符串常量池中存放的 堆中 "aa" 的地址) == s3的地址 sout(s3 == s4); // 6 返回 false,7/8 返回 true- 問題2參考上述有關 intern() “復制” 的說明:JDK1.6 及之前復制的是對象,將字符串對象從堆復制一份放在永久代,創(chuàng)建了新對象(1.6 字符串常量池、靜態(tài)變量仍在永久代中);1.7 及之后復制的是字符串對象的引用地址,將地址放入字符串常量池(1.7 字符串常量池、靜態(tài)變量移動到了堆中)
- 注意以上僅針對 intern() “復制” ,new String(...) 只是單純地 創(chuàng)建兩個對象
- 關鍵是 intern() 之前常量池里是否已有字符串,即 intern() 是否起作用
- 補充三道例題和圖解:
十一 垃圾回收相關概念
1 什么是垃圾
- 垃圾:運行程序中沒有任何指針指向的對象
- 垃圾回收的對象是堆和方法區(qū),重點是堆。從次數(shù)上講,頻繁收集年輕代,較少收集老年代,基本不收集永久代
- 垃圾回收的步驟分為 標記階段 和 清除階段
2 內存溢出
- 沒有空閑內存,并且垃圾收集器無法提供更多內存時,發(fā)生 OOM
- 在拋出 OOM 前,通常 GC 會執(zhí)行垃圾回收,盡可能清理出空間
- 發(fā)生原因:
可能是 JVM 堆內存設置不夠;
也可能是代碼中創(chuàng)建了大量大對象,并且長時間不能被垃圾收集器回收;
或者是申請了超大對象,超過了堆的最大值,此時不觸發(fā) GC 直接 OOM
3 內存泄漏
- 嚴格來說,只有對象不會再被程序用到了,但是 GC 又不能回收他們的情況,才稱為內存泄漏
- 一些不好的實踐導致對象生命周期變長,甚至進一步導致 OOM,是寬泛意義上的內存泄漏
- 舉例:
1.單例模式。單例對象的生命周期和應用程序是一樣長的,如果單例對象持有對外部對象的引用的話,那么這個外部對象就不能 被回收,導致內存泄漏
2.一些資源未手動關閉導致內存泄漏。數(shù)據(jù)庫連接、套接字連接、IO連接必須手動關閉,否則不能被回收
4 強引用:存在就不回收
- 最傳統(tǒng)的“引用”,默認的引用類型,無論任何情況下, 只要強引用還存在,垃圾收集器就永遠不會回收被引用的對象
- 四種引用中,唯一需要為 OOM 負責的引用類型,即只有強引用才會導致 OOM
- 強引用可以直接訪問目標對象
- 強引用指向的對象在任何時候都不會被回收,即使 OOM
5 軟引用:內存不足時回收
- 在即將 OOM 之前,垃圾收集器會回收 僅 具有軟引用的對象,如果 GC 后仍內存不足則 OOM
- 和弱引用類似,只不過 JVM 會盡量讓軟引用的對象存活得更久,迫不得已時才回收
6 弱引用:發(fā)現(xiàn)即回收
- 僅 具有弱引用的對象只能生存到下一次 GC 之前,無論內存是否足夠,在執(zhí)行 GC 時都會回收這類對象
- 由于 GC 線程的優(yōu)先級很低,所以弱引用對象也能存在一定的時間
- 和軟引用都適合存放可有可無的緩存數(shù)據(jù)
7 虛引用:形同虛設,回收跟蹤
- 虛引用不會對對象的生存周期造成影響,也無法通過虛引用獲得對象(除此之外都可以通過引用獲取對象),虛引用的作用僅僅是在對象被回收時收到系統(tǒng)通知
- 四種引用中,唯一一種不能用來獲取被引用的對象的引用類型
- 虛引用可以跟蹤對象的回收時間,因此可以將一些資源釋放操作放置在虛引用對象中執(zhí)行記錄
- 必須和引用隊列一起使用,當 GC 執(zhí)行時,如果發(fā)現(xiàn)一個待回收對象具有虛引用,就會在對象回收后將虛引用加入到引用隊列,以通知對象的回收情況
十二 垃圾回收算法
1 標記階段:可達性分析算法
- 基本思路是,從 GC Roots 出發(fā)按照從上到下的搜索方式,確定對象是否可達,如果目標對象沒有任何引用鏈相連,則是不可達的,標記為垃圾對象。只有能被根對象集合直接或間接到達的對象才是存活對象
- 可以解決循環(huán)引用問題
- 可以作為 GC Roots 的對象類型:
虛擬機棧(棧幀中的本地變量表)中引用的對象
本地方法棧中引用的對象
方法區(qū)中 類的靜態(tài)屬性 引用的對象
方法區(qū)中 常量 引用的對象(字符串常量池中的對象)
被同步鎖持有的對象 - 如果一個引用指向堆內存里的對象,引用本身又不在堆內存里,那么這個對象就是一個 GC Root
- 分析工作需要在能保障一致性的快照中進行,所以執(zhí)行時必須 Stop the World
2 finalize
- 垃圾回收器回收對象之前,總會先調用該對象的 finalize()
- 重寫該方法可以自定義對象被銷毀之前的處理邏輯,用于對象回收時的資源釋放
- 不要主動調用對象的 finalize() 方法,而要交給垃圾回收器調用,原因:
finalize() 可能導致對象復活
糟糕的 finalize() 會嚴重影響 GC 性能
何時執(zhí)行 finalize() (即何時回收對象)是沒有保障的,應該由 GC 決定 - 虛擬機中的對象處于 可觸及、可復活、不可觸及 三種狀態(tài)
可觸及:對象由 GC Roots 可達
可復活:對象所有引用都被釋放,但 finalize() 沒有調用,有可能在 finalize() 中復活
不可觸及:對象的 finalize() 已被調用并且沒有復活,此時對象可以被安全回收。不可觸及的對象不可能被復活,因為對象的 finalize() 只會調用一次
3 判斷對象是否可回收的流程(至少兩次標記)
① 如果對象沒有重寫 finalize() 方法,或 finalize() 被調用過,則視為“沒有必要執(zhí)行”,對象被判定為不可觸及,執(zhí)行垃圾回收
② 如果對象重寫了 finalize() 方法,并且未執(zhí)行過,則對象被插入到 F-Queue 隊列中,由虛擬機自動創(chuàng)建的低優(yōu)先級線程 Finalizer 執(zhí)行其 finalize() 方法
③ 稍后 GC 對 F-Queue 中的對象進行二次標記,如果對象執(zhí)行 finalize() 后和引用鏈上的任意對象產生聯(lián)系,則被移出“即將回收”集合。對象會再次出現(xiàn)沒有引用存在的情況時,該對象的 finalize() 不會再被調用,一旦 GC Roots 不可達則立刻進入不可觸及狀態(tài)
4 清除階段:標記-清除算法
- 首先,垃圾收集器從根節(jié)點開始遍歷,標記所有 引用的對象(而非標記垃圾對象);然后垃圾收集器 對堆內存從頭到尾進行線性遍歷,如果對象沒有被標記則將其回收
- 缺點:
這種方式清理出來的空閑內存是不連續(xù)的
這里的清除指的是,把清除的對象地址保存在空閑地址列表里,以便再次為對象分配內存時使用,因此需要維護一個空閑鏈表
STW
5 清除階段:標記-復制算法
- 將內存空間分為兩塊,每次僅使用其中的一塊。在垃圾回收時將使用的內存塊中的存活對象復制到未被使用的塊中,然后清除正在使用的內存塊的所有對象,交換兩個內存塊的角色
- 適合存活對象很少,垃圾對象很多的場景(尤其是新生代)
- 優(yōu)點:
保證垃圾回收后空間的連續(xù)性,不會出現(xiàn)碎片問題
三種算法中效率最高 - 缺點:
需要兩倍的內存空間
STW
(黑色箭頭代表引用關系)
6 清除算法:標記-壓縮算法
- 首先,垃圾收集器從根節(jié)點開始遍歷,標記所有 引用的對象(而非標記垃圾對象);然后將存活的對象壓縮到內存的一側,按照順序排放;最后清理邊界外的所有空間
- 和 標記-清除算法 的本質差異在于,標記-清除算法 是非移動式的回收算法,標記-壓縮算法 是移動式的
- 優(yōu)點:
內存有序分布,可以使用指針碰撞的方式為新對象分配內存,效率高
解決了 標記-清除算法 的碎片問題 - 缺點:
效率低于上述兩種算法
STW
7 三種清除算法的對比
| 執(zhí)行速度 | 中等 | 最快 | 最慢 |
| 空間開銷 | 少 | 多 | 少 |
| 是否產生碎片 | 是 | 否 | 否 |
| 是否移動對象 | 否 | 是 | 是 |
8 分代收集算法
- 核心思想是,不同生命周期的對象采用不同的收集方式
- 新生代:區(qū)域比老年代小,對象生命周期短、存活率低、回收頻繁。適用 標記-復制算法
- 老年代:區(qū)域較大,對象生命周期長、存活率高、回收不頻繁。一般采用 標記-清除算法 和 標記-壓縮算法 混合實現(xiàn)
十三 垃圾回收器
1 主要性能指標
- 吞吐量:運行用戶代碼的時間占總運行時間的比例
- 暫停時間:執(zhí)行垃圾收集時,程序的工作線程被暫停的時間
- 內存占用:執(zhí)行垃圾收集時占用的堆空間大小
優(yōu)秀的垃圾收集器最多三者得其二。其中,吞吐量和暫停時間也是相互矛盾的目標,如果選擇更大的吞吐量,就會降低垃圾回收的頻率,導致暫停時間更長;反之,選擇更短的暫停時間,提高了垃圾回收的頻率,降低了吞吐量。當前垃圾收集器的準則是,在保證吞吐量的前提下,盡量縮短暫停時間
總結
以上是生活随笔為你收集整理的后端学习 - JVM(上)内存与垃圾回收的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 五款Windows看小说应用
- 下一篇: U盘和固态硬盘的区别