日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

搞JAVA開發的朋友,最怕的就是之一:JVM調優。實話實說,在工作中用的不是很多,只有出現問題了才會用到(也可以在項目發布時調整好相關參數,避免線上出問題)。但是在面試中,這一塊就是必須掌握的了,否則和HR聊薪水都會受到限制。

主要內容:

  • 小白也能看懂的JVM運行時數據區詳解。
  • 垃圾回收,到底是怎么回收的?
  • JDK8為什么要引入元空間替換永久代?
  • CMS和G1垃圾收集器介紹
  • 常見JVM參數介紹
  • 堆空間如何設置?
  • 元空間如何設置?
  • 棧相關參數如何設置?
  • 日志參數如何設置?
  • 垃圾收集器如何配置?
  • CMS和G1參數設置介紹
  • 其他參數設置介紹
  • 調優案例

適合人群:所有Java開發人員,對JVM調優感興趣的朋友

我們先從 JVM(Java Virtual machine)的基本知識點開始聊。Java 中的一些代碼優化技巧,和JVM的關系非常的大,比如逃逸分析對非捕獲型 Lambda 表達式的優化。

在進行這些優化之前,你需要對 JVM 的一些運行原理有較深刻的認識,在優化時才會有針對性的方向。

JVM 內存區域劃分

學習 JVM,內存區域劃分是繞不過去的知識點,這幾乎是面試必考的題目。如下圖所示,內存區域劃分主要包括堆、Java 虛擬機棧、程序計數器、本地方法棧、元空間和直接內存這五部分,我將逐一介紹。

程序員必備:JVM核心知識點總結

 

1.堆

如 JVM 內存區域劃分圖所示,JVM 中占用內存最大的區域,就是堆(Heap),我們平常編碼創建的對象,大多數是在這上面分配的,也是垃圾回收器回收的主要目標區域。

2.Java 虛擬機棧

JVM 的解釋過程是基于棧的,程序的執行過程也就是入棧出棧的過程,這也是 Java 虛擬機棧這個名稱的由來。

Java 虛擬機棧是和線程相關的。當你啟動一個新的線程,Java 就會為它分配一個虛擬機棧,之后所有這個線程的運行,都會在棧里進行。

Java 虛擬機棧,從方法入棧到具體的字節碼執行,其實是一個雙層的棧結構,也就是棧里面還包含棧。

程序員必備:JVM核心知識點總結

 

如上圖,Java 虛擬機棧里的每一個元素,叫作棧幀。每一個棧幀,包含四個區域: 局部變量表 、操作數棧、動態連接和返回地址。

其中,操作數棧就是具體的字節碼指令所操作的棧區域,考慮到下面這段代碼:

package com.tian.utils;

public class Test {
    public int test() {
        int a = 1;
        a++;
        return a;
    }
}

JVM 將會為 test 方法生成一個棧幀,然后入棧,等 test 方法執行完畢,就會將對應的棧幀彈出。在對變量 a 進行加一操作的時候,就會對棧幀中的操作數棧運用相關的字節碼指令。

我們對上面這個類進行編譯成Test.class文件后,使用命令:

javap -verbose -c Test.class >test.txt

這樣就會把這個類的字節碼指令輸出到test.txt文件中:

Classfile /E:/workspace/other/hAppy-mall/target/classes/com/tian/utils/Test.class
  Last modified 2022-3-23; size 369 bytes
  MD5 checksum 58d655b96f21dd36600ad0a8df0efa70
  Compiled from "Test.java"
public class com.tian.utils.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#17         // java/lang/Object."<init>":()V
   #2 = Class              #18            // com/tian/utils/Test
   #3 = Class              #19            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lcom/tian/utils/Test;
  #11 = Utf8               test
  #12 = Utf8               ()I
  #13 = Utf8               a
  #14 = Utf8               I
  #15 = Utf8               SourceFile
  #16 = Utf8               Test.java
  #17 = NameAndType        #4:#5          // "<init>":()V
  #18 = Utf8               com/tian/utils/Test
  #19 = Utf8               java/lang/Object
{
  public com.tian.utils.Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/tian/utils/Test;

  public int test();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_1
         1: istore_1
         2: iinc          1, 1
         5: iload_1
         6: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/tian/utils/Test;
            2       5     1     a   I
}
SourceFile: "Test.java"

上面這段字節碼代碼,可能很多人都看不懂,建議結合:

程序員必備:JVM核心知識點總結

 

推薦博客(如果英語好的話,也可以去官網查看):

字節碼指令大全

然后,再結合我們上面的Java虛擬機棧這塊知識,就能輕松閱讀了。

棧幀的創建是需要耗費資源的,尤其是對于 Java 中常見的 getter、setter 方法來說,這些代碼通常只有一行,每次都創建棧幀的話就太浪費了。

另外,Java 虛擬機棧對代碼的執行,采用的是字節碼解釋的方式,考慮到下面這段代碼,變量 a 聲明之后,就再也不被使用,要是按照字節碼指令解釋執行的話,就要做很多無用功。

另外,我們了解到垃圾回收器回收的目標區域主要是堆,堆上創建的對象越多,GC 的壓力就越大。要是能把一些變量,直接在棧上分配,那 GC 的壓力就會小一些。

3.程序計數器

既然是線程,就要接受操作系統的調度,但總有時候,某些線程是獲取不到 CPU 時間片的,那么當這個線程恢復執行的時候,它是如何確保找到切換之前執行的位置呢?這就是程序計數器的功能。

和 Java 虛擬機棧一樣,它也是線程私有的。程序計數器只需要記錄一個執行位置就可以,所以不需要太大的空間。事實上,程序計數器是 JVM 規范中唯一沒有規定 OutOfMemoryError 情況的區域。

4.本地方法棧

與 Java 虛擬機棧類似,本地方法棧,是針對 native 方法的。我們常用的 HotSpot,將 Java 虛擬機棧和本地方法棧合二為一,其實就是一個本地方法棧,大家注意規范里的這些差別就可以了。

5.元空間

元空間是一個容易引起混淆的區域,原因就在于它經歷了多次迭代才成為現在的模樣。關于這部分區域,我們來講解兩個面試題,大家就明白了。

  • 元空間是在堆上嗎?

答案:元空間并不是在堆上分配的,而是在堆外空間進行分配的,它的大小默認沒有上限,我們常說的方法區,就在元空間中。

  • 字符串常量池在那個區域中?

答案:這個要看 JDK 版本。

在 JDK 1.8 之前,是沒有元空間這個概念的,當時的方法區是放在一個叫作永久代的空間中。

而在 JDK 1.7 之前,字符串常量池也放在這個叫作永久帶的空間中。但在 JDK 1.7 版本,已經將字符串常量池從永久帶移動到了堆上。

所以,從 1.7 版本開始,字符串常量池就一直存在于堆上。

  • 為什么使用元空間替換永久代?

表面上看是為了避免OOM異常。因為通常使用PermSize和MaxPermSize設置永久代的大小就決定了永久代的上限,但是不是總能知道應該設置為多大合適, 如果使用默認值很容易遇到OOM錯誤。當使用元空間時,可以加載多少類的元數據就不再由MaxPermSize控制, 而由系統的實際可用空間來控制啦。

6.直接內存

直接內存,指的是使用了 Java 的直接內存 API,進行操作的內存。這部分內存可以受到 JVM 的管控,比如 ByteBuffer 類所申請的內存,就可以使用具體的參數進行控制。

需要注意的是直接內存和本地內存不是一個概念。

  • 直接內存比較專一,有具體的 API(這里指的是ByteBuffer),也可以使用 -XX:MaxDirectMemorySize 參數控制它的大小;
  • 本地內存是一個統稱,比如使用 native 函數操作的內存就是本地內存,本地內存的使用 JVM 是限制不住的,使用的時候一定要小心。

GC Roots

對象主要是在堆上分配的,我們可以把它想象成一個池子,對象不停地創建,后臺的垃圾回收進程不斷地清理不再使用的對象。當內存回收的速度,趕不上對象創建的速度,這個對象池子就會產生溢出,也就是我們常說的 OOM。

把不再使用的對象及時地從堆空間清理出去,是避免 OOM 有效的方法。那 JVM 是如何判斷哪些對象應該被清理,哪些對象需要被繼續使用呢?

這里首先強調一個概念,這對理解垃圾回收的過程非常有幫助,面試時也能很好地展示自己。

垃圾回收,并不是找到不再使用的對象,然后將這些對象清除掉。它的過程正好相反,JVM 會找到正在使用的對象,對這些使用的對象進行標記和追溯,然后一股腦地把剩下的對象判定為垃圾,進行清理。

了解了這個概念,我們就可以看下一些基本的衍生分析:

  • GC 的速度,和堆內存活對象的多少有關,與堆內所有對象的數量無關;
  • GC 的速度與堆的大小無關,32GB 的堆和 4GB 的堆,只要存活對象是一樣的,垃圾回收速度也會差不多;
  • 垃圾回收不必每次都把垃圾清理得干干凈凈,最重要的是不要把正在使用的對象判定為垃圾。

那么,如何找到這些存活對象,也就是哪些對象是正在被使用的,就成了問題的核心。

大家可以想一下寫代碼的時候,如果想要保證一個 HashMap 能夠被持續使用,可以把它聲明成靜態變量,這樣就不會被垃圾回收器回收掉。我們把這些正在使用的引用的入口,叫作GC Roots。

這種使用 tracing 方式尋找存活對象的方法,還有一個好聽的名字,叫作可達性分析法

概括來講,GC Roots 包括:

  • Java 線程中,當前所有正在被調用的方法的引用類型參數、局部變量、臨時值等。也就是與我們棧幀相關的各種引用;
  • 所有當前被加載的 Java 類;
  • Java 類的引用類型靜態變量;
  • 運行時常量池里的引用類型常量(String 或 Class 類型);
  • JVM 內部數據結構的一些引用,比如 sun.jvm.hotspot.memory.Universe 類;
  • 用于同步的監控對象,比如調用了對象的 wait() 方法;
  • JNI handles,包括 global handles 和 local handles。

對于這個知識點,不要死記硬背,可以對比著 JVM 內存區域劃分那張圖去看,入口大約有三個:線程、靜態變量和 JNI 引用。

強、軟、弱、虛引用

那么,通過 GC Roots 能夠追溯到的對象,就一定不會被垃圾回收嗎?這要看情況。

Java 對象與對象之間的引用,存在著四種不同的引用級別,強度從高到低依次是:強引用、軟引用、弱引用、虛引用。

  • 強應用 默認的對象關系是強引用,也就是我們默認的對象創建方式。這種引用屬于最普通最強硬的一種存在,只有在和 GC Roots 斷絕關系時,才會被消滅掉。
  • 軟引用 用于維護一些可有可無的對象。在內存足夠的時候,軟引用對象不會被回收;只有在內存不足時,系統則會回收軟引用對象;如果回收了軟引用對象之后,仍然沒有足夠的內存,才會拋出內存溢出異常。
  • 弱引用 級別就更低一些,當 JVM 進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。軟引用和弱引用在堆內緩存系統中使用非常頻繁,可以在內存緊張時優先被回收掉。
  • 虛引用 是一種形同虛設的引用,在現實場景中用得不是很多。這里有一個冷門的知識點:Java 9.0 以后新加入了 Cleaner 類,用來替代 Object 類的 finalizer 方法,這就是虛引用的一種應用場景。

分代垃圾回收

上面我們提到,垃圾回收的速度,是和存活的對象數量有關系的,如果這些對象太多,JVM 再做標記和追溯的時候,就會很慢。

一般情況下,JVM 在做這些事情的時候,都會停止業務線程的所有工作,進入 SafePoint 狀態,這也就是我們通常說的 Stop the World。所以,現在的垃圾回收器,有一個主要目標,就是減少 STW 的時間。

其中一種有效的方式,就是采用分代垃圾回收,減少單次回收區域的大小。這是因為,大部分對象,可以分為兩類:

  • 大部分對象的生命周期都很短
  • 其他對象則很可能會存活很長時間

這個假設我們稱之為弱代假設(weak generational hypothesis)。

如下圖,分代垃圾回收器會在邏輯上,把堆空間分為兩部分:年輕代(Young generation)和老年代(Old generation)。

程序員必備:JVM核心知識點總結

 

1.年輕代

年輕代中又分為一個伊甸園空間(Eden),兩個幸存者空間(Survivor)。對象會首先在年輕代中的 Eden 區進行分配,當 Eden 區分配滿的時候,就會觸發年輕代的 GC。

此時,存活的對象會被移動到其中一個 Survivor 分區(以下簡稱 from);年輕代再次發生垃圾回收,存活對象,包括 from 區中的存活對象,會被移動到 to 區。所以,from 和 to 兩個區域,總有一個是空的。

Eden、from、to 的默認比例是 8:1:1,所以只會造成 10% 的空間浪費。這個比例,是由參數 -XX:SurvivorRatio 進行配置的(默認為 8)。

2.老年代

對垃圾回收的優化,就是要讓對象盡快在年輕代就回收掉,減少到老年代的對象。那么對象是如何進入老年代的呢?它主要有以下四種方式。

  • 正常提升(Promotion)

上面提到了年輕代的垃圾回收,如果對象能夠熬過年輕代垃圾回收,它的年齡(age)就會加一,當對象的年齡達到一定閾值,就會被移動到老年代中。

  • 分配擔保

如果年輕代的空間不足,又有新的對象需要分配空間,就需要依賴其他內存(這里是老年代)進行分配擔保,對象將直接在老年代創建。

  • 大對象直接在老年代分配

超出某個閾值大小的對象,將直接在老年代分配,可以通過
-XX:PretenureSizeThreshold 配置這個閾值。

  • 動態對象年齡判定

有的垃圾回收算法,并不要求 age 必須達到 15 才能晉升到老年代,它會使用一些動態的計算方法。比如 G1,通過 TargetSurvivorRatio 這個參數,動態更改對象提升的閾值。

老年代的空間一般比較大,回收的時間更長,當老年代的空間被占滿了,將發生老年代垃圾回收。

目前,被廣泛使用的是 G1 垃圾回收器。G1 的目標是用來干掉 CMS 的,它同樣有年輕代和老年代的概念。不過,G1 把整個堆切成了很多份,把每一份當作一個小目標,部分上目標很容易達成。

程序員必備:JVM核心知識點總結

 

如上圖,G1 也是有 Eden 區和 Survivor 區的概念的,只不過它們在內存上不是連續的,而是由一小份一小份組成的。G1 在進行垃圾回收的時候,將會根據最大停頓時間(MaxGCPauseMillis)設置的值,動態地選取部分小堆區進行垃圾回收。

G1 的配置非常簡單,我們只需要配置三個參數,一般就可以獲取優異的性能:

  • MaxGCPauseMillis 設置最大停頓的預定目標,G1 垃圾回收器會自動調整,選取特定的小堆區;
  • G1HeapRegionSize 設置小堆區的大小;
  • InitiatingHeapOccupancyPercent當整個堆內存使用達到一定比例(默認是45%),并發標記階段就會被啟動。

逃逸分析

下面著重講解一下逃逸分析,這個知識點在面試的時候經常會被問到。

我們常說的對象,除了基本數據類型,一定是在堆上分配的嗎?

答案是否定的,通過逃逸分析,JVM 能夠分析出一個新的對象的使用范圍,從而決定是否要將這個對象分配到堆上。逃逸分析現在是 JVM 的默認行為,可以通過參數 -XX:-DoEscapeAnalysis 關掉它。

那什么樣的對象算是逃逸的呢?可以看一下下面的兩種典型情況。

如代碼所示,對象被賦值給成員變量或者靜態變量,可能被外部使用,變量就發生了逃逸。

public class EscapeAttr {
    Object attr;
    public void test() {
        attr = new Object();
    }
}

再看下面這段代碼,對象通過 return 語句返回。由于程序并不能確定這個對象后續會不會被使用,外部的線程能夠訪問到這個結果,對象也發生了逃逸。

 public class EscapeReturn {
    Object attr;
    public Object test() {
        Object obj = new Object();
        return obj;
    }
}

那逃逸分析有什么好處呢? 1. 棧上分配

如果一個對象在子程序中被分配,指向該對象的指針永遠不會逃逸,對象有可能會被優化為棧分配。棧分配可以快速地在棧幀上創建和銷毀對象,不用再分配到堆空間,可以有效地減少 GC 的壓力。

2. 分離對象或標量替換

但對象結構通常都比較復雜,如何將對象保存在棧上呢?

JIT 可以將對象打散,全部替換為一個個小的局部變量,這個打散的過程,就叫作標量替換(標量就是不能被進一步分割的變量,比如 int、long 等基本類型)。也就是說,標量替換后的對象,全部變成了局部變量,可以方便地進行棧上分配,而無須改動其他的代碼。

從上面的描述我們可以看到,并不是所有的對象或者數組,都會在堆上分配。由于JIT的存在,如果發現某些對象沒有逃逸出方法,那么就有可能被優化成棧分配。

3.同步消除

如果一個對象被發現只能從一個線程被訪問到,那么對于這個對象的操作可以不考慮同步。

注意這是針對 synchronized 來說的,JUC 中的 Lock 并不能被消除。

要開啟同步消除,需要加上-XX:+EliminateLocks參數。由于這個參數依賴逃逸分析,所以同時要打開-XX:+DoEscapeAnalysis 選項。

JVM 常見優化參數

現在大家用得最多的 Java 版本是 Java 8,如果你的公司比較保守,那么使用較多的垃圾回收器就是 CMS 。但 CMS 已經在 Java 14 中被正式廢除,隨著 ZGC 的誕生和 G1 的穩定,CMS 終將成為過去式。

Java 9 之后,Java 版本已經進入了快速發布階段,大約是每半年發布一次,Java 8 和 Java 11 是目前支持的 LTS 版本。

由于 JVM 一直處在變化之中,所以一些參數的配置并不總是有效的。有時候你加入一個參數,“感覺上”運行速度加快了,但通過 -XX:+PrintFlagsFinal來查看,卻發現這個參數默認就是這樣了。

所以,在不同的 JVM 版本上,不同的垃圾回收器上,要先看一下這個參數默認是什么,不要輕信別人的建議,命令行示例如下:

java -XX:+PrintFlagsFinal -XX:+UseG1GC 2>&1 | grep UseAdaptiveSizePolicy

還有一個與之類似的參數叫作PrintCommandLineFlags,通過它,你能夠查看當前所使用的垃圾回收器和一些默認的值。

可以看到下面的 JVM 默認使用的就是并行收集器:

# java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=127905216 -XX:MaxHeapSize=2046483456 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
openjdk version "1.8.0_41"
OpenJDK Runtime Environment (build 1.8.0_41-b04)
OpenJDK 64-Bit Server VM (build 25.40-b25, mixed mode)

下面我大致羅列一些JVM調優參數:

堆相關參數

-Xms10g :JVM啟動時申請的初始堆內存值
-Xmx20G :JVM可申請的最大Heap值
-Xmn3g : 新生代大小,一般設置為堆空間的1/3 1/4左右,新生代大則老年代小
-Xss :Java每個線程的Stack大小
-XX:PermSize :持久代(方法區)的初始內存大小
-XX:MaxPermSize : 持久代(方法區)的最大內存大小
-XX:SurvivorRatio : 設置新生代eden空間和from/to空間的比例關系,關系(eden/from=eden/to)
-XX:NewRatio : 設置新生代和老年代的比例老年代/新生代

日志相關參數

-XX:+PrintGC :打印GC日志
-XX:+PrintGCDetailsGC :時的詳細堆信息
-XX:+PrintHeapAtGC :打印GC前后的堆信息
-XX:+PrintGCTimeStamps :輸出GC發生時間,輸出的時間為虛擬機啟動的偏移量
-XX:+PrintGCApplicationConcurrentTime :輸出應用程序執行時間
-XX:+PrintGCApplicationStoppedTime :輸出應用程序由于GC產生停頓的時間
-XX:+PrintRefrenceGC :輸出軟引用、弱引用、虛引用和Finalize隊列
-XX:+HeapDumpOnOutOfMemoryError :產生OOM時可以在內存溢出時導出整個堆信息
-XX:HeapDumpPath :導出堆文件存放路徑
-XX:+TraceClassLoading :跟蹤類加載信息
-XX:+TraceClassUnloading :跟:蹤類卸載信息
-XX:PrintClassHitogram :查看系統中的類的分布情況(占用空間最多、實例數量空間大小)
-XX:+PrintVMOptions :打印虛擬機接收到的命令行顯示參數
-XX:+PrintCommandLineFlags :打印虛擬機的顯式和隱式參數
-XX:+PrintFlagsFinal :打印虛擬機的所有系統參數

GC相關參數

-XX:+UseSerialGC :新生代、老年代使用串行收集器
-XX:SurvivorRatio :設置eden區和survivor區大小的比例
-XX:PretenureSizeThreshold,:當對象大小超過此值時,直接分配到老年代
-XX:MaxTenuringThreshold :設置對象進入老年代的最大年齡
-XX:+UseParNewGC :新生代使用并行收集器
-XX:+UseParallelOldGC :老年代使用并行回收收集器
-XX:+ParallelGCThreads :設置垃圾回收線程數,一般最好與CPU數量相當,默認情況下,當CPU數量小于8個時,ParallelGCThreads的值相當于CPU數量,當CPU數量大于8個時,ParallelGCThreads的值等于3+((5*CPU_COUNT)/8
-XX:MaxGCPauseMillis :設置最大垃圾收集停頓時間
-XX:GCTimeRatio :設置吞吐量大小,它的值是一個0~100之間的整數,假設值為n,那么系統將花費不超過1/(1+n)的時間用于垃圾收集
-XX:+UseAdaptiveSizePolicy :打開自適應GC策略,JVM對新生代的大小、eden和survivior的比例、晉升老年代對象年齡等參數自動調整
-XX:+UseConcMarkSweepGC :啟用CMS
-XX:ParallelCMSThreads :設置CMS線程數量
-XX:CMSInitiatingOccupancyFraction :默認68當老年代的空間超過68%時會執行一次CMS回收
-XX:UseCMSCompactAtFullCollection :設置CMS結束后是否需要進行一次內存空間整理
-XX:CMSFullGCsBeforeCompaction :進行多少次CMS后進行內存空間壓縮
-XX:+CMSClassUnloadingEnabled :允許對類元數據區進行回收
-XX:CMSInitiatingPermOccupancyFraction :當永久區占用率達到此值時進行CMS回收(須激活CMSClassUnloadingEnabled)
-XX:UseCMSInitiatingOccupancyOnly:只要達到閾值時進行CMS回收
-XX:+UseG1GC :使用G1
-XX:MaxGCPauseMillis :最大垃圾收集停頓時間
-XX:GCPauseIntervalMillis :最大停頓間隔時間

JVM 的參數配置繁多,但大多數不需要我們去關心。

調優案例

下面,我們通過對 ES 服務的 JVM 參數分析,來看一下常見的優化點。

ElasticSearch(簡稱 ES)是一個高性能的開源分布式搜索引擎。ES 是基于 Java 語言開發的,既然是Java開發,那肯定會涉及到JVM調優了,在它的 conf 目錄下,有一個叫作jvm.options的文件,JVM 的配置就放在這里。

堆空間的配置

下面是 ES 對于堆空間大小的配置。

-Xms1g
-Xmx1g

JVM 中空間最大的一塊就是堆,垃圾回收也主要是針對這塊區域。通過 Xmx 可指定堆的最大值,通過 Xms 可指定堆的初始大小。我們通常把這兩個參數,設置成一樣大小的,可避免堆空間在動態擴容時的時間開銷。

在配置文件中還有AlwaysPreTouch這個參數。

-XX:+AlwaysPreTouch

其實,通過 Xmx 指定了的堆內存,只有在 JVM 真正使用的時候,才會進行分配。這個參數,在 JVM 啟動的時候,就把它所有的內存在操作系統分配了。在堆比較大的時候,會加大啟動時間,但它能夠減少內存動態分配的性能損耗,提高運行時的速度。

如下圖,JVM 的內存,分為堆和堆外內存,其中堆的大小可以通過 Xmx 和 Xms 來配置。

程序員必備:JVM核心知識點總結

 

但我們在配置 ES 的堆內存時,通常把堆的初始化大小,設置成物理內存的一半。這是因為 ES 是存儲類型的服務,我們需要預留一半的內存給文件緩存 ,等下次用到相同的文件時,就不用與磁盤進行頻繁的交互。這一塊區域一般叫作 PageCache,占用的空間很大。

對于計算型節點來說,比如我們普通的 Web 服務,通常會把堆內存設置為物理內存的 2/3,剩下的 1/3 就是給堆外內存使用的。

我們這張圖,對堆外內存進行了非常細致的劃分,解釋如下:

  • 元空間 參數 -XX:MaxMetaspaceSize 和 -XX:MetaspaceSize,分別指定了元空間的最大內存和初始化內存。因為元空間默認是沒有上限的,所以極端情況下,元空間會一直擠占操作系統剩余內存。
  • JIT 編譯后代碼存放 -XX:ReservedCodeCacheSize。JIT 是 JVM 一個非常重要的特性,CodeCahe 存放的,就是即時編譯器所生成的二進制代碼。另外,JNI 的代碼也是放在這里的。
  • 本地內存 本地內存是一些其他 attch 在 JVM 進程上的內存區域的統稱。比如網絡連接占用的內存、線程創建占用的內存等。在高并發應用下,由于連接和線程都比較多,這部分內存累加起來還是比較可觀的。
  • 直接內存 這里要著重提一下直接內存,因為它是本地內存中唯一可以使用參數來限制大小的區域。使用參數 -XX:MaxDirectMemorySize,即可設定 ByteBuffer 類所申請的內存上限。
  • JNI 內存 上面談到 CodeCache 存放的 JNI 代碼,JNI 內存就是指的這部分代碼所 malloc 的具體內存。很可惜的是,這部分內存的使用 JVM 是無法控制的,它依賴于具體的 JNI 代碼實現。

日志參數配置

下面是 ES 的日志參數配置,由于 Java 8 和 Java 9 的參數配置已經完全不一樣了,ES 在這里也分了兩份。

Java8參數設置:

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-Xloggc:logs/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=32
-XX:GCLogFileSize=64m

Java9參數設置:

-Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m

下面解釋一下這些參數的意義,以 Java 8 為例。

  • PrintGCDetails 打印詳細 GC 日志。
  • PrintGCDateStamps 打印當前系統時間,更加可讀;與之對應的是 PrintGCTimeStamps,打印的是 JVM 啟動后的相對時間,可讀性較差。
  • PrintTenuringDistribution 打印對象年齡分布,對調優 MaxTenuringThreshold 參數幫助很大。
  • PrintGCApplicationStoppedTime 打印 STW 時間
  • 下面幾個日志參數是配置了類似于 Logback 的滾動日志,比較簡單,不再詳細介紹

從 Java 9 開始,JVM 移除了 40 多個 GC 日志相關的參數,具體參見 JEP 158。所以這部分的日志配置有很大的變化,GC 日志的打印方式,已經完全不一樣了,比以前的日志參數規整了許多。

參數如下所示:

-Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m

再來看下 ES 在異常情況下的配置參數:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=data
-XX:ErrorFile=logs/hs_err_pid%p.log


HeapDumpOnOutOfMemoryError、HeapDumpPath、ErrorFile是每個 Java 應用都應該配置的參數。正常情況下,我們通過 jmap 獲取應用程序的堆信息;異常情況下,比如發生了 OOM,通過這三個配置參數,即可在發生OOM的時候,自動 dump 一份堆信息到指定的目錄中。

拿到了這份 dump 信息,我們就可以使用 MAT 等工具詳細分析,找到具體的 OOM 原因。

垃圾回收器配置

ES 默認使用 CMS 垃圾回收器,它有以下三行主要的配置。

-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

下面介紹一下這兩個參數:

  • UseConcMarkSweepGC,表示年輕代使用 ParNew,老年代的用 CMS 垃圾回收器
  • -XX:CMSInitiatingOccupancyFraction 由于 CMS 在執行過程中,用戶線程還需要運行,那就需要保證有充足的內存空間供用戶使用。如果等到老年代空間快滿了,再開啟這個回收過程,用戶線程可能會產生“Concurrent Mode Failure”的錯誤,這時會臨時啟用 Serial Old 收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了(STW)。

這部分空間預留,一般在 30% 左右即可,那么能用的大概只有 70%。參數
-XX:CMSInitiatingOccupancyFraction 用來配置這個比例,但它首先必須配置 -XX:+UseCMSInitiatingOccupancyOnly 參數。

另外,對于 CMS 垃圾回收器,常用的還有下面的配置參數:

  • -XX:ExplicitGCInvokesConcurrent 當代碼里顯示的調用了 System.gc(),實際上是想讓回收器進行FullGC,如果發生這種情況,則使用這個參數開始并行 FullGC。建議加上。
  • -XX:CMSFullGCsBeforeCompaction 默認為 0,就是每次FullGC都對老年代進行碎片整理壓縮,建議保持默認。
  • -XX:CMSScavengeBeforeRemark 開啟或關閉在 CMS 重新標記階段之前的清除(YGC)嘗試。可以降低 remark 時間,建議加上。
  • -XX:+ParallelRefProcEnabled 可以用來并行處理 Reference,以加快處理速度,縮短耗時。

CMS 垃圾回收器,已經在 Java14 中被移除,由于它的 GC 時間不可控,有條件應該盡量避免使用。

針對 Java10(普通 Java 應用在 Java 8 中即可開啟 G1),ES 可采用 G1 垃圾回收器。

G1垃圾收集器,它可以通過配置參數 MaxGCPauseMillis,指定一個期望的停頓時間,使用相對比較簡單。

下面是主要的配置參數:

  • -XX:MaxGCPauseMillis 設置目標停頓時間,G1 會盡力達成。
  • -XX:G1HeapRegionSize 設置小堆區大小。這個值為 2 的次冪,不要太大,也不要太小。如果是在不知道如何設置,保持默認。
  • -XX:InitiatingHeapOccupancyPercent 當整個堆內存使用達到一定比例(默認是45%),并發標記階段就會被啟動。
  • -XX:ConcGCThreads 并發垃圾收集器使用的線程數量。默認值隨 JVM 運行的平臺不同而不同。不建議修改。

JVM 支持非常多的垃圾回收器,下面是最常用的幾個,以及配置參數:

  • -XX:+UseSerialGC 年輕代和老年代都用串行收集器
  • -XX:+UseParallelGC 年輕代使用 ParallerGC,老年代使用 Serial Old
  • -XX:+UseParallelOldGC 新生代和老年代都使用并行收集器
  • -XX:+UseG1GC 使用 G1 垃圾回收器
  • -XX:+UseZGC 使用 ZGC 垃圾回收器

額外配置

我們再來看下幾個額外的配置。

-Xss1m

-Xss設置每個 Java 虛擬機棧的容量為 1MB。這個參數和 -XX:ThreadStackSize是一樣的,默認就是 1MB。

-XX:-OmitStackTraceInFastThrow

把 - 換成 +,可以減少異常棧的輸出,進行合并。雖然會對調試有一定的困擾,但能在發生異常時顯著增加性能。隨之而來的就是異常信息不好排查,ES 為了找問題方便,就把錯誤合并給關掉了。

-Djava.awt.headless=true

Headless 模式是系統的一種配置模式,在該模式下,系統缺少了顯示設備、鍵盤或鼠標。在服務器上一般是沒這些設備的,這個參數是告訴虛擬機使用軟件去模擬這些設備。

-Djava.locale.providers=COMPAT
-Dfile.encoding=UTF-8
-Des.networkaddress.cache.ttl=60
-Des.networkaddress.cache.negative.ttl=10
-Dio.netty.noUnsafe=true
-Dio.netty.noKeySetOptimization=true
-Dio.netty.recycler.maxCapacityPerThread=0
-Dlog4j.shutdownHookEnabled=false
-Dlog4j2.disable.jmx=true
-Djava.io.tmpdir=${ES_TMPDIR}
-Djna.nosys=true

上面這些參數,通過 -D 參數,在啟動一個 Java 程序時,設置系統屬性值,也就是在 System 類中通過getProperties()得到的一串系統屬性。

這部分自定義性比較強,不做過多介紹。

其他調優

以上就是 ES 默認的 JVM 參數配置,大多數還是比較基礎的。在平常的應用服務中,我們希望得到更細粒度的控制,其中比較常用的就是調整各個分代之間的比例。

  • -Xmn 年輕代大小,默認年輕代占堆大小的 1/3。高并發快消亡場景可適當加大這個區域,對半或者更多都是可以的。但是在 G1 下,就不用再設置這個值了,它會自動調整;
  • -XX:SurvivorRatio 默認值為 8,表示伊甸區和幸存區的比例;
  • -XX:MaxTenuringThreshold 這個值在 CMS 下默認為 6,G1 下默認為 15。這個值和我們前面提到的對象提升有關,改動效果會比較明顯。對象的年齡分布可以使用-XX:+PrintTenuringDistribution打印,如果后面幾代的大小總是差不多,證明過了某個年齡后的對象總能晉升到老年代,就可以把晉升閾值設的小一些;
  • PretenureSizeThreshold 超過一定大小的對象,將直接在老年代分配,不過這個參數用得不是很多。

分享到:
標簽:JVM
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定