天天看點

JVM知識詳解

JVM

1、JVM記憶體結構

1.1、JVM體系概述
JVM知識詳解
1.2、常見的垃圾回收算法
  • 引用計數法

    顧名思義,此種算法會在每一個對象上記錄這個對象被引用的次數,隻要有任何一個對象引用了次對象,這個對象的計數器就+1,取消對這個對象的引用時,計數器就-1。任何一個時刻,如果該對象的計數器為0,那麼這個對象就是可以回收的。

    JVM知識詳解

    優點:

    引用計數收集器可以很快的執行,交織在程式運作中。對程式需要不被長時間打斷的實時環境比較有利。

    缺點:

    無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能為0.而且每次加減非常浪費記憶體。

  • 複制算法

    Java堆從GC的角度還可以細分為: 新生代(Eden 區、From Survivor 區和To Survivor 區)和老年代。

    JVM知識詳解

    MinorGC的過程(複制->清空->互換):

    a. Eden、SurvivorFrom複制到SurvivorTo,年齡+1

    首先,當Eden區滿的時候會觸發第一次GC,把還活着的對象拷貝到SurvivorFrom區,當Eden區再次觸發GC的時候會掃描Eden區和From區域,對這兩個區域進行垃圾回收,經過這次回收後還存活的對象,則直接複制到To區域(如果有對象的年齡已經達到了老年的标準,則指派到老年代區),同時把這些對象的年齡+1。

    b. 清空eden-SurvivorErom

    然後,清空Eden和Survivor From中的對象,也即複制之後有交換,誰空誰是To。

    c. Survivor To和 Survivor From互換

    最後,Survivor To和Survivor From互換,原SurvivorTo成為下一次GC時的Survivor From區。部分對象會在From和To區域中複制來複制去,如此交換15次(由ⅣM參數MaxTenuringThreshold決定,這個參數預設是15),最終如果還是存活,就存入到老年代。

    複制算法用于在新生代垃圾回收

    它的主要缺點有兩個:

    ​ (1)效率問題:在對象存活率較高時,複制操作次數多,效率降低;

    ​ (2)空間問題:內存縮小了一半;需要額外空間做配置設定擔保(老年代)

  • 标記清除

    算法分成标記和清除兩個階段,先标記出要回收的對象,然後統一回收這些對象。

JVM知識詳解

​ 标記-清除(Mark-Sweep)算法顧名思義,主要就是兩個動作,一個是标記,另一個就是清除。

​ 标記就是根據特定的算法(如:引用計數算法,可達性分析算法等)标出記憶體中哪些對象可以回收,哪些對象還要繼續使用,在标記完成後統一回收 掉所有被标記的對象 ,标記訓示對象還要繼續使用的,那就原地不動留下。

​ 缺點:

1. 标記與清除效率低;
           

​ 2. 清除之後記憶體會産生大量碎片;

  • 标記整理
JVM知識詳解

​ 标記整理法在标記清除基礎之上做了優化,把存活的對象壓縮到記憶體一端,而後進行垃圾清理,通過這種方式來進行減少碎片的目的。

​ (java中老年代使用的就是标記壓縮法) 

2、CG roots

2.1、什麼是垃圾?

簡單的說就是記憶體中已經不再被使用到的空間就是垃圾。

2.2、如何判斷一個對象是否可以被回收?
  • 引用計數法

    顧名思義,此種算法會在每一個對象上記錄這個對象被引用的次數,隻要有任何一個對象引用了次對象,這個對象的計數器就+1,取消對這個對象的引用時,計數器就-1。任何一個時刻,如果該對象的計數器為0,那麼這個對象就是可以回收的。

    優點:

    引用計數收集器可以很快的執行,交織在程式運作中。對程式需要不被長時間打斷的實時環境比較有利。

    缺點:

    無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能為0.而且每次加減非常浪費記憶體。

  • 枚舉根節點做可達性分析(根搜尋路徑) 又叫可達性分析算法

    通過一系列名為”GC Roots”的對象作為起始點,從這個被稱為GC Roots的對象開始向下搜尋,通過引用關系周遊對象,能被周遊到的(可到達的)對象就被判定為存活;沒有被周遊到的就自然被判定為死亡。

2.3、Java中可以作為GC Roots的對象?
  • 虛拟機棧(棧幀中的局部變量區,也叫做局部變量表)中引用的對象。
  • 方法區中的類靜态屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(Native方法)引用的對象。

3、JVM調優及參數配置

3.1、JVM的參數類型

官網位址:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html

JVM的參數類型:

  • 标配參數
    • -version

      java -version

    • -help

      java -heip

    • 指的是那些基本上不會改變的參數
  • X參數(了解)
    • -Xint:解釋執行
    • -Xcomp:第一次使用就編譯成本地代碼
    • -Xmixed:混合模式
  • XX參數(下一節)
    JVM知識詳解
3.2、JVM的XX參數

布爾類型:

公式:-XX:+ 或者 - 某個屬性值(+表示開啟,-表示關閉)

鍵值對類型:

公式:

-XX:屬性key=值value

。比如

-XX:Metaspace=128m

-XX:MaxTenuringThreshold=15

如何檢視一個正在運作中的java程式,它的某個jvm參數是否開啟?具體值是多少?

jps -l 檢視一個正在運作中的java程式,得到Java程式号。

JVM知識詳解

jinfo -flag PrintGCDetails Java程序号 檢視它的某個jvm參數(如PrintGCDetails )是否開啟。

JVM知識詳解

jinfo -flags Java程序号 檢視它的所有jvm參數

JVM知識詳解
3.3、VM Xms/Xmx參數

兩個經典參數:

  • -Xms等價于-XX:InitialHeapSize,初始大小記憶體,預設實體記憶體1/64
JVM知識詳解
  • -Xmx等價于-XX:MaxHeapSize,最大配置設定記憶體,預設為實體記憶體1/4
    JVM知識詳解
3.4、JVM檢視初始預設值

檢視初始預設參數值

-XX:+PrintFlagsInitial

指令行: java -XX:+PrintFlagsInitial

C:\Users\abc>java -XX:+PrintFlagsInitial
[Global flags]
      int ActiveProcessorCount                     = -1                                        {product} {default}
    uintx AdaptiveSizeDecrementScaleFactor         = 4                                         {product} {default}
    uintx AdaptiveSizeMajorGCDecayTimeScale        = 10                                        {product} {default}
    uintx AdaptiveSizePolicyCollectionCostMargin   = 50                                        {product} {default}
    uintx AdaptiveSizePolicyInitializingSteps      = 20                                        {product} {default}
    uintx AdaptiveSizePolicyOutputInterval         = 0                                         {product} {default}
    uintx AdaptiveSizePolicyWeight                 = 10                                        {product} {default}
... 

           

檢視修改更新參數值

-XX:+PrintFlagsFinal

公式:

java -XX:+PrintFlagsFinal

C:\Users\abc>java -XX:+PrintFlagsFinal
...
   size_t HeapBaseMinAddress                       = 2147483648                             {pd product} {default}
     bool HeapDumpAfterFullGC                      = false                                  {manageable} {default}
     bool HeapDumpBeforeFullGC                     = false                                  {manageable} {default}
     bool HeapDumpOnOutOfMemoryError               = false                                  {manageable} {default}
    ccstr HeapDumpPath                             =                                        {manageable} {default}
    uintx HeapFirstMaximumCompactionCount          = 3                                         {product} {default}
    uintx HeapMaximumCompactionInterval            = 20                                        {product} {default}
    uintx HeapSearchSteps                          = 3                                         {product} {default}
   size_t HeapSizePerGCThread                      = 43620760                                  {product} {default}
     bool IgnoreEmptyClassPaths                    = false                                     {product} {default}
     bool IgnoreUnrecognizedVMOptions              = false                                     {product} {default}
    uintx IncreaseFirstTierCompileThresholdAt      = 50                                        {product} {default}
     bool IncrementalInline                        = true                                   {C2 product} {default}
   size_t InitialBootClassLoaderMetaspaceSize      = 4194304                                   {product} {default}
    uintx InitialCodeCacheSize                     = 2555904                                {pd product} {default}
   size_t InitialHeapSize                          := 268435456                                 {product} {ergonomic}
...

           

=表示預設,:=表示修改過的。

列印指令行參數

-XX:+PrintCommandLineFlags

指令行:java -XX:+PrintCommandLineFlags -version

C:\Users\abc>java -XX:+PrintCommandLineFlags -version
-XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=266613056 -XX:MarkStackSize=4
194304 -XX:MaxHeapSize=4265808896 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+Seg
mentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
openjdk version "15.0.1" 2020-10-20
OpenJDK Runtime Environment (build 15.0.1+9-18)
OpenJDK 64-Bit Server VM (build 15.0.1+9-18, mixed mode)

           

檢視虛拟機參數文檔:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/index.html

3.5、常用的JVM基礎參數

-Xmx/-Xms:

  • -Xmx:初始的堆記憶體大小,預設實體記憶體1/64
  • -Xms:最大的堆記憶體大小,預設實體記憶體1/4

-Xss:

等價于

-XX:ThresholdStackSize

。 單個線程棧的大小,系統預設值是0,代表使用的是預設大小。

他的大小是根據作業系統的不同,有不同的值。比如64位的Linux系統是1024K,window的預設值取決于虛拟記憶體。

-Xmn:

設定年輕代大小,一般不調

-XX:MetaspaceSize:

設定元空間大小。指令行:-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m

JDK 1.8以及之後将最初的永久代取消了,由元空間取代。

元空間(Java8)與永久代(Java7)之間最大的差別在于:

永久帶使用的JVM的堆記憶體,但是Java8以後的元空間并不在虛拟機中而是使用本機實體記憶體

是以,預設情況下,元空間的大小僅受本地記憶體限制。類的中繼資料放入native memory,字元串池和類的靜态變量放入java堆中,這樣可以加載多少類的中繼資料就不再由MaxPermSize控制,而由系統的實際可用空間來控制。

-XX:+PrintGCDetails:

輸出詳細GC收集日志資訊。

設定參數 -Xms10m -Xmx10m -XX:+PrintGCDetails 運作程式

import java.util.concurrent.TimeUnit;

public class PrintGCDetailsDemo {

	
	public static void main(String[] args) throws InterruptedException {
		byte[] byteArray = new byte[10 * 1024 * 1024];
		
		TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
	}
}

           

輸出結果

[GC (Allocation Failure) [PSYoungGen: 1984K->488K(2560K)] 1984K->759K(9728K), 0.0031655 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 488K->504K(2560K)] 759K->783K(9728K), 0.0009001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 279K->719K(7168K)] 783K->719K(9728K), [Metaspace: 3283K->3283K(1056768K)], 0.0076688 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 719K->719K(9728K), 0.0003738 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 719K->701K(7168K)] 719K->701K(9728K), [Metaspace: 3283K->3283K(1056768K)], 0.0087387 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 2560K, used 57K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0e788,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 701K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 9% used [0x00000000ff600000,0x00000000ff6af4e8,0x00000000ffd00000)
 Metaspace       used 3315K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 362K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.example.test.test.Test.main(Test.java:14)
           

對回收類型GC的解釋

JVM知識詳解

[GC (Allocation Failure) [PSYoungGen: 1984K->488K(2560K)] 1984K->759K(9728K), 0.0031655 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

對回收類型FULL GC的解釋

JVM知識詳解

[Full GC (Allocation Failure) [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 279K->719K(7168K)] 783K->719K(9728K), [Metaspace: 3283K->3283K(1056768K)], 0.0076688 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

-XX:SurvivorRatio:

新生代中,

Eden

區和兩個

Survivor

區的比例,預設是

8:1:1

。通過

-XX:SurvivorRatio=4

改成

4:1:1

JVM知識詳解

預設的占比

-XX:NewRatio:

老生代和新年代的比列,預設是2,即老年代占2,新生代占1。如果改成

-XX:NewRatio=4

,則老年代占4,新生代占1。

-XX:MaxTenuringThreshold:

新生代設定進入老年代的時間,預設是新生代逃過15次GC後,進入老年代。如果改成0,那麼對象不會在新生代配置設定,直接進入老年代。

java8設定的範圍隻能在0-15内

4、強、軟、弱、虛引用

Reference類以及繼承派生的類

JVM知識詳解
4.1、強引用
// 這樣定義的預設就是強應用
Object obj1 = new Object();
           

當記憶體不足,JVM開始垃圾回收,對于強引用的對象,就算是出現了OOM也不會對該對象進行回收,死都不收。

4.2、軟引用

軟引用是一種相對強引用弱化了一些的引用,需要用java.lang.ref.SoftReference類來實作,可以讓對象豁免一些垃圾收集。對于隻有軟引用的對象來說,

當系統記憶體充足時它不會被回收,當系統記憶體不足時它會被回收。

軟引用通常用在對記憶體敏感的程式中,比如高速緩存就有用到軟引用,記憶體夠用的時候就保留,不夠用就回收!

4.3、弱引用

弱引用需要用java.lang.ref.WeakReference類來實作,它比軟引用的生存期更短,對于隻有弱引用的對象來說,隻要垃圾回收機制一運作不管JVM的記憶體空間是否足夠,都會回收該對象占用的記憶體。

4.4、虛引用

虛引用需要java.lang.ref.PhantomReference類來實作。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收,它不能單獨使用也不能通過它通路對象,虛引用必須和引用隊列(ReferenceQueue)聯合使用。虛引用的主要作用是跟蹤對象被垃圾回收的狀态

設定虛引用關聯的唯一目的,就是在這個對象要被收集器回收的時候收到一個系統通知或者後續添加進一步的處理。

4.5、ReferenceQueue引用隊列

引用的對象被回收之前,可以用隊列儲存,然後做一些特定的操作。

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;

public class ReferenceQueueDemo {
    public static void main(String[] args) {
        Object o1 = new Object();

        // 建立引用隊列
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();

        // 建立一個弱引用
        WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);

        System.out.println(o1);
        System.out.println(weakReference.get());
        // 取隊列中的内容
        System.out.println(referenceQueue.poll());

        System.out.println("==================");
        
        o1 = null;
        System.gc();
        System.out.println("執行GC操作");

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(o1);
        System.out.println(weakReference.get());
        // 取隊列中的内容
        System.out.println(referenceQueue.poll());

    }
}

           

輸出結果

[email protected]
[email protected]
null
==================
執行GC操作
null
null
[email protected]

           
4.6、GCRoots和四大引用小總結
JVM知識詳解

5、OOM(OutOfMemoryError)

5.1、StackOverflowError

​ 棧溢出

public class StackOverflowErrorDemo {

	public static void main(String[] args) {
		main(args);
	}
}
============================================================================

Exception in thread "main" java.lang.StackOverflowError
	at com.lun.jvm.StackOverflowErrorDemo.main(StackOverflowErrorDemo.java:6)
	at com.lun.jvm.StackOverflowErrorDemo.main(StackOverflowErrorDemo.java:6)
	at com.lun.jvm.StackOverflowErrorDemo.main(StackOverflowErrorDemo.java:6)
	...
           
5.2、java heap space

​ 堆空間不足

public class OOMEJavaHeapSpaceDemo {

	/**
	 * 
	 * -Xms10m -Xmx10m
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		byte[] array = new byte[80 * 1024 * 1024];
	}

}
=====================================================================================

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.lun.jvm.OOMEJavaHeapSpaceDemo.main(OOMEJavaHeapSpaceDemo.java:6)

           
5.3、GC overhead limit exceeeded

超出GC開銷限制,GC回收時間過長時會抛出OutOfMemroyError。過長的定義是,超過98%的時間用來做GC并且回收了不到2%的堆記憶體,連續多次GC 都隻回收了不到2%的極端情況下才會抛出。

import java.util.ArrayList;
import java.util.List;

public class OOMEGCOverheadLimitExceededDemo {

    /**
     * 
     * -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m
     * 
     * @param args
     */
    public static void main(String[] args) {
        int i = 0;
        List<String> list = new ArrayList<>();
        try {
            while(true) {
                list.add(String.valueOf(++i).intern());
            }
        } catch (Exception e) {
            System.out.println("***************i:" + i);
            e.printStackTrace();
            throw e;
        }
    }

}

=============================================================================
    [GC (Allocation Failure) [PSYoungGen: 2048K->498K(2560K)] 2048K->1658K(9728K), 0.0033090 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2323K->489K(2560K)] 3483K->3305K(9728K), 0.0020911 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2537K->496K(2560K)] 5353K->4864K(9728K), 0.0025591 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2410K->512K(2560K)] 6779K->6872K(9728K), 0.0058689 secs] [Times: user=0.09 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 512K->0K(2560K)] [ParOldGen: 6360K->6694K(7168K)] 6872K->6694K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0894928 secs] [Times: user=0.42 sys=0.00, real=0.09 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2048K->1421K(2560K)] [ParOldGen: 6694K->6902K(7168K)] 8742K->8324K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0514932 secs] [Times: user=0.34 sys=0.00, real=0.05 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2048K->2047K(2560K)] [ParOldGen: 6902K->6902K(7168K)] 8950K->8950K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0381615 secs] [Times: user=0.13 sys=0.00, real=0.04 secs] 
...
***************i:147041
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7050K->7048K(7168K)] 9098K->9096K(9728K), [Metaspace: 2670K->2670K(1056768K)], 0.0371397 secs] [Times: user=0.22 sys=0.00, real=0.04 secs] 
java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) 	at java.lang.Integer.toString(Integer.java:401)
[PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7051K->7050K(7168K)] 9099K->9097K(9728K), [Metaspace: 2676K->2676K(1056768K)], 0.0434184 secs] [Times: user=0.38 sys=0.00, real=0.04 secs] 
	at java.lang.String.valueOf(String.java:3099)
	at com.lun.jvm.OOMEGCOverheadLimitExceededDemo.main(OOMEGCOverheadLimitExceededDemo.java:19)
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7054K->513K(7168K)] 9102K->513K(9728K), [Metaspace: 2677K->2677K(1056768K)], 0.0056578 secs] [Times: user=0.11 sys=0.00, real=0.01 secs] 
	at java.lang.Integer.toString(Integer.java:401)
	at java.lang.String.valueOf(String.java:3099)
	at com.lun.jvm.OOMEGCOverheadLimitExceededDemo.main(OOMEGCOverheadLimitExceededDemo.java:19)
Heap
 PSYoungGen      total 2560K, used 46K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0bb90,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 513K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 7% used [0x00000000ff600000,0x00000000ff6807f0,0x00000000ffd00000)
 Metaspace       used 2683K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K

           
5.4、Direct buffer memory

導緻原因:

寫NIO程式經常使用ByteBuffer來讀取或者寫入資料,這是一種基于通道(Channel)與緩沖區(Buffer)的IO方式,它可以使用Native函數庫直接配置設定堆外記憶體,然後通過一個存儲在Java堆裡面的DirectByteBuffer對象作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高性能,因為避兔了在Java堆和Native堆中來回複制資料。

  • ByteBuffer.allocate(capability) 第一種方式是配置設定VM堆記憶體,屬于GC管轄範圍,由于需要拷貝是以速度相對較慢。
  • ByteBuffer.allocateDirect(capability) 第二種方式是配置設定OS本地記憶體,不屬于GC管轄範圍,由于不需要記憶體拷貝是以速度相對較快。

但如果不斷配置設定本地記憶體,堆記憶體很少使用,那麼JV就不需要執行GC,DirectByteBuffer對象們就不會被回收,這時候堆記憶體充足,但本地記憶體可能已經使用光了,再次嘗試配置設定本地記憶體就會出現OutOfMemoryError,那程式就直接崩潰了。

元空間存放的資訊:

  • 虛拟機加載的類資訊
  • 常量池
  • 靜态變量
  • 即時編譯後的代碼
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;

public class OOMEDirectBufferMemoryDemo {

	/**
	 * -Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
	 * 
	 * @param args
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws InterruptedException {
		System.out.println(String.format("配置的maxDirectMemory: %.2f MB",// 
				sun.misc.VM.maxDirectMemory() / 1024.0 / 1024));
		
		TimeUnit.SECONDS.sleep(3);
		
		ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
	}	
}
============================================================================
 [GC (Allocation Failure) [PSYoungGen: 1024K->504K(1536K)] 1024K->772K(5632K), 0.0014568 secs] [Times: user=0.09 sys=0.00, real=0.00 secs] 
配置的maxDirectMemory: 5.00 MB
[GC (System.gc()) [PSYoungGen: 622K->504K(1536K)] 890K->820K(5632K), 0.0009753 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 316K->725K(4096K)] 820K->725K(5632K), [Metaspace: 3477K->3477K(1056768K)], 0.0072268 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Exception in thread "main" Heap
 PSYoungGen      total 1536K, used 40K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 4% used [0x00000000ffe00000,0x00000000ffe0a3e0,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 4096K, used 725K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 17% used [0x00000000ffa00000,0x00000000ffab5660,0x00000000ffe00000)
 Metaspace       used 3508K, capacity 4566K, committed 4864K, reserved 1056768K
  class space    used 391K, capacity 394K, committed 512K, reserved 1048576K
java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at com.lun.jvm.OOMEDirectBufferMemoryDemo.main(OOMEDirectBufferMemoryDemo.java:20)


           
5.5、unable to create new native thread

不能夠建立更多的新的線程了,也就是說建立線程的上限達到了。高并發請求伺服器時,經常會出現異常

java.lang.OutOfMemoryError:unable to create new native thread

,準确說該native thread異常與對應的平台有關。

導緻原因:

  • 應用建立了太多線程,一個應用程序建立多個線程,超過系統承載極限
  • 伺服器并不允許你的應用程式建立這麼多線程,linux系統預設運作單個程序可以建立的線程為1024個,如果應用建立超過這個數量,就會報 java.lang.OutOfMemoryError:unable to create new native thread

解決方法:

  1. 想辦法降低你應用程式建立線程的數量,分析應用是否真的需要建立這麼多線程,如果不是,改代碼将線程數降到最低
  2. 對于有的應用,确實需要建立很多線程,遠超過linux系統預設1024個線程限制,可以通過修改Linux伺服器配置,擴大linux預設限制
public class OOMEUnableCreateNewThreadDemo {
    public static void main(String[] args) {
        for (int i = 0; ; i++) {
            System.out.println("************** i = " + i);
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}
===========================================================================
Exception in thread "main" java.lang.OutOfMemoryError: unable to cerate new native thread


           
5.6、unable to create new native thread上限調整

檢視系統線程限制數目:ulimit -u

修改系統線程限制數目: vim /etc/security/limits.d/90-nproc.conf

打開後發現除了root,其他賬戶都限制在1024個

JVM知識詳解
5.7、Metaspace

永久代(Java8後被原空向Metaspace取代了)存放了以下資訊:

  • 虛拟機加載的類資訊
  • 常量池
  • 靜态變量
  • 即時編譯後的代碼

模拟Metaspace空間溢出,我們借助CGLib直接操作位元組碼運作時不斷生成類往元空間灌,類占據的空間總是會超過Metaspace指定的空間大小的。

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.10</version>
</dependency>


==================================================

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class OOMEMetaspaceDemo {
    // 靜态類
    static class OOMObject {}

    /**
     * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
     * 
     * @param args
     */
    public static void main(final String[] args) {
        // 模拟計數多少次以後發生異常
        int i =0;
        try {
            while (true) {
                i++;
                // 使用Spring的動态位元組碼技術
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMObject.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        return methodProxy.invokeSuper(o, args);
                    }
                });
                enhancer.create();
            }
        } catch (Throwable e) {
            System.out.println("發生異常的次數:" + i);
            e.printStackTrace();
        } finally {

        }

    }
}
===================================================================

發生異常的次數:569
java.lang.OutOfMemoryError: Metaspace
	at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
	at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
	at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
	at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
	at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
	at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
	at com.lun.jvm.OOMEMetaspaceDemo.main(OOMEMetaspaceDemo.java:37)


           

6、垃圾回收器

GC算法(引用計數/複制/标清/标整)是記憶體回收的方法論,垃圾收集器就是算法落地實作。

6.1、垃圾回收器的種類
  • Serial 串行垃級回收器

    它為單線程環境設計且值使用一個線程進行垃圾收集,會暫停所有的使用者線程,隻有當垃圾回收完成時,才會重新喚醒主線程繼續執行。

  • Parallel 并行垃圾回收器

    多個垃圾收集線程并行工作,此時使用者線程也是阻塞的,進行垃圾收集時,主線程都會被暫停,但是并行垃圾收集器處理時間,肯定比串行的垃圾收集器要更短。

  • CMS 并發垃圾回收器

    使用者線程和垃圾收集線程同時執行(不一定是并行,可能是交替執行),不需要停頓使用者線程。

  • G1垃圾回收器

    G1垃圾回收器将堆記憶體分割成不同的區域然後并發的對其進行垃圾回收。

6.2、檢視預設的垃圾收集器

​ java -XX:+PrintCommandLineFlags -version

JVM知識詳解
6.3、JVM的垃圾收集器

Java中一共有7大垃圾收集器用于垃圾回收,

年輕代GC

  • UserSerialGC:串行垃圾收集器
  • UserParallelGC:并行垃圾收集器
  • UseParNewGC:年輕代的并行垃圾回收

老年代

  • UserSerialOldGC:串行老年代垃圾收集器(已經被移除)
  • UseParallelOldGC:老年代的并行垃圾回收器
  • UseConcMarkSweepGC:(CMS)并發标記清除

UseG1GC:G1垃圾收集器不區分新生代和老年代分區域進行

| 參數 新生代垃圾收集器 新生代算法 老年代垃圾收集器 老年代算法XX:+UseSerialGC SerialGC 複制 SerialOldGC 标記整理

-XX:+UseParNewGC ParNew 複制 SerialOldGC 标記整理

-XX:+UseParallelGC Parallel [Scavenge] 複制 Parallel Old 标記整理

-XX:+UseConcMarkSweepGC ParNew 複制 CMS + Serial Old的收集器組合 标記清除

-XX:+UseG1GC 不區分新生代和老年代 G1整體上采用标記整理算法 每一個區域使用局部複制
6.4、SerialGC

一個單線程的收集器,在進行垃圾收集時候,必須暫停其他所有的工作線程直到它收集結束。

JVM知識詳解

STW: Stop The World 指的是暫停使用者線程

JVM參數是:-XX:+UseSerialGC ,指令作用的是新生代,老年代會自動比對相對應的垃圾回收器。

開啟後會使用:Serial(Young區用) + Serial Old(Old區用)的收集器組合,新生代、老年代都會使用串行回收收集器,新生代使用複制算法,老年代使用标記-整理算法

6.5、ParNewGC

使用多線程進行垃圾回收,在垃圾收集時,會Stop-The-World暫停其他所有的工作線程直到它收集結束。

JVM知識詳解

ParNew收集器其實就是Serial收集器新生代的并行多線程版本,,其餘的行為和Seria收集器完全一樣,ParNew垃圾收集器在垃圾收集過程中同樣也要暫停所有其他的工作線程。但是執行的效率要比Serial的要高效。

JVM參數:

  • -XX:+UseParNewGC啟用ParNew收集器,隻影響新生代的收集,不影響老年代。
  • -XX:ParallelGCThreads限制線程數量,預設開啟和CPU數目相同的線程數。

開啟上述參數後,會使用:ParNew(Young區)+ Serial Old的收集器組合,新生代使用複制算法,老年代采用标記-整理算法

6.6、ParallelGC

Parallel / Parallel Scavenge

JVM知識詳解

Parallel Scavenge收集器類似ParNew也是一個新生代垃圾收集器,使用複制算法,也是一個并行的多線程的垃圾收集器,俗稱吞吐量優先收集器。

優點:

  • 高吞吐量,高吞吐量意味着高效利用CPU的時間
  • 自适應調節政策,自适應調節政策也是ParallelScavenge收集器與ParNew收集器的一個重要差別,自适應調節政策:虛拟機會根據目前系統的運作情況收集性能監控資訊,動态調整這些參數以提供最合适的停頓時間(-XX:MaxGCPauseMillis)或最大的吞吐量。

JVM參數:

  • -XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器。
  • -XX:ParallelGCThreads=數字N 表示啟動多少個GC線程 cpu>8 N= 5/8 cpu<8 N=實際個數

開啟該參數後:會使用:ParallelGC(Young區)+ ParallelOldGC(Old區)的收集器組合, 新生代使用複制算法,老年代使用标記-整理算法。

6.7、ParallelOldGC

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多線程的标記-整理算法,Parallel Old收集器在JDK1.6才開始提供。Parallel Old 是為了在年老代同樣提供吞吐量優先的垃圾收集器

JVM常用參數:-XX:+UseParallelOldGC使用Parallel Old收集器,設定該參數後,新生代Parallel+老年代 Parallel Old。 新生代使用複制算法,老年代使用标記-整理算法。

6.8、SerialOldGC

Serial Old是Serial垃圾收集器老年代版本,它同樣是個單線程的收集器,使用标記-整理算法。現在基本已經不使用了。

6.9、ConcMarkSweepGC

又叫做CMS,CMS收集器(Concurrent Mark Sweep:并發标記清除)是一種以擷取最短回收停頓時間為目标的收集器。

JVM知識詳解

Concurrent Mark Sweep并發标記清除,并發收集,低停頓,并發指的是與使用者線程一起執行

開啟該收集器的JVM參數:

  • -XX:+UseConcMarkSweepGC 老年代使用的
  • -XX:CMSFullGCsBeForeCompaction (預設O,即每次都進行記憶體整理)來指定多少次CMS收集之後,進行一次壓縮的Full GC。

開啟該參數後,使用ParNew(Young區用)+ CMS(Old區用)+ Serial Old的收集器組合,Serial Old将作為CMS出錯的後備收集器。

執行過程:

  • 初始标記(CMS initial mark) - 隻是标記一下GC Roots能直接關聯的對象,速度很快,仍然需要暫停所有的工作線程。
  • 并發标記(CMS concurrent mark)和使用者線程一起 - 進行GC Roots跟蹤的過程,和使用者線程一起工作,不需要暫停工作線程。主要标記過程,标記全部對象。
  • 重新标記(CMS remark)- 為了修正在并發标記期間,因使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,仍然需要暫停所有的工作線程。由于并發标記時,使用者線程依然運作,是以在正式清理前,再做修正。
  • 并發清除(CMS concurrent sweep) - 清除GCRoots不可達對象,和使用者線程一起工作,不需要暫停工作線程。基于标記結果,直接清理對象,由于耗時最長的并發标記和并發清除過程中,垃圾收集線程可以和使用者現在一起并發工作,是以總體上來看CMS 收集器的記憶體回收和使用者線程是一起并發地執行。
JVM知識詳解

優點:并發收集低停頓,響應速度快,使用者體驗好

缺點:并發執行,對CPU資源壓力大,采用的标記清除算法會導緻大量碎片。

由于并發進行,CMS在收集與應用線程會同時會增加對堆記憶體的占用,也就是說,CMS必須要在老年代堆記憶體用盡之前完成垃圾回收,否則CMS回收失敗時,将觸發擔保機制,串行老年代收集器将會以STW的方式進行一次GC,進而造成較大停頓時間。

标記清除算法無法整理空間碎片,老年代空間會随着應用時長被逐漸耗盡,最後将不得不通過擔保機制對堆記憶體進行壓縮。CMS也提供了參數-XX:CMSFullGCsBeForeCompaction(預設O,即每次都進行記憶體整理)來指定多少次CMS收集之後,進行一次壓縮的Full GC。

6.10、如何選擇垃圾收集器

組合的選擇

  • 單CPU或者小記憶體,單機程式
    • -XX:+UseSerialGC
  • 多CPU,需要最大的吞吐量,如背景計算型應用
    • -XX:+UseParallelGC(這兩個互相激活)
    • -XX:+UseParallelOldGC
  • 多CPU,追求低停頓時間,需要快速響應如網際網路應用
    • -XX:+UseConcMarkSweepGC
    • -XX:+ParNewGC
JVM知識詳解
6.11、G1

G1收集器與之前垃圾收集器的一個顯著差別就是——之前收集器都有三個區域,新、老兩代和元空間。

而G1收集器隻有G1區和元空間。而G1區,不像之前的收集器,分為新、老兩代,而是一個一個Region,每個Region既可能包含新生代,也可能包含老年代。

G1`收集器既可以提高吞吐量,又可以減少GC時間。最重要的是STW可控,增加了預測機制,讓使用者指定停頓時間。

VM參數:

  • -XX:+UseG1GC G1整體上看是标整算法,在局部看又是複制算法,不會産生記憶體碎片。
  • -XX:G1HeapRegionSize=n 可指定分區大小(1MB~32MB,且必須是2的幂),預設将整堆劃分為2048個分區。

底層原理:

區域化記憶體劃片Region,整體編為了一些列不連續的記憶體區域,避免了全記憶體區的GC操作

核心思想是将整個堆記憶體區域分成大小相同的子區域(Region),在JVM啟動時會自動設定這些子區域的大小,在堆的使用上,G1并不要求對象的存儲一定是實體上連續的隻要邏輯上連續即可,每個分區也不會固定地為某個代服務,可以按需在年輕代和老年代之間切換。啟動時可以通過參數

-XX:G1HeapRegionSize=n可指定分區大小(1MB~32MB,且必須是2的幂),預設将整堆劃分為2048個分區。

大小範圍在1MB~32MB,最多能設定2048個區域,也即能夠支援的最大記憶體為:32 M B ∗ 2048 = 65536 M B = 64 G 32MB*2048=65536MB=64G32MB∗2048=65536MB=64G記憶體。

G1算法将堆劃分為若幹個區域(Region),它仍然屬于分代收集器。

這些Region的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應用線程的方式,将存活對象拷貝到老年代或者Survivor空間。

這些Region的一部分包含老年代,G1收集器通過将對象從一個區域複制到另外一個區域,完成了清理工作。

在G1中,還有一種特殊的區域,叫Humongous區域。

如果一個對象占用的空間超過了分區容量50%以上,G1收集器就認為這是一個巨型對象。這些巨型對象預設直接會被配置設定在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。

為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC。

G1收集器下的Young GC

  • 針對Eden區進行收集,Eden區耗盡後會被觸發,主要是小區域收集+形成連續的記憶體塊,避免記憶體碎片
  • Eden區的資料移動到Survivor區,假如出現Survivor區空間不夠,Eden區資料會部會晉升到Old區。
  • Survivor區的資料移動到新的Survivor區,部會資料晉升到Old區。
  • 最後Eden區收拾幹淨了,GC結束,使用者的應用程式繼續執行。
    JVM知識詳解

4步過程:

  1. 初始标記:隻标記GC Roots能直接關聯到的對象
  2. 并發标記:進行GC Roots Tracing的過程
  3. 最終标記:修正并發标記期間,因程式運作導緻标記發生變化的那一部分對象
  4. 篩選回收:根據時間來進行價值最大化的回收

特點:

  1. 并行和并發:充分利用多核、多線程CPU,盡量縮短STW。
  2. 分代收集:雖然還保留着新、老兩代的概念,但實體上不再隔離,而是融合在Region中。
  3. 空間整合:

    G1

    整體上看是标整算法,在局部看又是複制算法,不會産生記憶體碎片。
  4. 可預測停頓:使用者可以指定一個GC停頓時間,

    G1

    收集器會盡量滿足。
6.12、G1參數配置
  • -XX:+UseG1GC
  • -XX:G1HeapRegionSize=n:設定的G1區域的大小。值是2的幂,範圍是1MB到32MB。目标是根據最小的Java堆大小劃分出約2048個區域。
  • -XX:MaxGCPauseMillis=n:最大GC停頓時間,這是個軟目标,JVM将盡可能(但不保證)停頓小于這個時間。
  • -XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的時候就觸發GC,預設為45。
  • -XX:ConcGCThreads=n:并發GC使用的線程數。
  • -XX:G1ReservePercent=n:設定作為空閑空間的預留記憶體百分比,以降低目标空間溢出的風險,預設值是10%。

開發人員僅僅需要聲明以下參數即可:

  1. -XX:+UseG1GC
  2. -Xmx32g
  3. -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=n:最大GC停頓時間機關毫秒,這是個軟目标,JVM将盡可能(但不保證)停頓小于這個時間

G1和CMS比較

  • G1不會産生内碎片
  • 是可以精準控制停頓。該收集器是把整個堆(新生代、老年代)劃分成多個固定大小的區域,每次根據允許停頓的時間去收集垃圾最多的區域。

區資料會部會晉升到Old區。

  • Survivor區的資料移動到新的Survivor區,部會資料晉升到Old區。
  • 最後Eden區收拾幹淨了,GC結束,使用者的應用程式繼續執行。

[外鍊圖檔轉存中…(img-HIW1kfyS-1617879000684)]

4步過程:

  1. 初始标記:隻标記GC Roots能直接關聯到的對象
  2. 并發标記:進行GC Roots Tracing的過程
  3. 最終标記:修正并發标記期間,因程式運作導緻标記發生變化的那一部分對象
  4. 篩選回收:根據時間來進行價值最大化的回收

特點:

  1. 并行和并發:充分利用多核、多線程CPU,盡量縮短STW。
  2. 分代收集:雖然還保留着新、老兩代的概念,但實體上不再隔離,而是融合在Region中。
  3. 空間整合:

    G1

    整體上看是标整算法,在局部看又是複制算法,不會産生記憶體碎片。
  4. 可預測停頓:使用者可以指定一個GC停頓時間,

    G1

    收集器會盡量滿足。
6.12、G1參數配置
  • -XX:+UseG1GC
  • -XX:G1HeapRegionSize=n:設定的G1區域的大小。值是2的幂,範圍是1MB到32MB。目标是根據最小的Java堆大小劃分出約2048個區域。
  • -XX:MaxGCPauseMillis=n:最大GC停頓時間,這是個軟目标,JVM将盡可能(但不保證)停頓小于這個時間。
  • -XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的時候就觸發GC,預設為45。
  • -XX:ConcGCThreads=n:并發GC使用的線程數。
  • -XX:G1ReservePercent=n:設定作為空閑空間的預留記憶體百分比,以降低目标空間溢出的風險,預設值是10%。

開發人員僅僅需要聲明以下參數即可:

  1. -XX:+UseG1GC
  2. -Xmx32g
  3. -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=n:最大GC停頓時間機關毫秒,這是個軟目标,JVM将盡可能(但不保證)停頓小于這個時間

G1和CMS比較

  • G1不會産生内碎片
  • 是可以精準控制停頓。該收集器是把整個堆(新生代、老年代)劃分成多個固定大小的區域,每次根據允許停頓的時間去收集垃圾最多的區域。