簡單談談對GC垃圾回收的通俗理解


簡單談談對GC垃圾回收的通俗理解

文章簡介

《簡單談談對GC垃圾回收的理解》是我的第一篇博客,了解並學習了JVM的垃圾回收機制后,把自己的一些理解記錄下來,通過輸出博客的方式來沉淀,我覺得是一個不錯的方式!

垃圾回收是指什么

所謂的垃圾,顧名思義,就是指的在程序運行的過程中,有類的誕生、初始化、銷毀,在這一系列的過程中,我們的程序自然會產生一些已經消亡的,不需要的類、實例等等。

而這些對於程序不需要的東西或者阻礙程序正常運行的東西就是垃圾。

而垃圾回收指的就是將這些垃圾從我們程序運行的環境中清除出去,至於這些垃圾會去向哪里,自然是不需要我們去關心的,畢竟你也從未關心過垃圾收集車🚗將垃圾運往何處!

為什么需要垃圾回收

我們都知道,小區的垃圾回收車🚗每天都會將垃圾集中運走處理,還小區一個干凈舒適的環境。

那么同樣,程序的運行也需要一個暢通無阻的環境。如果垃圾過多,可能導致程序運行時間變長,就像我們的電腦C盤空間不足的時候,電腦不僅運行緩慢,還會出現死機、藍屏等惡況!

程序中比較經典的因為垃圾過多導致的錯誤就有OOM(java.lang.OutOfMemoryError)。

OOM 內存不足

官方語言是這樣描述的:當Java虛擬機由於內存不足而無法分配對象,並且垃圾回收器無法再提供更多內存時,拋出該異常。通俗來說,就是內存滿了,爆了。

而我們知道,java中錯誤(Error)和異常(Exception)是不同的。異常還可以被捕獲、拋出來通過程序代碼來處理,但是錯誤已經嚴重到不足以被應用處理,會導致程序直接崩潰停止!在真實項目中這樣的情況肯定是不允許發生的。

那么我們可以分析一下導致內存不足的原因!無外乎有以下兩點:

  • 分配到的內存太少。
  • 程序使用的太多。

分配太少:我們知道,虛擬機可以使用的內存也是有個限度的,當被分配的內存大小完全不夠支持我們程序正常運行就會OOM。我們可以通過以下代碼查看JVM初始化的總內存大小和JVM試圖使用的最大內存。

long maxMemory = Runtime.getRuntime().maxMemory();//虛擬機試圖使用的最大內存
long totalMemory = Runtime.getRuntime().totalMemory();//jvm初始化的總內存

System.out.println("maxMemory=" + maxMemory+"字節\t" + (maxMemory/(double)1024/1024) + "MB" );
System.out.println("totalMemory=" + totalMemory+"字節\t" + (totalMemory/(double)1024/1024) + "MB" );

在默認情況下,初始化的總內存約為我們電腦內存的1/64,試圖使用的最大內存約為電腦內存的1/4。這里的計算誤差可以自行了解。

而這個分配給虛擬機的內存其實是可以手動調節的。我們可以通過調節虛擬機參數來自定義分配內存大小。因此我們可以嘗試去增大分配內存,測試程序是否仍然OOM!如果還是OOM那可能就需要去檢查我們的代碼是不是出問題了。

-Xms4028m -Xmx4028m

使用的太多:意思就是本來分配的內存完全是足夠程序正常運行的,但是由於某些錯誤的使用導致內存使用后沒有及時釋放,造成內存泄露或內存溢出。

  • 內存泄露:(memory leak)指的是程序在申請內存后,無法釋放已申請的內存空間,導致虛擬機無法將該塊內存分配給其他程序使用。
  • 內存溢出:( out of memory)指的是程序申請的內存超出了JVM能提供的內存大小。

在之前沒有垃圾自動回收的日子里,比如C語言和C++語言,我們必須親自負責內存的申請與釋放操作,如果申請了內存,用完后又忘記了釋放,比如C++中的new了但是沒有delete,那么就可能造成內存泄露。偶爾的內存泄露可能不會造成問題,而大量的內存泄露可能會導致內存溢出

而在Java語言中,由於存在了垃圾自動回收機制,所以,我們一般不用去主動釋放不用的對象所占的內存,也就是理論上來說,是不會存在“內存泄露”的。但是,如果編碼不當,比如,將某個對象的引用放到了全局的Map中,雖然方法結束了,但是由於垃圾回收器會根據對象的引用情況來回收內存,導致該對象不能被及時的回收。如果該種情況出現次數多了,就會導致內存溢出,比如系統中經常使用的緩存機制。Java中的內存泄露,不同於C++中的忘了delete,往往是邏輯上的原因泄露。

在我們擴大了JVM內存后仍然OOM的話,就需要使用內存快照分析工具來快速定位內存泄漏。使用JProfiler來分析Dump出的內存文件。還是修改JVM的啟動參數並導出內存文件,然后用JProfiler打開分析。

-Xms10m -Xmx80m -XX:+HeapDumpOnOutOfMemoryError


這樣我們便能快速定位內存泄漏,並進行改進!

怎么進行垃圾回收

垃圾回收的地方

JVM的位置

垃圾回收的地方自然是在JVM中。因此,首先我們需要了解JVM在哪里?下面通過一張圖展示JVM的位置。

JVM與操作系統交互,而程序是在JVM上運行的。這里的JVM其實就是JRE,而不是JDK!

jvm的體系結構

那我們知道了JVM的位置,但是垃圾回收也並不是存在於JVM的所有角落的。因為JVM中也是划分為了好幾塊區域的,下面我們看看JVM中區域的划分。

我們從上到下來分析一下JVM的體系結構。

類加載器

顧名思義,類加載器就是用於加載一個類的。我們都知道,我們寫的代碼剛開始是一個.java文件,經過idea編譯之后變成.class文件,最后通過類加載器加載成為一個Class,我們可以通過這一個Class,new出很多個實例對象。


類加載器又分為以下:

  1. 虛擬機自帶的加載器。
  2. 啟動類加載器/根加載器 Bootstrap 位於\jdk_1.8\jre\lib\rt.jar下
  3. 擴展程序加載器 ExtClassLoader 位於\jdk_1.8\jre\lib\ext下
  4. 應用程序加載器/系統類加載器 AppClassLoader

既然有這么多ClassLoader,它們是從哪里加載class的,這個問題jdk源碼中sun.misc.Launcher已經給出回答:Bootstrap ClassLoder加載的是System.getProperty("sun.boot.class.path");、ExtClassLoader加載的是System.getProperty("java.ext.dirs")、AppClassLoader加載的是System.getProperty("java.class.path")。

另外不得不了解的是雙親委派機制

說白了就是需要加載一個類的時候,子加載器都特別懶,都想依靠父加載器,只有父加載器加載不了這個類的時候,子加載器才去加載。

Java的類加載使用雙親委派模式,即一個類加載器在加載類時,先把這個請求委托給自己的父類加載器去執行,如果父類加載器還存在父類加載器,就繼續向上委托,直到頂層的啟動類加載器。如果父類加載器能夠完成類加載,就成功返回,如果父類加載器無法完成加載,那么子加載器才會嘗試自己去加載。

那么誰是父,誰是子?通過以下代碼即可查看。

car car1 = new car();
Class<? extends car> car1Class = car1.getClass();
System.out.println(car1Class.getClassLoader());//AppClassLoader
System.out.println(car1Class.getClassLoader().getParent());//ExtClassLoader 
System.out.println(car1Class.getClassLoader().getParent().getParent());//null 1.不存在 2.java程序找不到

AppClassLoader -------->ExtClassLoader-------->Bootstrap 由子到父。

這種機制的好處:

  • 避免類的重復加載。
  • 防止java的核心API被篡改。即使我們定義了與java核心api相同的類,雙親委派機制也會引導去加載核心api。

舉個例子:

  • 自定義類:java.lang.String

與核心api重名,根據雙親委派機制,引導類會加載核心api中的String,忽略掉自定義String類,如果不采用該機制,那么自定義的String將有可能被加載,那將會導致非常崩潰的情況,比如功能無法實現,項目信息泄露等等。

  • 自定義類:java.lang.MyClass

包名的命名與核心api包重合,那么理論上就會由引導類加載器完成加載,可是經過檢驗發現是沒有權限訪問核心包路徑的,那么就會拒絕加載,避免對引導類加載器本身與核心包造成威脅。

類加載器了解這么多就差不多了。接下來看看運行時數據區。

運行時數據區

這塊區域里面會發生很多事情。變量的初始化、常量定義、對象實例化、方法調用等。在這一系列過程中產生垃圾是不可避免的。而我們一定要記住的是以下兩塊區域,有一塊是不會產生垃圾的。

先說說不會產生垃圾這一塊區域

我們看到這一塊區域有三部分組成:

  • java棧
    • 棧是一種先進先出的數據結構。
    • 主要存儲8大基本類型、對象引用、實例的方法。
    • 棧里面要使用的東西就會壓棧,不使用的就會彈棧。其實就是進出棧。用完即彈出,所以不會產生垃圾。
    • 這里常問到的問題是main方法為何先調用卻最后結束?以及java.lang.StackOverflowError 棧溢出。
  • 本地方法棧
    • 這里主要用於登記帶有native關鍵字修飾的本地方法。
    • 我們都知道Java是用C++寫的,這里的native修飾的方法也是C++的本地方法庫,Java通過JNI(本地方法接口)去調用C++的庫方法,來實現Java不容易實現的功能。
  • 程序計數器
    • 保存下一條將要執行的指令地址。也稱PC寄存器。

再說說有垃圾的這一塊區域

我們看到這一塊區域有兩部分組成:

  • 方法區:
    • 靜態變量、常量、類信息(構造方法、接口定義)、運行時常量池存在方法區,但是實例變量存在堆內存中,和方法區無關。
    • 方法區是被所有線程共享的。
  • 堆:
    • 類加載器讀取了類文件之后,一般會把什么東西放到堆中?類、方法、常量、變量~,保存我們所有引用類型的真實對象。
    • 堆內存又分為三個區:新生區、養老區、永久區。
    • 之前所說的OOM就是指的堆內存不足。所謂的JVM調優也主要指堆內存調優。

關於這一塊區域暫時先了解到這里,后面會更詳細的說明這一塊區域的信息。

堆的三大區域

先看一張圖:

如下,左邊的元空間指永久區、中間的3小塊指新生區,右邊老年區也就是養老區。接下來詳細看看這幾個區的特性。

新生區:

  • 類誕生、成長、甚至死亡的地方。
  • 伊甸園區。所有的對象都是在伊甸園區new出來的。伊甸園區滿了就會觸發一次輕GC。幸存下來的會放到幸存者區。
  • 幸存者區0、1。(也稱from、to區)伊甸園區和幸存區都滿了就會觸發一次重GC。幸存下來的會放到老年區。(輕重GC先不理會)
  • 真理:經過研究,99%的對象都是臨時對象!所以很少OOM。

養老區:

  • 大部分對象都是比較穩定,不容易消亡的對象。

元空間:

  • 這個區域是常駐內存的。用來存放一些jdk自身攜帶的class對象和接口元數據。儲存的是java運行時的一些環境和類信息,這個區域不存在垃圾回收,關閉VM虛擬就會釋放這個區域的內存。
  • 關於元空間和永久代的關系可以自行了解。

輕GC和重GC

分類

  • Minor GC

    • 輕GC

    • 清理年輕代。

    • Minor GC指新生代GC,即發生在新生代(包括Eden區和Survivor區)的垃圾回收操作,當新生代無法為新生對象分配內存空間的時候,會觸發Minor GC。因為新生代中大多數對象的生命周期都很短,所以發生Minor GC的頻率很高。

  • Major GC

    • 輕GC

    • 清理老年代。

    • Major GC清理Tenured區,用於回收老年代,出現Major GC通常會出現至少一次Minor GC。

  • Full GC

    • 重GC

    • 清理整個堆空間—包括年輕代、老年代、元空間。

    • Full GC是針對整個新生代、老生代、元空間(metaspace,java8以上版本取代perm gen)的全局范圍的GC。Full GC不等於Major GC,也不等於Minor GC+Major GC,發生Full GC需要看使用了什么垃圾收集器組合,才能解釋是什么樣的垃圾回收。

分別觸發的條件

Minor GC:

  1. Eden區域滿。
  2. 新創建的對象大小 > Eden所剩空間。

Full GC:

  1. 每次晉升到老年代的對象平均大小>老年代剩余空間。
  2. MinorGC后存活的對象超過了老年代剩余空間。
  3. 永久代空間不足。
  4. 手動調用System.gc()

GC回收算法

在堆中存放着對象實例,GC回收器在對堆進行回收前,需要確定哪些對象需要被回收,即確定哪些對象還存活,哪些對象已經死去。而GC算法正是起這種作用的。

種類

  • 標記清除法。
  • 標記壓縮。
  • 復制算法。
  • 引用計數器。(已經不再使用)

引用計數器

給對象添加一個引用計數器,每當一個地方引用它時,計數器加1,每當引用失效時,計數器減少1.當計數器的數值為0時,也就是對象無法被引用時,表明對象不可在使用,這種方法實現簡單,效率較高,大部分情況下不失為一個有效的方法。但是主流的Java虛擬機如HotSpot並 沒有選取引用計數法 來回收內存,主要的原因難以解決對象之間的相互循環引用的問題。

標記清除法

  • 缺點:兩次掃描嚴重浪費時間,會產生內存碎片。
  • 優點:不需要額外的空間。

標記壓縮

復制算法


  • 好處:沒有內存碎片
  • 壞處:浪費了內存空間。多一半空間永遠是空 to。
  • 復制算法最佳使用場景:對象存活度較低的時候。

總結

  • 內存效率:復制算法>標記清除算法>標記壓縮算法 (時間復雜度)
  • 內存整齊度:復制算法=標記壓縮>標記清除算法
  • 內存利用率:標記壓縮算法=標記清除算法>復制算法

思考一個問題:難道沒有最優算法嗎?

沒有。沒有最好的算法,只有最合適的。GC也被稱為分代收集算法

年輕代:

  • 存活率低 - 復制算法。

老年代:

  • 區域大,存活率高 - 標記清除和標記壓縮混合實現。

以上

感謝您花時間閱讀我的博客,以上就是我對GC垃圾回收的一些理解,若有不對之處,還望指正,期待與您交流。

本篇博文系原創,僅用於個人學習,轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM