性能測(cè)試
在了解性能調(diào)優(yōu)之前,首先得知道什么是性能測(cè)試,我們的程序怎樣的性能表現(xiàn)才需要進(jìn)行性能調(diào)優(yōu)
一、性能測(cè)試概念
1.概念
用最低的資源換取最高的處理能力和低的響應(yīng)時(shí)間, 在一定環(huán)境下做性能需求
2.問(wèn)題:
- 環(huán)境很難真實(shí)
- 需求一般很模糊
3.指標(biāo):
- 響應(yīng)時(shí)間: 完成一個(gè)業(yè)務(wù)所需要的時(shí)間綜合(越短越好)
- 吞吐量:?jiǎn)挝粫r(shí)間內(nèi)處理的業(yè)務(wù)數(shù)量(越多越好)
- 資源利用率(CPU 、內(nèi)存、IO)
二、性能測(cè)試的標(biāo)準(zhǔn)
1.Tpc
給一個(gè)標(biāo)準(zhǔn)的業(yè)務(wù),比較完成業(yè)務(wù)的能力
2.Spec
物理處理能力,比較固定業(yè)務(wù)換算的指標(biāo)
三、性能測(cè)試的難點(diǎn)
1.用戶層面
用戶總希望在最小的代價(jià)下?lián)Q回最大的收益
2.代碼層面
項(xiàng)目一旦確定了架構(gòu),其性能也就確定了(開(kāi)發(fā)者不遵守規(guī)范體系進(jìn)行開(kāi)發(fā))
四、如何進(jìn)行性能測(cè)試
1.模擬用戶請(qǐng)求
模擬客戶端對(duì)服務(wù)端的多線程調(diào)用,使用Testng、jemeter等工具模擬高并發(fā)
2.性能測(cè)試工具的要求
- 并發(fā)負(fù)載用戶
- 參數(shù)化:避免緩存帶來(lái)的性能問(wèn)題
- 關(guān)聯(lián):業(yè)務(wù)前后依賴
- 事務(wù):通過(guò)函數(shù)來(lái)明確具體業(yè)務(wù)的時(shí)間范圍
- 監(jiān)控:監(jiān)控負(fù)載和監(jiān)控資源
監(jiān)控負(fù)載可以計(jì)算出響應(yīng)時(shí)間和吞吐量
監(jiān)控資源的工具
- jvm監(jiān)控工具:jrock、jmap、jprofile
- zabbix
- elk
- Prometheus
- top命令
五、性能測(cè)試模型
響應(yīng)時(shí)間隨著負(fù)載的上升先穩(wěn)定后上升,并且越來(lái)越快
TPS隨著負(fù)載的上升先到峰值,后穩(wěn)定,然后下降
TPS(Transction per second) 每秒處理請(qǐng)求的能力,從發(fā)起一次請(qǐng)求到服務(wù)器做出響應(yīng)的過(guò)程
QPS(Query per second)每秒查詢的次數(shù),一般針對(duì)一個(gè)特定的查詢,服務(wù)器在一秒中所處理的流量
TPS可以認(rèn)為是一種特殊的QPS
JVM相關(guān)概念
一、JVM是什么
jvm(java virtual machine) java虛擬機(jī),是保證java程序能夠在不同的操作系統(tǒng)上正常運(yùn)行的基礎(chǔ)。write once run everywhere
JRE(Java Runtime Envirment) java運(yùn)行環(huán)境,JRE中包含JVM
JDK通常我們?cè)陂_(kāi)發(fā)java項(xiàng)目所安裝的都是JDK,它包含了java類(lèi)庫(kù)以及JRE
Java程序在運(yùn)行的時(shí)候,首先將java文件編譯成class文件,JVM就負(fù)責(zé)將class文件解析成不同的操作系統(tǒng)所能理解的字節(jié)碼
二、為什么要學(xué)習(xí)JVM
Java開(kāi)發(fā)中,不需要管理對(duì)象的銷(xiāo)毀,JVM已經(jīng)幫我們處理好了,雖然在大多數(shù)情況下,我們不需要對(duì)JVM進(jìn)行任何操作,程序也能正常運(yùn)行,但是如果我們能夠了解類(lèi)是如何加載的,我們編寫(xiě)的java對(duì)象、方法是如何在JVM中運(yùn)行的,對(duì)象是如何進(jìn)行回收的,程序出現(xiàn)OOM問(wèn)題后我們?cè)撛趺崔k,那我們能開(kāi)發(fā)一個(gè)更健壯的系統(tǒng)。
三、JVM的具體構(gòu)成與原理
1.JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)

先分析進(jìn)程共享的區(qū)域
- 堆:Java程序中所有的對(duì)象以及數(shù)組都在堆上進(jìn)行分配。
- 方法區(qū):保存類(lèi)信息、常量、靜態(tài)變量、JIT編譯后的代碼。
在分析線程共享的區(qū)域
- 虛擬機(jī)棧:線程執(zhí)行方法的區(qū)域,線程執(zhí)行方法的過(guò)程就是向虛擬機(jī)棧入棧和出棧的過(guò)程,每一個(gè)方法就是一個(gè)棧幀。虛擬機(jī)棧還包括:
- 本地變量表:存放方法中的臨時(shí)變量,如果是引用對(duì)象,則存放其在堆中的實(shí)例地址的引用
- 操作數(shù)棧:方法內(nèi)部進(jìn)行各種操作的指令
- 動(dòng)態(tài)鏈接:把方法中的符號(hào)引用轉(zhuǎn)換為直接引用(類(lèi)加載中解析的過(guò)程是將靜態(tài)的符號(hào)引用轉(zhuǎn)換為直接引用)
- 返回:每一個(gè)方法都有一個(gè)返回。
- 本地方法棧:執(zhí)行本地方法的區(qū)域,其結(jié)構(gòu)與虛擬機(jī)棧類(lèi)似,只是執(zhí)行的是c/c++語(yǔ)言的方法
- 程序計(jì)數(shù)器:保存當(dāng)前線程正在執(zhí)行的操作的指令的字節(jié)碼或者行號(hào)(CPU調(diào)度切換時(shí)使用)
2.類(lèi)加載
想要更好的理解JVM運(yùn)行時(shí)數(shù)據(jù)區(qū),必須了解這些數(shù)據(jù)是怎么加載到JVM中的。下面就介紹一下java中類(lèi)加載的過(guò)程。
java類(lèi)的加載主要分為3個(gè)步驟:加載、連接和初始化,而連接又分為三個(gè)步驟:驗(yàn)證、準(zhǔn)備和解析。具體來(lái)看一下每一個(gè)過(guò)程都做了那些事情。
- 加載:
- 根據(jù)類(lèi)的全限定名,讀取class文件中的二進(jìn)制數(shù)據(jù)流。
- 將類(lèi)中的靜態(tài)數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
- 在堆中創(chuàng)建一個(gè)
java.lang.Class的對(duì)象作為訪問(wèn)這些數(shù)據(jù)的入口。
- 驗(yàn)證:驗(yàn)證主要是驗(yàn)證類(lèi)的正確性。
- 驗(yàn)證文件格式
- 驗(yàn)證元數(shù)據(jù)
- 驗(yàn)證字節(jié)碼
- 驗(yàn)證符號(hào)引用
- 準(zhǔn)備:將靜態(tài)變量在堆中進(jìn)行分配,并設(shè)置相應(yīng)對(duì)象的默認(rèn)值(類(lèi)的靜態(tài)變量保存在方法區(qū)中)
- 解析:將類(lèi)中符號(hào)變量轉(zhuǎn)換為直接引用,這里會(huì)將一部分的符號(hào)引用轉(zhuǎn)化為直接引用。轉(zhuǎn)化這部分的方法調(diào)用必須是在程序運(yùn)行之前就有一個(gè)可以確定的調(diào)用版本。包括:靜態(tài)方法、私有方法、實(shí)例構(gòu)造方法、父類(lèi)方法。
- 初始化:為在準(zhǔn)備階段的靜態(tài)變量進(jìn)行賦值(類(lèi)的其他成員變量會(huì)執(zhí)行構(gòu)造函數(shù)的時(shí)候,隨對(duì)象一起分配在內(nèi)存中)
符號(hào)引用:以一組符號(hào)來(lái)描述所引用的對(duì)象,可以是任何形式的字面量,只要在解析的時(shí)候能夠根據(jù)這個(gè)字面量無(wú)歧義的定位到目標(biāo)即可,能根據(jù)這個(gè)字符串定位到指定的數(shù)據(jù),比如java/lang/String
直接引用:直接指向目標(biāo)的指針、相對(duì)偏移量或者是一個(gè)間接定位的句柄
理解
這里簡(jiǎn)單分析一下類(lèi)加載之后,具體與JVM之間的關(guān)系以及JVM各個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)的聯(lián)系:
- 類(lèi)加載之后將類(lèi)進(jìn)行拆分,把對(duì)應(yīng)的數(shù)據(jù)放在
jvm運(yùn)行時(shí)數(shù)據(jù)區(qū)。 - 一個(gè)類(lèi)初始化后,其對(duì)象頭中包括一個(gè)
Class Pointer指向方法區(qū)中對(duì)應(yīng)的類(lèi)信息。 -
虛擬機(jī)棧中的動(dòng)態(tài)連接就是把方法區(qū)中存放的方法的符號(hào)引用根據(jù)運(yùn)行時(shí)的狀態(tài)把其轉(zhuǎn)換為直接引用。
image.png
3.GC回收
JVM內(nèi)存模型
先來(lái)看一下JVM內(nèi)存模型的圖

JDK1.8中內(nèi)存模型主要有一下幾個(gè)部分,簡(jiǎn)單說(shuō)明一下每個(gè)部分
-
JVM將整個(gè)堆分為兩個(gè)部分,新生代和老年代,其中新生代又分為Eden、S0和S1區(qū)。 - 對(duì)象的創(chuàng)建都在
Eden區(qū)中進(jìn)行,S0和S1是用來(lái)存放MinorGC后存活的對(duì)象。 - 老年代用來(lái)存放新生代多次(默認(rèn)年齡是15,可以通過(guò)
MaxTenuringThreshold修改)GC后存活的對(duì)象,或者是S0、S1存放不下的對(duì)象。
垃圾回收算法
首先先介紹一下,JVM如何判斷哪個(gè)對(duì)象是否需要回收,這里有兩種算法,一個(gè)是引用計(jì)數(shù)法,一個(gè)是可達(dá)性算法。
- 引用計(jì)數(shù)法:對(duì)一個(gè)對(duì)象而言,只要程序中有持有該對(duì)象的引用,就把引用計(jì)數(shù)+1,釋放該對(duì)象就-1,當(dāng)該對(duì)象的引用計(jì)數(shù)為0時(shí),說(shuō)明該對(duì)象沒(méi)有被引用,可以被
GC。缺點(diǎn):不能解決循環(huán)引用的問(wèn)題。finalize - 可達(dá)性算法:通過(guò)
GCRoot對(duì)象,向下尋找,看某個(gè)對(duì)象是否可達(dá),如果不可達(dá),則可以被GC。(垃圾回收的時(shí)候會(huì)再調(diào)用finalize方法,可以在該方法中將該對(duì)象與GCRoot關(guān)聯(lián)。)
JVM中使用的可達(dá)性算法,那么有哪些對(duì)象可以作為GCRoot呢?
- 虛擬機(jī)棧中本地變量表所引用的對(duì)象
- 方法區(qū)中類(lèi)靜態(tài)變量引用的屬性
- 本地方法棧中引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
方法區(qū)中的對(duì)象是隨著JVM進(jìn)程的存在而存在的,他不會(huì)被回收,虛擬機(jī)棧和本地方法棧的變量是當(dāng)前正在執(zhí)行的方法,變量也不會(huì)被回收,所以他們可以作為GCRoot。
1.標(biāo)記清除算法
找出內(nèi)存中需要回收的對(duì)象,并標(biāo)記出來(lái),然后清除他們。缺點(diǎn):會(huì)造成內(nèi)存不連續(xù)
2.標(biāo)記整理算法
找出內(nèi)存中需要回收的對(duì)象,并標(biāo)記出來(lái),然后把存活的對(duì)象向一邊移動(dòng),然后清空另一邊。缺點(diǎn):
3.復(fù)制回收算法
將內(nèi)存區(qū)域分為兩個(gè)部分,每次只使用一塊,當(dāng)一塊使用完了之后,將不需要回收的對(duì)象復(fù)制到另一塊內(nèi)存中。缺點(diǎn):內(nèi)存利用率低且如果有大量對(duì)象存活的時(shí)候,復(fù)制會(huì)消耗很多資源。
垃圾回收器

1.Serial/SerialOld垃圾收集器
單線程收集器,回收垃圾的時(shí)候會(huì)觸發(fā)STW。新生代使用復(fù)制回收算法,老年代使用標(biāo)記整理算法。
優(yōu)點(diǎn):簡(jiǎn)單高效。
缺點(diǎn):GC會(huì)暫停用戶線程。
使用場(chǎng)景:?jiǎn)魏薈PU
2.ParNew垃圾收集器
多線程收集器,回收垃圾的時(shí)候會(huì)觸發(fā)STW。新生代使用,采用復(fù)制回收算法。
優(yōu)點(diǎn):多CPU情況下,比Serial效率高。
缺點(diǎn):會(huì)觸發(fā)STW,單核CPU效率低。
使用場(chǎng)景:Server模式下首選的新生代收集器。
3.Parallel Scavenge /Parallel Old垃圾收集器
和ParNew一樣是多線程收集器,但是它更注重吞吐量(運(yùn)行用戶代碼的時(shí)間 / (運(yùn)行用戶代碼的時(shí)間 + 垃圾回收時(shí)間))
新生代使用復(fù)制回收算法,老年代使用標(biāo)記整理算法
4.CMS(Concurrent Mark Sweep)垃圾收集器
CMS是以獲取最短回收停頓時(shí)間為目標(biāo)的收集器 采用標(biāo)記清除算法,真?zhèn)€步驟分為:
- 初始標(biāo)記(STW)
- 并發(fā)標(biāo)記(并發(fā))
- 重新標(biāo)記(STW)
- 并發(fā)清除(并發(fā))
整個(gè)過(guò)程中,并發(fā)標(biāo)記和并發(fā)清除可以和用戶線程一起執(zhí)行,降低了回收停頓的時(shí)間。
優(yōu)點(diǎn):并發(fā)收集,低停頓
缺點(diǎn):產(chǎn)生大量的空間碎片,并發(fā)階段會(huì)降低吞吐量
5.G1垃圾收集器
JDK7中開(kāi)始使用,新生代和老年代使用同一個(gè)垃圾回收器。使用該垃圾收集器的時(shí)候,Java內(nèi)存布局和其他收集器有很大的區(qū)別,它將整個(gè)Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),新生代和老年代不再是物理隔離了,他們都是一部分Region。
其過(guò)程可一分為下面幾步:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 最終標(biāo)記
- 篩選回收 對(duì)各個(gè)
Region的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的GC停頓時(shí)間制定回收計(jì)劃。
image.png
總結(jié):
上面所列舉的垃圾收集器可以進(jìn)行簡(jiǎn)要分類(lèi)
- 串行收集器:
Serial和Serial Old適用于內(nèi)存比較小的嵌入式設(shè)備。 - 并行收集器[吞吐量?jī)?yōu)先]:
Parallel Scavange和Parallel Old多條垃圾回收線程并行工作,適合多CPU條件。 - 并發(fā)收集器[停頓時(shí)間優(yōu)先]:
CMS和G1用戶線程和垃圾收集線程同時(shí)執(zhí)行(但不一定是并行的,可能交替執(zhí)行),垃圾收集線程執(zhí)行的時(shí)候不會(huì)停頓用戶線程,適用于對(duì)時(shí)間要求比較高的場(chǎng)景,比如Web應(yīng)用。

四、如何對(duì)JVM進(jìn)行調(diào)優(yōu)
1.常用參數(shù)
在上面已經(jīng)介紹了有關(guān)JVM的內(nèi)存結(jié)構(gòu)以及垃圾回收等相關(guān)的知識(shí),那么對(duì)于我們開(kāi)發(fā)者來(lái)說(shuō),如何去設(shè)置這些參數(shù)呢?下面的表格里,我列出了應(yīng)該算是比較常用的一些命令。根據(jù)這些命令,我們可以很輕松的在IDE或者Tomcat中去配置這些參數(shù)。

2.常用命令
常用的查看JVM相關(guān)數(shù)據(jù)的命令有以下幾個(gè):
-
jps查看當(dāng)前運(yùn)行的java進(jìn)程,jps -l可以打印程序的全路徑

-
jstat -gc/class/compiler/gcutil pid interval count查看當(dāng)前pid的gc信息、class信息、編譯信息、gc匯總等,interval表示每隔多少毫秒輸出一次,count表示總共輸出幾次

-
jinfo -flag MaxHeapSize pid查看當(dāng)前進(jìn)程的最大堆內(nèi)存大小

-
jmap -heap pid打印當(dāng)前進(jìn)程的所有堆棧信息,還可以使用jmap -dump:formate=b ,file=heap.hprof pid導(dǎo)出當(dāng)前的堆棧信息到指定文件中。

-
jstack pid打印當(dāng)前pid所有的線程信息
3.常用工具
-
jconsole pid打開(kāi)一個(gè)工具并且連接到當(dāng)前的java進(jìn)程,可以在工具中查看當(dāng)前堆、線程等一些信息

-
jvisualvm控制臺(tái)輸入該命令后后會(huì)啟動(dòng)一個(gè)客戶端,在左側(cè)列表和選擇本地的進(jìn)程進(jìn)行連接

那么當(dāng)我們的項(xiàng)目出現(xiàn)
OutOfMemeryError或者StackOfFlowError等錯(cuò)誤的時(shí)候如何定位到問(wèn)題呢?
- 第一種辦法是我們上面提到的
jmap命令,它可以dump出當(dāng)前java進(jìn)程的堆棧信息,然后進(jìn)行分析。 - 第二種辦法就是在啟動(dòng)
java進(jìn)程的時(shí)候設(shè)置一些參數(shù),讓jvm能夠在發(fā)生異常的時(shí)候輸出hprof文件到本地。- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=dump.hprof
得到hprof文件后我們需要借助一些工具來(lái)進(jìn)行分析,這里推薦Eclipse Memory Analyzer工具(下載地址),安裝好之后直接open file打開(kāi)hprof后就會(huì)自動(dòng)對(duì)其進(jìn)行分析。

可以看到這里提供了很多的工具,你可以具體的查看來(lái)分析可能的問(wèn)題。
4.總結(jié)
對(duì)于JVM的調(diào)優(yōu),沒(méi)有一個(gè)確定的辦法,只能根據(jù)具體的問(wèn)題做出具體的分析。但是只要你對(duì)JVM內(nèi)存模型、垃圾收集等具體的原理了解清楚,當(dāng)出現(xiàn)問(wèn)題的時(shí)候,你就知道該從哪里下手,如何能快速的定位到問(wèn)題。


