JVM調優實戰及常量池詳解

阿里巴巴Arthas詳解

Arthas

Alibaba

在 2018 年 9 月開源的

Java 診斷

工具。支援

JDK6+

, 採用命令列互動模式,可以方便的定位和診斷線上程式執行問題。

Arthas

官方文件十分詳細,詳見:

https://alibaba。github。io/arthas

Arthas使用場景

得益於

Arthas

強大且豐富的功能,讓

Arthas

能做的事情超乎想象。下面僅僅列舉幾項常見的使用情況,更多的使用場景可以在熟悉了

Arthas

之後自行探索。

是否有一個全域性視角來檢視系統的執行狀況?

為什麼 CPU 又升高了,到底是哪裡佔用了 CPU ?

執行的多執行緒有死鎖嗎?有阻塞嗎?

程式執行耗時很長,是哪裡耗時比較長呢?如何監測呢?

這個類從哪個 jar 包載入的?為什麼會報各種類相關的 Exception?

我改的程式碼為什麼沒有執行到?難道是我沒 commit?分支搞錯了?

遇到問題無法在線上 debug,難道只能透過加日誌再重新發布嗎?

有什麼辦法可以監控到 JVM 的實時執行狀態?

Arthas使用

# github下載arthas wget https://alibaba。github。io/arthas/arthas-boot。jar # 或者 Gitee 下載 wget https://arthas。gitee。io/arthas-boot。jar

用java -jar執行即可,可以識別機器上所有Java程序(我們這裡之前已經運行了一個Arthas測試程式,程式碼見下方)

Arthas使用

# github下載arthas wget https://alibaba。github。io/arthas/arthas-boot。jar # 或者 Gitee 下載 wget https://arthas。gitee。io/arthas-boot。jar

用java -jar執行即可,可以識別機器上所有Java程序(我們這裡之前已經運行了一個Arthas測試程式,程式碼見下方)

public class LogData { private static HashSet hashSet = new HashSet(); public static void main(String[] args) { // 模擬 CPU 過高 cpuHigh(); // 模擬執行緒死鎖 deadThread(); // 不斷的向 hashSet 集合增加資料 addHashSetThread(); } /** * 不斷的向 hashSet 集合新增資料 */ public static void addHashSetThread() { // 初始化常量 new Thread(() -> { int count = 0; while (true) { try { hashSet。add(“count” + count); Thread。sleep(1000); count++; } catch (InterruptedException e) { e。printStackTrace(); } } })。start(); } public static void cpuHigh() { new Thread(() -> { while (true) { } })。start(); } /** * 死鎖 */ private static void deadThread() { /** 建立資源 */ Object resourceA = new Object(); Object resourceB = new Object(); // 建立執行緒 Thread threadA = new Thread(() -> { synchronized (resourceA) { System。out。println(Thread。currentThread() + “ get ResourceA”); try { Thread。sleep(1000); } catch (InterruptedException e) { e。printStackTrace(); } System。out。println(Thread。currentThread() + “waiting get resourceB”); synchronized (resourceB) { System。out。println(Thread。currentThread() + “ get resourceB”); } } }); Thread threadB = new Thread(() -> { synchronized (resourceB) { System。out。println(Thread。currentThread() + “ get ResourceB”); try { Thread。sleep(1000); } catch (InterruptedException e) { e。printStackTrace(); } System。out。println(Thread。currentThread() + “waiting get resourceA”); synchronized (resourceA) { System。out。println(Thread。currentThread() + “ get resourceA”); } } }); threadA。start(); threadB。start(); }}

選擇程序序號1,進入程序資訊操作

JVM調優實戰及常量池詳解

輸入

dashboard

可以檢視整個程序的執行情況,執行緒、記憶體、GC、執行環境資訊:

JVM調優實戰及常量池詳解

輸入

thread

可以檢視執行緒詳細情況

輸入

thread加上執行緒ID

可以檢視執行緒堆疊

輸入

thread -b

可以檢視執行緒死鎖

輸入 jad加類的全名 可以反編譯,這樣可以方便我們檢視線上程式碼是否是正確的版本

使用 ognl 命令可以檢視線上系統變數的值,甚至可以修改變數的值

GC日誌詳解

對於java應用我們可以透過一些配置把程式執行過程中的gc日誌全部打印出來,然後分析gc日誌得到關鍵性指標,分析GC原因,調優JVM引數。

列印GC日誌方法,在JVM引數裡增加引數,

%t 代表時間

-Xloggc:。/gc-%t。log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M

Tomcat則直接加在

JAVA_OPTS變數裡。

如何分析GC日誌

執行程式加上對應gc日誌

java -jar -Xloggc:。/gc-%t。log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M portrait-v1。0。1。jar

JVM調優實戰及常量池詳解

從日誌可以發現幾次fullgc都是由於元空間不夠導致的,所以我們可以將元空間調大點

java -jar -Xloggc:。/gc-adjust-%t。log -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M portrait-v1。0。1。jar

CMS

-Xloggc:d:/gc-cms-%t。log -Xms50M -Xmx50M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC

G1

-Xloggc:d:/gc-g1-%t。log -Xms50M -Xmx50M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M -XX:+UseG1GC

上面的這些引數,能夠幫我們檢視分析GC的垃圾收集情況。但是如果GC日誌很多很多,成千上萬行。就算你一目十行,看完了,腦子也是一片空白。所以我們可以藉助一些功能來幫助我們分析,這裡推薦一個gceasy(

https://gceasy。io

),可以上傳gc檔案,然後他會利用視覺化的介面來展現GC情況。具體下圖所示

JVM調優實戰及常量池詳解

JVM調優實戰及常量池詳解

JVM引數彙總檢視命令

java -XX:+PrintFlagsInitial 表示打印出所有引數選項的預設值

java -XX:+PrintFlagsFinal 表示打印出所有引數選項在執行程式時生效的值

Class常量池與執行時常量池

Class常量池可以理解為是Class檔案中的資源倉庫。 Class檔案中除了包含類的版本、欄位、方法、介面等描述資訊外,還有一項資訊就是

常量池(constant pool table)

,用於存放編譯期生成的各種

字面量(Literal)和符號引用(Symbolic References)

字面量

字面量就是指由字母、數字等構成的字串或者數值常量

字面量只可以右值出現,所謂右值是指等號右邊的值,如:int a=1 這裡的a為左值,1為右值。在這個例子中1就是字面量。

int a = 1; int b = 2; int c = “abcdefg”; int d = “abcdefg”;

符號引用

符號引用是編譯原理中的概念,是相對於直接引用來說的。主要包括了以下三類常量:

類和介面的全限定名

欄位的名稱和描述符

方法的名稱和描述符

上面的a,b就是欄位名稱,就是一種符號引用,還有Math類常量池裡的 Lcom/tuling/jvm/Math 是類的全限定名,main和compute是方法名稱,()是一種UTF8格式的描述符,這些都是符號引用。

這些常量池現在是靜態資訊,只有到執行時被載入到記憶體後,這些符號才有對應的記憶體地址資訊,這些常量池一旦被裝入記憶體就變成

執行時常量池

,對應的符號引用在程式載入或執行時會被轉變為被載入到記憶體區域的程式碼的直接引用,也就是我們說的

動態連結了。例如,compute()這個符號引用在執行時就會被轉變為compute()方法具體程式碼在記憶體中的地址,主要透過物件頭裡的型別指標去轉換直接引用。

字串常量池

字串常量池的設計思想

字串的分配,和其他的物件分配一樣,耗費高昂的時間與空間代價,作為最基礎的資料型別,大量頻繁的建立字串,極大程度地影響程式的效能

JVM為了提高效能和減少記憶體開銷,在例項化字串常量的時候進行了一些最佳化

為字串開闢一個字串常量池,類似於快取區

建立字串常量時,首先查詢字串常量池是否存在該字串

存在該字串,返回引用例項,不存在,例項化該字串並放入池中

三種字串操作(Jdk1.7 及以上版本)

直接賦值字串

String s = “huangege”; // s指向常量池中的引用

這種方式建立的字串物件,只會在常量池中。

因為有“

huangege

”這個字面量,建立物件s的時候,JVM會先去常量池中透過 equals(key) 方法,判斷是否有相同的物件

如果有,則直接返回該物件在常量池中的引用;

如果沒有,則會在常量池中建立一個新物件,再返回引用。

new String();

String s1 = new String(“huangege”); // s1指向記憶體中的物件引用

這種方式會保證字串常量池和堆中都有這個物件,沒有就建立,最後返回堆記憶體中的物件引用。

步驟大致如下:

因為有“

huangege

”這個字面量,所以會先檢查字串常量池中是否存在字串“

huangege

不存在,先在字串常量池裡建立一個字串物件;再去記憶體中建立一個字串物件“

huangege

”;

存在的話,就直接去堆記憶體中建立一個字串物件“

huangege

”;

最後,將記憶體中的引用返回。

intern方法

String s1 = new String(“huangege”); String s2 = s1。intern(); System。out。println(s1 == s2); //false

String中的intern方法是一個 native 的方法,當呼叫 intern方法時,如果池已經包含一個等於此String物件的字串(用equals(oject)方法確定),則返回池中的字串。

否則,將intern返回的引用指向當前字串 s1

jdk1.6版本需要將 s1 複製到字串常量池裡

字串常量池位置

Jdk1。6及之前: 有永久代, 執行時常量池在永久代,執行時常量池包含字串常量池

Jdk1。7:有永久代,但已經逐步“去永久代”,字串常量池從永久代裡的執行時常量池分離到堆裡

Jdk1。8及之後: 無永久代,執行時常量池在元空間,字串常量池裡依然在堆裡

字串常量池設計原理

字串常量池底層是hotspot的C++實現的,底層類似一個 HashTable, 儲存的本質上是字串物件的引用。

看一道比較常見的面試題,下面的程式碼建立了多少個 String 物件?

String s1 = new String(“he”) + new String(“llo”); String s2 = s1。intern(); System。out。println(s1 == s2); // 在 JDK 1。6 下輸出是 false,建立了 6 個物件 // 在 JDK 1。7 及以上的版本輸出是 true,建立了 5 個物件 // 當然我們這裡沒有考慮GC,但這些物件確實存在或存在過

為什麼輸出會有這些變化呢?主要還是字串池從永久代中脫離、移入堆區的原因, intern() 方法也相應發生了變化:

1、在 JDK 1。6 中,呼叫 intern() 首先會在字串池中尋找 equal() 相等的字串,假如字串存在就返回該字串在字串池中的引用;假如字串不存在,虛擬機器會重新在永久代上建立一個例項,將 StringTable 的一個表項指向這個新建立的例項。

JVM調優實戰及常量池詳解

2、在 JDK 1。7 (及以上版本)中,由於字串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的物件。字串存在時和 JDK 1。6一樣,但是字串不存在時不再需要重新建立例項,可以直接指向堆上的例項。

JVM調優實戰及常量池詳解

由上面兩個圖,也不難理解為什麼 JDK 1。6 字串池溢位會丟擲 OutOfMemoryError: PermGen space ,而在 JDK 1。7 及以上版本丟擲 OutOfMemoryError: Java heap space 。

String常量池問題的幾個例子

示例1:

String s0=“huangege”;

String s1=“huangege”;

String s2=“huange” + “ge”;

System。out。println( s0==s1 ); //true

System。out。println( s0==s2 ); //true

分析:因為例子中的 s0和s1中的”huangege”都是字串常量,它們在編譯期就被確定了,所以s0==s1為true;而”huange”和”ge”也都是字串常量,當一個字 符串由多個字串常量連線而成時,它自己肯定也是字串常量,所以s2也同樣在編譯期就被最佳化為一個字串常量“huangege”,所以s2也是常量池中” huangege”的一個引用。所以我們得出s0==s1==s2;

String a = “a1”;String b = “a” + 1;System。out。println(a == b); // true String a = “atrue”;String b = “a” + “true”;System。out。println(a == b); // true String a = “a3。4”;String b = “a” + 3。4;System。out。println(a == b); // true

分析:JVM對於字串常量的“+”號連線,將在程式編譯期,JVM就將常量字串的“+”連線最佳化為連線後的值,拿“a” + 1來說,經編譯器最佳化後在class中就已經是a1。在編譯期其字串常量的值就確定下來,故上面程式最終的結果都為true。

示例4:

String a = “ab”; String bb = “b”; String b = “a” + bb; System。out。println(a == b); // false

分析:JVM對於字串引用,由於在字串的“+”連線中,有字串引用存在,而引用的值在程式編譯期是無法確定的,即“a” + bb無法被編譯器最佳化,只有在程式執行期來動態分配並將連線後的新地址賦給b。所以上面程式的結果也就為false。

示例5:

String a = “ab”; final String bb = “b”; String b = “a” + bb; System。out。println(a == b); // true

分析:和示例4中唯一不同的是bb字串加了final修飾,對於final修飾的變數,它在編譯時被解析為常量值的一個本地複製儲存到自己的常量池中或嵌入到它的位元組碼流中。所以此時的“a” + bb和“a” + “b”效果是一樣的。故上面程式的結果為true。

示例6:

String a = “ab”; final String bb = getBB(); String b = “a” + bb; System。out。println(a == b); // false private static String getBB() { return “b”; }

分析:JVM對於字串引用bb,它的值在編譯期無法確定,只有在程式執行期呼叫方法後,將方法的返回值和“a”來動態連線並分配地址為b,故上面 程式的結果為false。

關於String是不可變的

透過上面例子可以得出得知:

String s = “a” + “b” + “c”; //就等價於String s = “abc”; String a = “a”; String b = “b”; String c = “c”; String s1 = a + b + c;

s1 這個就不一樣了,可以透過觀察其

JVM指令碼

發現s1的“+”操作會變成如下操作:

StringBuilder temp = new StringBuilder(); temp。append(a)。append(b)。append(c); String s = temp。toString();

最後再看一個例子

//字串常量池:“計算機”和“技術” 堆記憶體:str1引用的物件“計算機技術” //堆記憶體中還有個StringBuilder的物件,但是會被gc回收,StringBuilder的toString方法會new String(),這個String才是真正返回的物件引用 String str2 = new StringBuilder(“計算機”)。append(“技術”)。toString(); //沒有出現“計算機技術”字面量,所以不會在常量池裡生成“計算機技術”物件 System。out。println(str2 == str2。intern()); //true//“計算機技術” 在池中沒有,但是在heap中存在,則intern時,會直接返回該heap中的引用//字串常量池:“ja”和“va” 堆記憶體:str1引用的物件“java” //堆記憶體中還有個StringBuilder的物件,但是會被gc回收,StringBuilder的toString方法會new String(),這個String才是真正返回的物件引用 String str1 = new StringBuilder(“ja”)。append(“va”)。toString(); //沒有出現“java”字面量,所以不會在常量池裡生成“java”物件 System。out。println(str1 == str1。intern()); //false//java是關鍵字,在JVM初始化的相關類裡肯定早就放進字串常量池了 String s1=new String(“test”); System。out。println(s1==s1。intern()); //false//“test”作為字面量,放入了池中,而new時s1指向的是heap中新生成的string物件,s1。intern()指向的是“test”字面量之前在池中生成的字串物件 String s2=new StringBuilder(“abc”)。toString(); System。out。println(s2==s2。intern()); //false

八種基本型別的包裝類和物件池

java中基本型別的包裝類的大部分都實現了常量池技術(嚴格來說應該叫

物件池,

在堆上),這些類是Byte,Short,Integer,Long,Character,Boolean,另外兩種浮點數型別的包裝類則沒有實現。另外Byte,Short,Integer,Long,Character這5種整型的包裝類也只是在對應值小於等於127時才可使用物件池,也即物件不負責建立和管理大於127的這些類的物件。因為一般這種比較小的數用到的機率相對較大。

package com。yundasys。usercenter。channelclosedloop。sign。db。kafka;/** * @program: usercenter-userportrait-channelclosedloop * @description: Test * @author: yxh-word * @create: 2021-08-24 * @version: v1。0。0 建立檔案, yxh-word, 2021-08-24 **/public class Test { public static void main(String[] args) { //5種整形的包裝類Byte,Short,Integer,Long,Character的物件, //在值小於127時可以使用物件池 Integer i1 = 127; //這種呼叫底層實際是執行的Integer。valueOf(127),裡面用到了IntegerCache物件池 Integer i2 = 127; System。out。println(i1 == i2);//輸出true //值大於127時,不會從物件池中取物件 Integer i3 = 128; Integer i4 = 128; System。out。println(i3 == i4);//輸出false //用new關鍵詞新生成物件不會使用物件池 Integer i5 = new Integer(127); Integer i6 = new Integer(127); System。out。println(i5 == i6);//輸出false //Boolean類也實現了物件池技術 Boolean bool1 = true; Boolean bool2 = true; System。out。println(bool1 == bool2);//輸出true //浮點型別的包裝類沒有實現物件池技術 Double d1 = 1。0; Double d2 = 1。0; System。out。println(d1 == d2);//輸出false }}