Java虛拟機
運作在作業系統之上的,它與硬體沒有直接的互動
作用:
- Java虛拟機就是二進制位元組碼的運作環境,負責裝載位元組碼到其内部,解釋/編譯為對應平台上的機器指令執行。
特點:
- 一次編譯。到處運作
- 自動記憶體管理
- 自動垃圾回收功能
JVM整體結構:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIyVGduV2YfNWawNCM38FdsYkRGZkRG9lcvx2bjxiNx8VZ6l2cs0TPn1ENBpWTyEFROBDOsJGcohVYsR2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLxMTM2IzN0AjM0ADMxAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
Java代碼的執行流程:
JVM的生命周期
虛拟機的啟動:
Java虛拟機的啟動是通過引導類加載器建立一個初始類來完成的,這個類是由虛拟機的具體實作指定的。
虛拟機的執行:
- 一個運作中的Java虛拟機有着一個清晰的任務:執行Java程式
- 程式開始執行時他才運作,程式結束時他就停止(或者錯誤導緻終止)
- 執行一個所謂的Java程式的時候,真真正正在執行的是一個叫做Java虛拟機的程序
虛拟機的退出:
- 程式正常執行結束
- 過程中遇到異常或者是錯誤
- 作業系統錯誤導緻
- 某線程主動調用方法結束
1、類加載器子系統(Class loader)
類加載器子系統負責從檔案系統中或者網絡中加載Class檔案。
類的加載過程
1、加載
2、連結
3、初始化
任何一個類聲明以後,内部至少存在一個類的構造器 —> init
類的加載器分類
- JVM支援兩種類型的類加載器,分别為引導類加載器和自定義類加載器
- 引導類加載器(啟動類加載器):虛拟機自帶,沒有父加載器;用來Java的核心類庫;加載 擴充類和應用程式類加載器,并指定為他們的父類加載器
- 自定義類加載器:将所有派生于抽象類ClassLoader的類加載器都劃分為自定義類加載器(擴充類加載器和應用程式類加載器都屬于自定義類加載器)
- 擴充類加載器:虛拟機自帶;派生于ClassLoader類;父類加載器為啟動類加載器(注意:如果使用者建立的JAR放在jre/lib/ext子目錄下,也會自動由擴充類加載器加載)
- 應用程式類加載器(系統類加載器):Java語言編寫;派生于ClassLoader類;負責加載環境變量classpath或系統屬性 java,class.path 指定路徑下的類庫;該類是程式中預設的類加載器;通過ClassLoader.getSystemClassLoader() 方法來擷取到該類加載器
- 注意:幾種類加載器之間的關系時包含關系,不是上層和下層,也不是子父類的繼承關系。
- 對于使用者自定義類來說:預設使用系統類加載器進行加載
- Java的核心類庫:都是使用引導類加載器進行加載的
使用者自定義類加載器:
為什麼需要自定義類加載器?
- 隔離加載類
- 修改加載類的方式
- 擴充加載源
- 防止源碼洩露
JVM基礎初篇-記憶體與垃圾回收 關于ClassLoader類:
是一個抽象類,其後所有的類加載器都在繼承自ClassLoader(不包括啟動類加載器)
JVM基礎初篇-記憶體與垃圾回收
雙親委派機制
- Java虛拟機對class檔案采用的是按需加載的方式,也就是說需要使用該類時才會将它的class檔案加載到記憶體生成class對象。而且加載某個類的class檔案時,Java虛拟機采用的是雙親委派機制,即把請求交由父類處理,它是一種任務委派模式。
- 工作原理:
JVM基礎初篇-記憶體與垃圾回收 -
優勢:
避免類的重複加載
保護程式安全,防止核心API被随意篡改
沙箱安全機制
-
JVM基礎初篇-記憶體與垃圾回收
補充内容:
-
在JVM中表示兩個class對象是否為同一個類存在兩個必要條件:
類的完整類名必須一緻,包括包名
加載這個類的ClassLoader(指ClassLoader執行個體對象)必須相同
- 換句話說,在JVM中,及時這兩個類對象(class對象)來源同一個Class檔案,被同一個虛拟機所加載,但隻要加載它們的ClassLoader執行個體對象不同,那麼這兩個類對象也是不相等的
類的主動使用和被動使用:
2、運作時資料區(Runtime Data Area)
其中:
- 紅色區域是多個線程共享的,也就是與程序對應
- 灰色的區域為單線程私有的,也就是與線程一一對應
有關線程:
- 線程是一個程式裡的運作單元。JVM允許一個應用有多個線程并行的執行
-
在Hotspot JVM裡,每個線程都與作業系統的本地線程直接映射
當一個Java線程準備好執行以後,此時一個作業系統的本地線程也同時建立。Java線程終止後,本地線程也會回收。
- 作業系統負責所有線程的安排排程到任何一個可用的CPU上。一旦本地線程初始化成功,它就會調用Java線程中的run()方法
JVM系統線程:
#
2.1、程式計數器(PC寄存器)
作用:PC寄存器用來存儲指向下一條指令的位址,也就是即将要執行的指令代碼。由執行引擎讀取下一條指令。
因為是線程私有的,是以生命周期與線程的生命周期保持一緻。
有關PC寄存器的兩個常見問題:
-
1、使用PC寄存器存儲位元組碼指令位址有什麼用?為什麼使用PC寄存器記錄目前線程的執行位址呢?
因為CPU需要不停的切換各個線程,這時候切換回來以後,就得知道接着從哪裡開始繼續執行;JVM的位元組碼解釋器就需要通過改變PC寄存器的值來明确下一條應該執行什麼樣的位元組碼指令。
-
2、PC寄存器為什麼會被設定為線程私有?
所謂的多線程在一個特定的時間段内隻會執行其中一個線程方法,CPU會不停地做任務切換,這樣必然導緻經常中斷或恢複,如何保證分毫無差呢?為了能夠準确地記錄各個線程正在執行的目前位元組碼指令位址,最好的辦法自然是為每一個線程都配置設定一個PC寄存器,這樣一來各個線程之間便可以進行獨立計算,進而不會出現互相幹擾的情況;
由于CPU時間片輪限制,衆多線程在并發執行過程中,任何一個确定的時刻,一個處理器或者多核處理器中的一個核心,隻會執行某個線程的一條指令;
這也必然導緻經常中斷或恢複,如何保證分毫無差?每個線程在建立後,都會産生自己的程式計數器和棧幀,程式計數器在各個線程之間互不影響
CPU時間片:
CPU配置設定給各個程式的時間,每個線程被配置設定一個時間段,稱作它的時間片
2.2、虛拟機棧
記憶體中的棧與堆:
棧是運作時的機關,而堆是存儲的機關,即:棧解決程式的運作問題,即程式如何執行。堆解決的是資料存儲的問題,即資料怎麼放、放在哪兒。
每個線程在建立時都會建立一個虛拟機棧,其内部儲存一個個的棧幀,對應着一次次的Java方法調用(一個棧幀對應一個方法)
虛拟機棧是線程私有的,生命周期與線程一緻
作用:
- 主管Java程式的運作,它儲存方法的局部變量(8中基本資料類型、對象的引用位址)、部分結果,并參與方法的調用和傳回
局部變量 VS 成員變量(或屬性)
基本資料類型 VS 引用類型變量(類、數組、接口)
JVM對Java棧的操作隻有兩個:
- 每個方法執行,伴随着進棧(入棧、壓棧)
- 執行結束後的出棧工作
對棧來說不存在垃圾回收的問題,但存在棧記憶體溢出問題
棧的存儲機關
- 每個線程都有自己的棧,棧中的資料都是以棧幀的格式存在
- 在這個線程上正在執行的每個方法都各自對應一個棧幀
- 棧幀是一個記憶體區塊,是一個資料集,維系着方法執行過程中的各種資料資訊
每個棧幀中存儲着:
- 局部變量表
- 操作數棧(或表達式棧)
- 動态連結(或指向運作時常量池的方法引用)
- 方法傳回位址(或方法正常退出或者異常退出的定義)
- 一些附加資訊
局部變量表:
- 局部變量表也被稱之為局部變量數組或和本地變量表
- 定義為一個數字數組,主要用于存儲方法參數和定義在方法體内的局部變量,這些資料類型包括各類基本資料類型、對象引用,以及returnAddress類型
- 由于局部變量表是建立線上程的棧上,是線程的私有資料,是以不存在資料安全問題
- 局部變量表,最基本的存儲單元是Slot(變量槽),32位以内的類型占一個Slot,64位的類型占用兩個Slot
- 如果目前幀是右構造方法或者執行個體方法建立的,那麼該對象引用this将會存放在index為0的slot處,其餘參數按照參數表順序繼續排列
變量的分類:
按照資料類型分:
- 基本資料類型
- 引用資料類型
按照在類中聲明的位置分:
-
成員變量:(在使用前,都經過預設初始化指派)
類變量(靜态修飾):linking的prepare階段:給類變量預設指派;initial階段:給類變量顯式指派即靜态代碼塊指派
執行個體變量:随着對象的建立,會在堆空間中配置設定執行個體變量空間,并進行預設指派
- 局部變量:(在使用前,必須進行顯式指派!!!否則,編譯不通過)
操作數棧(表達式棧):
- 在方法執行過程中,根據位元組碼指令,往棧中寫入資料或提取資料,即入棧/出棧
- 主要用于儲存計算過程的中間結果,同時作為計算過程中變量臨時的存儲空間(之後取出存入局部變量表)
- 如果被調用的方法有傳回值的話,其傳回值将會被壓入目前棧幀的操作數棧中,并更新PC寄存器中下一條需要執行的位元組碼指令
動态連結(或指向運作時常量池的方法引用)
- 在Java源檔案被編譯成位元組碼檔案中時,所有變量和方法引用都作為符号引用儲存在class檔案的常量池裡。比如,描述一個方法調用了另外的其他方法時,就是通過常量池中指向方法的符号引用來表示的,那麼動态連結的作用就是為了将這些符号引用轉換為調用方法的直接引用
方法的調用
- 關鍵在于什麼時候被确定下來
JVM基礎初篇-記憶體與垃圾回收 JVM基礎初篇-記憶體與垃圾回收
非虛方法:
- 如果方法在編譯期就确定了具體的調用版本,這個版本在運作時是不可變的。這樣的方法稱為非虛方法。
- 靜态方法、私有方法、final方法、執行個體構造器、父類方法都是非虛方法
- 其他的方法稱為虛方法
JVM基礎初篇-記憶體與垃圾回收
方法傳回位址:
- 存放調用該方法的pc寄存器的值
-
一個方法的結束,有兩種方式:
正常執行完成
出現未處理的異常,非正常退出
- 無論通過哪種方式退出,在方法退出後都傳回到該方法被調用的位置。方法正常退出時,調用者的pc寄存器的值作為傳回位址,即調用該方法的指令的下一條指令的位址。而通過異常退出的,傳回位址是要通過異常表來确定,棧幀一般不會儲存這部分資訊
一些附加資訊
- 棧幀中還允許攜帶跟Java虛拟機實作相關的一些附加資訊
(附加)本地方法接口
什麼是本地方法:
- 簡單地講,一個Native Method就是一個Java調用非Java代碼的接口
注意:辨別符native可以與其他的Java辨別符連用,但是abstract除外
2.3、本地方法棧
- Java虛拟機棧用于管理Java方法的調用,而本地方法棧用于管理本地方法的調用
- 本地方法棧,也是線程私有的
- 允許被實作成固定或者是可動态擴充的記憶體你大小(在記憶體溢出方面與Java虛拟機棧是相同的)
- 本地方法是使用c語言實作的
- 它的具體做法是Native Method Stack中登記Native方法,在Execution Engine 執行時加載本地方法庫
- 并不是所有的JVM都支援本地方法
- 在Hotspot JVM中,直接将本地方法棧和虛拟機棧合二為一
2.4、堆(Heap)(重要)
堆的一些核心概述:
- 一個JVM執行個體隻存在一個堆記憶體,堆也是Java記憶體管理的核心區域
- Java堆區在JVM啟動的時候即被建立,其空間大小也就确定了。是JVM管理的最大的一塊記憶體空間(堆記憶體大小是可以調節的)
- 堆可以處于實體上不聯系的記憶體空間中,但是在邏輯上它應該被視為連續的
- 所有線程共享Java堆,在這裡還可以劃分線程私有的緩沖區
- 所有的對象執行個體以及數組都應當在運作時配置設定在堆上
- 數組和對象永遠不會存儲在棧上,因為棧幀中儲存引用,這個引用指向對象或者數組在堆中的位置
JVM基礎初篇-記憶體與垃圾回收 - 在方法結束後,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候才會被移除
- 堆,是GC(Garbage Collection,垃圾收集器)執行垃圾回收的重點區域
記憶體的細分:
- Java7及之前堆記憶體邏輯上分為三部分:新生區+養老區+永久區
- Java8及之後堆記憶體邏輯上分為三部分:新生區+養老區+元空間
堆空間大小的設定:
Java堆區用于存儲Java對象執行個體,那麼堆的大小在JVM啟動時就已經設定好了,可以通過選項“-Xmx”和“-Xms”來進行設定
- “-Xms”:用來設定堆空間(新生區+養老區)的初始記憶體空間
- “-Xmx”:用來設定堆空間(新生區+養老區)的最大記憶體空間
-
預設情況下:
初始記憶體大小:電腦實體記憶體大小 / 64
最大記憶體大小:電腦實體記憶體大小 / 4
- 手動設定時建議将初始堆記憶體和最大堆記憶體設定為相同的大小
新生區和養老區(年輕代和老年代)
其中新生區又可以劃分為Eden空間、Survivor 0空間和Survivor 1空間(有時也叫做from區、to區)
可以配置新生區和老年區在堆結構中的占比(預設比例是1:2)
對象配置設定的過程:
-
JVM基礎初篇-記憶體與垃圾回收 - 針對幸存者s0,s1區的總結:複制之後有交換,誰空誰是to
- 關于垃圾回收:頻繁在新生區收集,很少在養老區收集,幾乎不在永久區/元空間收集
常用的調優工具:
Minor GC、Major GC、Full GC
JVM在進行GC時,并非每次都對前文中三個各記憶體(新生區、老年區:方法區)區域一起回收的,大部分時候回收的都是新生代
針對Hotspot VM的實作,它裡面的GC按照回收區域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)
-
部分收集:不是完整收集整個Java堆的垃圾收集。其中由分為:
新生代收集(Minor GC / Young GC):隻是新生代的垃圾收集器
老年代收集(Major GC / Old GC):隻是老年代的垃圾收集
目前,隻有CMS GC會有單獨收集老年代的行為
注意:很多時候Major GC和Full GC混淆使用,需要具體分辨老年代回收還是整堆回收
混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集
目前,隻有G1 GC會有這種行為
- 整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集
堆空間的分代思想
理由:優化GC性能
記憶體配置設定政策
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
TLAB(Thread Local Allocation Buffer)
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
堆空間的參數設定
…
堆是配置設定對象存儲的唯一選擇嗎?
- 在Java虛拟機中,對象是Java堆中配置設定記憶體的,這是一個普遍的常識。但是存在一種特殊情況,那就是如果經過逃逸分析後發現,一個對象并沒有逃逸出去的話,那麼就可能被優化成棧上配置設定。這樣就無需在堆上配置設定記憶體,也無需進行垃圾回收了。這也是常見的堆外存儲技術
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
堆的小結:
2.5、方法區
1、棧、堆、方法區的互動關系
-
JVM基礎初篇-記憶體與垃圾回收
互動關系
-
JVM基礎初篇-記憶體與垃圾回收
方法區的基本了解:
- 方法區(Method Area)與Java堆一樣,是各個線程共享的記憶體區域
- 方法區在JVM啟動的時候被建立,并且它的實際的實體記憶體空間中和Java堆區一樣都可以是不連續的
- 方法區的大小,跟堆空間一樣,可以選擇固定大小或者擴充
- 方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導緻方法區溢出,虛拟機同樣會抛出記憶體溢出錯誤:OutOfMemoryError:PermGen Space或者OutOfMemoryError:Metaspace
- 關閉JVM就會釋放這個區域的記憶體
邏輯上方法區是屬于堆的
-
JVM基礎初篇-記憶體與垃圾回收 - JDK8之後完全廢棄了永久代的概念,改用元空間來代替
- 元空間與永久代的最大差別在于:元空間不在虛拟機設定的記憶體中,而是使用本地記憶體
方法區存儲什麼?
- 類型資訊、運作時常量池、靜态變量、JTL代碼緩存、域資訊(成員變量、屬性)、方法資訊
方法區中的運作時常量池:
- 位元組碼檔案中包含了常量池:一個有效的位元組碼檔案中除了包含類的版本資訊、字段、方法以及接口等描述資訊外,還包含一項資訊那就是常量池表,包括各種字面量和對類型、域和方法的符号引用
-
位元組碼文中的常量池中有什麼:
數量值
字元串值
類引用
字段引用
方法引用
- 位元組碼檔案中的常量池中的内容,經過類加載器加載後,放入到方法區的運作時常量池中
-
JVM基礎初篇-記憶體與垃圾回收
方法區随着JDK版本變化的演進:
- HotSpot中方法區的變化:
JVM基礎初篇-記憶體與垃圾回收 -
永久代為什麼要被元空間替換:随着Java8的到來,HotSpot VM 中再也見不到永久代了,但是這并不意味着類的中繼資料資訊也消失了。這些資料被移到了一個與堆不相連的本地記憶體區域,這個區域叫做元空間
由于類的中繼資料配置設定到本地記憶體中,元空間的最大可配置設定空間就是系統可用記憶體空間
-
永久代被元空間替代的原因:
1、為永久代設定空間大小是很難确定的
2、對永久代進行調優是很困難的
方法區中靜态變量的存放:
- 靜态引用對應的對象實體始終都存在堆空間
- new 出來的對象不管是不是靜态都是存放在堆空間中
方法區的垃圾回收:
方法區中的垃圾回收主要回收兩部分内容:常量池中廢棄的常量和不再使用的類型
2.6、運作時資料區總結
3、對象的執行個體化記憶體布局與通路定位
3.1、對象的執行個體化
-
JVM基礎初篇-記憶體與垃圾回收
3.2、對象的記憶體布局
-
JVM基礎初篇-記憶體與垃圾回收 - 對象頭主要包含什麼?對象頭中的資訊是什麼?
-
JVM基礎初篇-記憶體與垃圾回收
3.3、對象的通路定位
-
JVM基礎初篇-記憶體與垃圾回收
4、直接記憶體(Direct Memory)
概述:
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
對直接記憶體的簡單了解:程序記憶體 = 堆記憶體 + 直接記憶體
5、執行引擎
概述:
- 執行引擎是Java虛拟機核心的組成部分之一
- “虛拟機”是一個相對“實體機”的概念,這兩種機器都有代碼執行能力,其差別是實體機的執行引擎是建立在處理器、緩存、指令集合作業系統層面上的,而虛拟機的執行引擎是由軟體自行實作的,是以可以不受實體條件制約地定制指令集與執行引擎的結構體系,能夠執行那些不被硬體直接支援的指令集格式
- JVM的主要任務是負責裝載位元組碼檔案到其内部,但位元組碼并不能直接運作在作業系統之上,如果想要一個Java程式運作起來,執行引擎的任務就是将位元組碼指令解釋/編譯為對應平台上的本地機器指令才可以
執行引擎的工作過程:
-
JVM基礎初篇-記憶體與垃圾回收
5.1、Java代碼編譯和執行過程中的細節問題
-
JVM基礎初篇-記憶體與垃圾回收 - 橙色部分由前端編譯器完成(javac),如下圖
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
什麼是解釋器(Interpreter),什麼是JIT編譯器?
- 解釋器:當Java虛拟機啟動時會根據預定義的規範對位元組碼采用逐行解釋的方式執行,将每條位元組碼檔案中的内容翻譯為對應平台的本地機器指令執行
- JIT編譯器:就是虛拟機将源代碼直接編譯成本地機器平台相關的機器語言
解釋器:
- 解釋器真正意義上所承擔的角色就是一個運作時“翻譯者”,将位元組碼檔案中的内容“翻譯”為對應平台的本地機器指令執行
- 當一條位元組碼指令被解釋執行完成後,接着再根據PC寄存器中記錄的下一條需要被執行的位元組碼指令執行解釋操作
(Just in time)JIT編譯器:
- 速度快
為什麼要同時存在解釋器和JIT編譯器?
-
JVM基礎初篇-記憶體與垃圾回收
熱點代碼及探測方式:
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
6、String
6.1、String的基本特性
- String聲明是為final的,不可被繼承
- 代表不可變的字元序列,簡稱:不可變性(底層是數組)
- 字元串常量池是不會存儲相同内容的字元串的
6.2、String的記憶體配置設定
String類型的常量池比較特殊。它的主要使用方法有兩種:
- 直接使用雙引号聲明出來的String對象會直接存儲在常量池中
- 如果不是雙引号直接聲明的對象,可以使用String提供的intern()方法
Java7以及以後,字元串常量池被調整在堆結構中。
6.3、intern()的使用
字元串拼接操作:
- 常量與常量的拼接結果在常量池,原理是編譯期優化
- 常量池中不會存在相同内容的常量
- 隻要其中有一個是變量,結果就在堆中。變量拼接的原理是StringBuilder
- 如果拼接的結果調用inern()方法,則主動将常量池中還沒有的字元串對象放入池中,并傳回此對象位址
例如:
- 上圖中,如果拼接字元号前後出現了變量,則相當于在堆空間中new String(),具體的内容為拼接的結果。
- intern():判斷字元串常量池中是否存在()值,如果存在,則傳回常量池中()值的位址;如果字元串常量池中不存在()值,則在常量池中加載一份()值,并傳回該對象的位址
intern()的使用:
-
JVM基礎初篇-記憶體與垃圾回收 -
如何保證變量指向的是字元串常量池中的資料?
方式一:例如,String s = “hello”; //字面量定義的方式
方式二:例如,String s = new String(“hello”).intern();也就是調用intern()方法
面試題1: new String(“ab”)會建立幾個對象?
- 2個對象
- 如何證明:看位元組碼檔案;其中,一個對象是通過new關鍵字在堆空間中建立;另一個對象是字元串常量池中的對象
面試題2: new String(“a”) + new String(“b”) 呢?
- 5個對象
- 對象1:new StringBuilder()
- 對象2:new String(“a”)
- 對象3:常量池中的"a"
- 對象4:new String(“b”)
- 對象5:常量池中的"b"
-
如果深入StringBuilder()
對象6:new String(“ab”)
注意:toString()的調用,在字元創常量池中,沒有生成"ab"
intern()使用總結:
-
JVM基礎初篇-記憶體與垃圾回收 - 在空間使用上:對于程式中大量存在的字元串,尤其其中存在很多重讀字元串時,使用intern()可以節省很多記憶體空間
7、垃圾回收的概述
什麼是垃圾?
- 垃圾是指在運作程式中沒有任何指針指向的對象,這個對象就是需要被回收的垃圾
- 如果不及時對記憶體中的垃圾進行清理,那麼,這些垃圾對象所占的記憶體空間會一直保留到應用程式結束,被保留的空間無法被其他對象使用,甚至可能導緻記憶體溢出
為什麼需要GC?
- 如果不進行垃圾回收,記憶體遲早都是會被消耗完
- 除了釋放沒用的對象,垃圾回收也可以清除記憶體裡面的記錄碎片。碎片整理将所占用的堆記憶體移到堆的一端,以便JVM将整理出的記憶體配置設定給新的對象
- 沒有GC不能保證程式的正常運作
主要是對方法區和堆區進行垃圾回收(GC),垃圾回收器可以對年輕代回收,也可以對老年代回收,甚至是全堆和方法區回收
- 其中,Java堆是垃圾收集器的工作重點
-
從次數上來說:
頻繁收集年輕代
較少收集老年代
基本不動元空間(方法區)
8、垃圾回收相關算法
- 在堆裡存放着幾乎所有的Java對象執行個體,在GC執行垃圾回收之前,首先需要區分出記憶體中哪些是存活對象,哪些是已經死亡的對象。隻有被标記為已經死亡的對象,GC才會在執行垃圾回收時,釋放掉其所占用的記憶體空間,是以這個過程我們可以稱之為垃圾标記階段
- 判斷對象存活一般有兩種方式:引用計數法和可達性分析法
8.1、垃圾标記階段的算法之引用計數算法
- 引用計數器,對每一個對象儲存一個整型的引用計數器屬性。用于記錄對象被引用的情況
- 對于一個對象,隻要有任何一個對象引用了這個對象,則這個對象的引用計數器就+1;失去引用時,引用計數器-1;隻要對象的引用計數器的值為0,表示該對象不能再被使用,可進行回收
- **優點:**實作簡單,垃圾對象便于辨識;判定效率高,回收沒有延遲性
-
缺點:
1、它需要單獨的字段存儲計數器,這樣的做法增加了存儲空間的開銷
2、每次指派都需要更新計數器,伴随着加法和減法操作,這增加了時間開銷
3、引用計數器有一個嚴重的問題,即無法處理循環引用的情況。是以Java垃圾回收器一般不适用它
8.2、垃圾标記階段的算法之可達性分析算法
- 相對于引用計數算法而言,可達性分析算法不僅同樣具備實作簡單和執行高效等特點,更重要的是該算法可以有效地解決在引用計數算法中循環引用的問題,防止記憶體洩露的發生
- 相比較于引用計數算法,這裡的可達性分析就是Java的選擇,這種類型的垃圾收集通常也叫作追蹤性垃圾收集(根搜尋算法)
基本思路:
- 可達性分析算法是以根對象集合(GC Roots)為起始點,按照從上至下的方式搜尋被根對象集合所連接配接的目标對象是否可達
- 使用可達性分析算法後,記憶體中的存活對象都會被根對象集合直接或間接連接配接着,搜尋走過的路徑稱為引用鍊
- 如果目标對象沒有任何引用鍊相連,則是不可到達的,這就意味着該對象已經死亡,可以标記為垃圾對象
- 在可達性分析算法中,隻有能夠被根對象集合直接或者間接連接配接的對象才是存活對象
-
虛拟機棧中引用的對象
比如:各個線程被調用的方法中使用到的參數,局部變量等。
- 本地方法棧内JNI(通常說的本地方法)引用的對象
-
方法區中類的靜态屬性引用的對象
比如:Java類的引用類型靜态變量
-
方法區中常量引用的對象
比如:字元串常量池裡的引用
- 所有被同步鎖(synchronized)持有的對象
-
Java虛拟機内部的引用
基本資料類型對象的class對象,一些常駐的異常對象,系統加載類
- 反映Java虛拟機内部情況的JMXBean、JVMTI中注冊的回調、本地代碼緩存等
判斷是否為GC Roots的小技巧:
- 由于Root采用棧方式存放變量和指針,是以如果一個指針,它儲存了堆記憶體裡面的對象,但是自己又不存放在堆記憶體裡面,那麼它就是一個Root
8.3、對象的finalization機制
- Java語言提供了對象終止機制來允許開發人員提供對象被銷毀之前的自定義處理邏輯
- 當垃圾回收器發現沒有引用指向一個對象,即:垃圾回收此對象之前,總會先調用這個對象的finalize()方法
- finalize()方法允許在子類中被重寫,用于在對象被回收時進行資源釋放。通常在這個方法中進行一些資源釋放和清理工作,比如關閉檔案、套接字和資料庫連接配接等
- 永遠不要主動調用某個對象的finalize()方法,應該交給垃圾回收機制調用
- 由于 finalize()方法的存在,虛拟機中的對象一般處于三種可能的狀态
JVM基礎初篇-記憶體與垃圾回收 注意:一個對象的finalize方法隻會被調用一次JVM基礎初篇-記憶體與垃圾回收
8.4、垃圾清除階段的算法
當成功區分出記憶體中存活對象和死亡對象後,GC接下來的任務就是執行垃圾回收,釋放掉對象所占用的記憶體空間
目前在JVM中比較常見的三種垃圾算法是标記-清除算法(Mark-Sweep)、複制算法(Copying)、标記-壓縮算法(Mark-Compact)
8.4.1、垃圾清除階段的算法之标記-清除算法
執行過程:
-
JVM基礎初篇-記憶體與垃圾回收 - 注意:标記的是可達對象,不是垃圾對象
缺點:
- 效率不算高
- 在進行GC的時候,需要停止整個應用程式,導緻使用者體驗差
- 這種方式清理出來的空閑記憶體不是連續的,産生記憶體碎片。需要維護一個空閑清單
注意:何為清除?
- 這裡所謂的清除并不是真的置空,而是把需要清除的對象位址儲存在空閑的位址清單裡。下次有新對象需要加載的時候,判斷垃圾的位置空間是否夠,如果夠,就存放
8.4.2、垃圾清除階段的算法之複制算法
核心思想:
-
JVM基礎初篇-記憶體與垃圾回收
優點:
- 沒有标記和清除過程,實作簡單,運作高效
- 複制過去之後保證空間的連續性,不會出現“碎片問題”
缺點:
- 此算法的缺點也是很明顯的,就是需要兩倍的記憶體空間
- 對于G1這種分拆成為大量region的GC,複制而不是移動,意味着GC需要維護region之間對象引用關系,不管是記憶體占用或者時間開銷也不小
特别的:
- 如果系統中的垃圾對象很多,複制算法就不會太理想,因為複制算法需要複制的存活對象數量并不會太大,或者說非常低才行(會造成無效移動)
應用場景:
新生代中的幸存者0和1區
8.4.3、垃圾清除階段的算法之标記-壓縮(整理)算法(Mark-Compact)
執行過程:
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
優點:
- 消除了标記-清除算法中,記憶體區域分散的缺點,我們需要給新對象配置設定記憶體時,JVM隻需要持有一個記憶體的起始位址即可
- 消除了複制算法中記憶體減半的高額代價
缺點:
- 從效率上來說,标記-整理算法要低于複制算法
- 移動對象的同時,如果對象被其他對象引用,則還需要調整引用位址
- 移動過程中,需要全程暫停使用者應用程式。即:STW
8.4.4、總結
8.5、分代收集算法(Generational Collecting)
CMS是一個針對老年代的垃圾回收器
-
JVM基礎初篇-記憶體與垃圾回收
8.6、增量收集算法、分區算法
8.6.1、增量收集算法
基本思想:
- 并發的方式執行
-
JVM基礎初篇-記憶體與垃圾回收
缺點:
- 使用這種方式,由于在垃圾回收過程中,間接性地執行了應用程式代碼,是以能減少系統的停頓時間。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降
8.6.2、分區算法
9、垃圾回收相關概念
9.1、System.gc()的了解
9.2、記憶體溢出與記憶體洩露
記憶體溢出(OOM):
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
記憶體洩露(Memory Leak):
-
JVM基礎初篇-記憶體與垃圾回收 - 記憶體洩露舉例:
JVM基礎初篇-記憶體與垃圾回收
9.3、Stop The World
9.4、垃圾回收的并行和并發
并發:
-
JVM基礎初篇-記憶體與垃圾回收
并行:
-
JVM基礎初篇-記憶體與垃圾回收
兩者對比:
- 并發,指的是多個事情,在同一個時間段内同時發生了
- 并行,指的是多個事情,在同一個時間點上同時發生了
- 并發的多個任務之間是互相搶占資源的
- 并行的多個任務之間是不互相搶占資源的
- 隻有在多CPU或者一個CPU多核的情況下,才會發生并行;否則,看似同時發生的事情,其實都是并發執行的
9.5、安全點與安全區域
程式執行時并非所有地方都能停頓下來開始GC,隻有在特定的位置才能停頓下來開始GC,這些位置稱為“安全點”(Safe Point)
如何在GC發生時,檢查所有線程都跑到最近的安全點停頓下來?
安全區域:
-
JVM基礎初篇-記憶體與垃圾回收 - 實際執行時:
JVM基礎初篇-記憶體與垃圾回收
9.6、引用
在JDK1.2之後,Java對引用的概念進行了擴充,将引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)四種,這四種引用強度依次減弱
簡要概述:
-
JVM基礎初篇-記憶體與垃圾回收
強引用(Strong Reference)——不回收:
-
JVM基礎初篇-記憶體與垃圾回收
軟引用(Soft Reference)—— 記憶體不足即回收:
-
JVM基礎初篇-記憶體與垃圾回收
簡單來說:
當記憶體足夠時,不會回收軟引用的可達對象
當記憶體不夠時,才會回收軟引用的可達對象
弱引用(Weak Reference)—— 發現即回收
-
JVM基礎初篇-記憶體與垃圾回收 - 弱引用對象與軟引用對象的最大不同就在于,當GC在進行回收時,需要通過算法檢查是否回收軟引用對象,而對于弱引用對象,GC總是進行回收。弱引用對象更容易、更快被GC回收
虛引用(Phantom Reference)—— 對象回收跟蹤
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
10、垃圾回收器
10.1、垃圾回收器的分類和性能名額
垃圾回收器的分類
1、按線程數分,可以分為串行垃圾回收器和并行垃圾回收器
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
2、按照工作模式分,可以分為并發式垃圾回收器和獨占式垃圾回收器
- 并發式垃圾回收器與應用程式交替工作,以盡可能減少應用程式的停頓時間
- 獨占式垃圾回收器一旦運作,就停止運作程式中的所有使用者線程,直到垃圾回收過程完全結束
-
JVM基礎初篇-記憶體與垃圾回收
3、按照碎片處理方式分,可分為壓縮式垃圾回收器和非壓縮式垃圾回收器
- 壓縮式垃圾回收器會在回收完成後,對存活對象進行壓縮整理,消除回收後的碎片
- 非壓縮式的垃圾回收器不進行這步操作
4、按工作的記憶體區間分,又可以分為年輕代垃圾回收器和老年代垃圾回收器
評估垃圾回收器的性能名額
-
JVM基礎初篇-記憶體與垃圾回收 - 上圖中紅色三個性能名額共同構成一個“不可能三角”。三者總體表現會随着技術進步而越來越好。一款優秀的垃圾收集器通常最多同時滿足其中兩項
-
簡單來說,主要注意兩點:
吞吐量
暫停時間
性能名額:吞吐量
-
JVM基礎初篇-記憶體與垃圾回收
性能名額:暫停時間
-
JVM基礎初篇-記憶體與垃圾回收
是以,我們需要在最大吞吐量優先的情況下, 降低停頓時間
10.2、不同的垃圾回收器概述
7款經典的垃圾收集器:
- 串行垃圾回收器:Serial、Serial Old
- 并行垃圾回收器:ParNew、Parallel Scavenge、Parallel Old
- 并發垃圾回收器:CMS、G1
7款經典垃圾回收器與垃圾分代之間的關系
-
JVM基礎初篇-記憶體與垃圾回收 - 新生代垃圾收集器:Serial、ParNew、Parallel Scavenge
- 老年代垃圾收集器:Serial Old、Parallel Old、CMS
- 整堆垃圾收集器:G1
垃圾回收器的組合關系(-JDK14):
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
如何檢視預設的垃圾回收器:
- -XX:+PrintCommandLineFlags:檢視指令行相關參數(包含使用的垃圾回收器)
- 使用指令行指令:jinfo -flag 相關垃圾回收器參數 程序ID(jps 檢視目前所有程序)
10.3、Serial回收器:串行回收
10.4、ParNew回收器:并行回收
10.5、Parallel回收器:吞吐量優先
參數配置:
10.6、CMS垃圾回收器:低延遲
CMS的工作原理:
特點與弊端:
可以設定的參數:
10.7、垃圾回收器小結
垃圾回收器的小結:
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
10.8、G1(Garbage First)垃圾回收器:區域化分代式
G1垃圾回收器的特點:
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
G1垃圾回收器的參數設定:
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
有關region的詳細說明:
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
G1垃圾回收器的垃圾回收過程:
G1 GC的垃圾回收過程主要包括如下三個環節:
- 年輕代GC(Young GC)
- 老年代并發标記過程(Concurrent Marking)
- 混合回收(Mixed GC)
- (如果需要,單線程、獨占式、高強度的Full GC)還是繼續存在的。它針對GC的評估失敗提供了一種失敗保護機制,即強力回收)
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
記憶集(Remembered Set):
-
JVM基礎初篇-記憶體與垃圾回收
具體過程——年輕代的回收:
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
具體過程——并發标記過程:
-
JVM基礎初篇-記憶體與垃圾回收
具體過程——混合回收:
-
JVM基礎初篇-記憶體與垃圾回收 -
JVM基礎初篇-記憶體與垃圾回收
具體過程(可能會出現的過程)—— Full GC:
-
JVM基礎初篇-記憶體與垃圾回收
G1垃圾回收的優化建議:
-
JVM基礎初篇-記憶體與垃圾回收
10.9、垃圾回收器總結
垃圾回收器的選擇:
有關垃圾回收器的面試問題或者注意點:
- 垃圾收集的算法有哪些?如何判斷一個對象是否可以回收?
- 垃圾收集器工作的基本流程
10.10、GC日志分析
記憶體配置設定與垃圾回收的參數清單:
10.11、垃圾回收器的新發展
JDK11新特性:
open JDK12中:
ZGC的簡單介紹: