日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

后端学习 - JVM(上)内存与垃圾回收

發布時間:2023/12/4 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 后端学习 - JVM(上)内存与垃圾回收 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

JVM 架構圖

文章目錄

  • 一 JVM 簡介
  • 二 類加載子系統:
    • 1 作用
    • 2 類的三個加載過程
    • 3 類加載器的分類
    • 4 雙親委派機制
    • 5 兩個 class 對象為同一個類的必要條件
  • 三 運行時數據區:PC寄存器(Program Counter Register)
  • 四 運行時數據區:虛擬機棧
    • 1 概述
    • 2 棧可能出現的異常
    • 3 棧的存儲結構和運行原理
    • 4 棧幀的組成
  • 五 方法的調用
    • 1 靜態鏈接與動態鏈接
    • 2 早期綁定與晚期綁定
    • 3 (非)虛方法
    • 4 JVM 方法調用的指令
    • 5 虛方法表
  • 六 運行時數據區:本地方法棧
  • 七 運行時數據區:堆
    • 1 概述
    • 2 堆的內存結構
    • 3 YGC / Minor GC
    • 4 Major GC & Full GC
    • 5 內存分配策略
    • 6 線程分配緩沖區(Thread Local Allocation Buffer, TLAB)
    • 7 逃逸分析與優化
    • 8 對象的內存分配流程
  • 八 運行時數據區:方法區(永久代 / 元空間)
    • 1 概述
    • 2 方法區與堆棧的交互
    • 3 方法區的內部結構
    • 4 方法區的發展:為什么需要元空間
    • 5 方法區的垃圾回收
  • 九 對象的實例化、內存布局、訪問定位
    • 1 對象創建的方法和步驟
    • 2 !!對象的內存布局
  • 十 String Table
    • 1 String 的基本特性
    • 2 String 拼接
    • 3 intern()
    • 4 創建了幾個對象?
    • 5 兩道難題*
  • 十一 垃圾回收相關概念
    • 1 什么是垃圾
    • 2 內存溢出
    • 3 內存泄漏
    • 4 強引用:存在就不回收
    • 5 軟引用:內存不足時回收
    • 6 弱引用:發現即回收
    • 7 虛引用:形同虛設,回收跟蹤
  • 十二 垃圾回收算法
    • 1 標記階段:可達性分析算法
    • 2 finalize
    • 3 判斷對象是否可回收的流程(至少兩次標記)
    • 4 清除階段:標記-清除算法
    • 5 清除階段:標記-復制算法
    • 6 清除算法:標記-壓縮算法
    • 7 三種清除算法的對比
    • 8 分代收集算法
  • 十三 垃圾回收器
    • 1 主要性能指標


一 JVM 簡介

  • JVM 本質上是二進制字節碼的運行環境,是運行在操作系統上的,與硬件沒有直接的交互
  • Java 是跨平臺的語言:一次編寫,到處運行
  • JVM 是跨語言的平臺:JVM 是面向字節碼文件的,只要符合 JVM 規范,JVM 不僅可以處理 Java 語言編譯的字節碼文件,還支持其它語言編譯的字節碼文件

二 類加載子系統:

1 作用

  • 負責加載文件開頭有特定的標識的 class 文件
  • 只負責文件的加載,而不保證 class 文件可以運行(能否運行由執行引擎決定)
  • 加載的類信息存放在方法區,除此之外,方法區還會存放運行時的常量池信息
  • 在 class file -> JVM -> 元數據模板 的過程中作為“快遞員”的角色

2 類的三個加載過程

  • Loading(創建 Class 類型的對象)
    • 通過類的全限定名獲取定義此類的二進制字節流
    • 將該字節流代表的靜態存儲結構,轉化為方法區的運行時數據結構
    • 在內存中聲明一個 java.lang.Class 類型的對象,作為方法區的該類的各種數據的訪問入口
  • Linking(類變量分配內存空間,賦初始值)
    • 包含 驗證(verify)、準備(prepare)、解析(resolve) 三個階段
    • 驗證階段:確保 class 文件符合虛擬機要求,保證被加載類的正確性
    • 準備階段:為 類變量(static 修飾) 申請內存空間(不包含 final 修飾的 static 類變量,因為這些變量在編譯時分配了內存空間),并賦初始零值(final 修飾的 static 類變量為指定值);此外,該階段不會為 實例變量(通過 this 引用) 初始化,因為實例變量隨著對象分配到堆中,而類變量分配到方法區中
    • 解析階段
  • Initialization(類變量的賦值,執行靜態代碼塊語句)
    • 該階段的任務是,執行類構造器方法 <clinit>() 的過程,該方法是 javac 編譯器(前端編譯器)自動收集 類變量的賦值動作靜態代碼塊的語句 合并得到的
    • 子類的 <clinit>() 在父類的 <clinit>() 執行后才能執行
    • <clinit>() 不同于類的構造器,在 JVM 視角下,類的構造器是 <init>() 方法
    • 虛擬機保證 <clinit>() 在多線程下被同步加鎖,避免同一個類加載多次

    3 類加載器的分類

    • BootStrap ClassLoader(啟動類加載器):使用 C/C++ 實現,沒有父類(上級,非繼承意義的父類)加載器,用于加載 Java 核心庫
    • Extension ClassLoader(擴展類加載器):繼承自 ClassLoader 類,父類(上級,非繼承意義的父類)加載器為啟動類加載器
    • AppClassLoader(應用類加載器):繼承自 ClassLoader 類,父類(上級,非繼承意義的父類)加載器為擴展類加載器,是程序默認的類加載器
    • 用戶自定義類加載器

    4 雙親委派機制

    • 是 JVM 加載類的 class 文件的機制,避免類的重復加載,防止核心 API 被篡改
    • 具體地,如果有人想替換系統級別的類:String.java,篡改它的實現,在這種機制下這些系統的類已經被 Bootstrap classLoader 加載過了(因為當一個類需要加載的時候,最先去嘗試加載的就是 BootstrapClassLoader),所以其他類加載器并沒有機會再去加載,從一定程度上防止了危險代碼的植入
    • 具體地,如果一個類加載器收到了類加載請求,它不會直接執行類的加載,而是將請求委托到上級的加載器,上級的加載器遞歸執行該過程,最終請求到達啟動類加載器
    • 如果上級加載器可以執行指定類的加載,則過程結束;否則向下級傳遞該請求,直到類可以被加載

    5 兩個 class 對象為同一個類的必要條件

    • 全類名一致
    • 加載類的 ClassLoader(指 ClassLoader 實例)相同,即:同一個 class 文件,被同一個 JVM 的不同 ClassLoader 實例加載,不能算作同一個類對象

    三 運行時數據區:PC寄存器(Program Counter Register)

    • 用于存儲指向下一條指令的地址(執行引擎負責讀取下一條指令)
    • 線程私有,生命周期和線程保持一致
    • 是 Java 內存中唯一一個沒有規定 OutOfMemoryError 的區域
    • 使用PC寄存器存儲字節碼指令地址的作用,為什么要記錄當前線程的執行地址?
      一個PC寄存器記錄一個線程的字節碼指令地址,程序運行時,CPU 需要在各個線程間切換,切換到某個線程時需要還原它切換之前的現場,通過PC寄存器確定繼續執行的位置

    四 運行時數據區:虛擬機棧

    1 概述

    • 主管 Java 程序的運行,保存方法的局部變量(8種基本數據類型+對象的引用地址)、部分結果,參與方法的調用和返回
    • 棧是運行時的單位,解決程序運行的問題;堆是存儲的單位,解決數據存儲的問題
    • 線程私有,生命周期和線程保持一致

    2 棧可能出現的異常

    • JVM 允許棧的容量為動態的,或是固定的
    • 棧容量動態時,如果棧嘗試擴展并無法申請到足夠的內存,或是創建新線程時沒有足夠的內存創建對應的虛擬機棧,拋出 OutOfMemoryError
    • 棧容量固定時,請求的容量超過指定容量時,拋出 StackOverflowError

    3 棧的存儲結構和運行原理

    • 棧的存儲格式是棧幀,棧幀是一個內存區塊,維護著方法執行過程中的數據信息
    • 棧幀和執行的方法是一一對應的
    • 在一個活動線程中,同一時刻只有棧頂的棧幀是活動的(即:一個線程同一時刻只能執行一個方法),執行引擎運行的所有字節碼指令只針對當前棧幀進行操作
    • 不同線程中包含的棧幀不允許相互引用,即不能在某個棧幀中引用另外一個線程的棧幀
    • 方法返回時(使用 return 指令 / 拋出未處理的異常),當前棧幀會將執行結果傳遞給前一個棧幀,之后丟棄該棧幀,使得其下一個棧幀成為新的棧頂棧幀

    4 棧幀的組成

    組成作用
    局部變量表存儲方法的參數、定義在方法體內的局部變量
    操作數棧保存計算過程的中間結果,同時作為計算過程中變量的臨時存儲空間
    動態鏈接將符號引用轉換為調用方法的直接引用
    方法返回地址存放調用該方法的PC寄存器的值,即調用該方法的指令的下一條指令的地址
    附加信息

    4.1 局部變量表

    • 是一個數字數組,主要用于存儲方法的參數、定義在方法體內的局部變量
    • 線程私有,所以不存在數據安全問題
    • 所需容量大小在編譯時確定,方法運行時不會更改
    • 最基本的存儲單元是 Slot

    有關 Slot

  • 引用類型、byte、short、char…占用1個 Slot;long、double 占用兩個 Slot
  • 如果當前方法可以訪問 this.xxx(即:當前棧幀由構造方法或實例方法創建),則對當前對象的引用 this 放在首個 Slot(這解釋了為什么靜態方法不能訪問 this.xxx,因為局部變量表里沒有 this)
  • Slot 是可重用的,如果某個局部變量超出其作用域,則該 Slot 可以被之后聲明的變量使用
    • 局部變量表中的變量,是重要的垃圾回收根節點,只要被局部變量表直接或間接引用的對象都不會被回收
    • 成員變量(包括靜態變量、實例變量)和局部變量的對比
    變量類型初始化過程
    靜態變量在類加載的 Linking 階段申請內存空間并賦初始0值,在 Initialization 階段顯式賦值(靜態代碼塊賦值)
    實例變量對象創建時,在堆中申請內存空間并賦初始0值
    局部變量無初始0值,使用前必須要顯式賦值

    4.2 操作數棧

    • 主要用于保存計算過程的中間結果,同時作為計算過程中變量的臨時存儲空間
    • 方法剛開始執行時,操作數棧被創建,在編譯時確定其最大深度:引用類型、byte、short、char…占用1個單位深度;long、double 占用兩個單位深度
    • 不能通過索引訪問數據,只能通過棧的 push / pop
    • 如果方法具有返回值,則返回值會被壓入當前棧幀的操作數棧中,并更新PC寄存器為下一條需要執行的字節碼指令

    4.3 動態鏈接

    • Java 源代碼被編譯成字節碼文件時,所有的變量和方法引用都作為符號引用保存在 class 文件的常量池(屬于方法區)里,動態鏈接的作用是,將符號引用轉換為調用方法的直接引用
    • 為了實現動態鏈接,每個棧幀都包含一個指向運行時常量池中的該棧幀所屬方法的引用

    4.4 方法返回地址

    • 方法的結束有兩種方式:正常退出;出現未處理的異常,非正常退出
    • 正常退出的方法會給調用者返回值,而非正常退出的方法不會
    • 無論通過哪種方式退出,方法退出后都要返回其被調用的位置
    • 方法返回地址的作用是,在方法正常退出時,存放調用該方法的PC寄存器的值,即調用該方法的指令的下一條指令的地址

    五 方法的調用

    1 靜態鏈接與動態鏈接

    • 此處的 “鏈接” 是將調用方法的符號引用轉換為直接引用的過程,針對的是方法調用
    • 某種程度上,動態鏈接對應語言的多態特性
    • 靜態鏈接:字節碼文件被裝載到 JVM 內部時,被調用的方法在編譯期間可知,且運行時保持不變。這種情況下,將調用方法的符號引用轉換為直接引用的過程,稱為靜態鏈接
    • 動態鏈接:字節碼文件被裝載到 JVM 內部時,被調用的方法在編譯期間不可知,只有在運行時才能將方法調用符號引用轉換為直接引用,稱為動態鏈接

    2 早期綁定與晚期綁定

    • “綁定” 指的是字段、方法、類的符號引用轉換為直接引用的過程
    • 早期綁定:對應靜態鏈接,在編譯時可以執行引用的轉換
    • 晚期綁定:對應動態鏈接,只能在運行時執行引用的轉換

    3 (非)虛方法

    • 非虛方法:在編譯時可以確定具體的調用版本,且在運行時不變,則該方法為非虛方法
    • 靜態方法、私有方法、final 方法、構造器方法、父類的方法 均為非虛方法,其它方法稱為虛方法
    • 多態的前提是類的繼承或方法的重寫,所以不涉及到繼承和重寫的方法均為非虛方法

    4 JVM 方法調用的指令

    指令作用
    invokestatic調用靜態方法
    invokespecial調用<init>方法、私有方法、父類方法
    invokevirtual調用虛方法(包括 final 修飾的方法)
    invokeinterface調用接口方法
    invokedynamic動態解析并執行需要調用的方法
    • invokestatic 調用的方法, invokespecial 調用的方法,invokevirtual 調用的 final 方法,為非虛方法
    • invokedynamic 是 Java8 中 lambda 表達式引入的新指令

    5 虛方法表

    • JVM 在每個類的方法區建立虛方法表(非虛方法不會在此出現),表中存放的是各個方法的實際入口,以提高動態鏈接情況下的查找性能
    • 使用舉例:

    六 運行時數據區:本地方法棧

    • 本地方法:Java 調用的非 Java 語言實現的方法
    • 本地方法不是抽象方法,有具體實現,但非 Java 語言,所以 native 不能與 abstract 共同使用
    • 虛擬機棧用于管理 Java 方法的調用,本地方法棧用于管理本地方法的調用
    • 本地方法棧的容量可以設置為可變,也可以設置為固定

    七 運行時數據區:堆

    1 概述

    • 一個 JVM 實例只存在一個堆空間,在 JVM 啟動時堆的大小已確定
    • 堆在物理內存上可以不連續,但在邏輯上被視為連續的
    • 除了 TLAB(Thread Local Allocation Buffer)區域,所有的線程共享堆內存
    • 堆是 GC(Garbage Collection) 執行垃圾回收的重點區域。方法結束后,堆中的對象不會被立刻回收,而是在垃圾回收時被移除
    • 所有的 對象實例數組,在運行時都在堆上分配,而不是在棧上(虛擬機棧的棧幀保存的是對象實例和數組的引用)

    2 堆的內存結構

    • 新生代(伊甸園區、幸存者1區、幸存者2區;默認比例為8:1:1)、老年代
    • 永久代 / 元空間是 Hotspot JVM 對于方法區的具體實現,不屬于堆
    • 幾乎所有對象都是在伊甸園區被創建的
    • 分代的唯一理由是優化 GC 性能

    3 YGC / Minor GC

    • 是新生代的垃圾回收機制
    • 相較于 Major GC、Full GC,執行更頻繁,所需時間更短
    • 執行流程:
    • 當伊甸園區滿的時候觸發(幸存者區滿時不會觸發)
    • 對新生代(包括伊甸園區和幸存者區)執行垃圾回收。對于沒有回收的實例,將伊甸園區的實例轉移到幸存者區,在幸存者區的實例從 from 區轉移到 to 區
    • 對于幸存者區的實例,轉移次數超過設定值時,實例從幸存者區轉移到老年代,轉移到老年代的實例不再參與 Minor GC

    4 Major GC & Full GC

    • Major GC 針對老年代進行垃圾回收,如果進行之后內存仍不足,則 OOM
    • Full GC 針對新生代、老年代、永久代進行垃圾回收,所需時間最長,通過調優盡量避免

    5 內存分配策略

    • 對象優先分配到伊甸園區
    • 大對象直接分配到老年代
    • 長期存活的對象分配到老年代
    • 動態對象年齡判斷:幸存者區中,年齡相同的對象如果占幸存者區空間的一半以上,則大于等于該年齡的對象直接進入老年代,而不用到達閾值

    6 線程分配緩沖區(Thread Local Allocation Buffer, TLAB)

    • 在伊甸園區,為每個線程分配一塊線程獨有的內存區域
    • 多線程同時分配內存時,使用 TLAB 可以避免一系列線程安全問題,同時提升了內存分配吞吐量
    • JVM 將 TLAB 作為內存分配的首選,無法在 TLAB 分配時,JVM 嘗試使用加鎖機制保證線程安全,并直接在伊甸園區分配內存
    • 類的實例化過程:

    7 逃逸分析與優化

    • 逃逸分析是 減少 Java 程序的同步負載堆分配壓力的 跨函數全局流分析算法
    • 如果經過逃逸分析發現,對象并沒有逃逸出方法的話,該對象可能被優化為棧上分配而非堆上分配。這么做的好處是,無需對該對象進行垃圾回收,并減緩了堆的壓力
    • 結論:能使用局部變量的,就不要在方法外定義
    public class EscapeAnalysis {public EscapeAnalysis obj;/*方法返回EscapeAnalysis對象,發生逃逸*/public EscapeAnalysis getInstance(){return obj == null? new EscapeAnalysis() : obj;}/*為成員屬性賦值,發生逃逸*/public void setObj(){this.obj = new EscapeAnalysis();}//思考:如果當前的obj引用聲明為static的?仍然會發生逃逸。/*對象的作用域僅在當前方法中有效,沒有發生逃逸*/public void useEscapeAnalysis(){EscapeAnalysis e = new EscapeAnalysis();}/*引用成員變量的值,發生逃逸*/public void useEscapeAnalysis1(){EscapeAnalysis e = getInstance();//getInstance().xxx()同樣會發生逃逸} }
    • 優化方法與作用
    優化方法作用
    棧上分配如果對象沒有逃逸出方法,可能被優化為棧上分配,避免了對其GC,減輕堆的壓力
    同步省略借助逃逸分析,判斷同步的代碼塊使用的鎖對象是否只能被一個線程訪問,如果是則取消代碼塊的同步
    標量替換將符合條件的對象“打散”,分配在棧上,避免對象的創建,因此不使用堆的內存
    • 標量替換的實例
    /*標量替換前*/ class Point {private int x;private int y;// 構造方法省略... } private static void alloc() {Point p = new Point(1, 2);System.out.println("x" + p.x + "y" + p.y); }/*標量替換后*/ private static void alloc() {int x = 1;int y = 2;System.out.println("x" + x + "y" + y); }

    8 對象的內存分配流程

  • 對象優先分配在 TLAB 上
  • 如果 TLAB 無法容納,則分配在伊甸園區
  • 伊甸園區無法容納,執行 YGC(虛線部分為 YGC 的一部分流程,“Survivor放得下” 指的是從伊甸園區轉移過來的對象能否被幸存者區完全容納,非新對象)
  • YGC 后伊甸園仍然無法容納,嘗試分配到老年代
  • 執行 FGC 后老年代無法容納則 OOM

  • 八 運行時數據區:方法區(永久代 / 元空間)

    1 概述

    • 方法區用于存儲已被虛擬機加載的 類型信息、常量、靜態變量、即時編譯器編譯后的代碼緩存等
    • 類似于堆,在 JVM 啟動時創建,物理內存空間可以不連續,容量可以設置為固定或可變
    • JDK7 之前稱方法區為永久代,8 及之后稱為元空間。元空間最大的區別在于使用的是本地內存,而非 JVM 設置的內存
    • 永久代 ≠ 方法區,因為永久代僅僅是針對 Hotspot JVM 的概念,而方法區是 JVM 的概念
    • 方法區的容量決定了系統可以保存多少個類(類的個數而非類的實例個數),如果超出容量則 OOM: PermGen Space(JDK8 及之后為OOM: MetaSpace)

    2 方法區與堆棧的交互

    • 方法區和堆是線程共享的,而虛擬機棧、本地方法棧、PC寄存器是線程私有的
    • 容量滿時的異常類型不同:

      -對象創建時,方法區與堆棧的交互:
    • 程序執行實例:字節碼指令存在于 class 文件
    public static void main(String[] args) {int a = 500;int b = 100;int c = a / b;int d = 50;System.out.println(c + d); }


    3 方法區的內部結構

    • 類型信息
      對于每個加載的類型(class, interface, enum, annotation),JVM存儲:
      該類型的完整有效名稱、該類型的直接父類的有效名稱、該類型的直接接口的有效名稱有序列表、該類型的修飾符

    • 域信息
      域聲明順序 存儲:
      域名稱、域類型、域修飾符

    • 方法信息
      方法聲明順序 存儲:
      方法聲明順序、方法的返回值類型、方法參數的數量和類型(按聲明順序)、方法的修飾符、方法的字節碼(方法名和方法體)、操作數棧大小、局部變量表大小、異常表

    • 非 final 的類變量
      即有 static 無 final 修飾的變量,類變量隨著類的加載而加載,和類數據屬于同一邏輯部分
      被 final 修飾的類變量在編譯時完成加載

    • 運行時常量池
      字節碼文件常量池(可以看作一張表,存放索引到類名、方法名、參數類型、字面量等類型的映射,以及相互之間的調用關系) 經過類加載后得到的結果,存放在方法區中
      JVM 為每個已加載的類型維護一個運行時常量池,運行時常量池的每一項通過索引訪問

    4 方法區的發展:為什么需要元空間

    • 永久代的需要的空間難以估計,而元空間的容量僅受限于本地內存,不易產生 OOM
    • 對永久代調優比較困難
    版本變化
    1.6及之前有永久代
    1.7去永久代,運行時常量池中的字符串常量池、靜態變量移動到堆中,原因:永久代的回收效率很低,只有老年代或永久代空間不足時觸發 Full GC 才會進行回收;而放在堆里能及時回收內存
    1.8及之后無永久代,運行時常量池中的字符串常量池、靜態變量仍在堆中,永久代的其余部分移動到本地內存中

    5 方法區的垃圾回收

    主要回收的內容:常量池中不再使用的常量,和不再使用的類型

    • 常量池中不再使用的常量:類似于堆中實例的回收,一旦沒有被任何地方引用,就可以被回收
    • 不再使用的類型:實現比較困難,需要滿足以下三個條件
      該類的所有實例都被回收(包括派生子類)
      該類的類加載器已被回收
      該類對應的 java.lang.Class 對象在任何地方都沒有被引用,即無法通過反射訪問該類的方法

    九 對象的實例化、內存布局、訪問定位

    1 對象創建的方法和步驟

    2 !!對象的內存布局

    • 在 main 方法中創建了一個名為 cust 的對象,其內存布局如下圖所示
    • 因為是靜態方法,所以局部變量表的首位不是 this

    十 String Table

    1 String 的基本特性

    • 聲明為 final,不可繼承
    • 實現了 Serializable 接口,可序列化;實現了 Comparable 接口,可以比較大小
    • JDK8 及之前使用 char[] 存儲,JDK 9 之后使用 byte[],同時 StringBuffer 和 StringTable 也隨之更改
    • 通過字面量的方式(而非 new)給一個字符串賦值,此時字符串聲明在字符串常量池中
    • 字符串常量池不會存儲內容相同的字符串
    • 具有不可變性,對字符串修改時,必須重新申請內存區域進行賦值,而不能在原內存空間中修改
    • JDK6 屬于永久代 -> JDK7 及之后屬于堆空間(詳見運行時數據區:方法區)

    2 String 拼接

    • 常量和常量的拼接結果,仍然放在字符串常量池,原理是編譯期優化

    • 拼接過程中只要有一個是變量(如果聲明時被 final 修飾則不能視為“變量”,編譯期優化),結果就在堆中,原理是變量拼接使用 StringBuilder,拼接后調用 toString(),類似于 new String()

    • 區別:new String() 生成的字符串會在常量池中保存一個字符串對象的復制(對象而非地址的復制),而 toString() 不會

    • 如果拼接的結果調用 intern(),則將拼接得到的字符串放入常量池(如果使用 equals() 判斷字符串已經存在則無需放入),并返回字符串在常量池中的地址

    3 intern()

    • 某個字符串調用 intern() 方法,該方法會從字符串常量池中查詢當前字符串是否存在,若不存在則復制到字符串常量池中,并返回它在字符串常量池的地址
    • 有關 intern() “復制” 的說明:JDK1.6 及之前復制的是對象,將字符串對象從堆復制一份放在永久代(此時字符串常量池、靜態變量仍在永久代中);1.7 及之后復制的是字符串對象的引用地址,將地址放入字符串常量池(此時的字符串常量池、靜態變量移動到了堆中)
    • 注意以上僅針對 intern() “復制” ,new String(...) 只是單純地 創建兩個對象
    • 調用任意字符串的 intern() 方法,返回結果指向的實例,和以常量形式出現的字符串實例完全相同
    String s1 = "aa"; String s2 = "bb"; (s1 + s2).intern() == "aabb"; // 成立

    4 創建了幾個對象?

    • new String() 生成的字符串會在常量池中保存一份 對象的復制,而 toString() 不會
    • 以下對 JDK 6/7 均成立
    String s = new String("abc"); // 創建了2個對象,分別是堆中和字符串常量池中的 String 類型的實例 "abc" // 如何證明?看字節碼文件 String s = new String("aa") + new String("bb"); /* 創建了6個對象 1. new StringBuilder(),因為涉及到字符串拼接 2. new String("aa") 3. 字符串常量池中的 "aa" 4. new String("bb") 5. 字符串常量池中的 "bb" 6. StringBuilder 在拼接后調用 toString(),方法內執行 new String("aabb"),但不把 "aabb" 放入常量池!! */

    5 兩道難題*

    String s1 = new String("a"); // 不涉及到intern()的復制,只是單純創建兩個對象,無論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(...)放入,區別于上面 String s4 = "aa"; // 6:s4 ==(字符串常量池的實例 "aa" 的地址) != s3的地址 // 7:s4 ==(字符串常量池中存放的 堆中 "aa" 的地址) == s3的地址 sout(s3 == s4); // 6 返回 false,7/8 返回 true
    • 問題2參考上述有關 intern() “復制” 的說明:JDK1.6 及之前復制的是對象,將字符串對象從堆復制一份放在永久代,創建了新對象(1.6 字符串常量池、靜態變量仍在永久代中);1.7 及之后復制的是字符串對象的引用地址,將地址放入字符串常量池(1.7 字符串常量池、靜態變量移動到了堆中)
    • 注意以上僅針對 intern() “復制” ,new String(...) 只是單純地 創建兩個對象
    • 關鍵是 intern() 之前常量池里是否已有字符串,即 intern() 是否起作用
    • 補充三道例題和圖解:



    十一 垃圾回收相關概念

    1 什么是垃圾

    • 垃圾:運行程序中沒有任何指針指向的對象
    • 垃圾回收的對象是堆和方法區,重點是堆。從次數上講,頻繁收集年輕代,較少收集老年代,基本不收集永久代
    • 垃圾回收的步驟分為 標記階段清除階段

    2 內存溢出

    • 沒有空閑內存,并且垃圾收集器無法提供更多內存時,發生 OOM
    • 在拋出 OOM 前,通常 GC 會執行垃圾回收,盡可能清理出空間
    • 發生原因:
      可能是 JVM 堆內存設置不夠;
      也可能是代碼中創建了大量大對象,并且長時間不能被垃圾收集器回收;
      或者是申請了超大對象,超過了堆的最大值,此時不觸發 GC 直接 OOM

    3 內存泄漏

    • 嚴格來說,只有對象不會再被程序用到了,但是 GC 又不能回收他們的情況,才稱為內存泄漏
    • 一些不好的實踐導致對象生命周期變長,甚至進一步導致 OOM,是寬泛意義上的內存泄漏
    • 舉例:
      1.單例模式。單例對象的生命周期和應用程序是一樣長的,如果單例對象持有對外部對象的引用的話,那么這個外部對象就不能 被回收,導致內存泄漏
      2.一些資源未手動關閉導致內存泄漏。數據庫連接、套接字連接、IO連接必須手動關閉,否則不能被回收

    4 強引用:存在就不回收

    • 最傳統的“引用”,默認的引用類型,無論任何情況下, 只要強引用還存在,垃圾收集器就永遠不會回收被引用的對象
    • 四種引用中,唯一需要為 OOM 負責的引用類型,即只有強引用才會導致 OOM
    • 強引用可以直接訪問目標對象
    • 強引用指向的對象在任何時候都不會被回收,即使 OOM

    5 軟引用:內存不足時回收

    • 在即將 OOM 之前,垃圾收集器會回收 具有軟引用的對象,如果 GC 后仍內存不足則 OOM
    • 和弱引用類似,只不過 JVM 會盡量讓軟引用的對象存活得更久,迫不得已時才回收
    Object obj = new Object(); // 聲明強引用 SoftReference<Object> soft = new SoftReference<Object>(obj); // 聲明軟引用 obj = null; // 銷毀強引用

    6 弱引用:發現即回收

    • 具有弱引用的對象只能生存到下一次 GC 之前,無論內存是否足夠,在執行 GC 時都會回收這類對象
    • 由于 GC 線程的優先級很低,所以弱引用對象也能存在一定的時間
    • 和軟引用都適合存放可有可無的緩存數據
    Object obj = new Object(); // 聲明強引用 WeakReference<Object> soft = new WeakReference<Object>(obj); // 聲明弱引用 obj = null; // 銷毀強引用

    7 虛引用:形同虛設,回收跟蹤

    • 虛引用不會對對象的生存周期造成影響,也無法通過虛引用獲得對象(除此之外都可以通過引用獲取對象),虛引用的作用僅僅是在對象被回收時收到系統通知
    • 四種引用中,唯一一種不能用來獲取被引用的對象的引用類型
    • 虛引用可以跟蹤對象的回收時間,因此可以將一些資源釋放操作放置在虛引用對象中執行記錄
    • 必須和引用隊列一起使用,當 GC 執行時,如果發現一個待回收對象具有虛引用,就會在對象回收后將虛引用加入到引用隊列,以通知對象的回收情況
    Object obj = new Object(); // 聲明強引用 ReferenceQueue queue = new ReferenceQueue(); // 引用隊列 PhantomReference<Object> soft = new PhantomReference<Object>(obj, queue); // 聲明虛引用,需要傳入引用隊列 obj = null; // 銷毀強引用

    十二 垃圾回收算法

    1 標記階段:可達性分析算法

    • 基本思路是,從 GC Roots 出發按照從上到下的搜索方式,確定對象是否可達,如果目標對象沒有任何引用鏈相連,則是不可達的,標記為垃圾對象。只有能被根對象集合直接或間接到達的對象才是存活對象
    • 可以解決循環引用問題
    • 可以作為 GC Roots 的對象類型:
      虛擬機棧(棧幀中的本地變量表)中引用的對象
      本地方法棧中引用的對象
      方法區中 類的靜態屬性 引用的對象
      方法區中 常量 引用的對象(字符串常量池中的對象)
      被同步鎖持有的對象
    • 如果一個引用指向堆內存里的對象,引用本身又不在堆內存里,那么這個對象就是一個 GC Root
    • 分析工作需要在能保障一致性的快照中進行,所以執行時必須 Stop the World

    2 finalize

    • 垃圾回收器回收對象之前,總會先調用該對象的 finalize()
    • 重寫該方法可以自定義對象被銷毀之前的處理邏輯,用于對象回收時的資源釋放
    • 不要主動調用對象的 finalize() 方法,而要交給垃圾回收器調用,原因:
      finalize() 可能導致對象復活
      糟糕的 finalize() 會嚴重影響 GC 性能
      何時執行 finalize() (即何時回收對象)是沒有保障的,應該由 GC 決定
    • 虛擬機中的對象處于 可觸及、可復活、不可觸及 三種狀態
      可觸及:對象由 GC Roots 可達
      可復活:對象所有引用都被釋放,但 finalize() 沒有調用,有可能在 finalize() 中復活
      不可觸及:對象的 finalize() 已被調用并且沒有復活,此時對象可以被安全回收。不可觸及的對象不可能被復活,因為對象的 finalize() 只會調用一次

    3 判斷對象是否可回收的流程(至少兩次標記)

  • 如果沒有 GC Roots 到對象的引用鏈,則第一次標記
  • 判斷該對象有無必要執行 finalize() 方法:
    ① 如果對象沒有重寫 finalize() 方法,或 finalize() 被調用過,則視為“沒有必要執行”,對象被判定為不可觸及,執行垃圾回收
    ② 如果對象重寫了 finalize() 方法,并且未執行過,則對象被插入到 F-Queue 隊列中,由虛擬機自動創建的低優先級線程 Finalizer 執行其 finalize() 方法
    ③ 稍后 GC 對 F-Queue 中的對象進行二次標記,如果對象執行 finalize() 后和引用鏈上的任意對象產生聯系,則被移出“即將回收”集合。對象會再次出現沒有引用存在的情況時,該對象的 finalize() 不會再被調用,一旦 GC Roots 不可達則立刻進入不可觸及狀態
  • 4 清除階段:標記-清除算法

    • 首先,垃圾收集器從根節點開始遍歷,標記所有 引用的對象(而非標記垃圾對象);然后垃圾收集器 對堆內存從頭到尾進行線性遍歷,如果對象沒有被標記則將其回收
    • 缺點:
      這種方式清理出來的空閑內存是不連續的
      這里的清除指的是,把清除的對象地址保存在空閑地址列表里,以便再次為對象分配內存時使用,因此需要維護一個空閑鏈表
      STW

    5 清除階段:標記-復制算法

    • 將內存空間分為兩塊,每次僅使用其中的一塊。在垃圾回收時將使用的內存塊中的存活對象復制到未被使用的塊中,然后清除正在使用的內存塊的所有對象,交換兩個內存塊的角色
    • 適合存活對象很少,垃圾對象很多的場景(尤其是新生代)
    • 優點:
      保證垃圾回收后空間的連續性,不會出現碎片問題
      三種算法中效率最高
    • 缺點:
      需要兩倍的內存空間
      STW

      (黑色箭頭代表引用關系)

    6 清除算法:標記-壓縮算法

    • 首先,垃圾收集器從根節點開始遍歷,標記所有 引用的對象(而非標記垃圾對象);然后將存活的對象壓縮到內存的一側,按照順序排放;最后清理邊界外的所有空間
    • 標記-清除算法 的本質差異在于,標記-清除算法 是非移動式的回收算法,標記-壓縮算法 是移動式的
    • 優點:
      內存有序分布,可以使用指針碰撞的方式為新對象分配內存,效率高
      解決了 標記-清除算法 的碎片問題
    • 缺點:
      效率低于上述兩種算法
      STW

    7 三種清除算法的對比

    標記-清除標記-復制標記-壓縮
    執行速度中等最快最慢
    空間開銷
    是否產生碎片
    是否移動對象

    8 分代收集算法

    • 核心思想是,不同生命周期的對象采用不同的收集方式
    • 新生代:區域比老年代小,對象生命周期短、存活率低、回收頻繁。適用 標記-復制算法
    • 老年代:區域較大,對象生命周期長、存活率高、回收不頻繁。一般采用 標記-清除算法標記-壓縮算法 混合實現

    十三 垃圾回收器

    1 主要性能指標

    • 吞吐量:運行用戶代碼的時間占總運行時間的比例
    • 暫停時間:執行垃圾收集時,程序的工作線程被暫停的時間
    • 內存占用:執行垃圾收集時占用的堆空間大小

    優秀的垃圾收集器最多三者得其二。其中,吞吐量和暫停時間也是相互矛盾的目標,如果選擇更大的吞吐量,就會降低垃圾回收的頻率,導致暫停時間更長;反之,選擇更短的暫停時間,提高了垃圾回收的頻率,降低了吞吐量。當前垃圾收集器的準則是,在保證吞吐量的前提下,盡量縮短暫停時間

    總結

    以上是生活随笔為你收集整理的后端学习 - JVM(上)内存与垃圾回收的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。