7.JVM調優-方法區,堆,棧調優詳解

通常我們都知道在堆空間新生代Eden區滿了,會觸發minor GC, 在老年代滿了會觸發full GC, 觸發full GC會導致Stop The World, 那你們知道還有一個區域滿了一會觸發Full GC么?而且這個區域滿了會直接影響我們的開發效率。

一、方法區參數調優

我們可以對運行時數據區的記憶體進行參數設置. 這是jvm調優的重點. 參數的變化將影響到整體效率

核心參數設置如下:

java -Xms2048M -Xmx1024M -Xss512k -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar

這是一個通用的設置。圖中具體含義如下:

  • -Xms:堆空間最小值
  • -Xmx:堆空間最大值
  • -Xmn:新生代占堆空間的大小
  • -XX:MetaspaceSize:方法區(元空間)初始值
  • -XX:MaxMetaspaceSize:方法區(元空間)最大值
  • -Xss:每一個執行緒的空間大小

下面主要研究方法區參數設置

1. 方法區(元空間)參數設置

在jdk8之前有個區域叫做永久代, 在jdk8及以後改名字了, 叫做元空間. 這塊記憶體空間佔用的是直接的物理記憶體.

元空間有一個特點: 可以動態擴容。如果, 我們沒有設置元空間的上限, 那麼他可以擴大到整個記憶體. 比如記憶體條是8G的, 堆和棧分配了4G的空間, 那麼元空間最多可以使用4G。

我們可以通過參數來設置使用的元空間記憶體。

對於64位的JVM來說, 元空間默認大小是21M, 元空間的默認最大值是無上限的, 他的上限就是記憶體空間

  • -XX:MetaspaceSize: 元空間的初始空間大小, 以位元組位單位, 默認是21M,達到該值就會觸發full GC, 同時收集器會對該值進行調整, 如果釋放了大量的空間, 就適當降低該值, 如果釋放了很少的空間, 提升該值,但最到不超過-XX:MaxMetaspaceSize設置的值
比如: 
初始值是21M, 第一次回收了20M, 那麼只有1M沒有被回收, 下一次, 元空間會自動調整大小, 可能會調整到15M
初始大小依然是21M, 第二次回收發現回收了1M, 有20M沒有被回收, 他就會自動擴大空間, 可能擴大到30M,也可能是40M
  • -XX:MaxMetaspaceSize: 設置元空間的最大值, 默認是-1, 即不限制, 或者說只受限於本地記憶體的大小

由於調整元空間的大小需要full GC, 這是非常昂貴的操作, 如果應用在啟動的時候發生大量的full GC, 通常都是由於永久代或元空間發生了大小的調整, 基於這種情況, 一般建議在JVM參數中將-XX:MetaspaceSize和-XX:MaxMetaspaceSize設置成一樣的值, 並設置的比初始值還要大, 對於8G物理記憶體的機器來說, 一般會將這兩個值設置為256M或者512M都可以

2. 建議設置元空間值, 如果不設置會怎麼樣?

不設置元空間默認就是21M, 很容易就會放滿, 通常我們的war可能都是幾十M, 甚至幾個G. 如果我們在啟動程式的時候, 會啟動幾分鐘. 這很有可能是沒有設置元空間的大小.

放滿後會發生full GC, 然後在擴大一點元空間, 擴大到25M, 重新開始, 過了一會又放滿了, 再次full GC, 在擴大一點, 元空間擴大到30M, 就這樣一直發生full GC, 然後一直擴大元空間, 直到擴大的元空間大小合適, 不再發生full gc, 程式才會正常啟動運行. 這是個很耗時耗性能的操作, 這樣的full GC也是沒有必要的.

如果項目啟動較慢,多次重複啟動,考慮是不是元空間設置不合理,或者記憶體不夠導致。

二. 執行緒棧參數調優

-Xss512k:設置棧空間參數的

這個參數就是用來設置棧空間的. 他是設置的一個執行緒棧佔用的空間, 一個程式啟動後可能有多個執行緒棧, 那麼他們佔用的空間都是512k。

一個程式啟動以後,系統為其分配的棧空間是固定的。理論上來說,如果每一個執行緒佔用的空間少,那麼就能分配更多的執行緒。否則則相反。但這也不是確定的,還有和系統的cpu,記憶體有關係。

當執行緒分配的空間用完的時候,就會拋出棧溢出異常。下面來看一個例子

package com.lxl.jvm;

public class StackOverflowTest {
    /**
     * jvm 設置-Xss128M, (默認是1M)
     */
    static int count = 0;
    public static void redo(){
        count ++;
        redo();
    }

    public static void main(String[] args) {
        try {
            redo();
        }catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }

}

這裡定義了一個變數count, main方法里調用了redo()方法. 當我們執行main方法的時候, 執行緒棧模型是什麼樣的呢?

當程式執行到main方法的時候, 會在執行緒棧中開闢一個main方法的棧幀

繼續執行, 執行到redo()的時候, 會在執行緒棧在開闢一塊redo方法棧幀

redo方法里又調用了redo方法. 繼續開闢一塊redo方法棧幀,

…….

棧幀是佔用記憶體空間的. 總有一個時刻會把棧記憶體消耗完. 就會報棧記憶體溢出了

我們看到程式一共運行了16979次發生了棧溢出.

當棧空間設置的小一些呢?比如256k

我們運行看效果

當運行到2079次的時候, 發生了棧溢出。