凱哥帶你從零學大數據系列之Java篇---第二十四章:JVM優化

課程重點:

  • JVM的簡單理解

課程目錄

第一章. JVM簡介

1.1. JVM位置

 

JVM是運行在操作系統之上的,它與硬件沒有直接的交集。 JVM直接翻譯爲Java虛擬機但實際應該是Java虛擬機規範。

1.2. 三種JVM

Sun公司的HotSpot、 BEA公司的JRockit、 IBM公司的J9 VM

提起HotSpot VM,相信所有Java程序員都知道,它是Sun JDK和OpenJDK中所帶的虛擬機,也是目前使用範圍最廣的Java虛擬機。但不一定所有人都知道的是,這個目前看起來“血統純正”的虛擬機在最初並非由Sun公司開發, 而是由一家名爲“Longview Technologies”的小公司設計的; 甚至這個虛擬機最初並非是爲Java語言而開發的,它來源於Strongtalk VM,而這款虛擬機中相當多的技術又是來源於一款支持Self語言實現“達到C語言50%以上的執行效率”的目標而設計的虛擬機,Sun公司注意到了這款虛擬機在JIT編譯上有許多優秀的理念和實際效果,在1997年收購了Longview Technologies公司,從而獲得了HotSpot VM。
​
BEA公司02年從Appeal Virtual Machines收購獲得,專注於服務端應用,曾經號稱世界上速度最快的虛擬機在2008年和2009年,Oracle公司分別收購了BEA公司和Sun公司,這樣Oracle就同時擁有了兩款優秀的Java虛擬機: JRockit VM和HotSpot VM。Oracle公司宣佈在不久的將來(大約應在發佈JDK 8的時候)會完成這兩款虛擬機的整合工作,使之優勢互補。整合的方式大致上是在HotSpot的基礎上,移植JRockit的優秀特性,譬如使用JRockit的垃圾回收器與MissionControl服務,使用HotSpot的JIT編譯器與混合的運行時系統。
​
J9是IBM公司開發的虛擬機,其爲IBM公司各種產品的執行平臺。

1.3. JVM體系結構概覽

第二章. 類加載器ClassLoader

類加載器, 複雜加載class文件。 class文件在文件開頭有特定的文件標示。

ClassLoader只負責class文件的加載, 至於它是否可以運行, 則由Execution Engine決定。

Car.class 相當於是我們編寫的類模板, 封裝着屬性和行爲, 但是.class文件是存儲在物理內存中的。 我們通過ClassLoader類的加載, 加載到JVM中, 此時可以得到Car Class這裏的Car。 Class就相當於是JVM中的模板, 那麼創建Car實例都是一樣的。 我們之前學過反射, 那麼我們知道通過反射可以獲取屬性和行爲, 就是因爲我們獲取到了JVM中的Car Class。

  1. 啓動類加載器:
    這個類加載器負責放在 <JAVA_HOME>\lib 目錄中的, 或者被 -Xbootclasspath 參數所指定的路徑中的, 並且是虛擬機識別的類庫。 用戶無法直接使用。 就相當於是爲什麼我們可以通過new創建出來對象。
  2. 擴展類加載器:
    這個類加載器由 sun.misc.Launcher$AppClassLoader 實現。它負責 <JAVA_HOME>\lib\ext 目錄中的,或者被 java.ext.dirs 系統變量所指定的路徑中的所有類庫。用戶可以直接使用。
  3. 應用程序類加載器:
    這個類由 sun.misc.Launcher$AppClassLoader 實現。是ClassLoader中 getSystemClassLoader() 方法的返回值。它負責用戶路徑 ClassPath 所指定的類庫。用戶可以直接使用。如果用戶沒有自己定義類加載器,默認使用這個。
  4. 自定義加載器:
    用戶自己定義的類加載器(只有做框架纔會用到)
public class Demo { 
    public static void main(String[] args) { 
        //Object類是系統提供的 
        Object obj = new Object(); 
        //Demo是自己創建的 
        Demo demo = new Demo(); 
        //分別獲取Object和Demo類中給的ClassLoader 
        //獲取反射對象的三種方式 
        //1.通過對象.getClass 
        //2.通過類名.class 
        //3.Class.forName(類的全限定名) 
        System.out.println("ClassLoader is:"+obj.getClass().getClassLoader()); 
        System.out.println("----------------------------------分割線---------------------------------------"); 
        System.out.println("ClassLoader is:"+demo.getClass().getClassLoader()); 
        System.out.println("ClassLoader father is:"+demo.getClass().getClassLoader().getParent()); 
        System.out.println("ClassLoader grandfather is:"+demo.getClass().getClassLoader().getParent().getParent()); 
        /**
         * getClassLoader打印是null的原因 
         * 提到這裏不得不提一下jvm的類加載機制。自上而下加載,自下而上檢查。 
         * Bootstrap ClassLoader是由C++編寫的,它本身是虛擬機的一部分,所以它並不是一個JAVA類,也就是無法在java代碼中獲取它的引用, 
         * JVM啓動時通過Bootstrap類加載器加載rt.jar等核心jar包中的class文件,所以當系統類通過.getClass或是.class都是由它加載。 
         * 然後呢,我們通過執行自定義類的打印發現,JVM初始化sun.misc.Launcher並創建AppClassLoader和 Extension ClassLoader實例。 
         * ExtClassLoader設置爲AppClassLoader的父加載器。Bootstrap沒有父加載器,但是它卻可以作用一 個ClassLoader的父加載器。 
         * 比如ExtClassLoader。這也可以解釋之前通過ExtClassLoader的getParent方法獲取爲Null的現象。 
         * 具體爲什麼是這個原因,下面就給大家解釋一下 
         *
         * ps:這裏需要注意的是父加載器不是父類 
         * 這些類可以在jdk安裝目錄中的jre文件夾中lib文件夾下有一個rt.jar包可以通過壓縮的方式打開然後通過包名的形式找到對應和也會發現Object.class也是存在的,
         * 這就是爲什麼說我們創建的類默認繼承於Object 
         * Object類在於java.lang.Object下邊 
         */
    }
}

第三章. JVM雙親委派機制和沙箱機制

3.1. 雙親委派

一個類加載器查找class和resource時,是通過“委託模式”進行的,它首先判斷這個class是不是已經加載成功,如果沒有的話它並不是自己進行查找,而是先通過父加載器,然後遞歸下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果沒有找到,則一級一級返回,最後到達自身去查找這些對象。這種機制就叫做雙親委託。

可以看到2根箭頭,藍色的代表類加載器向上委託的方向,如果當前的類加載器沒有查詢到這個class對象已經加載就請求父加載器(不一定是父類)進行操作,然後以此類推。直到Bootstrap ClassLoader。如果Bootstrap ClassLoader也沒有加載過此class實例,那麼它就會從它指定的路徑中去查找,如果查找成功則返回,如果沒有查找成功則交給子類加載器,也就是ExtClassLoader,這樣類似操作直到終點,也就是我上圖中的紅色箭頭示例。 用序列描述一下:

  1. 一個AppClassLoader查找資源時,先看看緩存是否有,緩存有從緩存中獲取,否則委託給父加載器。
  2. 遞歸,重複第1部的操作。
  3. 如果ExtClassLoader也沒有加載過,則由Bootstrap ClassLoader出面,它首先查找緩存,如果沒有找到的話,就去找自己的規定的路徑下,也就是sun.mic.boot.class下面的路徑。找到就返回,沒有找到,讓子加載器自己去找。
  4. Bootstrap ClassLoader如果沒有查找成功,則ExtClassLoader自己在java.ext.dirs路徑中去查找,查找成功就返回,查找不成功,再向下讓子加載器找。
  5. ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路徑下查找。找到就返回。如果沒有找到就讓子類找,如果沒有子類會怎麼樣?拋出各種異常。

3.2. 沙箱機制

沙箱機制也就是雙親委派模型的安全機制。

// 在寫代碼中,系統會提供對應的系統類給我們使用,比如String字符串,那麼我們來進行一個模擬操作, 自定義一個java.lang.String類 
package java.lang; 
/**
 * 創建一個String類包名和類名完全和系統中的String一致 
 */ 
public final class String { 
    public static void main(String[] args) { 
        System.out.println("談戀愛嗎?做監獄的那種!"); 
    } 
}
// 當我們執行的時候回發現: 
// 錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義爲: public static void main(String[] args) 否則JavaFX應用程序類必須擴展javafx.application.Application
// 代碼中是明明有main的爲什麼會出錯就是時雙親委託中的沙箱機制了 
// 自定義的java.lang.String類永遠都不會被加載進內存。
// 因爲首先是最頂端的類加載器加載系統的java.lang.String 類,最終自定義的類加載器無法加載java.lang.String類這樣一來的好處就是可以保證不被惡意的修改系統中原有的類
瞭解:
雙親委託和沙箱機制不是絕對安全的因爲可以寫自定義ClassLoader,自定義的類加載器裏面強制加載自定義的 java.lang.String 類,不去通過調用父加載器 ,完成類的加載. 當ClassLoader加載成功後,Execution Engine執行引擎負責解釋命令,提交操作系統執行。

第四章. 本地方法棧和本地方法接口

我們可以發現Object類中有很多方法是使用native修飾,並且沒有方法體,那說明這些方法超出了Java的範圍。 所以底層實現需要使用JNI--->Java Native Interface。

native修飾的方法就是告訴java本身,此時需要調用外部的本地類庫即C/C++類庫來執行這個方法

4.1. Native Interface本地接口

本地接口的作用是融合不同的編程語言爲 Java 所用,它的初衷是融合 C/C++程序,Java 誕生的時候是 C/C++橫行的時候,要想立足,必須有調用 C/C++程序,於是就在內存中專門開闢了一塊區域處理標記爲native的代碼,它的具體做法是 Native Method Stack中登記 native方法,在Execution Engine 執行時加載native libraies。目前該方法使用的越來越少了,除非是與硬件有關的應用,比如通過Java程序驅動打印機或者Java系統管理生產設備,在企業級應用中已經比較少見。因爲現在的異構領域間的通信很發達,比如可以使用 Socket通信,也可以使用WebService等等,不多做介紹。

4.2. Native Method Stack

它的具體做法是Native Method Stack中登記native方法,在Execution Engine 執行時加載本地方法庫。

問題:現在我們發現虛擬機中有Java棧和本地方法棧兩個棧區,那麼我們在創建對象或是聲明變量的時候對應的變量或對象是在Java棧還是本地方法棧中呢?

Person p = new Person();

那麼對象p是在Java棧還是本地方法棧?

不帶native的進入到Java棧中,只有帶native的進入到本地方法棧,所以p對象是在Java棧中,而我們平時所說的棧就是Java棧。

第五章. PC寄存器

每個線程都有一個程序計數器,是線程私有的,就是一個指針,指向方法區中的方法字節碼(用來存儲指向下一條指令的地址,也即將要執行的指令代碼),由執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以忽略不記。

pc寄存器的作用:

public class Demo { 
    public static void show1() {} 
    public static void show2() {} 
    public static void show3() {} 
    public static void main(String[] args) { 
        /*
         *當執行代碼的時候都是到是從上至下順序執行 
         *可以發現代碼中是調用了三個方法,那麼這個三個方法也是順序執行 
         *那麼這個三個方法是如何計入順序執行的呢? 
         *就是PC寄存器 
         */
        show1(); 
        show2(); 
        show3(); 
    }
}

第六章. Method Area方法區

方法區是被所有線程共享,所有字段和方法字節碼,以及一些特殊方法如構造函數,接口代碼也在此定義。簡單說,所有定義的方法的信息都保存在該區域,此區屬於共享區間

存放在方法區的:靜態變量+常量+類信息(構造方法/接口定義)+運行時常量池存在方法區中

ps:只要是被線程私有獨享的一律沒有會後,只有是線程共享纔能有回收

第七章. Stack棧是什麼

ps:有一句對但是是廢話的一句話, 程序=算法+數據結構 實際應該是 程序= 框架+業務邏輯

介紹兩個數據結構:

  • 棧:先進後出(FILO)
  • 隊列:先進先出(FIFO)

ps:棧中main方法一定是在棧底

棧也叫棧內存,主管Java程序的運行,是在線程創建時創建,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放,對於棧來說不存在垃圾回收問題,只要線程一結束該棧就Over,生命週期和線程一致,是線程私有的。

棧中存儲什麼: 8種基本類型的變量+對象的引用變量+實例方法都是在函數的棧內存中分配

7.1. 棧存儲什麼

棧幀中主要保存3 類數據:

  • 本地變量(Local Variables):輸入參數和輸出參數以及方法內的變量;
  • 棧操作(Operand Stack):記錄出棧、入棧的操作;
  • 棧幀數據(Frame Data):包括類文件、方法等等

7.2. 棧運行原理

棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個內存區塊,是一個數據集,是一個有關方法(Method)和運行期數據的數據集,當一個方法A被調用時就產生了一個棧幀 F1,並被壓入到棧中。

A方法又調用了 B方法,於是產生棧幀 F2 也被壓入棧,

B方法又調用了 C方法,於是產生棧幀 F3 也被壓入棧,

……

執行完畢後,先彈出F3棧幀,再彈出F2棧幀,再彈出F1棧幀……

7.3. StackOverflowError

StackOverflflowError:拋出這個錯誤是因爲遞歸太深。 其實真正的原因是因爲Java線程操作是基於棧的,當調用方法內部方法也就是進行一次遞歸的時候就會把當前方法壓入棧直到方法內部的方法執行完全之後,就會返回上一個方法,也就是出棧操作執行上一個方法。

7.4. 總結

只要我們知道錯誤發生的原因了。當出現相就的問題就可以快速定位問題,迅速解決問題。

  • StackOverflflowError:遞歸過深,遞歸沒有出口。
  • OutOfMemoryError:JVM空間溢出,創建對象速度高於GC回收速度。

第八章. Heap堆

一個JVM實例只存在一個堆內存,堆內存的大小是可以調節的。類加載器讀取了類文件後,需要把類、方法、常變量放到堆內存中,保存所有引用類型的真實信息,以方便執行器執行,堆內存分爲三部分:

  • Young Generation Space 新生區 Young/New
  • Tenure generation space 養老區 Old/ Tenure
  • Permanent Space 永久區 Perm

 

7.1. 新生區

新生區是類的誕生、成長、消亡的區域,一個類在這裏產生,應用,最後被垃圾回收器收集,結束生命。新生區又分爲兩部分: 伊甸區(Eden space)和倖存者區(Survivor pace) ,所有的類都是在伊甸區被new出來的。倖存區有兩個: 0區(Survivor 0 space)和1區(Survivor 1 space)。當伊甸園的空間用完時,程序又需要創建對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC)即輕量垃圾回收,將伊甸園區中的不再被其他對象所引用的對象進行銷燬。然後將伊甸園中的剩餘對象移動到倖存 0區。若倖存 0區也滿了,再對該區進行垃圾回收,然後移動到 1 區。那如果1 區也滿了呢?再移動到養老區。若養老區也滿了,那麼這個時候將產生MajorGC(FullGC)即重量垃圾回收,進行養老區的內存清理。若養老區執行了Full GC之後發現依然無法進行對象的保存,就會產生OOM異常“OutOfMemoryError”。

7.2. OutOfMemoryError

如果出現java.lang.OutOfMemoryError: Java heap space異常,說明Java虛擬機的堆內存不夠。原因有二:

  • Java虛擬機的堆內存設置不夠,可以通過參數-Xms、-Xmx來調整。
  • 代碼中創建了大量大對象,並且長時間不能被垃圾收集器收集(存在被引用)。

7.3. 永久區

永久存儲區是一個常駐內存區域,用於存放JDK自身所攜帶的 Class,Interface 的元數據,也就是說它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉 JVM 纔會釋放此區域所佔用的內存。

如果出現java.lang.OutOfMemoryError: PermGen space,說明是Java虛擬機對永久代Perm內存設置不夠。一般出現這種情況,都是程序啓動需要加載大量的第三方jar包。例如:在一個Tomcat下部署了太多的應用。或者大量動態反射生成的類不斷被加載,最終導致Perm區被佔滿。

  • Jdk1.6及之前: 有永久代, 常量池1.6在方法區
  • Jdk1.7: 有永久代,但已經逐步“去永久代”,常量池1.7在堆
  • Jdk1.8及之後: 無永久代,常量池1.8在元空間

實際而言,方法區(Method Area)和堆一樣,是各個線程共享的內存區域,它用於存儲虛擬機加載的:類信息+普通常量+靜態常量+編譯器編譯後的代碼等等,雖然JVM規範將方法區描述爲堆的一個邏輯部分,但它卻還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。

對於HotSpot虛擬機,很多開發者習慣將方法區稱之爲“永久代(Parmanent Gen)” ,但嚴格本質上說兩者不同,或者說使用永久代來實現方法區而已,永久代是方法區(相當於是一個接口interface)的一個實現,jdk1.7的版本中,已經將原本放在永久代的字符串常量池移走。

ps:直白一點其實就是方法區就是永久帶的一種落地實現

常量池(Constant Pool)是方法區的一部分,Class文件除了有類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池,這部分內容將在類加載後進入方法區的運行時常量池中存放。

第九章. 堆內存調優

9.1. Java7

9.2. Java8

Java8之後將最初的永久代取消了,由元空間取代。

 

 

9.3. Java調優內存量計算

public class Demo { 
    public static void main(String[] args) { 
        long maxMemory = Runtime.getRuntime().maxMemory() ;
        //返回 Java 虛擬機試圖使用的最大內存量。 
        long totalMemory = Runtime.getRuntime().totalMemory() ;
        //返回 Java 虛擬機中的內存總量。 
        System.out.println("MAX_MEMORY = " + maxMemory + "(字節)、" + (maxMemory / (double)1024 / 1024) + "MB"); 
        System.out.println("TOTAL_MEMORY = " + totalMemory + "(字節)、" + (totalMemory / (double)1024 / 1024) + "MB"); 
    } 
}
// 發現默認的情況下分配的內存是總內存的“1 / 4”、而初始化的內存爲“1 / 64” 
// MAX_MEMORY = 3799515136(字節)、3623.5MB 
// TOTAL_MEMORY = 257425408(字節)、245.5MB

VM參數

  • -Xms1024m : 最大內存
  • -Xmx1024m : 初始化內存
  • -XX:+printGCDetails : 打印內存日誌信息

 

用新生代大小/1024+養老代大小/1024 = 初始化的大小

來看一下OOM

先修改VM參數:-Xms8m -Xmx8m -XX:+PrintGCDetails

import java.util.Random; 
public class Demo { 
    public static void main(String[] args) { 
        String str = "1234567" ; 
        while(true) { 
            str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999) ; 
        } 
    } 
}

第十章. MAT

在Eclipes中Help--->Install New Software中

下載地址: http://download.eclipse.org/mat/1.7/update-site/

 

import java.util.ArrayList; 
public class Demo { 
    byte[] byteArray = new byte[1024*1024]; 
    public static void main(String[] args) { 
        int count = 1; 
        ArrayList<Demo> list = new ArrayList<>(); 
        try {
            while(true) { 
                list.add(new Demo()); 
                count++; 
            } 
        } catch(Exception e) { 
            System.out.println("*****************count"+count); 
            e.printStackTrace(); 
        } 
    } 
}

-XX:+HeapDumpOnOutOfMemoryErrorOOM時導出堆到文件。

運行設置

-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

執行完代碼後刷新工程出現

 

回去看代碼會發現在catch中的輸出是沒有輸出,而是一場打印是出來了。 所以這個就是一個小技巧了就是通過其父類進行抓起將Exception修改成Throwable即可。

第十一章. 垃圾回收機制GC

11.1. GC是什麼

Garbage Collection, 簡稱GC, 是分代垃圾收集算法。 頻繁收集Yong區, 較少收集Old區, 基本不動Perm區。

11.2. GC算法的總體概述

JVM在進行GC時,並非每次都對上面三個內存區域一起回收的,大部分時候回收的都是指新生代。 因此GC按照回收的區域又分了兩種類型,一種是普通GC(minor GC),一種是全局GC(major GC or Full GC), 普通GC(minor GC):只針對新生代區域的GC。 全局GC(major GC or Full GC):針對年老代的GC,偶爾伴隨對新生代的GC以及對永久代的GC。

11.3. GC4大算法

11.3.1. 引用計數法(瞭解)

無法解決循環引用的問題, 不被Java採納。

11.3.2. 複製算法(Copying)

年輕代中使用的是MinorGC,這種GC算法採用的是複製算法。

Minor GC會把Eden中的所有活的對象都移到Survivor區域中,如果Survivor區中放不下,那麼剩下的活的對象就被移到Old generation中,也即一旦收集後,Eden是就變成空的了。 當對象在 Eden ( 包括一個 Survivor 區域,這裏假設是 from 區域 ) 出生後,在經過一次 Minor GC 後,如果對象還存活,並且能夠被另外一塊 Survivor 區域所容納( 上面已經假設爲 from 區域,這裏應爲 to 區域,即 to 區域有足夠的內存空間來存儲 Eden 和 from 區域中存活的對象 ),則使用複製算法將這些仍然還存活的對象複製到另外一塊 Survivor 區域 ( 即 to 區域 ) 中,然後清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),並且將這些對象的年齡設置爲1,以後對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,通過-XX:MaxTenuringThreshold 來設定參數),這些對象就會成爲老年代。

-XX:MaxTenuringThreshold — 設置對象在新生代中存活的次數

年輕代中的GC,主要是複製算法(Copying)

HotSpot JVM把年輕代分爲了三部分:1個Eden區和2個Survivor區(分別叫from和to)。默認比例爲8:1:1, 一般情況下,新創建的對象都會被分配到Eden區(一些大對象特殊處理),這些對象經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區。對象在Survivor區中每熬過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中。因爲年輕代中的對象基本都是朝生夕死的(90%以上),所以在年輕代的垃圾回收算法使用的是複製算法,複製算法的基本思想就是將內存分爲兩塊,每次只用其中一塊,當這一塊內存用完,就將還活着的對象複製到另外一塊上面。複製算法不會產生內存碎片。

在GC開始的時候,對象只會存在於Eden區和名爲“From”的Survivor區,Survivor區“To”是空的。緊接着進行GC,Eden區中所有存活的對象都會被複制到“To”,而在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會將所有對象移動到年老代中。

因爲Eden區對象一般存活率較低,一般的,使用兩塊10%的內存作爲空閒和活動區間,而另外80%的內存,則是用來給新建對象分配內存的。一旦發生GC,將10%的from活動區間與另外80%中存活的eden對象轉移到10%的to空閒區間,接下來,將之前90%的內存全部釋放,以此類推。

缺點:

複製算法它的缺點也是相當明顯的。 1、它浪費了一半的內存,這太要命了。 2、如果對象的存活率很高,我們可以極端一點,假設是100%存活,那麼我們需要將所有對象都複製一遍,並將所有引用地址重置一遍。複製這一工作所花費的時間,在對象存活率達到一定程度時,將會變的不可忽視。 所以從以上描述不難看出,複製算法要想使用,最起碼對象的存活率要非常低纔行,而且最重要的是,我們必須要克服50%內存的浪費。

11.3.3. 標記清除法(Mark-Sweep)

老年代一般是由標記清除或者是標記清除與標記整理的混合實現。

當堆中的有效內存空間(available memory)被耗盡的時候,就會停止整個程序(也被稱爲stop the world),然後進行兩項工作,第一項則是標記,第二項則是清除。 標記:從引用根節點開始標記所有被引用的對象。標記的過程其實就是遍歷所有的GC Roots,然後將所有GC Roots可達的對象 標記爲存活的對象。 清除:遍歷整個堆,把未標記的對象清除。 缺點:此算法需要暫停整個應用,會產生內存碎片用通俗的話解釋一下標記/清除算法,就是當程序運行期間,若可以使用的內存被耗盡的時候,GC線程就會被觸發並將程序暫停,隨後將依舊存活的對象標記一遍,最終再將堆中所有沒被標記的對象全部清除掉,接下來便讓程序恢復運行。

缺點:

  1. 首先,它的缺點就是效率比較低(遞歸與全堆對象遍歷),而且在進行GC的時候,需要停止應用程序,這會導致用戶體驗非常差勁
  2. 其次,主要的缺點則是這種方式清理出來的空閒內存是不連續的,這點不難理解,我們的死亡對象都是隨即的出現在內存的各個角落的,現在把它們清除之後,內存的佈局自然會亂七八糟。而爲了應付這一點,JVM就不得不維持一個內存的空閒列表,這又是一種開銷。而且在分配數組對象的時候,尋找連續的內存空間會不太好找。

11.3.4. 標記壓縮(Mark-Compact)

老年代一般是由標記清除或者是標記清除與標記整理的混合實現。

在整理壓縮階段,不再對標記的對像做回收,而是通過所有存活對像都向一端移動,然後直接清除邊界以外的內存。 可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閒列表顯然少了許多開銷。

標記/整理算法不僅可以彌補標記/清除算法當中,內存區域分散的缺點,也消除了複製算法當中,內存減半的高額代價

缺點:

標記/整理算法唯一的缺點就是效率也不高,不僅要標記所有存活對象,還要整理所有存活對象的引用地址。從效率上來說,標記/整理算法要低於複製算法。

11.3.5. 標記清除壓縮(Mark-Sweep-Compact)

11.3.6. 總結

內存效率:複製算法>標記清除算法>標記整理算法(此處的效率只是簡單的對比時間複雜度,實際情況不一定如此)。 內存整齊度:複製算法=標記整理算法>標記清除算法。 內存利用率:標記整理算法=標記清除算法>複製算法。

可以看出,效率上來說,複製算法是當之無愧的老大,但是卻浪費了太多內存,而爲了儘量兼顧上面所提到的三個指標,標記/整理算法相對來說更平滑一些,但效率上依然不盡如人意,它比複製算法多了一個標記的階段,又比標記/清除多了一個整理內存的過程

難道就沒有一種最優算法嗎? 猜猜看,下面還有

回答:無,沒有最好的算法,只有最合適的算法。==========>分代收集算法。

年輕代(Young Gen)

年輕代特點是區域相對老年代較小,對像存活率低。這種情況複製算法的回收整理,速度是最快的。複製算法的效率只和當前存活對像大小有關,因而很適用於年輕代的回收。而複製算法內存利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解。

老年代(Tenure Gen)

老年代的特點是區域較大,對像存活率高。

這種情況,存在大量存活率高的對像,複製算法明顯變得不合適。一般是由標記清除或者是標記清除與標記整理的混合實現。

Mark階段的開銷與存活對像的數量成正比,這點上說來,對於老年代,標記清除或者標記整理有一些不符,但可以通過多核/線程利用,對併發、並行的形式提標記效率。

Sweep階段的開銷與所管理區域的大小形正相關,但Sweep“就地處決”的特點,回收的過程沒有對像的移動。使其相對其它有對像移動步驟的回收算法,仍然是效率最好的。但是需要解決內存碎片問題。

Compact階段的開銷與存活對像的數據成開比,如上一條所描述,對於大量對像的移動是很大開銷的,做爲老年代的第一選擇並不合適。

基於上面的考慮,老年代一般是由標記清除或者是標記清除與標記整理的混合實現。以hotspot中的CMS回收器爲例,CMS是基於Mark-Sweep實現的,對於對像的回收效率很高,而對於碎片問題,CMS採用基於Mark-Compact算法的Serial Old回收器做爲補償措施:當內存回收不佳(碎片導致的Concurrent Mode Failure時),將採用Serial Old執行Full GC以達到對老年代內存的整理。

 

第十二章. 擴展堆外內存

  1. 堆內內存(on-heap memory)
    回顧
    堆外內存和堆內內存是相對的二個概念,其中堆內內存是平常中接觸比較多的,我們在jvm參數中只要使用-Xms,-Xmx等參數就可以設置堆的大小和最大值,理解jvm的堆還需要知道下面這個公式:
    堆內內存 = 新生代+老年代+持久代(元空間)
    使用堆內內存(on-heap memory)的時候,完全遵守JVM虛擬機的內存管理機制,採用垃圾回收器(GC)統一進行內存管理,GC會在某些特定的時間點進行一次徹底回收,也就是Full GC,GC會對所有分配的堆內內存進行掃描,在這個過程中會對JAVA應用程序的性能造成一定影響,還可能會產生Stop The World。
  2. 堆外內存(off-heap memory)
    和堆內內存相對應,堆外內存就是把內存對象分配在Java虛擬機的堆以外的內存,這些內存直接受操作系統管理(而不是虛擬機),這樣做的結果就是能夠在一定程度上減少垃圾回收對應用程序造成的影響。

12.1. 爲什麼使用堆外內存

  1. 減少了垃圾回收
    使用堆外內存的話,堆外內存是直接受操作系統管理( 而不是虛擬機 )。這樣做的結果就是能保持一個較小的堆內內存,以減少垃圾收集對應用的影響。
  2. 提升複製速度(io效率)
    堆內內存由JVM管理,屬於“用戶態”;而堆外內存由OS管理,屬於“內核態”。如果從堆內向磁盤寫數據時,數據會被先複製到堆外內存,即內核緩衝區,然後再由OS寫入磁盤,使用堆外內存避免了這個操作。
ps:堆外內存是可以通過java中的未公開的Unsafe和NIO包下ByteBuffffer來創建堆外內存
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章