一篇文章掌握整個JVM,JVM超詳細解析!!!

JVM

不懂JVM看完這一篇文章你就會非常懂了,文章很長,非常詳細!!!
在這裏插入圖片描述

先想想一些問題

1 我們開發人員編寫的Java代碼是怎麼讓電腦認識的

首先先了解電腦是二進制的系統,他只認識 01010101

比如我們經常要編寫 HelloWord.java 電腦是怎麼認識運行的
HelloWord.java是我們程序員編寫的,我們人可以認識,但是電腦不認識

Java文件編譯的過程
因此就需要編譯:

  1. 程序員編寫的.java文件
  2. 由javac編譯成字節碼文件.class:(爲什麼編譯成class文件,因爲JVM只認識.class文件)
  3. 在由JVM編譯成電腦認識的文件 (對於電腦系統來說 文件代表一切)

(這是一個大概的觀念 抽象畫的概念)
在這裏插入圖片描述

2 爲什麼說java是跨平臺語言

這個誇平臺是中間語言(JVM)實現的誇平臺
java有JVM從軟件層面屏蔽了底層硬件、指令層面的細節讓他兼容各種系統

難道 C 和 C++ 不能誇平臺嗎 其實也可以
C和C++需要在編譯器層面去兼容不同操作系統的不同層面,寫過C和C++的就知道不同操作系統的有些代碼是不一樣

3 Jdk和Jre和JVM的區別

看Java官方的圖片,Jdk中包括了Jre,Jre中包括了JVM

Jvm在倒數第二層 由他可以在(最後一層的)各種平臺上運行

Jre大部分都是 C 和 C++ 語言編寫的,他是我們在編譯java時所需要的基礎的類庫

Jdk還包括了一些Jre之外的東西 ,就是這些東西幫我們編譯Java代碼的, 還有就是監控Jvm的一些工具

在這裏插入圖片描述

4 爲什麼要學習JVM

爲什麼要學習Jvm,學習Jvm可以幹什麼

首先先想:爲什麼Java可以霸佔企業級開發那麼多年 因爲:內存管理

我們在java開發中何時考慮過內存管理 
不像c和c++還要考慮什麼時候釋放資源
我們java只需要考慮業務實現就行了

那就有些人可能又會要說了,Jvm都做完了這些操作,爲什麼我們還要學習,學習個屁啊

假如:內存出現問題了,出現了內存溢出 ,內存泄漏問題怎麼辦

這就好像一個人一樣,我一般情況喫什麼從來不用考慮進入了身體那一個部位,可是總有一天,假如吃了不該喫的也是要進醫院的


深入學習JVM

註釋:JVM就是Java虛擬機,Java虛擬機就是JVM

1 JVM運行時數據區

什麼是運行時數據區(就是我們java運行時的東西是放在那裏的)
在這裏插入圖片描述

2 解析JVM運行時數據區

2.1 方法區(Method Area)

  1. 方法區是所有線程共享的內存區域,它用於存儲已被Java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
  2. 它有個別命叫Non-Heap(非堆)。當方法區無法滿足內存分配需求時,拋出OutOfMemoryError異常。

2.2 Java堆(Java Heap)

  1. java堆是java虛擬機所管理的內存中最大的一塊,是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例。
  2. 在Java虛擬機規範中的描述是:所有的對象實例以及數組都要在堆上分配。
  3. java堆是垃圾收集器管理的主要區域,因此也被成爲“GC堆”。
  4. 從內存回收角度來看java堆可分爲:新生代和老生代。
  5. 從內存分配的角度看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區。
  6. 無論怎麼劃分,都與存放內容無關,無論哪個區域,存儲的都是對象實例,進一步的劃分都是爲了更好的回收內存,或者更快的分配內存。
  7. 根據Java虛擬機規範的規定,java堆可以處於物理上不連續的內存空間中。當前主流的虛擬機都是可擴展的(通過 -Xmx 和 -Xms 控制)。如果堆中沒有內存可以完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。

2.3 程序計數器(Program Counter Register)

  1. 程序計數器是一塊較小的內存空間,它可以看作是:保存當前線程所正在執行的字節碼指令的地址(行號)
  2. 由於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,一個處理器都只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都有一個獨立的程序計數器,各個線程之間計數器互不影響,獨立存儲。稱之爲“線程私有”的內存。程序計數器內存區域是虛擬機中唯一沒有規定OutOfMemoryError情況的區域。

總結:也可以把它叫做線程計數器

例子:在java中最小的執行單位是線程,線程是要執行指令的,執行的指令最終操作的就是我們的電腦,就是 CPU。在CPU上面去運行,有個非常不穩定的因素,叫做調度策略,這個調度策略是時基於時間片的,也就是當前的這一納秒是分配給那個指令的。

假如:線程A在看直播
在這裏插入圖片描述
突然,線程B來了一個視頻電話,就會搶奪線程A的時間片,就會打斷了線程A,線程A就會掛起
在這裏插入圖片描述
然後,視頻電話結束,這時線程A究竟該幹什麼?
(線程是最小的執行單位,他不具備記憶功能,他只負責去幹,那這個記憶就由:程序計數器來記錄

在這裏插入圖片描述

2.4 Java虛擬機棧(Java Virtual Machine Stacks)

  1. java虛擬機是線程私有的,它的生命週期和線程相同。
  2. 虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。

解釋:每虛擬機棧中是有單位的,單位就是棧幀,一個方法一個棧幀。一個棧幀中他又要存儲,局部變量,操作數棧,動態鏈接,出口等。

在這裏插入圖片描述
解析棧幀:

  1. 局部變量表:是用來存儲我們臨時8個基本數據類型、對象引用地址、returnAddress類型。(returnAddress中保存的是return後要執行的字節碼的指令地址。)
  2. 操作數棧:操作數棧就是用來操作的,例如代碼中有個 i = 6*6,他在一開始的時候就會進行操作,讀取我們的代碼,進行計算後再放入局部變量表中去
  3. 動態鏈接:假如我方法中,有個 service.add()方法,要鏈接到別的方法中去,這就是動態鏈接,存儲鏈接的地方。
  4. 出口:出口是什呢,出口正常的話就是return 不正常的話就是拋出異常落

思考:.
一個方法調用另一個方法,會創建很多棧幀嗎?
答:會創建。如果一個棧中有動態鏈接調用別的方法,就會去創建新的棧幀,棧中是由順序的,一個棧幀調用另一個棧幀,另一個棧幀就會排在調用者下面

棧指向堆是什麼意思?
棧指向堆是什麼意思,就是棧中要使用成員變量怎麼辦,棧中不會存儲成員變量,只會存儲一個應用地址,堆中的數據等下講

遞歸的調用自己會創建很多棧幀嗎?
遞歸的話也會創建多個棧幀,就是一直排下去

2.5 本地方法棧(Native Method Stack)

  1. 本地方法棧很好理解,他很棧很像,只不過方法上帶了 native 關鍵字的棧字
  2. 它是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)的服務
  3. native關鍵字的方法是看不到的,必須要去oracle官網去下載纔可以看的到,而且native關鍵字修飾的大部分源碼都是C和C++的代碼。
  4. 同理可得,本地方法棧中就是C和C++的代碼

3 Java內存結構

在這裏插入圖片描述
上面已經講了運行時數據區,這裏就差幾個小組件了

3.1 直接內存(Direct Memory)

  1. 直接內存不是虛擬機運行時數據區的一部分,也不是java虛擬機規範中定義的內存區域。但是既然是內存,肯定還是受本機總內存(包括RAM以及SWAP區或者分頁文件)大小以及處理器尋址空間的限制。
  2. 在JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O 方式,它可以使用native 函數庫直接分配堆外內存,然後通脫一個存儲在Java堆中的DirectByteBuffer 對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在Java堆和Native(本地)堆中來回複製數據。

直接內存與堆內存的區別:
直接內存申請空間耗費很高的性能,堆內存申請空間耗費比較低
直接內存的IO讀寫的性能要優於堆內存,在多次讀寫操作的情況相差非常明顯

代碼示例:(報錯修改time 值)

package com.lijie;

import java.nio.ByteBuffer;

/**
 * 直接內存 與 堆內存的比較
 */
public class ByteBufferCompare {

    public static void main(String[] args) {
        allocateCompare();   //分配比較
        operateCompare();    //讀寫比較
    }

    /**
     * 直接內存 和 堆內存的 分配空間比較
     */
    public static void allocateCompare() {
        int time = 10000000;    //操作次數
        long st = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {

            ByteBuffer buffer = ByteBuffer.allocate(2);      //非直接內存分配申請
        }
        long et = System.currentTimeMillis();
        System.out.println("在進行" + time + "次分配操作時,堆內存:分配耗時:" + (et - st) + "ms");
        long st_heap = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接內存分配申請
        }
        long et_direct = System.currentTimeMillis();
        System.out.println("在進行" + time + "次分配操作時,直接內存:分配耗時:" + (et_direct - st_heap) + "ms");
    }

    /**
     * 直接內存 和 堆內存的 讀寫性能比較
     */
    public static void operateCompare() {
        //如果報錯修改這裏,把數字改小一點
        int time = 1000000000;
        ByteBuffer buffer = ByteBuffer.allocate(2 * time);
        long st = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            buffer.putChar('a');
        }
        buffer.flip();
        for (int i = 0; i < time; i++) {
            buffer.getChar();
        }
        long et = System.currentTimeMillis();
        System.out.println("在進行" + time + "次讀寫操作時,堆內存:讀寫耗時:" + (et - st) + "ms");
        ByteBuffer buffer_d = ByteBuffer.allocateDirect(2 * time);
        long st_direct = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            buffer_d.putChar('a');
        }
        buffer_d.flip();
        for (int i = 0; i < time; i++) {
            buffer_d.getChar();
        }
        long et_direct = System.currentTimeMillis();
        System.out.println("在進行" + time + "次讀寫操作時,直接內存:讀寫耗時:" + (et_direct - st_direct) + "ms");
    }
}

測試結果:

在進行10000000次分配操作時,堆內存:分配耗時:98ms
在進行10000000次分配操作時,直接內存:分配耗時:8895ms
在進行1000000000次讀寫操作時,堆內存:讀寫耗時:5666ms
在進行1000000000次讀寫操作時,直接內存:讀寫耗時:884ms

代碼來源:「獼猴桃0303」
鏈接爲:https://blog.csdn.net/leaf_0303/article/details/78961936

3.2 JVM字節碼執行引擎

虛擬機核心的組件就是執行引擎,它負責執行虛擬機的字節碼,一般戶先進行編譯成機器碼後執行。

“虛擬機”是一個相對於“物理機”的概念,虛擬機的字節碼是不能直接在物理機上運行的,需要JVM字節碼執行引擎編譯成機器碼後纔可在物理機上執行。

3.3 垃圾收集系統

程序在運行過程中,會產生大量的內存垃圾(一些沒有引用指向的內存對象都屬於內存垃圾,因爲這些對象已經無法訪問,程序用不了它們了,對程序而言它們已經死亡),爲了確保程序運行時的性能,java虛擬機在程序運行的過程中不斷地進行自動的垃圾回收(GC)。

垃圾收集系統是Java的核心,也是不可少的,Java有一套自己進行垃圾清理的機制,開發人員無需手工清理

4 JVM的垃圾回收機制

垃圾回收機制簡稱GC

GC主要用於Java堆的管理。Java 中的堆是 JVM 所管理的最大的一塊內存空間,主要用於存放各種類的實例對象。

4.1 什麼是垃圾回收機制

程序在運行過程中,會產生大量的內存垃圾(一些沒有引用指向的內存對象都屬於內存垃圾,因爲這些對象已經無法訪問,程序用不了它們了,對程序而言它們已經死亡),爲了確保程序運行時的性能,java虛擬機在程序運行的過程中不斷地進行自動的垃圾回收(GC)。

GC是不定時去堆內存中清理不可達對象。不可達的對象並不會馬上就會直接回收, 垃圾收集器在一個Java程序中的執行是自動的,不能強制執行清楚那個對象,即使程序員能明確地判斷出有一塊內存已經無用了,是應該回收的,程序員也不能強制垃圾收集器回收該內存塊。程序員唯一能做的就是通過調用System.gc 方法來"建議"執行垃圾收集器,但是他是否執行,什麼時候執行卻都是不可知的。這也是垃圾收集器的最主要的缺點。當然相對於它給程序員帶來的巨大方便性而言,這個缺點是瑕不掩瑜的。

手動執行GC:

System.gc(); // 手動回收垃圾

4.2 finalize方法作用

  1. finalize()方法是在每次執行GC操作之前時會調用的方法,可以用它做必要的清理工作。
  2. 它是在Object類中定義的,因此所有的類都繼承了它。子類覆蓋finalize()方法以整理系統資源或者執行其他清理工作。finalize()方法是在垃圾收集器刪除對象之前對這個對象調用的。

代碼示例

package com.lijie;

public class Test {
	public static void main(String[] args) {
		Test test = new Test();
		test = null;
		System.gc(); // 手動回收垃圾
	}

	@Override
	protected void finalize() throws Throwable {
		// gc回收垃圾之前調用
		System.out.println("gc回收垃圾之前調用的方法");
	}
}

4.3 新生代、老年代、永久代(方法區)的區別

  1. Java 中的堆是 JVM 所管理的最大的一塊內存空間,主要用於存放各種類的實例對象。

  2. 在 Java 中,堆被劃分成兩個不同的區域:新生代 ( Young )、老年代 ( Old )。

先不要管爲什麼要分代,後面有例子

  1. 老年代就一個區域。新生代 ( Young ) 又被劃分爲三個區域:Eden、From Survivor、To Survivor。

  2. 這樣劃分的目的是爲了使 JVM 能夠更好的管理堆內存中的對象,包括內存的分配以及回收。

  3. 默認的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值爲 1:2 ( 該值可以通過參數 –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小。老年代 ( Old ) = 2/3 的堆空間大小。

  4. 其中,新生代 ( Young ) 被細分爲 Eden 和 兩個 Survivor 區域,這兩個 Survivor 區域分別被命名爲 From Survivor 和 ToSurvivor ,以示區分。

  5. 默認的,Edem : From Survivor : To Survivor = 8 : 1 : 1 ( 可以通過參數 –XX:SurvivorRatio 來設定 ),即: Eden = 8/10 的新生代空間大小,From Survivor = To Survivor = 1/10 的新生代空間大小。

  6. JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來爲對象服務,所以無論什麼時候,總是有一塊 Survivor 區域是空閒着的。

  7. 因此,新生代實際可用的內存空間爲 9/10 ( 即90% )的新生代空間。

  8. 永久代就是JVM的方法區。在這裏都是放着一些被虛擬機加載的類信息,靜態變量,常量等數據。這個區中的東西比老年代和新生代更不容易回收。

4.3.1 爲什麼要這樣分代:

其實主要原因就是可以根據各個年代的特點進行對象分區存儲,更便於回收,採用最適當的收集算法:

  1. 新生代中,每次垃圾收集時都發現大批對象死去,只有少量對象存活,便採用了複製算法,只需要付出少量存活對象的複製成本就可以完成收集。

  2. 而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須採用“標記-清理”或者“標記-整理”算法。

新生代又分爲Eden和Survivor (From與To,這裏簡稱一個區)兩個區。加上老年代就這三個區。數據會首先分配到Eden區當中(當然也有特殊情況,如果是大對象那麼會直接放入到老年代(大對象是指需要大量連續內存空間的java對象)。當Eden沒有足夠空間的時候就會觸發jvm發起一次Minor GC,。如果對象經過一次Minor-GC還存活,並且又能被Survivor空間接受,那麼將被移動到Survivor空間當中。並將其年齡設爲1,對象在Survivor每熬過一次Minor GC,年齡就加1,當年齡達到一定的程度(默認爲15)時,就會被晉升到老年代中了,當然晉升老年代的年齡是可以設置的。

4.3.2 Minor GC、Major GC、Full GC區別及觸發條件
  1. Minor GC是新生代GC,指的是發生在新生代的垃圾收集動作。由於java對象大都是朝生夕死的,所以Minor GC非常頻繁,一般回收速度也比較快。

  2. Major GC是老年代GC,指的是發生在老年代的GC,通常執行Major GC會連着Minor GC一起執行。Major GC的速度要比Minor GC慢的多。

  3. Full GC是清理整個堆空間,包括年輕代和老年代

Minor GC 觸發條件一般爲:

  1. eden區滿時,觸發MinorGC。即申請一個對象時,發現eden區不夠用,則觸發一次MinorGC。
  2. ​ 新創建的對象大小 > Eden所剩空間

Major GC和Full GC 觸發條件一般爲:
Major GC通常是跟full GC是等價的

  1. 每次晉升到老年代的對象平均大小>老年代剩餘空間
  2. MinorGC後存活的對象超過了老年代剩餘空間
  3. 永久代空間不足
  4. 執行System.gc()
  5. CMS GC異常
  6. 堆內存分配很大的對象

4.4 如何判斷對象是否存活

4.4.1 引用計數法
  1. 引用計數法就是如果一個對象沒有被任何引用指向,則可視之爲垃圾。這種方法的缺點就是不能檢測到環的存在。

  2. 首先需要聲明,至少主流的Java虛擬機裏面都沒有選用引用計數算法來管理內存。

  3. 什麼是引用計數法:每個對象在創建的時候,就給這個對象綁定一個計數器。每當有一個引用指向該對象時,計數器加一;每當有一個指向它的引用被刪除時,計數器減一。這樣,當沒有引用指向該對象時,計數器爲0就代表該對象死亡

引用計數法的優點:

  • 引用計數算法的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的算法,

引用計數法的缺點:

  • 主流的Java虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的原因是它很難解決對象之間相互循環引用的問題。
  • 例如:
package com.lijie;

public class Test {
	public Object object = null;
	public static void main(String[] args) {
		Test a = new Test();
		Test b = new Test();
		/**
		 * 循環引用,此時引用計數器法失效
		 */
		a.object = b;
		b.object = a;

		a = null;
		b = null;
	}
}

引用計數法的應用場景:

  • 建議不要用
4.4.2 可達性分析法
  1. 該種方法是從GC Roots開始向下搜索,搜索所走過的路徑爲引用鏈。當一個對象到GC Roots沒用任何引用鏈時,則證明此對象是不可用的,表示可以回收。
    在這裏插入圖片描述

  2. 上圖上圖中Object1、Object2、Object3、Object4、Object5到GC Roots是可達的,表示它們是有引用的對象,是存活的對象不可以進行回收

  3. Object6、Object7、Object8雖然是互相關聯的,但是它們到GC Roots是不可達的,所以他們是可以進行回收的對象。

那些可以作爲GC Roots 的對象:

1、虛擬機棧(棧幀中的本地變量表)中引用的對象;
2、方法區中類靜態屬於引用的對象;
3、方法區中常量引用的對象;
4、本地方法棧中JNI(即一般說的Native方法)引用的對象。
等

可達性算法的優點:

  • 解決相互循環引用問題。

可達性算法的優點:

  • 目前和引用計數法比沒得缺點

可達性算法的應用場景:

  • 這是目前主流的虛擬機都是採用的算法

4.5 垃圾回收機制策略(也稱爲GC的算法)

4.5.1 引用計數算法(Reference counting)

每個對象在創建的時候,就給這個對象綁定一個計數器。每當有一個引用指向該對象時,計數器加一;每當有一個指向它的引用被刪除時,計數器減一。這樣,當沒有引用指向該對象時,計數器爲0就代表該對象死亡,這時就應該對這個對象進行垃圾回收操作。

引用計數法的優點:

  • 引用計數算法的實現簡單,判定效率也很高。

引用計數法的缺點:

  • 主流的Java虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的原因是它很難解決對象之間相互循環引用的問題。
  • 例如:
package com.lijie;

public class Test {
	public Object object = null;
	public static void main(String[] args) {
		Test a = new Test();
		Test b = new Test();
		/**
		 * 循環引用,此時引用計數器法失效
		 */
		a.object = b;
		b.object = a;

		a = null;
		b = null;
	}
}

引用計數法的應用場景:

  • 建議不要用

4.5.2 標記–清除算法(Mark-Sweep)

爲每個對象存儲一個標記位,記錄對象的狀態(活着或是死亡)。
分爲兩個階段,一個是標記階段,這個階段內,爲每個對象更新標記位,檢查對象是否死亡;第二個階段是清除階段,該階段對死亡的對象進行清除,執行 GC 操作。

標記清除算法的優點:

  • 是可以解決循環引用的問題
  • 必要時纔回收(內存不足時)

標記清除算法的缺點:

  • 回收時,應用需要掛起,也就是stop the world。
  • 標記和清除的效率不高,尤其是要掃描的對象比較多的時候
  • 會造成內存碎片(會導致明明有內存空間,但是由於不連續,申請稍微大一些的對象無法做到),

標記清除算法的應用場景:

  • 該算法一般應用於老年代,因爲老年代的對象生命週期比較長。

4.5.3 標記–整理算法

標記清除算法和標記壓縮算法非常相同,但是標記壓縮算法在標記清除算法之上解決內存碎片化(有些人叫"標記整理算法"爲"標記壓縮算法")

標記-整理法是標記-清除法的一個改進版。同樣,在標記階段,該算法也將所有對象標記爲存活和死亡兩種狀態;不同的是,在第二個階段,該算法並沒有直接對死亡的對象進行清理,而是將所有存活的對象整理一下,放到另一處空間,然後把剩下的所有對象全部清除。這樣就達到了標記-整理的目的。

標記–整理算法優點:

  • 解決標記清除算法出現的內存碎片問題,

標記–整理算法缺點:

  • 壓縮階段,由於移動了可用對象,需要去更新引用。

標記–整理算法應用場景:

  • 該算法一般應用於老年代,因爲老年代的對象生命週期比較長。

4.5.4 複製算法

該算法將內存平均分成兩部分,然後每次只使用其中的一部分,當這部分內存滿的時候,將內存中所有存活的對象複製到另一個內存中,然後將之前的內存清空,只使用這部分內存,循環下去。

這個算法與標記-整理算法的區別在於,該算法不是在同一個區域複製,而是將所有存活的對象複製到另一個區域內。

複製算法的優點:

  • 在存活對象不多的情況下,性能高,能解決內存碎片和java垃圾回收算法之-標記清除 中導致的引用更新問題。

複製算法的缺點::

  • 會造成一部分的內存浪費。不過可以根據實際情況,將內存塊大小比例適當調整;如果存活對象的數量比較大,複製算法的性能會變得很差。

複製算法的應用場景:

  • 複製算法一般是使用在新生代中,因爲新生代中的對象一般都是朝生夕死的,存活對象的數量並不多,這樣使用複製算法進行拷貝時效率比較高。
  • jvm將Heap(堆)內存劃分爲新生代與老年代。又將新生代劃分爲Eden與2塊Survivor Space(倖存者區) ,然後在Eden –>Survivor Space 與To Survivor之間實行復制算法。
  • 不過jvm在應用複製算法時,並不是把內存按照1:1來劃分的,這樣太浪費內存空間了。一般的jvm都是8:1。也即是說,Eden區:From區:To區域的比例是始終有90%的空間是可以用來創建對象的,而剩下的10%用來存放回收後存活的對象。

4.5.5 分代算法(主要的算法就是上面四種,這個是附加的)

這種算法,根據對象的存活週期的不同將內存劃分成幾塊,新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。可以用抓重點的思路來理解這個算法。
新生代對象朝生夕死,對象數量多,只要重點掃描這個區域,那麼就可以大大提高垃圾收集的效率。另外老年代對象存儲久,無需經常掃描老年代,避免掃描導致的開銷。

新生代

  • 在新生代,每次垃圾收集器都發現有大批對象死去,只有少量存活,採用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。

老年代

  • 而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須“標記清除法或者標記整理算法進行回收。

5 垃圾收集器

5.1 什麼是垃圾收集器?

  • 垃圾收集器是垃圾回收算法(引用計數法、標記清楚法、標記整理法、複製算法)的具體實現,不同垃圾收集器、不同版本的JVM所提供的垃圾收集器可能會有很在差別。
  • 我這以JDK8爲準:
    在這裏插入圖片描述

圖中展示了7種不同分代的收集器:
Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old、G1

而它們所處區域,則表明其是屬於新生代還是老年代的收集器:

  • 新生代收集器:Serial、ParNew、Parallel Scavenge

  • 老年代收集器:CMS、Serial Old、Parallel Old

  • 整堆收集器:G1

兩個收集器間有連線,表明它們可以搭配使用:

Serial / Serial Old
Serial / CMS
ParNew / Serial Old
ParNew / CMS
Parallel Scavenge / Serial Old
Parallel Scavenge / Parallel Old
G1

5.2 垃圾回收器詳解

垃圾回收器 工作區域 回收算法 工作線程 用戶線程並行 描述
Serial 新生帶 複製算法 單線程 Client模式下默認新生代收集器。簡單高效
ParNew 新生帶 複製算法 多線程 Serial的多線程版本,Server模式下首選, 可搭配CMS的新生代收集器
Parallel Scavenge 新生帶 複製算法 多線程 目標是達到可控制的吞吐量
Serial Old 老年帶 標記-整理 單線程 Serial老年代版本,給Client模式下的虛擬機使用
Parallel Old 老年帶 標記-整理 多線程 Parallel Scavenge老年代版本,吞吐量優先
CMS 老年帶 標記-清楚 多線程 追求最短回收停頓時間
G1 新生帶 + 老年帶 標記-整理 + 複製算法 多線程 JDK1.9默認垃圾收集器
5.2.1 Serial
  • Serial 收集器:新生代。發展歷史最悠久的收集器。它是一個單線程收集器,它只會使用一個 CPU 或者線程去完成垃圾收集工作,而且在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。

特點:

  1. 新生代收集器,使用複製算法收集新生代垃圾。
  2. 單線程的收集器,GC工作時,其它所有線程都將停止工作。
  3. 簡單高效,適合單 CPU 環境。單線程沒有線程交互的開銷,因此擁有最高的單線程收集效率。

使用方式:

//如何設置JVM參數底下會講解:這裏只是列舉一部分參數:

設置垃圾收集器:"-XX:+UseSerialGC"  --添加該參數來顯式的使用改垃圾收集器;

5.2.2 ParNew
  • ParNew 收集器:新生代。Serial 的多線程版本,即同時啓動多個線程去進行垃圾收集。

特點:

  1. 新生代收集器。ParNew垃圾收集器是Serial收集器的多線程版本,採用複製算法。
  2. 除了多線程外,其餘的行爲、特點和Serial收集器一樣。
  3. 只有它能與 CMS 收集器配合使用。
  4. 但在單個CPU環境中,不比Serail收集器好,多線程使用它比較好。

使用方式:

//如何設置JVM參數底下會講解:這裏只是列舉一部分參數:

設置垃圾收集器:"-XX:+UseParNewGC"  --強制指定使用ParNew;    
設置垃圾收集器: "-XX:+UseConcMarkSweepGC"  --指定使用CMS後,會默認使用ParNew作爲新生代收集器;
設置垃圾收集器參數:"-XX:ParallelGCThreads"  --指定垃圾收集的線程數量,ParNew默認開啓的收集線程與CPU的數量相同;

5.2.3 Parallel Scavenge
  • Parallel Scavenge 收集器:新生代。和 ParNew 的關注點不一樣,該收集器更關注吞吐量,儘快地完成計算任務。

特點:

  1. 新生代收集器。
  2. 採用複製算法。
  3. 多線程收集。
  4. 與ParNew 不同的是:高吞吐量爲目標,(減少垃圾收集時間,讓用戶代碼獲得更長的運行時間)

使用方式:

//如何設置JVM參數底下會講解:這裏只是列舉一部分參數:

設置垃圾收集器:"-XX:+UseParallelGC"  --添加該參數來顯式的使用改垃圾收集器;
設置垃圾收集器參數:"-XX:MaxGCPauseMillis"  --控制垃圾回收時最大的停頓時間(單位ms)
設置垃圾收集器參數:"-XX:GCTimeRatio"  --控制程序運行的吞吐量大小吞吐量大小=代碼執行時間/(代碼執行時間+gc回收的時間)
設置垃圾收集器參數:"-XX:UseAdaptiveSizePolicy"  --內存調優交給虛擬機管理

5.2.4 Serial Old
  • Serial Old 收集器:Serial 的老年代版本,使用標記 - 整理算法。

特點:

  1. 老年代收集器, 採用"標記-整理"算法。
  2. 單線程收集。

使用方式:

//如何設置JVM參數底下會講解:這裏只是列舉一部分參數:

在JDK1.5及之前,與Parallel Scavenge收集器搭配使用,
在JDK1.6後有Parallel Old收集器可搭配。
現在的作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用

5.2.5 Parallnel old
  • Parallnel old 收集器,多線程:Parallel 的老年代版本,使用標記 - 整理算法。

特點:

  1. 針對老年代。
  2. 採用"標記-整理"算法。
  3. 多線程收集。
  4. 但在單個CPU環境中,不比Serial Old收集器好,多線程使用它比較好。

使用方式:

//如何設置JVM參數底下會講解:這裏只是列舉一部分參數:

 設置垃圾收集器:"-XX:+UseParallelOldGC":指定使用Parallel Old收集器;

5.2.6 CMS
  • CMS 收集器:老年代。是一種以獲取最短回收停頓時間爲目標的收集器,適用於互聯網站或者 B/S 系統的服務端上。

特點:

  1. 針對老年代,採用標記-清楚法清除垃圾;
  2. 基於"標記-清除"算法(不進行壓縮操作,產生內存碎片);
  3. 以獲取最短回收停頓時間爲目標;
  4. 併發收集、低停頓;
  5. CMS收集器有3個明顯的缺點:1.對CPU資源非常敏感、2.無法處理浮動垃圾,可能出現"Concurrent Mode Failure"失敗、3.產生大量內存碎片
  6. 垃圾收集線程與用戶線程(基本上)可以同時工作

使用方式:

//如何設置JVM參數底下會講解:這裏只是列舉一部分參數:

設置垃圾收集器:"-XX:+UseConcMarkSweepGC":指定使用CMS收集器;

5.2.7 G1
  • G1 收集器:分代收集器。當今收集器技術發展最前沿成果之一,是一款面向服務端應用的垃圾收集器。G1可以說是CMS的終極改進版,解決了CMS內存碎片、更多的內存空間登問題。雖然流程與CMS比較相似,但底層的原理已是完全不同。

特點:

  1. 能充分利用多CPU、多核環境下的硬件優勢;
  2. 可以並行來縮短(Stop The World)停頓時間;
  3. 也可以併發讓垃圾收集與用戶程序同時進行;
  4. 分代收集,收集範圍包括新生代和老年代
  5. 能獨立管理整個GC堆(新生代和老年代),而不需要與其他收集器搭配;
  6. 能夠採用不同方式處理不同時期的對象;
  7. 應用場景可以面向服務端應用,針對具有大內存、多處理器的機器;
  8. 採用標記-整理 + 複製算法來回收垃圾

使用方式:

//如何設置JVM參數底下會講解:這裏只是列舉一部分參數:

設置垃圾收集器:"-XX:+UseG1GC":指定使用G1收集器;
設置垃圾收集器參數:"-XX:InitiatingHeapOccupancyPercent":當整個Java堆的佔用率達到參數值時,開始併發標記階段;默認爲45;
設置垃圾收集器參數:"-XX:MaxGCPauseMillis":爲G1設置暫停時間目標,默認值爲200毫秒;
設置垃圾收集器參數:"-XX:G1HeapRegionSize":設置每個Region大小,範圍1MB到32MB;目標是在最小Java堆時可以擁有約2048個Region

6 JVM參數配置

6.1 JVM內存參數簡述

#常用的設置
-Xms:初始堆大小,JVM 啓動的時候,給定堆空間大小。 

-Xmx:最大堆大小,JVM 運行過程中,如果初始堆空間不足的時候,最大可以擴展到多少。 

-Xmn:設置堆中年輕代大小。整個堆大小=年輕代大小+年老代大小+持久代大小。 

-XX:NewSize=n 設置年輕代初始化大小大小 

-XX:MaxNewSize=n 設置年輕代最大值

-XX:NewRatio=n 設置年輕代和年老代的比值。如: -XX:NewRatio=3,表示年輕代與年老代比值爲 1:3,年輕代佔整個年輕代+年老代和的 1/4 

-XX:SurvivorRatio=n 年輕代中 Eden 區與兩個 Survivor 區的比值。注意 Survivor 區有兩個。8表示兩個Survivor :eden=2:8 ,即一個Survivor佔年輕代的1/10,默認就爲8

-Xss:設置每個線程的堆棧大小。JDK5後每個線程 Java 棧大小爲 1M,以前每個線程堆棧大小爲 256K。

-XX:ThreadStackSize=n 線程堆棧大小

-XX:PermSize=n 設置持久代初始值	

-XX:MaxPermSize=n 設置持久代大小
 
-XX:MaxTenuringThreshold=n 設置年輕帶垃圾對象最大年齡。如果設置爲 0 的話,則年輕代對象不經過 Survivor 區,直接進入年老代。

#下面是一些不常用的

-XX:LargePageSizeInBytes=n 設置堆內存的內存頁大小

-XX:+UseFastAccessorMethods 優化原始類型的getter方法性能

-XX:+DisableExplicitGC 禁止在運行期顯式地調用System.gc(),默認啓用	

-XX:+AggressiveOpts 是否啓用JVM開發團隊最新的調優成果。例如編譯優化,偏向鎖,並行年老代收集等,jdk6紙之後默認啓動

-XX:+UseBiasedLocking 是否啓用偏向鎖,JDK6默認啓用	

-Xnoclassgc 是否禁用垃圾回收

-XX:+UseThreadPriorities 使用本地線程的優先級,默認啓用	

等等等......

6.2 JVM的GC收集器設置

-XX:+UseSerialGC:設置串行收集器,年輕帶收集器 

 -XX:+UseParNewGC:設置年輕代爲並行收集。可與 CMS 收集同時使用。JDK5.0 以上,JVM 會根據系統配置自行設置,所以無需再設置此值。

-XX:+UseParallelGC:設置並行收集器,目標是目標是達到可控制的吞吐量

-XX:+UseParallelOldGC:設置並行年老代收集器,JDK6.0 支持對年老代並行收集。 

-XX:+UseConcMarkSweepGC:設置年老代併發收集器

-XX:+UseG1GC:設置 G1 收集器,JDK1.9默認垃圾收集器

6.3 JVM參數在哪設置

6.3.1 IDEA在哪裏設置JVM參數

1、單個項目的應用
在這裏插入圖片描述
在這裏插入圖片描述
2、全局的配置

  1. 找到IDEA安裝目錄中的bin目錄
  2. 找到idea.exe.vmoptions文件
  3. 打開該文件編輯並保存。
    在這裏插入圖片描述
6.3.2 Eclipse在哪裏設置JVM參數

1、配置單個項目

點擊綠色圖標右邊的小箭頭
在這裏插入圖片描述
在點擊:Run Configurations ->VM arguments
在這裏插入圖片描述

2、配置全局JVM參數
修改Eclipse的配置文件,在eclipse安裝目錄下的:eclipse.ini文件
在這裏插入圖片描述

6.3.3 war(Tomcat)包在哪裏設置JVM參數

war肯定是部署在Tomcat上的,那就是修改Tomcat的JVM參數

1、在Windows下就是在文件/bin/catalina.bat,
增加如下設置:JAVA_OPTS(JAVA_OPTS,就是用來設置 JVM 相關運行參數的變量)

set "JAVA_OPTS=-Xms512M -Xmx1024M ...等等等 JVM參數"

在這裏插入圖片描述

2、Linux要在tomcat 的bin 下的catalina.sh 文件裏添加

注意:位置要在cygwin=false前
JAVA_OPTS="-Xms512M -Xmx1024M ...等等等 JVM參數"

在這裏插入圖片描述

6.3.4 Jar包在哪裏設置JVM參數

Jar包簡單,一般都是SpringBoot項目打成Jar包來運行

#運行時java -jar是直接插入JVM命令就好了
java -Xms1024m -Xmx1024m ...等等等 JVM參數 -jar springboot_app.jar & 

6.4 調優總結

  1. 在實際工作中,我們可以直接將初始的堆大小與最大堆大小相等,
    這樣的好處是可以減少程序運行時垃圾回收次數,從而提高效率。
  2. 初始堆值和最大堆內存內存越大,吞吐量就越高,
    但是也要根據自己電腦(服務器)的實際內存來比較。
  3. 最好使用並行收集器,因爲並行收集器速度比串行吞吐量高,速度快。
    當然,服務器一定要是多線程的
  4. 設置堆內存新生代的比例和老年代的比例最好爲1:2或者1:3。
    默認的就是1:2
  5. 減少GC對老年代的回收。設置生代帶垃圾對象最大年齡,進量不要有大量連續內存空間的java對象,因爲會直接到老年代,內存不夠就會執行GC

註釋:其實最主要的還是服務器要好,你硬件都跟不上,軟件再好都沒用
註釋:老年代GC很慢,新生代沒啥事
註釋:默認的JVM堆大小好像是電腦實際內存的四分之一左右,

package com.lijie;

public class Test {
    public static void main(String[] args) {
        System.out.print("最大內存");
        System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
    }
}

我的電腦是8G的運行內存
在這裏插入圖片描述

7 類加載器

7.1 類加載的機制及過程

程序主動使用某個類時,如果該類還未被加載到內存中,則JVM會通過加載、連接、初始化3個步驟來對該類進行初始化。如果沒有意外,JVM將會連續完成3個步驟,所以有時也把這個3個步驟統稱爲類加載或類初始化。

Jvm執行class文件
在這裏插入圖片描述

1、加載
  • 加載指的是將類的class文件讀入到內存,並將這些靜態數據轉換成方法區中的運行時數據結構,並在堆中生成一個代表這個類的java.lang.Class對象,作爲方法區類數據的訪問入口,這個過程需要類加載器參與。

  • Java類加載器由JVM提供,是所有程序運行的基礎,JVM提供的這些類加載器通常被稱爲系統類加載器。除此之外,開發者可以通過繼承ClassLoader基類來創建自己的類加載器。

  • 類加載器,可以從不同來源加載類的二進制數據,比如:本地Class文件、Jar包Class文件、網絡Class文件等等等。

  • 類加載的最終產物就是位於堆中的Class對象(注意不是目標類對象),該對象封裝了類在方法區中的數據結構,並且向用戶提供了訪問方法區數據結構的接口,即Java反射的接口

2、連接過程
  • 當類被加載之後,系統爲之生成一個對應的Class對象,接着將會進入連接階段,連接階段負責把類的二進制數據合併到JRE中(意思就是將java類的二進制代碼合併到JVM的運行狀態之中)。類連接又可分爲如下3個階段。
  1. 驗證:確保加載的類信息符合JVM規範,沒有安全方面的問題。主要驗證是否符合Class文件格式規範,並且是否能被當前的虛擬機加載處理。

  2. 準備:正式爲類變量(static變量)分配內存並設置類變量初始值的階段,這些內存都將在方法區中進行分配

  3. 解析:虛擬機常量池的符號引用替換爲字節引用過程

3、初始化
  • 初始化階段是執行類構造器<clinit>() 方法的過程。類構造器<clinit>()方法是由編譯器自動收藏類中的所有類變量的賦值動作和靜態語句塊(static塊)中的語句合併產生,代碼從上往下執行。

  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化

  • 虛擬機會保證一個類的<clinit>() 方法在多線程環境中被正確加鎖和同步

總結就是:初始化是爲類的靜態變量賦予正確的初始值

7.2 類加載器的介紹

  1. 啓動(Bootstrap)類加載器
  2. 擴展(Extension)類加載器
  3. 系統類加載器
  4. 自定義加載器

在這裏插入圖片描述

1 根類加載器(bootstrap class loader)

它用來加載 Java 的核心類,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader(負責加載$JAVA_HOME中jre/lib/rt.jar裏所有的class,由C++實現,不是ClassLoader子類)。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啓動類加載器的引用,所以不允許直接通過引用進行操作。

2 擴展類加載器(extensions class loader)

擴展類加載器是指Sun公司(已被Oracle收購)實現的sun.misc.Launcher$ExtClassLoader類,由Java語言實現的,是Launcher的靜態內部類,它負責加載<JAVA_HOME>/lib/ext目錄下或者由系統變量-Djava.ext.dir指定位路徑中的類庫,開發者可以直接使用標準擴展類加載器。

3 系統類加載器(system class loader)

被稱爲系統(也稱爲應用)類加載器,它負責在JVM啓動時加載來自Java命令的-classpath選項、java.class.path系統屬性,或者CLASSPATH換將變量所指定的JAR包和類路徑。程序可以通過ClassLoader的靜態方法getSystemClassLoader()來獲取系統類加載器。如果沒有特別指定,則用戶自定義的類加載器都以此類加載器作爲父加載器。由Java語言實現,父類加載器爲ExtClassLoader。(Java虛擬機採用的是雙親委派模式即把請求交由父類處理,它一種任務委派模式,)

類加載器加載Class大致要經過如下8個步驟:

  1. 檢測此Class是否載入過,即在緩衝區中是否有此Class,如果有直接進入第8步,否則進入第2步。
  2. 如果沒有父類加載器,則要麼Parent是根類加載器,要麼本身就是根類加載器,則跳到第4步,如果父類加載器存在,則進入第3步。
  3. 請求使用父類加載器去載入目標類,如果載入成功則跳至第8步,否則接着執行第5步。
  4. 請求使用根類加載器去載入目標類,如果載入成功則跳至第8步,否則跳至第7步。
  5. 當前類加載器嘗試尋找Class文件,如果找到則執行第6步,如果找不到則執行第7步。
  6. 從文件中載入Class,成功後跳至第8步。
  7. 拋出ClassNotFountException異常。
  8. 返回對應的java.lang.Class對象

7.3 理解雙親委派模式

雙親委派機制,其工作原理的是,如果一個類加載器收到了類加載請求,它並不會自己先去加載,而是把這個請求委託給父類的加載器去執行,如果父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器,如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器纔會嘗試自己去加載,這就是雙親委派模式,即每個兒子都很懶,每次有活就丟給父親去幹,直到父親說這件事我也幹不了時,兒子自己纔想辦法去完成。
在這裏插入圖片描述
雙親委派機制的優勢:採用雙親委派模式的是好處是Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係,通過這種層級關可以避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。其次是考慮到安全因素,java核心api中定義類型不會被隨意替換,假設通過網絡傳遞一個名爲java.lang.Integer的類,通過雙親委託模式傳遞到啓動類加載器,而啓動類加載器在覈心Java API發現這個名字的類,發現該類已被加載,並不會重新加載網絡傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。

7.4 類加載器間的關係

我們進一步瞭解類加載器間的關係(並非指繼承關係),主要可以分爲以下4點
啓動類加載器,由C++實現,沒有父類。
拓展類加載器(ExtClassLoader),由Java語言實現,父類加載器爲null
系統類加載器(AppClassLoader),由Java語言實現,父類加載器爲ExtClassLoader
自定義類加載器,父類加載器肯定爲AppClassLoader。

8 JVM可視化工具

8.1爲什麼要可視化工具

開發大型 Java 應用程序的過程中難免遇到內存泄露、性能瓶頸等問題,比如文件、網絡、數據庫的連接未釋放,未優化的算法等。隨着應用程序的持續運行,可能會造成整個系統運行效率下降,嚴重的則會造成系統崩潰。爲了找出程序中隱藏的這些問題,在項目開發後期往往會使用性能分析工具來對應用程序的性能進行分析和優化。

8.2 visualVm

VisualVM 是一款免費的,集成了多個 JDK 命令行工具的可視化工具,它能爲您提供強大的分析能力,對 Java 應用程序做性能分析和調優。這些功能包括生成和分析海量數據、跟蹤內存泄漏、監控垃圾回收器、執行內存和 CPU 分析,同時,它能自動選擇更快更輕量級的技術儘量減少性能分析對應用程序造成的影響,提高性能分析的精度。

他作爲Oracle JDK 的一部分,位於 JDK 根目錄的 bin 文件夾下。VisualVM 自身要在 JDK6 以上的版本上運行,但是它能夠監控 JDK1.4 以上版本的應用程序

8.2.1 打開visualVm

位於 JDK 根目錄的 bin 文件夾下的jvisualvm.exe
:我的JDK11沒有,不知道爲什麼,我jdk8就找的到此工具
在這裏插入圖片描述
在這裏插入圖片描述

8.2.2 本地測試項目JVM運行狀態

我這本地有了好幾個進程,這是我IDea工具的
在這裏插入圖片描述
我運行一個SpringBoot項目
在這裏插入圖片描述
此時就開始監控了
在這裏插入圖片描述

8.2.3 測試服務器項目JVM運行狀態

省略:::

8.3 jconsole

從Java 5開始 引入了 JConsole。JConsole 是一個內置 Java 性能分析器,可以從命令行或在 GUI shell 中運行。您可以輕鬆地使用 JConsole(或者,它更高端的 “近親” VisualVM )來監控 Java 應用程序性能和跟蹤 Java 中的代碼。

8.3.1 啓動JConsole

點擊jdk/bin 目錄下面的jconsole.exe 即可啓動,然後會自動自動搜索本機運行的所有虛擬機進程。選擇其中一個進程可開始進行監控
在這裏插入圖片描述
在這裏插入圖片描述

8.3.2 遠程連接項目也很簡單,和 visualVm基本一致,可以自己研究一下
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章