大型Java進階專題(十一) 深入理解JVM (下)

前言

​ 前面我們瞭解了JVM相關的理論知識,這章節主要從實戰方面,去解讀JVM。

類加載機制

​ Java源代碼經過編譯器編譯成字節碼之後,最終都需要加載到虛擬機之後才能運行。虛擬機把描述類的數據從
Class 文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java 類型,這就是虛擬機的類加載機制。

類加載時機

​ 一個類型從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期將會經歷加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗證、準備、解析三個部分統稱爲連接(Linking)。這七個階段的發生順序下圖所示。

​ 上圖中,加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,類型的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是爲了支持Java語言的運行時綁定特性(也稱爲動態綁定或晚期綁定)。

​ 關於在什麼情況下需要開始類加載過程的第一個階段“加載”,《Java虛擬機規範》中並沒有進行強制約束,這點可以交給虛擬機的具體實現來自由把握。

​ 但是對於初始化階段,《Java虛擬機規範》則是嚴格規定了有且只有六種情況必須立即對類進行“初始化”(而加
載、驗證、準備自然需要在此之前開始):

  • 遇到new、getstatic、putstatic 或invokestatic 這4 條字節碼指令;
  • 使用java.lang.reflect 包的方法對類進行反射調用的時候;
  • 當初始化一個類的時候,發現其父類還沒有進行初始化的時候,需要先觸發其父類的初始化;
  • 當虛擬機啓動時,用戶需要指定一個要執行的主類,虛擬機會先初始化這個類;
  • 當使用JDK 1.7 的動態語言支持時,如果一個java.lang.invoke.MethodHandle 實例最後的解析結果
  • REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類沒有初始化。
  • 當一個接口中定義了JDK 8新加入的默認方法(被default關鍵字修飾的接口方法)時,如果有這個接口的實現。類發生了初始化,那該接口要在其之前被初始化。

​ 對於這六種會觸發類型進行初始化的場景,《Java虛擬機規範》中使用了一個非常強烈的限定語——“有且只有”,這六種場景中的行爲稱爲對一個類型進行主動引用。除此之外,所有引用類型的方式都不會觸發初始化,稱爲被動引用。

比如如下幾種場景就是被動引用:

  • 通過子類引用父類的靜態字段,不會導致子類的初始化;
  • 通過數組定義來引用類,不會觸發此類的初始化;
  • 常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化;

類加載過程

加載

在加載階段,Java虛擬機需要完成以下三件事情:

​ 通過一個類的全限定名來獲取定義此類的二進制字節流。

​ 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。

​ 在內存中生成一個代表這個類的java.lang. Class對象,作爲方法區這個類的各種數據的訪問入口。

驗證

​ 驗證是連接階段的第一步,這一階段的目的是確保Class文件的字節流中包含的信息符合《Java虛擬機規範》的全部約束要求,保證這些信息被當作代碼運行後不會危害虛擬機自身的安全。

驗證階段大致上會完成下面4 個階段的檢驗動作:

  • 文件格式驗證

    第一階段要驗證字節流是否符合Class 文件格式的規範,並且能夠被當前版本的虛擬機處理。驗證點主要包括:

    1. 是否以魔數0xCAFEBABE 開頭;
    2. 主、次版本號是否在當前虛擬機處理範圍之內;
    3. 常量池的常量中是否有不被支持的常量類型;
    4. Class 文件中各個部分及文件本身是否有被刪除的或者附加的其它信息等等。
  • 元數據驗證

    第二階段是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java 語言規範的要求,這個階段的驗證點包括:

    1. 這個類是否有父類;
    2. 這個類的父類是否繼承了不允許被繼承的類;
    3. 如果這個類不是抽象類,是否實現了其父類或者接口之中要求實現的所有方法;
    4. 類中的字段、方法是否與父類產生矛盾等等。
  • 字節碼驗證

    第三階段是整個驗證過程中最複雜的一個階段,主要目的是通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。

  • 符號引用驗證

    1. 最後一個階段的校驗發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動作將在連接的第三階段--解析階段中發生。
    2. 符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的各類信息進行匹配性校驗,通俗來說就是,該類是否缺少或者被禁止訪問它依賴的某些外部類、方法、字段等資源。

準備

​ 準備階段是正式爲類變量分配內存並設置類變量初始值的階段。

解析

​ 解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。

初始化

​ 類初始化階段是類加載過程中的最後一步,前面的類加載過程中,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全是由虛擬機主導和控制的。

​ 到了初始化階段,才真正開始執行類中定義的Java 程序代碼。

類加載器

​ 類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠超類加載階段。

​ 對於任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。

​ 這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class文件,被同一個Java虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

雙親委派模型

​ 從Java 虛擬機的角度來講,只存在兩種不同的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++ 來實現,是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都由Java 來實現,獨立於虛擬機外部,並且全都繼承自抽象類 java.lang.ClassLoader 。

​ 從Java 開發者的角度來看,類加載器可以劃分爲:

  • 啓動類加載器(Bootstrap ClassLoader):這個類加載器負責將存放在<java_home>\lib 目錄中的類庫加載到虛擬機內存中。啓動類加載器無法被Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給啓動類加載器,那直接使用null 代替即可;
  • 擴展類加載器(Extension ClassLoader):這個類加載器由 sun.misc.Launcher$ExtClassLoader 實現,它負責加載<java_home>\lib\ext 目錄中,或者被java.ext.dirs 系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器;
  • 應用程序類加載器(Application ClassLoader):這個類加載器由 sun.misc.Launcher$AppClassLoader 實現。 getSystemClassLoader() 方法返回的就是這個類加載器,因此也被稱爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫。開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

​ 我們的應用程序都是由這3 種類加載器互相配合進行加載的,在必要時還可以自己定義類加載器。它們的關係如下圖所示:

雙親委派模型要求除了頂層的啓動類加載器外,其餘的類加載器都應有自己的父類加載器。

雙親委派模型的工作過程是:

  • 如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類
  • 而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此
  • 因此所有的加載請求最終都應該傳送到最頂層的啓動類加載器中
  • 只有當父加載器反饋自己無法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去完成加載。

​ 這樣做的好處就是Java 類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如java.lang. Object,它放在rt.jar 中,無論哪一個類加載器要加載這個類,最終都是委派給處於模型頂端的啓動類加載器來加載,因此Object 類在程序的各種類加載器環境中都是同一個類。

​ 相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱爲java.lang. Object 的類,並放在程序的ClassPath 中,那系統中將會出現多個不同的Object 類,Java 類型體系中最基本的行爲也就無法保證了。

​ 雙親委派模型對於保證Java程序的穩定運作極爲重要,但它的實現卻異常簡單,用以實現雙親委派的代碼只有短短十餘行,全部集中在java.lang. ClassLoader的loadClass()方法之中:

protected synchronized Class<?> loadClass(String name, boolean resolve)
       throws ClassNotFoundException {
   // 首先,檢查請求的類是不是已經被加載過
   Class<?> c = findLoadedClass(name);
   if (c == null) {
       try {
           if (parent != null) {
               c = parent.loadClass(name, false);
           } else {
               c = findBootstrapClassOrNull(name);
           }
       } catch (ClassNotFoundException e) {
           // 如果父類拋出 ClassNotFoundException 說明父類加載器無法完成加載
       }
       if (c == null) {
           // 如果父類加載器無法加載,則調用自己的 findClass 方法來進行類加載
           c = findClass(name);
       }
   }
   if (resolve) {
       resolveClass(c);
   }
   return c;
}

JVM調優實戰

JVM運行參數

​ 在jvm中有很多的參數可以進行設置,這樣可以讓jvm在各種環境中都能夠高效的運行。絕大部分的參數保持默認即可。

三種參數類型

  • 標準參數
    • -help
    • -version
  • -X參數(非標準參數)
    • -Xint
    • -Xcomp
  • XX參數(使用率較高)
    • -XX:newSize
    • -XX:+UseSerialGC

-X參數

​ jvm的-X參數是非標準參數,在不同版本的jvm中,參數可能會有所不同,可以通過java -X查看非標準參數。

-XX參數

​ -XX參數也是非標準參數,主要用於jvm的調優和debug操作。

​ -XX參數的使用有2種方式,一種是boolean類型,一種是非boolean類型:

  • boolean類型
    • 格式:-XX:[+-] 表示啓用或禁用屬性
    • 如:-XX:+DisableExplicitGC 表示禁用手動調用gc操作,也就是說調用System.gc()無效
  • 非boolean類型
    • 格式:-XX:= 表示屬性的值爲
    • 如:-XX:NewRatio=4 表示新生代和老年代的比值爲1:4

-Xms和-Xmx參數

-Xms與-Xmx分別是設置jvm的堆內存的初始大小和最大大小。
-Xmx2048m:等價於-XX:MaxHeapSize,設置JVM最大堆內存爲2048M。
-Xms512m:等價於-XX:InitialHeapSize,設置JVM初始堆內存爲512M。
適當的調整jvm的內存大小,可以充分利用服務器資源,讓程序跑的更快。
示例:


[root@node01 test]# java -Xms512m -Xmx2048m TestJVM
itcast

jstat

​ jstat命令可以查看堆內存各部分的使用量,以及加載類的數量。命令的格式如下:
​ jstat [-命令選項] [vmid] [間隔時間/毫秒] [查詢次數]

查看class加載統計

F:\t>jstat -class 12076
Loaded  Bytes  Unloaded  Bytes     Time
 5962 10814.2        0     0.0       3.75

說明:
Loaded:加載class的數量
Bytes:所佔用空間大小
Unloaded:未加載數量
Bytes:未加載佔用空間
Time:時間

查看編譯統計

F:\t>jstat -compiler 12076
Compiled Failed Invalid   Time   FailedType FailedMethod
   3115      0       0     3.43          0

說明:
Compiled:編譯數量。
Failed:失敗數量
Invalid:不可用數量
Time:時間
FailedType:失敗類型
FailedMethod:失敗的方法

垃圾回收統計

F:\t>jstat -gc 12076
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU
  CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
3584.0 6656.0 3412.1  0.0   180224.0 89915.4   61440.0     5332.1   27904.0 2626
7.3 3840.0 3420.8      6    0.036   1      0.026    0.062
#也可以指定打印的間隔和次數,每1秒中打印一次,共打印5次
F:\t>jstat -gc 12076 1000 5
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU
  CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
3584.0 6656.0 3412.1  0.0   180224.0 89915.4   61440.0     5332.1   27904.0 2626
7.3 3840.0 3420.8      6    0.036   1      0.026    0.062
3584.0 6656.0 3412.1  0.0   180224.0 89915.4   61440.0     5332.1   27904.0 2626
7.3 3840.0 3420.8      6    0.036   1      0.026    0.062
3584.0 6656.0 3412.1  0.0   180224.0 89915.4   61440.0     5332.1   27904.0 2626
7.3 3840.0 3420.8      6    0.036   1      0.026    0.062
3584.0 6656.0 3412.1  0.0   180224.0 89915.4   61440.0     5332.1   27904.0 2626
7.3 3840.0 3420.8      6    0.036   1      0.026    0.062
3584.0 6656.0 3412.1  0.0   180224.0 89915.4   61440.0     5332.1   27904.0 2626
7.3 3840.0 3420.8      6    0.036   1      0.026    0.062

說明:
S0C:第一個Survivor區的大小(KB)
S1C:第二個Survivor區的大小(KB)
S0U:第一個Survivor區的使用大小(KB)
S1U:第二個Survivor區的使用大小(KB)
EC:Eden區的大小(KB)
EU:Eden區的使用大小(KB)
OC:Old區大小(KB)
OU:Old使用大小(KB)
MC:方法區大小(KB)
MU:方法區使用大小(KB)
CCSC:壓縮類空間大小(KB)
CCSU:壓縮類空間使用大小(KB)
YGC:年輕代垃圾回收次數
YGCT:年輕代垃圾回收消耗時間
FGC:老年代垃圾回收次數
FGCT:老年代垃圾回收消耗時間
GCT:垃圾回收消耗總時間

Jmap的使用以及內存溢出分析

​ 前面通過jstat可以對jvm堆的內存進行統計分析,而jmap可以獲取到更加詳細的內容,如:內存使用情況的彙總、對內存溢出的定位與分析。

查看內存使用情況

[root@node01 ~]# jmap -heap 6219
Attaching to process ID 6219, please wait... 
Debugger attached successfully.
Server compiler detected.
JVM version is 25.141-b15
using thread-local object allocation.
Parallel GC with 2 thread(s)
Heap Configuration: #堆內存配置信息
MinHeapFreeRatio         = 0
MaxHeapFreeRatio         = 100
MaxHeapSize              = 488636416 (466.0MB)
NewSize                  = 10485760 (10.0MB)
MaxNewSize               = 162529280 (155.0MB)
OldSize                  = 20971520 (20.0MB)
NewRatio                 = 2
SurvivorRatio            = 8
MetaspaceSize            = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize         = 17592186044415 MB
G1HeapRegionSize         = 0 (0.0MB)
Heap Usage: # 堆內存的使用情況
PS Young Generation #年輕代
Eden Space:
capacity = 123731968 (118.0MB)
used     = 1384736 (1.320587158203125MB)
free     = 122347232 (116.67941284179688MB)
1.1191416594941737% used
From Space:
capacity = 9437184 (9.0MB)
used     = 0 (0.0MB)
free     = 9437184 (9.0MB)
0.0% used
To Space:
capacity = 9437184 (9.0MB)
used     = 0 (0.0MB)
free     = 9437184 (9.0MB)
0.0% used
PS Old Generation #年老代
capacity = 28311552 (27.0MB)
used     = 13698672 (13.064071655273438MB)
free     = 14612880 (13.935928344726562MB)
48.38545057508681% used
13648 interned Strings occupying 1866368 bytes.

查看內存中對象數量及大小

#查看所有對象,包括活躍以及非活躍的
jmap -histo <pid> | more
#查看活躍對象 
jmap -histo:live <pid> | more
[root@node01 ~]# jmap -histo:live 6219 | more
num     #instances         #bytes  class name
----------------------------------------------1:         37437        7914608  [C
2:         34916         837984  java.lang.String
3:           884         654848  [B
4:         17188         550016  java.util.HashMap$Node
5:          3674         424968  java.lang.Class
6:          6322         395512  [Ljava.lang.Object;
7:          3738         328944  java.lang.reflect.Method
8:          1028         208048  [Ljava.util.HashMap$Node;
9:          2247         144264  [I
10:          4305         137760  java.util.concurrent.ConcurrentHashMap$Node
11:          1270         109080  [Ljava.lang.String;
12:            64          84128  [Ljava.util.concurrent.ConcurrentHashMap$Node;
13:          1714          82272  java.util.HashMap
14:          3285          70072  [Ljava.lang.Class;
15:          2888          69312  java.util.ArrayList
16:          3983          63728  java.lang.Object
17:          1271          61008  org.apache.tomcat.util.digester.CallMethodRule
18:          1518          60720  java.util.LinkedHashMap$Entry
19:          1671          53472  com.sun.org.apache.xerces.internal.xni.QName
20:            88          50880  [Ljava.util.WeakHashMap$Entry;
21:           618          49440  java.lang.reflect.Constructor
22:          1545          49440  java.util.Hashtable$Entry
23:          1027          41080  java.util.TreeMap$Entry
24:           846          40608  org.apache.tomcat.util.modeler.AttributeInfo
25:           142          38032  [S
26:           946          37840  java.lang.ref.SoftReference
27:           226          36816  [[C
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
#對象說明
B  byte
C  char
D  double
F  float
I  int
J  long
Z  boolean
[  數組,如[I表示int[]
[L+類名 其他對象

將內存使用情況dump到文件中

#用法:
jmap -dump:format=b,file=dumpFileName <pid>
#示例
jmap -dump:format=b,file=/tmp/dump.dat 6219

可以看到已經在/tmp下生成了dump.dat的文件。

通過jhat對dump文件進行分析

​ 在上一小節中,我們將jvm的內存dump到文件中,這個文件是一個二進制的文件,不方便查看,這時我們可以藉助於jhat工具進行查看。

#用法:
jhat -port <port> <file>
#示例:
[root@node01 tmp]# jhat -port 9999 /tmp/dump.dat 
Reading from /tmp/dump.dat...
Dump file created Mon Sep 10 01:04:21 CST 2018
Snapshot read, resolving...
Resolving 204094 objects...
Chasing references, expect 40 dots........................................
Eliminating duplicate references........................................
Snapshot resolved.
Started HTTP server on port 9999
Server is ready.

打開瀏覽器進行訪問:http://192.168.40.133:9999/

在最後由OQL查詢功能

Jmp使用以及內存溢出分析

使用MAT對內存溢出的定位與分析

​ 內存溢出在實際的生產環境中經常會遇到,比如,不斷的將數據寫入到一個集合中,出現了死循環,讀取超大的文件等等,都可能會造成內存溢出。

​ 如果出現了內存溢出,首先我們需要定位到發生內存溢出的環節,並且進行分析,是正常還是非正常情況,如果是正常的需求,就應該考慮加大內存的設置,如果是非正常需求,那麼就要對代碼進行修改,修復這個bug。首先,我們得先學會如何定位問題,然後再進行分析。如何定位問題呢,我們需要藉助於jmap與MAT工具進行定位分析。

接下來,我們模擬內存溢出的場景。

模擬內存溢出

​ 編寫代碼,向List集合中添加100萬個字符串,每個字符串由1000個UUID組成。如果程序能夠正常執行,最後打印ok。

public class TestJvmOutOfMemory {
public static void main(String[] args) { 
    List<Object> list = new ArrayList<>();
    for (int i = 0; i < 10000000; i++) {
        	String str = "";
            for (int j = 0; j < 1000; j++) {
            str += UUID.randomUUID().toString();
            }
       		 list.add(str);
        }
    System.out.println("ok");
	}
}

爲了演示效果,我們將設置執行的參數

#參數如下:
-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

運行測試

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid5348.hprof ...
Heap dump file created [8137186 bytes in 0.032 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at
java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at cn.itcast.jvm.TestJvmOutOfMemory.main(TestJvmOutOfMemory.java:14)
Process finished with exit code 1

可以看到,當發生內存溢出時,會dump文件到java_pid5348.hprof。

導入到MA T工具中進行分析

可以看到,有91.03%的內存由Object[]數組佔有,所以比較可疑。
分析:這個可疑是正確的,因爲已經有超過90%的內存都被它佔有,這是非常有可能出現內存溢出的。

可以看到集合中存儲了大量的uuid字符串

Jsatck的使用

​ 有些時候我們需要查看下jvm中的線程執行情況,比如,發現服務器的CPU的負載突然增高了、出現了死鎖、死循環等,我們該如何分析呢?

​ 由於程序是正常運行的,沒有任何的輸出,從日誌方面也看不出什麼問題,所以就需要看下jvm的內部線程的執行情況,然後再進行分析查找出原因。

​ 這個時候,就需要藉助於jstack命令了,jstack的作用是將正在運行的jvm的線程情況進行快照,並且打印出來:

#用法:jstack <pid>
[root@node01 bin]# jstack 2203
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.141-b15 mixed mode):
"Attach Listener" #24 daemon prio=9 os_prio=0 tid=0x00007fabb4001000 nid=0x906 waiting on
condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"http-bio-8080-exec-5" #23 daemon prio=5 os_prio=0 tid=0x00007fabb057c000 nid=0x8e1
waiting on condition [0x00007fabd05b8000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for  <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) 
at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueue
dSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-exec-4" #22 daemon prio=5 os_prio=0 tid=0x00007fab9c113800 nid=0x8e0
waiting on condition [0x00007fabd06b9000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for  <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueue
dSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-exec-3" #21 daemon prio=5 os_prio=0 tid=0x0000000001aeb800 nid=0x8df
waiting on condition [0x00007fabd09ba000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for  <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueue
dSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) 
at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-exec-2" #20 daemon prio=5 os_prio=0 tid=0x0000000001aea000 nid=0x8de
waiting on condition [0x00007fabd0abb000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for  <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueue
dSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-exec-1" #19 daemon prio=5 os_prio=0 tid=0x0000000001ae8800 nid=0x8dd
waiting on condition [0x00007fabd0bbc000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for  <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueue
dSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
"ajp-bio-8009-AsyncTimeout" #17 daemon prio=5 os_prio=0 tid=0x00007fabe8128000 nid=0x8d0
waiting on condition [0x00007fabd0ece000]
java.lang.Thread.State: TIMED_WAITING (sleeping) 
at java.lang.Thread.sleep(Native Method)
at org.apache.tomcat.util.net.JIoEndpoint$AsyncTimeout.run(JIoEndpoint.java:152)
at java.lang.Thread.run(Thread.java:748)
"ajp-bio-8009-Acceptor-0" #16 daemon prio=5 os_prio=0 tid=0x00007fabe82d4000 nid=0x8cf
runnable [0x00007fabd0fcf000]
java.lang.Thread.State: RUNNABLE
at java.net.PlainSocketImpl.socketAccept(Native Method)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
at
org.apache.tomcat.util.net.DefaultServerSocketFactory.acceptSocket(DefaultServerSocketFac
tory.java:60)
at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:220)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-AsyncTimeout" #15 daemon prio=5 os_prio=0 tid=0x00007fabe82d1800 nid=0x8ce
waiting on condition [0x00007fabd10d0000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at org.apache.tomcat.util.net.JIoEndpoint$AsyncTimeout.run(JIoEndpoint.java:152)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-Acceptor-0" #14 daemon prio=5 os_prio=0 tid=0x00007fabe82d0000 nid=0x8cd
runnable [0x00007fabd11d1000]
java.lang.Thread.State: RUNNABLE
at java.net.PlainSocketImpl.socketAccept(Native Method)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
at
org.apache.tomcat.util.net.DefaultServerSocketFactory.acceptSocket(DefaultServerSocketFac
tory.java:60)
at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:220)
at java.lang.Thread.run(Thread.java:748)
"ContainerBackgroundProcessor[StandardEngine[Catalina]]" #13 daemon prio=5 os_prio=0
tid=0x00007fabe82ce000 nid=0x8cc waiting on condition [0x00007fabd12d2000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at
org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.jav
a:1513)
at java.lang.Thread.run(Thread.java:748)
"GC Daemon" #10 daemon prio=2 os_prio=0 tid=0x00007fabe83b4000 nid=0x8b3 in Object.wait()
[0x00007fabd1c2f000]
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000e315c2d0> (a sun.misc.GC$LatencyLock)
at sun.misc.GC$Daemon.run(GC.java:117)
- locked <0x00000000e315c2d0> (a sun.misc.GC$LatencyLock) 
"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fabe80c3800 nid=0x8a5 runnable
[0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007fabe80b6800 nid=0x8a4 waiting
on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007fabe80b3800 nid=0x8a3 waiting
on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007fabe80b2000 nid=0x8a2 runnable
[0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007fabe807f000 nid=0x8a1 in Object.wait()
[0x00007fabd2a67000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000e3162918> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
- locked <0x00000000e3162918> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007fabe807a800 nid=0x8a0 in
Object.wait() [0x00007fabd2b68000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000e3162958> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000e3162958> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"main" #1 prio=5 os_prio=0 tid=0x00007fabe8009000 nid=0x89c runnable [0x00007fabed210000]
java.lang.Thread.State: RUNNABLE
at java.net.PlainSocketImpl.socketAccept(Native Method)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
at org.apache.catalina.core.StandardServer.await(StandardServer.java:453)
at org.apache.catalina.startup.Catalina.await(Catalina.java:777)
at org.apache.catalina.startup.Catalina.start(Catalina.java:723)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:321)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:455) 
"VM Thread" os_prio=0 tid=0x00007fabe8073000 nid=0x89f runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007fabe801e000 nid=0x89d runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007fabe8020000 nid=0x89e runnable
"VM Periodic Task Thread" os_prio=0 tid=0x00007fabe80d6800 nid=0x8a6 waiting on condition
JNI global references: 43

VisualVM工具的使用

​ VisualVM,能夠監控線程,內存情況,查看方法的CPU時間和內存中的對 象,已被GC的對象,反向查看分配的堆棧(如100個String對象分別由哪幾個對象分配出來的)。

​ VisualVM使用簡單,幾乎0配置,功能還是比較豐富的,幾乎囊括了其它JDK自帶命令的所有功能。

  • 內存信息
  • 線程信息
  • Dump堆(本地進程
  • Dump線程(本地進程)
  • 打開堆Dump。堆Dump可以用jmap來生成
  • 打開線程Dump
  • 生成應用快照(包含內存信息、線程信息等等)
  • 性能分析。CPU分析(各個方法調用時間,檢查哪些方法耗時多),內存分析(各類對象佔用的內存,檢查哪些類佔用內存多)
  • ......

啓動

在jdk的安裝目錄的bin目錄下,找到jvisualvm.exe,雙擊打開即可。

查看 CPU、內存、類、線程運行信息

參看線程信息

也可以點擊右上角Dump按鈕,將線程的信息導出,其實就是執行的jstack命令。

監控遠程JVM

VisualJVM不僅是可以監控本地jvm進程,還可以監控遠程的jvm進程,需要藉助於JMX技術實現。

什麼是JMX

​ JMX(Java Management Extensions,即Java管理擴展)是一個爲應用程序、設備、系統等植入管理功能的框架。JMX可以跨越一系列異構操作系統平臺、系統體系結構和網絡傳輸協議,靈活的開發無縫集成的系統、網絡和服務管理應用。

監控Tomcat

想要監控遠程的tomcat,就需要在遠程的tomcat進行對JMX配置,方法如下:

#在tomcat的bin目錄下,修改catalina.sh,添加如下的參數 
JAVA_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false" 
#這幾個參數的意思是: 
#-Dcom.sun.management.jmxremote :允許使用JMX遠程管理 
#-Dcom.sun.management.jmxremote.port=9999 :JMX遠程連接端口 
#-Dcom.sun.management.jmxremote.authenticate=false :不進行身份認證,任何用戶都可以連接 
#-Dcom.sun.management.jmxremote.ssl=false :不使用ssl

使用VisualJVM遠程連接Tomcat

添加主機

在一個主機下可能會有很多的jvm需要監控,所以接下來要在該主機上添加需要監控的jvm:

連接成功。使用方法和前面就一樣了,就可以和監控本地jvm進程一樣,監控遠程的tomcat進程。

可視化GC日誌分析工具

GC日誌輸出參數

​ 前面通過-XX:+PrintGCDetails可以對GC日誌進行打印,我們就可以在控制檯查看,這樣雖然可以查看GC的信息,但是並不直觀,可以藉助於第三方的GC日誌分析工具進行查看。

在日誌打印輸出涉及到的參數如下:

-XX:+PrintGC 輸出GC日誌 

-XX:+PrintGCDetails 輸出GC的詳細日誌 

-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式) 

-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800) 

-XX:+PrintHeapAtGC 在進行GC的前後打印出堆的信息 

-Xloggc:../logs/gc.log 日誌文件的輸出路徑 

測試:

-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xmx256m -XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC
-Xloggc:F://test//gc.log 

運行後就可以在E盤下生成gc.log文件。

使用GC Easy

它是一款在線的可視化工具,易用、功能強大,網站:http://gceasy.io/

調優實戰

先部署一個web項目(自行準備)

壓測

下面我們通過jmeter進行壓力測試,先測得在初始狀態下的併發量等信息,然後我們在對jvm做調優處理,再與初始狀態測得的數據進行比較,看調好了還是調壞了。

首先需要對jmeter本身的參數調整,jmeter默認的的內存大小隻有1g,如果併發數到達300以上時,將無法
正常執行,會拋出內存溢出等異常,所以需要對內存大小做出調整。
修改jmeter.bat文件:
set HEAP=-Xms1g -Xmx4g -XX:MaxMetaspaceSize=512m
在該文件中可以看到,jmeter默認使用的垃圾收集器是G1.
Defaults to '-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:G1ReservePercent=20'

添加gc相關參數

#內存設置較小是爲了更頻繁的gc,方便觀察效果,實際要比此設置的更大 JAVA_OPTS="-XX:+UseParallelGC -XX:+UseParallelOldGC -Xms64m -Xmx128m - XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC - Xloggc:../logs/gc.log -Dcom.sun.management.jmxremote - Dcom.sun.management.jmxremote.port=9999 - Dcom.sun.management.jmxremote.authenticate=false - Dcom.sun.management.jmxremote.ssl=false"

重啓tomcat

創建測試用例進行壓測

調優方向主要從以下幾個方面:

  • 調整內存
  • 更換垃圾收集器

對於JVM的調優,給出大家幾條建議:

  • 生產環境的JVM一定要進行參數設定,不能全部默認上生產。

  • 對於參數的設定,不能拍腦袋,需要通過實際併發情況或壓力測試得出結論。

  • 對於內存中對象臨時存在居多的情況,將年輕代調大一些。如果是G1或ZGC,不需要設定。

  • 仔細分析gceasy給出的報告,從中分析原因,找出問題。

  • 對於低延遲的應用建議使用G1或ZGC垃圾收集器。

  • 不要將焦點全部聚焦jvm參數上,影響性能的因素有很多,比如:操作系統、tomcat本身的參數等。

PerfMa

PerfMa提供了JVM參數分析、線程分析、堆內存分析功能,界面美觀,功能強大,我們在做jvm調優時,可以作爲一個輔助工具。官網:https://www.perfma.com/

Tomcat8優化

禁用AJP連接

在服務狀態頁面中可以看到,默認狀態下會啓用AJP服務,並且佔用8009端口。

什麼是AJP呢?

AJP(Apache JServer Protocol) AJPv13協議是面向包的。WEB服務器和Servlet容器通過TCP連接來交互;爲了節省SOCKET創建的昂貴代價,WEB服務器會嘗試維護一個永久TCP連接到servlet容器,並且在多個請求和響應週期過程會重用連接。

我們一般是使用Nginx+tomcat的架構,所以用不着AJP協議,所以把AJP連接器禁用。

修改conf下的server.xml文件,將AJP服務禁用掉即可。

<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

執行器(線程池)

在tomcat中每一個用戶請求都是一個線程,所以可以使用線程池提高性能。

修改server.xml文件:

<!--將註釋打開--> <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="500" minSpareThreads="50" prestartminSpareThreads="true" maxQueueSize="100"/> 
<!-- 參數說明: maxThreads:最大併發數,默認設置 200,一般建議在 500 ~ 1000,根據硬件設施和業務來判斷 minSpareThreads:Tomcat 初始化時創建的線程數,默認設置 25 prestartminSpareThreads: 在 Tomcat 初始化的時候就初始化 minSpareThreads 的參數值,如果不等於 true,minSpareThreads 的值就沒啥效果了 maxQueueSize,最大的等待隊列數,超過則拒絕請求 --> 

<!--在Connector中設置executor屬性指向上面的執行器--> 
<Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

保存退出,重啓tomcat,查看效果。

三種運行模式

tomcat的運行模式有3種:

\1. bio 默認的模式,性能非常低下,沒有經過任何優化處理和支持.

\2. nio nio(new I/O),是Java SE 1.4及後續版本提供的一種新的I/O操作方式(即java.nio包及其子包)。Java nio是一個基於緩衝區、並能提供非阻塞I/O操作的Java API,因此nio也被看成是non-blocking I/O的縮寫。它擁有比傳統I/O操作(bio)更好的併發運行性能。

\3. apr 安裝起來最困難,但是從操作系統級別來解決異步的IO問題,大幅度的提高性能.

推薦使用nio,不過,在tomcat8中有最新的nio2,速度更快,建議使用nio2.

設置nio2

<Connector executor="tomcatThreadPool" port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" connectionTimeout="20000" redirectPort="8443" />

代碼優化建議

儘可能使用局部變量

調用方法時傳遞的參數以及在調用中創建的臨時變量都保存在棧中速度較快,其他變量,如靜態變量、實例變量等,都在堆中創建,速度較慢。另外,棧中創建的變量,隨着方法的運行結束,這些內容就沒了,不需要額外的垃圾回收。

儘量減少對變量的重複計算

明確一個概念,對方法的調用,即使方法中只有一句語句,也是有消耗的。所以例如下面的操作:

for (int i = 0; i < list.size(); i++) {...}

建議替換爲:

int length = list.size(); for (int i = 0, i < length; i++) {...}

這樣,在list.size()很大的時候,就減少了很多的消耗。

儘量採用懶加載的策略,即在需要的時候才創建

String str = "aaa"; 
if (i == 1){ 
  list.add(str); 
}//建議替換成 
if (i == 1){ 
  String str = "aaa"; 
  list.add(str); 
}

異常不應該用來控制流程

​ 異常對性能不利。拋出異常首先要創建一個新的對象,Throwable接口的構造函數調用名爲fifillInStackTrace()的本地同步方 法,fifillInStackTrace()方法檢查堆棧,收集調用跟蹤信息。只要有異常被拋出,Java虛擬機就必須調整調用堆棧,因爲在處理過程中創建 了一個新的對象。異常只能用於錯誤處理,不應該用來控制程序流程。

不要將數組聲明爲public static final

因爲這毫無意義,這樣只是定義了引用爲static final,數組的內容還是可以隨意改變的,將數組聲明爲public更是一個安全漏洞,這意味着這個數組可以被外部類所改變。

不要創建一些不使用的對象,不要導入一些不使用的類

這毫無意義,如果代碼中出現"The value of the local variable i is not used"、"The import java.util is never used",那麼請刪除這些無用的內容

程序運行過程中避免使用反射

反射是Java提供給用戶一個很強大的功能,功能強大往往意味着效率不高。不建議在程序運行過程中使用尤其是頻繁使用反射機制,特別是 Method的invoke方法。

如果確實有必要,一種建議性的做法是將那些需要通過反射加載的類在項目啓動的時候通過反射實例化出一個對象並放入內存。

使用數據庫連接池和線程池

這兩個池都是用於重用對象的,前者可以避免頻繁地打開和關閉連接,後者可以避免頻繁地創建和銷燬線程。

容器初始化時儘可能指定長度

容器初始化時儘可能指定長度,如:new ArrayList<>(10); new HashMap<>(32); 避免容器長度不足時,擴容帶來的性能損耗。

ArrayList隨機遍歷快,LinkedList添加刪除快

使用Entry遍歷map

不要手動調用System().gc;

String儘量少用正則表達式

正則表達式雖然功能強大,但是其效率較低,除非是有需要,否則儘可能少用。

replace() 不支持正則 replaceAll() 支持正則

如果僅僅是字符的替換建議使用replace()。

日誌的輸出要注意級別

對資源的close()建議分開操作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章