jvm基礎

一、基礎概念
JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一種用於計算設備的規範,它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。一般的高級語言如果要在不同的平臺上運行,至少需要編譯成不同的目標代碼。而引入Java語言虛擬機後,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。Java虛擬機在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。這就是Java的能夠“一次編譯,到處運行”的原因。

jvm運行流程:

總的運行流程如下:

這裏寫圖片描述

詳細流程如下:

這裏寫圖片描述

所有的Java程序代碼必須保存在.java的文件之中,這些稱爲源代碼。而這些源代碼並不能夠直接執行,必須使用javac.exe命令將其編譯爲.class文件,而後利用java.exe命令在JVM進程之中解釋此程序。即 首先Java源代碼文件(.java後綴)會被Java編譯器編譯爲字節碼文件(.class後綴),然後由JVM中的類加載器加載各個類的字節碼文件,加載完畢之後,交由JVM執行引擎執行。在整個程序執行過程中,JVM會用一段空間來存儲程序執行期間需要用到的數據和相關信息,這段空間一般被稱作爲Runtime Data Area(運行時數據區),也就是我們常說的JVM內存。因此,在Java中我們常常說到的內存管理就是針對這段空間進行管理(如何分配和回收內存空間)。

一些命令:

要想編譯和運行java文件,很簡單,只需要兩個命令:

(1) javac:作用:編譯java文件;使用方法: javac Hello.java ,如果不出錯的話,在與Hello.java 同一目錄下會生成一個Hello.class文件,這個class文件是操作系統能夠使用和運行的文件。

(2) java: 作用:運行.class文件;使用方法:java Hello,如果不出錯的話,會執行Hello.class文件。注意:這裏的Hello後面不需要擴展名。默認執行的是類的main方法

JDK、JRE、JVM三者間的關係

他們的關係如下圖所示:
這裏寫圖片描述

@see http://playkid.blog.163.com/blog/static/56287260201372113842153/

類加載機制

整體流程
在這裏插入圖片描述

基礎概念:class文件加載至內存,鏈接(校驗、解析),初始化;最終形成JVM可以直接使用的JAVA類型的過程。

加載:在方法區形成該類的運行時數據結構;
鏈接:class文件是否存在問題;一些符號引號替換成直接引用。具體如下:

校驗:檢查導入類或接口的二進制數據的正確性;(文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證) 

準備:給類的靜態變量分配並初始化存儲空間; 

解析:將常量池中的符號引用轉成直接引用; 

初始化:到了初始化階段,才真正去執行Java裏面的代碼。主要爲靜態變量賦值和執行靜態塊。初始化一個類,先初始化它的父類。虛擬機會保證一個類的初始化在多線程環境中被正確加鎖和同步。

注意:類加載的過程中,是不會涉及到堆內存的。

類加載器
顧名思義就是來完成類加載這個過程的組件。

類加載器的層次結構

引導類加載器bootstrap classloader
加載JAVA核心庫($JAVA_HOME/jre/lib/rt.jar),原生代碼實現(C++),並不繼承自java.lang.ClassLoader。
擴展類加載器extensions classloader
JAVA可以提供一個擴展目錄($JAVA_HOME/jre/ext/*.jar)來加載Java類。
由sun.misc.Launcher.ExtClassLoader實現
應用程序類加載器application classloader(也稱系統類加載器)
一般來說,JAVA應用的類由它加載,即加載路徑是classpath下的路徑。
由sun.misc.Launcher.AppClassLoader實現。
自定義類加載器
開發人員繼承java.lang.ClassLoader實現自己的類加載器

在這裏插入圖片描述

java.lang.ClassLoader

ClassLoader的基本職責就是:

第一,根據指定的類名稱,找到或者生成對應的字節碼,並根據字節碼生成class對象

第二,加載JAVA應用所需的資源,如配置文件等。

雙親委託機制

JDK默認的類加載機制是雙親委派機制
簡單的所就是某個特定的類加載器接到加載類的請求時,首先將加載任務委託給父類加載器,依次追溯,比如說從應用加載器委託給擴展類加載器,從擴展類加載器委託給引導類加載器。這種委託,直至委託到層次最高的類加載器,即引導類加載器,如果委託的父類加載器可以完成加載任務,那麼成功返回;只有父類加載器無法完成時,纔去自己加載。

優點:
1.能夠有效確保一個類的全局唯一性,當程序中出現多個限定名相同的類時,類加載器在執行加載時,始終只會加載其中的某一個類。因爲已經被頂層加載的類不會再被加載。
2.保證了Java核心庫的類型安全。因爲頂層類加載器不是用Java語言寫的。

原理詳解:

在這裏插入圖片描述

例如:當jvm要加載Test.class的時候,

(1)首先會到自定義加載器中查找,看是否已經加載過,如果已經加載過,則返回字節碼。

(2)如果自定義加載器沒有加載過,則詢問上一層加載器(即AppClassLoader)是否已經加載過Test.class。

(3)如果沒有加載過,則詢問上一層加載器(ExtClassLoader)是否已經加載過。

(4)如果沒有加載過,則繼續詢問上一層加載(BoopStrap ClassLoader)是否已經加載過。

(5)如果BoopStrap ClassLoader依然沒有加載過,則到自己指定類加載路徑下(“sun.boot.class.path”)查看是否有Test.class字節碼,有則返回,沒有通

知下一層加載器ExtClassLoader到自己指定的類加載路徑下(java.ext.dirs)查看。

(6)依次類推,最後到自定義類加載器指定的路徑還沒有找到Test.class字節碼,則拋出異常ClassNotFoundException。
源碼分析:
java.lang.ClassLoader類的loadClass方法

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                   	 //父加載器爲空則,調用Bootstrap Classloader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //父加載器沒有找到,則調用findclass
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

寫一個自定義類加載器(略)

思考:
1.在自己的代碼中,如果創建一個java.lang.String類,這個類是否可以被類加載器加
載?爲什麼。

代碼測試:

package java.lang;

/**
 * @author daiwei
 * @date 2019/2/11 15:42
 */
public class String {
    public static void main(String[] args) {
//        String string=new String();
//        string.say();
        say();
    }

    public static void say(){
        System.out.println("hahah");
    }
}

結果如下:
在這裏插入圖片描述

原因分析:
更據Java的雙親委託機制,Java首先會自下而上查找java.lang.String是否已經被加載,結果發現bootstrap classloader類已經加載了,故不會再在加載java.lang.String類了,而被加載的java.lang.String類沒有main方法。如果換成package.String等其他包名,則可以被加載

2.Java創建對象的過程

Java是一門面向對象的編程語言,在Java程序運行過程中每時每刻都有對象被創建出來。在語言層面上,創建對象通常僅僅是一個new關鍵字而已,而在虛擬機中,對象的創建又是怎樣一個過程呢?new一個對象可以分爲兩個過程:類加載和創建對象。

1.類加載
參考類加載機制。
2.創建對象

1、在堆區分配對象需要的內存

分配的內存包括本類和父類的所有實例變量,但不包括任何靜態變量

2、對所有實例變量賦默認值

3、執行實例初始化代碼

初始化順序是先初始化父類再初始化子類,初始化時先執行實例代碼塊然後是構造方法

JVM的內存區域劃分

根據《Java虛擬機規範》的規定,運行時數據區通常包括這幾個部分:程序計數器(Program Counter Register)、Java棧(VM Stack)、本地方法棧(Native Method Stack)、方法區(Method Area)、堆(Heap)。

這裏寫圖片描述

1.程序計數器
想必學過彙編語言的朋友對程序計數器這個概念並不陌生,在彙編語言中,程序計數器是指CPU中的寄存器,它保存的是程序當前執行的指令的地址(也可以說保存下一條指令的所在存儲單元的地址),當CPU需要執行指令時,需要從程序計數器中得到當前需要執行的指令所在存儲單元的地址,然後根據得到的地址獲取到指令,在得到指令之後,程序計數器便自動加1或者根據轉移指針得到下一條指令的地址,如此循環,直至執行完所有的指令。

雖然JVM中的程序計數器並不像彙編語言中的程序計數器一樣是物理概念上的CPU寄存器,但是JVM中的程序計數器的功能跟彙編語言中的程序計數器的功能在邏輯上是等同的,也就是說是用來指示 執行哪條指令的。
在JVM規範中規定,如果線程執行的是非native方法,則程序計數器中保存的是當前需要執行的指令的地址;如果線程執行的是native方法,則程序計數器中的值是undefined。

簡單的說就是用來指示執行到哪條指令的。
2.Java棧

Java棧也稱作虛擬機棧(Java Vitual Machine Stack),也就是我們常常所說的棧,跟C語言的數據段中的棧類似。事實上,Java棧是Java方法執行的內存模型。爲什麼這麼說呢?下面就來解釋一下其中的原因。
  Java棧中存放的是一個個的棧幀,每個棧幀對應一個被調用的方法,在棧幀中包括局部變量表(Local Variables)、操作數棧(Operand Stack)、指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。
  簡單地說:當線程執行一個方法時,就會隨之創建一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀出棧。故java棧是更方法有關的。

這裏寫圖片描述

局部變量表,顧名思義,想必不用解釋大家應該明白它的作用了吧。就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態變量以及函數形參)。對於基本數據類型的變量,則直接存儲它的值,對於引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就可以確定其大小了,因此在程序執行期間局部變量表的大小是不會改變的。

操作數棧,想必學過數據結構中的棧的朋友想必對表達式求值問題不會陌生,棧最典型的一個應用就是用來對表達式求值。想想一個線程執行方法的過程中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。因此可以這麼說,程序中的所有計算過程都是在藉助於操作數棧來完成的。

指向運行時常量池的引用,因爲在方法執行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向運行時常量。

方法返回地址,當一個方法執行完畢之後,要返回之前調用它的地方,因此在棧幀中必須保存一個方法返回地址。

由於每個線程正在執行的方法可能不同,因此每個線程都會有一個自己的Java棧,互不干擾。故Java棧是線程私有的

注意:頂部的棧幀表示的是當前的方法;注意的是如果棧的深度過大。如遞歸過深虛擬機會拋出StackOverError。如果虛擬機允許棧動態擴展內存不足時拋出OutOfMemoryError.

3.本地方法棧
  本地方法棧與Java棧的作用和原理非常相似。區別只不過是Java棧是爲執行Java方法服務的,而本地方法棧則是爲執行本地方法(Native Method)服務的。在JVM規範中,並沒有對本地方發展的具體實現方法以及數據結構作強制規定,虛擬機可以自由實現它。在HotSopt虛擬機中直接就把本地方法棧和Java棧合二爲一。

4.堆(heap)

用來存儲類的實例,如通過new關鍵字和構造器創建的對象放在堆空間;

在C語言中,堆這部分空間是唯一一個程序員可以管理的內存區域。程序員可以通過malloc函數和free函數在堆上申請和釋放空間。那麼在Java中是怎麼樣的呢?
  Java中的堆是用來存儲對象本身的以及數組(當然,數組引用是存放在Java棧中的)。只不過和C語言中的不同,在Java中,程序員基本不用去關心空間釋放的問題,Java的垃圾回收機制會自動進行處理。因此這部分空間也是Java垃圾收集器管理的主要區域。另外,堆是被所有線程共享的,在JVM中只有一個堆。

注意:堆中是不會包含類的靜態變量的。
5.方法區
  方法區在JVM中也是一個非常重要的區域,它與堆一樣,是被線程共享的區域。在方法區中,存儲了類信息、靜態變量(普通成員變量是跟隨實例放在堆中)、常量以及編譯器編譯後的代碼等
OOM和SOF

在這裏插入圖片描述

運行時常量池

運行時常量池(Runtime Constant Pool),它是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後存放到常量池中。

運行時常量是相對於常量來說的,它具備一個重要特徵是:動態性。當然,值相同的動態常量與我們通常說的常量只是來源不同,但是都是儲存在池內同一塊內存區域。Java語言並不要求常量一定只能在編譯期產生,運行期間也可能產生新的常量,這些常量被放在運行時常量池中。這裏所說的常量包括:基本類型包裝類(包裝類不管理浮點型,整形只會管理-128到127)和String(也可以通過String.intern()方法可以強制將String放入常量池)

PermSpace

什麼是PermSpace?

PermSpace主要是存放靜態的類信息和方法信息,靜態的方法和變量,final標註的常量信息等。

@see JVM調優:PermSpace溢出 https://blog.csdn.net/blueheart20/article/details/39859733/

總結:
線程共享的區域是堆(heap)和方法區,其他都是線程私有的。除了程序技術器不會發生內存溢出,其它都會發生內存溢出。
棧內存溢出:棧滿時再做進棧必定產生空間溢出,叫上溢,棧空時再做退棧也產生空間溢出,稱爲下溢。

思考:
1.內存泄漏和內存溢出的區別和聯繫?
1)內存泄漏memory leak :內存泄漏是指你向系統申請分配內存進行使用(new),可是使用完了以後卻不歸還(delete),結果你申請到的那塊內存你自己也不能再訪問(也許你把它的地址給弄丟了),而系統也不能再次將它分配給需要的程序。就相當於你租了個帶鑰匙的櫃子,你存完東西之後把櫃子鎖上之後,把鑰匙丟了或者沒有將鑰匙還回去,那麼結果就是這個櫃子將無法供給任何人使用,也無法被垃圾回收器回收,因爲找不到他的任何信息。
自己理解:主要就是兩種問題,1.丟失了這塊地址的引用。2.已分配的內存無法被垃圾回收

2)、內存溢出 out of memory :指程序申請內存時,沒有足夠的內存供申請者使用,或者說,給了你一塊存儲int類型數據的存儲空間,但是你卻存儲long類型的數據,那麼結果就是內存不夠用,此時就會報錯OOM,即所謂的內存溢出。

關係:
1.一次內存泄漏似乎不會有大的影響,但內存泄漏堆積後的後果就是內存溢出。

@see JAVA內存泄漏和內存溢出的區別和聯繫https://blog.csdn.net/mashuai720/article/details/79557670

java堆內存模型

JDK1.7以前
在這裏插入圖片描述

JDK1.8

在這裏插入圖片描述

JDK 1.8之後將最初的永久代內存空間取消了爲了將HotSpot與JRockit兩個虛擬機標準聯合爲一個.

新生代:新的對象和沒有達到一定“年齡”的對象對象存放空間(活躍對象)

老年代:被長時間使用的對象,老年代空間要相對較大

元空間:一些操作的臨時對象,如方法中的臨時對象,直接使用物理內存

注意:永久代和元空間都是存放臨時對象的,但是永久代使用的是JVM直接分配的內存,而元空間使用的是物理內存

伸縮區
伸縮區:伸縮區的考慮在某個內存空間不足的時候,會自動打開伸縮區擴大內存,當發現當前的區域內存可以,滿足要求的時候,就可以進行收縮了。(其實質就是jvm所配置的最大值減去所配置的初始值)
伸縮區的優缺點:

如果不進行收縮的話優點是:可以提升堆內存的結構優化。
 如果不進行收縮的缺點:空間太大了。那麼沒有選擇合適的GC算法,就會造成堆內存的性能下降。   

伊甸園區:所有使用關鍵字new新實例化的對象一定會在伊甸園區進行保存(如果Eden中內存空間充足)
存活區:存活區保存的一定是已經在伊甸園區中存在好久,並且經過了好幾次的Minor GC還保存下來的活躍對象。
老年代: 老年代主要是接收由年輕代發送來的對象,一般情況下經過了好幾次的Minor GC之後還會保存下來的對象纔會進入到老年代。如果你要保存的對象超過了伊甸園區的大小,那麼此對象也將直接保存到老年代之中

Java垃圾回收機制

什麼是垃圾回收?

顧名思義,垃圾回收就是釋放垃圾佔用的空間,那麼在Java中,什麼樣的對象會被認定爲“垃圾”?那麼當一些對象被確定爲垃圾之後,採用什麼樣的策略來進行回收(釋放空間)?在目前的商業虛擬機中,有哪些典型的垃圾收集器?下面我們就來逐一探討這些問題。以下是本文的目錄大綱:

一.如何確定某個對象是“垃圾”?

二.典型的垃圾收集算法

三.典型的垃圾收集器

確定某個對象是“垃圾”的方法

1.引用計數法

在java中是通過引用來和對象進行關聯的,也就是說如果要操作對象,必須通過引用來進行。那麼很顯然一個簡單的辦法就是通過引用計數來判斷一個對象是否可以被回收。
不失一般性,如果一個對象沒有任何引用與之關聯,則說明該對象基本不太可能在其他地方被使用到,那麼這個對象就成爲可被回收的對象了。這種方式成爲引用計數法。

不足:無法解決對象之間的循環引用。

2.可達性分析法

該算法的基本思路就是通過一些被稱爲(GC Roots)的對象作爲起點,從這些節點開始向下搜索,搜索走過的路徑被稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時(即從GC Roots節點到該節點不可達),則證明該對象是不可用的。

在這裏插入圖片描述

在Java中,可作爲GC Root的對象包括以下幾種:

虛擬機棧(棧幀中的本地變量表)中引用的對象
方法區中類靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中JNI(即一般說的Native方法)引用的對象

finalize()方法最終判定對象是否存活

即使在可達性分析算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷再次標記過程。
標記的前提是對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈。

  1. 第一次標記並進行一次篩選。
    篩選的條件是此對象是否有必要執行finalize()方法。
    當對象沒有覆蓋finalize方法,或者finzlize方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”,對象被回收。
  2. 第二次標記
    如果這個對象被判定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲:F-Queue的隊列之中,並在稍後由一條虛擬機自動建立的、低優先級的Finalizer線程去執行。這裏所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這樣做的原因是,如果一個對象finalize()方法中執行緩慢,或者發生死循環(更極端的情況),將很可能會導致F-Queue隊列中的其他對象永久處於等待狀態,甚至導致整個內存回收系統崩潰。

Finalize()方法是對象脫逃死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模標記,如果對象要在finalize()中成功拯救自己----只要重新與引用鏈上的任何的一個對象建立關聯即可,譬如把自己賦值給某個類變量或對象的成員變量,那在第二次標記時它將移除出“即將回收”的集合。如果對象這時候還沒逃脫,那基本上它就真的被回收了。

1.如何確定某個對象是“垃圾”?
主要有以下幾種
1)顯示地將某個引用賦值爲null或者將已經指向某個對象的引用指向新的對象

Object obj = new Object();
obj = null;
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2;

原來的obj所引用的對象已經不能使用了,故可以被標記爲垃圾對象。obj1 已經指向了另一個對象obj2,故obj1原來所指向的對象也將被標記爲垃圾對象
2)局部引用所指向的對象,比如下面這段代碼:

void fun() {
 
.....
    for(int i=0;i<10;i++) {
        Object obj = new Object();
        System.out.println(obj.getClass());
    }   
}

循環每執行完一次,生成的Object對象都會成爲可回收的對象。

原理分析:
在一個方法的內部有一個強引用,這個引用保存在棧中,而真正的引用內容(Object)保存在堆中。當這個方法運行完成後就會退出方法棧,則引用內容的引用不存在,這個Object會被回收。
但是如果這個o是全局的變量時,就需要在不用這個對象時賦值爲null,因爲強引用不會被垃圾回收。
3)只有弱引用與其關聯的對象,比如

WeakReference<String> wr = new WeakReference<String>(new String("world"));

原理分析:
當一個對象僅僅被weak reference(弱引用)指向, 而沒有任何其他strong reference(強引用)指向的時候, 如果這時GC運行, 那麼這個對象就會被回收,不論當前的內存空間是否足夠,這個對象都會被回收。
2.典型的垃圾收集算法

1.Mark-Sweep(標記-清除)算法
這是最基礎的垃圾回收算法,之所以說它是最基礎的是因爲它最容易實現,思想也是最簡單的。標記-清除算法分爲兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間

缺陷:容易產生內存碎片,碎片太多可能會導致後續過程中需要爲大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。
2.Copying(複製)算法
爲了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。

在這裏插入圖片描述

即:另一塊主要用來保存存活的對象,真正可以使用的其實只用一半。
缺陷:
對內存空間的使用做出了高昂的代價,因爲能夠使用的內存縮減到原來的一半。而且Copying算法的效率跟存活對象的數目多少有很大的關係,如果存活對象很多,那麼Copying算法的效率將會大大降低。(因爲存活的對象少,不會佔用較大地空間)

3.Mark-Compact(標記-整理)算法
爲了解決Copying算法的缺陷,充分利用內存空間,提出了Mark-Compact算法。該算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。

在這裏插入圖片描述

4.Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不同的區域。一般情況下將堆區劃分爲老年代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點採取最適合的收集算法。

新生代的Copying算法:
  目前大部分垃圾收集器對於新生代都採取Copying算法,因爲新生代中每次垃圾回收都要回收大部分對象,也就是說需要複製的操作次數較少,但是實際中並不是按照1:1的比例來劃分新生代的空間的,一般來說是將新生代劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象複製到另一塊Survivor空間中,然後清理掉Eden和剛纔使用過的Survivor空間。

老年代的Mark-Compact算法:
  而由於老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。

注意,在堆區之外還有一個代就是永久代(Permanet Generation),它用來存儲class類、常量、方法描述等。對永久代的回收主要回收兩部分內容:廢棄常量和無用的類。

垃圾收集器

垃圾收集算法是 內存回收的理論基礎,而垃圾收集器就是內存回收的具體實現。下面介紹一下HotSpot(JDK 7)虛擬機提供的幾種垃圾收集器,用戶可以根據自己的需求組合出各個年代使用的收集器。

在這裏插入圖片描述
上面是目前比較常用的垃圾收集器,和他們直接搭配使用的情況,上面是新生代收集器,下面則是老年代收集器,這些收集齊都有自己的特點,根據不同的業務場景進行搭配使用。如果存在連線表示可以搭配使用。

查看Java的垃圾收集器命令
java -XX:+PrintCommandLineFlags -version

從結果可知Java默認使用的垃圾收集器是:新生代(Parallel Scavenge),老年代(Ps MarkSweep)組合。

注意:Ps MarkSweep是以Serial Old爲模板設計的

1.Serial/Serial Old

Serial/Serial Old收集器是最基本最古老的收集器,它是一個單線程收集器,並且在它進行垃圾收集時,必須暫停所有用戶線程。Serial收集器是針對新生代的收集器,採用的是Copying算法,Serial Old收集器是針對老年代的收集器,採用的是Mark-Compact算法。它的優點是實現簡單高效,但是缺點是會給用戶帶來停頓。

Serial/Serial Old運行流程如下:

在這裏插入圖片描述

2.ParNew
ParNew收集器是Serial收集器的多線程版本,使用多個線程進行垃圾收集。

ParNew運行流程如下

在這裏插入圖片描述

3.Parallel Scavenge

Parallel Scavenge收集器也是一個新生代收集器,它也是使用複製算法的收集器,又是並行多線程收集器。parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間,而parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。
吞吐量= 程序運行時間/(程序運行時間 + 垃圾收集時間),虛擬機總共運行了100分鐘。其中垃圾收集花掉1分鐘,那吞吐量就是99%。
Parallel Scavenge提供了兩個參數用來精確控制,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數

在注重吞吐量或CPU資源敏感的場合,可以優先考慮Parallel Scavenge收集器 + Parallel Old收集器

4.Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本(並行收集器),使用多線程和Mark-Compact算法。

5.CMS(Concurrent Mark Sweep)

CMS(Current Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器,它是一種併發收集器,採用的是Mark-Sweep算法。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
從名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基於“標記—清除”算法實現的,它的運作過程相對於前面幾種收集器來說更復雜一些,整個過程分爲4個步驟,包括:
1.初始標記(CMS initial mark)
2.併發標記(CMS concurrent mark)
3.重新標記(CMS remark)
4.併發清除(CMS concurrent sweep)
其中,初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。

執行流程如下

在這裏插入圖片描述
CMS收集器的不足

1.CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。

由於CMS併發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱爲“浮動垃圾”。也是由於在垃圾收集階段用戶線程還需要運行,那也就還需要預留有足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程序運作使用。在JDK 1.5的默認設置下,CMS收集器當老年代使用了68%的空間後就會被激活,這是一個偏保守的設置,如果在應用中老年代增長不是太快,可以適當調高參數-XX:CMSInitiatingOccupancyFraction的值來提高觸發百分比,以便降低內存回收次數從而獲取更好的性能,在JDK 1.6中,CMS收集器的啓動閾值已經提升至92%。要是CMS運行期間預留的內存無法滿足程序需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機將啓動後備預案:臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數-XX:CMSInitiatingOccupancyFraction設置得太高很容易導致大量“Concurrent Mode Failure”失敗,性能反而降低。

2.CMS是一款基於“標記—清除”算法實現的收集器,這意味着收集結束時會有大量空間碎片產生。

CMS是一款基於“標記—清除”算法實現的收集器,如果讀者對前面這種算法介紹還有印象的話,就可能想到這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數(默認就是開啓的),用於在CMS收集器頂不住要進行FullGC時開啓內存碎片的合併整理過程,內存整理的過程是無法併發的,空間碎片問題沒有了,但停頓時間不得不變長。虛擬機設計者還提供了另外一個參數-XX:CMSFullGCsBeforeCompaction,這個參數是用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的(默認值爲0,表示每次進入Full GC時都進行碎片整理)
3.CMS收集器對cpu資源非常敏感。

6.G1

G1收集器是當今收集器技術發展最前沿的成果,它是一款面向服務端應用的收集器,它能充分利用多CPU、多核環境。因此它是一款並行與併發收集器,並且它能建立可預測的停頓時間模型。

G1具備如下特點。

1.並行與併發:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓Java程序繼續執行。

2.分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。

3.空間整合:與CMS的“標記—清理”算法不同,G1從整體來看是基於“標記—整理”算法實現的收集器,從局部(兩個Region之間)上來看是基於“複製”算法實現的,但無論如何,這兩種算法都意味着G1運作期間不會產生內存空間碎片,收集後能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會因爲無法找到連續內存空間而提前觸發下一次GC。

4.可預測的停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特徵了。
參考資料:

在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存佈局就與其他收集器有很大差別,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。

G1 GC的流程如下:

1.初始標記
初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快
2.併發標記
進行可答性分析的過程。
3.最終標記
爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄
4.篩選回收
將更據用戶期望的回收時間,制定回收計劃。

在這裏插入圖片描述

幾種機制解釋:

串行(Serial):只有一個gc線程工作,如果發生GC 用戶線程會暫停
並行(Parallel):多個垃圾收集線程並行工作,用戶線程和GC線程可以同時執行,如果發生GC 用戶線程會暫停
併發(concurrent):用戶線程和GC線程可以同時執行(不一定並行,可能交替執行),如果發生GC,用戶線程依然可以執行

@see https://wangkang007.gitbooks.io/jvm/content/

JVM垃圾回收參數實例解析:

root 8508 1 0 11:51 ? 00:02:08 /bin/java -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -server -Xmx2g -Xms2g -Xmn512m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70

-server:

啓用-server時新生代默認採用並行收集,其他情況下,默認不啓用。

-XX:+UseAdaptiveSizePolicy:

上文中,因啓用-server模式,所以新生代使用並行收集器。

設置此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低相應時間或者收集頻率等,建議使用並行收集器時一直打開。

-XX:+DisableExplicitGC
關閉System.gc()

-XX:+UseConcMarkSweepGC:

設置老年代爲併發收集。

-XX:+CMSParallelRemarkEnabled
降低標記停頓

-XX:+UseCMSCompactAtFullCollection
在FULL GC的時候, 對老年代的壓縮。
CMS是不會移動內存的, 因此, 這個非常容易產生碎片, 導致內存不夠用, 因此, 內存的壓縮這個時候就會被啓用。 增加這個參數是個好習慣。
可能會影響性能,但是可以消除碎片

-XX:LargePageSizeInBytes

內存頁的大小不可設置過大, 會影響Perm的大小

-XX:+UseFastAccessorMethods

原始類型的快速優化

XX:+UseCMSInitiatingOccupancyOnly

使用手動定義初始化定義開始CMS收集,目的:禁止hostspot自行觸發CMS GC

-XX:CMSInitiatingOccupancyFraction=70
使用cms作爲垃圾回收,使用70%後開始CMS收集

其他相關參數:

-XX:PermSize
設置持久代(perm gen)初始值 默認爲:物理內存的1/64

-XX:MaxPermSize
設置持久代最大值 默認爲:物理內存的1/4

-XX:MaxTenuringThreshold

垃圾最大年齡.如果設置爲0的話,則年輕代對象不經過Survivor區,直接進入年老代. 對於年老代比較多的應用,可以提高效率.如果將此值設置爲一個較大值,則年輕代對象會在Survivor區進行多次複製,這樣可以增加對象再年輕代的存活 時間,增加在年輕代即被回收的概率
該參數只有在串行GC時纔有效.

@see 深入瞭解JVW https://www.cnblogs.com/yang-hao/p/5936059.html

對象的分配過程與垃圾回收

爲什麼要進行分代?

我們先來屢屢,爲什麼需要把堆分代?不分代不能完成他所做的事情麼?其實不分代完全可以,分代的唯一理由就是優化GC性能。你先想想,如果沒有分代,那我們所有的對象都在一塊,GC的時候我們要找到哪些對象沒用,這樣就會對堆的所有區域進行掃描。而我們的很多對象都是朝生夕死的,如果分代的話,我們把新創建的對象放到某一地方,當GC的時候先把這塊存“朝生夕死”對象的區域進行回收,這樣就會騰出很大的空間出來。

主要流程如下:
在這裏插入圖片描述

注意:在整個GC的流程之中是 針對新生代和老年代進行內存清理的操作,而元空間和永久代都不在GC的範圍之內。

  1. 當新創建一個對象的時候,那麼對象一定需要在堆內存中分配內存空間。所以就需要對該對象申請內存空間
  2. 首先會判斷Eden區中是否有充足的內存空間,如果有,那麼直接將對象保存在Eden區中
  3. 如果此時Eden中內存空間不足,那麼會自動執行一個Minor GC的操作,將伊甸園區的無用的內存空間進行清理,清理之後
    繼續判斷伊甸園區的內存空間是否充足。如果充足,那麼將對象直接在疑點園區中進行內存分配。
  4. 如果執行了MinorGC之後發現伊甸園區的內存依然不足,則會對存活區進行判斷,如果存活區內存足夠。那麼將伊甸園區的
    一部分活躍對象保存到存活區,隨後繼續判斷伊甸園區內存,如果夠進行新對象的內存分配。
  5. 如果此時存活區沒有足夠的內存空間,則繼續判斷老年區。如果老年區的內存空間充足,則將存活的部分活躍對象保存到
    老年代,而後存活區會出現剩餘空間。隨後將伊甸園區的活躍對象遷移到存活區,然後在伊甸園區開闢空間,保存新對象。
  6. 如果此時老年代也沒有剩餘空間,則執行MajorGC(FullGC),清理老年代內存
  7. 如果執行了FullGC之後依然無法保存對象,就會產生OOM異常“OutofMemoryError”。

年輕代中的GC詳解

HotSpot JVM把年輕代分爲了三部分:1個Eden區和2個Survivor區(分別叫from和to)。默認比例爲8:1,爲啥默認會是這個比例,接下來我們會聊到。一般情況下,新創建的對象都會被分配到Eden區(一些大對象特殊處理),這些對象經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區。對象在Survivor區中每熬過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中。

因爲年輕代中的對象基本都是朝生夕死的(80%以上),所以在年輕代的垃圾回收算法使用的是複製算法,複製算法的基本思想就是將內存分爲兩塊,每次只用其中一塊,當這一塊內存用完,就將還活着的對象複製到另外一塊上面。複製算法不會產生內存碎片。

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

在這裏插入圖片描述

一個對象的生命週期

我是一個普通的java對象,我出生在Eden區,在Eden區我還看到和我長的很像的小兄弟,我們在Eden區中玩了挺長時間。有一天Eden區中的人實在是太多了,我就被迫去了Survivor區的“From”區,自從去了Survivor區,我就開始漂了,有時候在Survivor的“From”區,有時候在Survivor的“To”區,居無定所。直到我18歲的時候,爸爸說我成人了,該去社會上闖闖了。於是我就去了年老代那邊,年老代裏,人很多,並且年齡都挺大的,我在這裏也認識了很多人。在年老代裏,我生活了20年(每次GC加一歲),然後被回收。

jvm Stop-The-World

Java中一種全局暫停的現象。全局停頓,所有Java代碼停止,native代碼可以執行,但不能和JVM交互 多半由於GC引起 Dump線程,死鎖檢查,堆Dump

GC時爲什麼會有全局停頓?
類比在聚會時打掃房間,聚會時很亂,又有新的垃圾產生,房間永遠打掃不乾淨,只有讓大家停止活動了,才能將房間打掃乾淨。

危害
長時間服務停止,沒有響應。遇到HA系統,可能引起主備切換,嚴重危害生產環境。

Minor GC、Major GC和Full GC

Minor GC
從年輕代空間(包括 Eden 和 Survivor 區域)回收內存被稱爲 Minor GC。這一定義既清晰又易於理解。但是,當發生Minor GC事件的時候,有一些有趣的地方需要注意到:

1.當 JVM 無法爲一個新的對象分配空間時會觸發 Minor GC,比如當 Eden 區滿了。所以分配率越高,越頻繁執行 Minor GC。

2.執行 Minor GC 操作時,不會影響到永久代。從永久代到年輕代的引用被當成 GC roots,從年輕代到永久代的引用在標記階段被直接忽略掉。

3.質疑常規的認知,所有的 Minor GC 都會觸發“全世界的暫停(stop-the-world)”,停止應用程序的線程。對於大部分應用程序,停頓導致的延遲都是可以忽略不計的。其中的真相就 是,大部分 Eden 區中的對象都能被認爲是垃圾,永遠也不會被複制到 Survivor 區或者老年代空間。如果正好相反,Eden 區大部分新生對象不符合 GC 條件,Minor GC 執行時暫停的時間將會長很多。

所以 Minor GC 的情況就相當清楚了——每次 Minor GC 會清理年輕代的內存。

Major GC
清理老年代。
Full GC
清理整個堆空間—包括年輕代和老年代。

jvm調優配置

常見配置

-XX:MetaspaceSize=128m (元空間默認大小) 
-XX:MaxMetaspaceSize=128m (元空間最大大小) 
-Xms1024m (堆默認大小) 
-Xmx1024m (堆最大大小) 
-Xmn256m (新生代大小) 
-Xss256k (棧最大深度大小) 
-XX:SurvivorRatio=8 (新生代分區比例 8:2) 
-XX:+UseConcMarkSweepGC (指定使用的垃圾收集器,這裏使用CMS收集器) 
-XX:+PrintGCDetails (打印詳細的GC日誌)

堆內存調整參數如圖所示:

在這裏插入圖片描述

我們可以發現每一個區域都有一個可變的伸縮區,當我們的內存空間不足的時候,會在可變的範圍內擴大內存空間,當我們的內存空間變得不緊張的時候我們再釋放可變空間。

在堆內存的調優之中我們要特別注意兩個參數-Xms初始化內存分配大小,默認爲物理內存的1/64,-Xmx 最大的分配內存默認爲物理內存的1/4。

參考資料:
1.淺析JVM 第一篇(JVM執行流程)http://blog.csdn.net/qq_25235807/article/details/61920877
2.JVM的內存區域劃分https://www.cnblogs.com/dolphin0520/p/3613043.html
3.Java垃圾回收機制https://www.cnblogs.com/dolphin0520/p/3783345.html
4.淺析JVM 第二篇(JVM內存模型和垃圾回收)https://blog.csdn.net/qq_25235807/article/details/61929343
5.淺析JVM 第三篇(堆內存參數調整) https://blog.csdn.net/qq_25235807/article/details/62054017
6.內存泄漏和內存溢出的區別和聯繫https://blog.csdn.net/ruiruihahaha/article/details/70270574
7.SpringBoot項目優化和Jvm調優(樓主親測,真實有效)http://www.cnblogs.com/jpfss/p/9753215.html
8.【隨筆】JVM核心:JVM運行和類加載 https://www.jianshu.com/p/d856ee954f9c
9.Java類加載機制及自定義加載器 https://www.cnblogs.com/gdpuzxs/p/7044963.html
10.《深入理解Java虛擬機》周志明著
11.聊聊JVM的年輕代 http://ifeve.com/jvm-yong-generation/
12.Minor GC、Major GC和Full GC之間的區別http://www.importnew.com/15820.html
13.java虛擬機:運行時常量池 https://www.cnblogs.com/xiaotian15/p/6971353.html

發佈了67 篇原創文章 · 獲贊 22 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章