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

道阻且長啊發表於2020-08-08

前言

​ 前面我們瞭解了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()建議分開操作

相關文章