軟件性能測試分析與調優實踐之路-Java應用程序的性能分析與調優-手稿節選


Java編程語言自從誕生起,就成為了一門非常流行的編程語言,覆蓋了互聯網、安卓應用、后端應用、大數據等很多技術領域,因此Java應用程序的性能分析和調優也是一門非常重要的課題。Java應用程序的性能直接關系到了很多大型電商網站的訪問承載能力、大數據的數據處理量等,它的性能分析和調優往往還可以節省很多的硬件成本。

5.1  JVM基礎知識

5.1.1  JVM簡介

JVM是Java Virtual Machine(Java虛擬機)的英文簡寫,通過在實際的計算機上仿真模擬各種計算機功能來實現的。Java編程語言在引入了Java虛擬機后,使得Java應用程序可以在不同操作系統平台上運行,而不需要再次重新編譯。Java編程語言通過使用Java虛擬機屏蔽了與具體操作系統平台相關的信息,保證了編譯后的應用程序的平台兼容性,使得Java應用程序只需編譯生成在Java虛擬機上運行的目標代碼(字節碼),就可以在不同的操作系統上部署和運行。Java虛擬機本質上可以認為是運行在操作系統上的一個程序、一個進程。Java虛擬機在啟動后就開始執行保存在字節碼文件中的指令,其內部組成結構如圖5-1-1所示。

 

圖5-1-1

在JDK1.8(Java 8)及以后的版本中,JVM的內部組成結構發生了一些小的變化,如圖5-1-2所示。

 

圖5-1-2

5.1.2  類加載器

類加載器(Class Loader)負責將編譯好的.class字節碼文件裝載到內存中,使得JVM可以實例化或以其他方式使用加載后的類。類加載器支持在運行時的動態加載,動態加載可以節省內存空間,靈活地從本地或者網絡上加載類,可以通過命名空間的分隔來實現類的隔離,增強了整個系統的安全性等。類加載器分為如下幾種:

l啟動類加載器(BootStrap Class Loader): 啟動類加載器是最底層的加載器由C/C++語言實現,非Java語言實現,負責加載JDK中的rt.jar文件中所有的Java字節碼文件。如圖5-1-3所示,rt.jar文件一般位於JDK的jre目錄下,里面存放中Java語言自身的核心字節碼文件。Java自身的核心字節碼文件一般都是由啟動類加載器進行加載。

 

圖5-1-3

l擴展類加載器(Extension Class Loader):負責加載一些擴展功能的jar包到內存中。一般負責加載<Java_Runtime_Home >/lib/ext目錄或者由系統變量-Djava.ext.dir指定位置中的字節碼文件。

l系統類加載器(System Class Loader):負責將系統類路徑java -classpath或-Djava.class.path參數所指定的目錄下的字節碼類庫加載到內存中。通常程序員自己編寫的Java程序也是由該類加載器進行加載。

類加載器加載類的過程如圖5-1-4所示,該圖同時也描述了一個class字節碼文件的整個生命周期。

 

圖5-1-4

本文作者:張永清,轉載請注明:https://www.cnblogs.com/laoqing/p/15950682.html 來源於博客園

類加載器加載過程詳細描述如表5-1所示。

表5-1 類加載器加載過程詳細描述

步驟

說明

加載

將指定的.calss字節碼文件加載到JVM中

連接

已經加載到JVM中的二進制字節流的類數據信息,合並到JVM的運行時狀態中,加載過程包括驗證、准備、解析三個步驟

驗證

校驗.class字節碼文件的正確性,確保該文件是符合規范定義的,並且適合當前JVM版本的使用。一般包含如下4個子步驟:

(1)文件格式校驗:校驗字節碼文件的格式是否符合規范、版本號是否正確並且對應的版本是否是當前JVM可以支持的、常量池中的常量是否有不被支持的類型等。

(2)元數據校驗:對字節碼描述的信息進行語義分析,以確保其描述的信息符合Java語言的規范。

(3)字節碼校驗:通過對字節碼文件的數據流和控制流進行分析,驗證代碼的語義是合法的、符合Java語言編程規范的。

(4)符號引用校驗:符號引用是指以一組符號來描述所引用的目標,校驗符號引用轉化成為真正的內存地址是否正確

准備

為加載到JVM中的類分配內存,同時初始化類中的靜態變量的初始值

解析

將符號引用轉換為直接引用,一般主要是把類的常量池中的符號引用解析為直接引用

初始化

初始化類中的靜態變量,並執行類中的static代碼、構造函數等。如果沒有構造函數,系統添加默認的無參構造函數。如果類的構造函數中沒有顯示的調用父類的構造函數,編譯器會自動生成一個父類的無參構造函數

被調用

指在運行時被使用

卸載

指將類從JVM中移除

5.1.3  Java虛擬機棧和本地方法棧

Java虛擬機棧是Java方法執行的內存模型,是線程私有的,和線程直接相關。每創建一個新的線程,JVM就會為該線程分配一個對應的Java棧。各個線程的Java棧的內存區域是不能互相直接被訪問的,以保證在並發運行時線程的安全性。每調用一個方法,Java虛擬機棧就會為每個方法生成一個棧幀(Stack Frame),調用方法時壓入棧幀(通常叫入棧),方法返回時彈出棧幀並拋棄(通常叫出棧)。棧幀中存儲局部變量、操作數棧、動態鏈接、中間運算結果、方法返回值等信息。每個方法被調用和完成的過程,都對應一個棧幀從虛擬機棧上入棧和出棧的過程。虛擬機棧的生命周期和線程是一樣的,棧幀中存儲的局部變量隨着線程運行的結束而結束。

本地方法棧類似於Java虛擬機棧,主要存儲了本地方法native method,指用native關鍵字修飾的方法)調用的狀態和信息,是為了方便JVM去調用本地方法(native method)和接口的棧區。

和棧相關的常見異常如下:

lStackOverflowError:俗稱棧溢出。一般當棧深度超過JVM虛擬機分配給線程的棧大小時,就會出現這個錯誤。在循環調用方法而無法退出的情況下,容易出現棧溢出錯誤。

lOutOfMemoryError:詳細錯誤信息一般為“Exception in thread "main" java.lang.OutOfMemoryError:unable to create new native thread”。Java 虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出 OutOfMemoryError 錯誤。

5.1.4  方法區與元數據區

本文作者:張永清,轉載請注明:https://www.cnblogs.com/laoqing/p/15950682.html 來源於博客園

方法區也就是我們常說的永久代區域,里面存儲着Java 類信息、常量池、靜態變量等數據,方法區占用的內存區域在JVM中是線程共享的。在JDK1.8及以后的版本中,方法區已經被移除,取而代之的是元數據區和本地內存,類的元數據信息直接存放到JVM管理的本地內存中。需要注意的是,本地內存不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域。常量池、靜態變量等數據則存放到了Java堆(Heap)中。這樣做的目的主要是為了減少加載的類過多時容易造成Full GC問題。

5.1.5  堆區

Java是一門面向對象的開發語言,而JVM堆區是真正存儲Java對象實例的內存區域,並且是所有線程共享的。所以Java程序在進行實例化對象等操作時,需要解決同步和線程安全問題。Java堆區可以細分為新生代區域和老年代區域。新生代還可以再細分為Eden空間區域、From Survivor空間區域、To Survivor空間區域,如圖5-1-5所示。堆區是發生GC垃圾回收最頻繁的內存區域,因此也是JVM性能調優的關鍵區域。

 

圖5-1-5

Java堆區內部結構說明如表5-2所示。

表5-2 Java堆區內部結構說明

區域

說明

新生代區

又稱年輕代區域,由Eden空間區域和Survivor空間區域共同組成。在新生代區域中JVM默認內存分配比例為Eden : From Survivo : To Survivor = 8 : 1 : 1

Eden空間區域

新生對象存放的內存區域,存放着首次創建的對象實例

Survivor空間區域

由From Survivor空間區域和To Survivor空間區域共同組成,並且這兩個區域中總是有一個是空的

From Survivor空間區域

存儲Eden空間區域發生GC垃圾回收后幸存的對象實例。From Survivor空間區域和To Survivor空間區域的作用是等價的,並且默認情況下這兩個區域的大小是一樣大的

To Survivor空間區域

存儲Eden空間區域發生GC垃圾回收后幸存的對象實例。當一個Survivor(幸存者)空間飽和,依舊存活的對象會被移動到另一個Survivor(幸存者)空間,然后會清空已經飽和的那個Survivor(幸存者)空間

老年代區域

JVM的垃圾回收器分代進行垃圾回收。在回收到一定次數(可以通過JVM參數設定)后,依然存活的新生代對象實例將會進入老年代區域

上圖5-1-5中的箭頭指示的方向就代表JVM堆區分代進行垃圾回收時數據的移動過程。對象在剛剛被創建之后是保存在Eden空間區域的,那些長期存活的對象會經由Survivor(幸存者)空間轉存到老年代空間區域(Old generation)。當然對於一些比較大的對象(需要分配一塊比較大的連續內存空間),則直接進入到老年代區域,這種情況一般在Survivor 空間區域內存不足的時候下會發生。

在JDK1.7以及之前的版本中,JVM的共享內存區域組成如圖5-1-6所示。

本文作者:張永清,轉載請注明:https://www.cnblogs.com/laoqing/p/15950682.html 來源於博客園

 

圖5-1-6

在JDK1.8以及之后的版本中,JVM的共享內存區域組成如圖5-1-7所示。

 

圖5-1-7

5.1.6  程序計數器

程序計數器是一個記錄着線程所執行的字節碼指令位置的指示器,裝載入JVM內存中的.class字節碼文件通過字節碼解釋器進行解釋執行,按照順序讀取字節碼指令。每讀取一個指令后,將該指令轉換成對應的操作,並根據這些操作進行分支、循環、條件判斷等流程處理。由於程序一般是多線程來協同執行的,並且JVM的多線程是通過CPU時間片輪轉(即線程輪流切換並分配公平爭搶CPU執行時間)算法來實現的,這樣就存在着某個線程在執行過程中可能會因為時間片耗盡而被掛起,而另一個線程獲取到時間片開始執行。當被掛起的線程重新獲取到CPU時間片的時候,它要想從被掛起的地方繼續執行,就必須知道它上次執行到哪個位置即代碼中的具體行號了,在JVM中就是通過程序計數器來記錄某個線程的字節碼指令的執行位置。因此,程序計數器是線程私有的、是線程隔離的,每個線程在運行時都有屬於自己的程序計數器。另外,如果是執行native方法,程序計數器的值為空,因為native方法是Java通過JNI(Java Native Interface)直接調用Java本地C/C++語言庫執行的,而C/C++語言實現的方法自然無法產生相應的.class字節碼(C/C++語言是按照C/C++語言的方式來執行的),因此Java的程序計數器此時是無值的。

5.1.7  垃圾回收

Java語言和別的編程語言不一樣,程序運行時的內存回收不需要開發者自己在代碼中進行手動回收和釋放,而是JVM自動進行內存回收。內存回收時會將已經不再使用的對象實例等從內存中移除掉,以釋放出更多的內存空間,這個過程就是常說的JVM垃圾回收機制。

垃圾回收一般也叫GC,新生代的垃圾回收一般稱作Minor GC,老年代的垃圾回收一般稱作Major GC或者Full GC。垃圾回收之所以如此重要,是因為發生垃圾回收時一般會伴隨着應用程序的暫停運行。一般發生垃圾回收時除GC所需的線程外,所有的其他線程都進入等待狀態,直到GC執行完成。GC調優最主要目標就是減少應用程序的暫停執行時間。

JVM垃圾回收的常見算法有根搜索算法、標記-清除算法、復制算法標記-整理算法增量回收算法

1. 根搜索算法

根搜索算法把垃圾回收線程把應用程序的所有引用關系看作一張圖,從一個節點GC ROOT(英文解釋為A garbage collection root is an object that is accessible from outside the heap,即一個可以從堆外訪問的對象) 開始,尋找對應的引用節點,找到這個節點后,繼續尋找這個節點的引用節點。當所有的引用節點尋找完畢后,剩余的節點則被認為是沒有被引用到的節點,即無用的節點,然后對這些節點執行垃圾回收。

如圖5-1-8所示,顏色較深的節點(實例對象6、實例對象7、實例對象8)就是可以被垃圾回收的節點,因為這些節點已經被引用了。

 

圖5-1-8

本文作者:張永清,轉載請注明:https://www.cnblogs.com/laoqing/p/15950682.html 來源於博客園

IBM網站頁面https://www.ibm.com/support/knowledgecenter/en/SS3KLZ/com.ibm.java. diagnostics.memory.analyzer.doc/gcroots.html介紹中認為JVM中可以作為GC ROOT節點的對象包括:

System[z8] class[u9]

A class that was loaded by the bootstrap loader, or the system class loader. For example, this category includes all classes in the rt.jar file (part of the Java™ runtime environment), such as those in the java.util.* package.

JNI local

A local variable in native code, for example user-defined JNI code or JVM internal code.

JNI global

A global variable in native code, for example user-defined JNI code or JVM internal code.

Thread block

An object that was referenced from an active thread block.

Thread

A running thread.

Busy monitor

Everything that called the wait() or notify() methods, or that is synchronized, for example by calling the synchronized(Object) method or by entering a synchronized method. If the method was static, the root is a class, otherwise it is an object.

Java local

A local variable. For example, input parameters, or locally created objects of methods that are still in the stack of a thread.

Native stack

Input or output parameters in native code, for example user-defined JNI code or JVM internal code. Many methods have native parts, and the objects that are handled as method parameters become garbage collection roots. For example, parameters used for file, network, I/O, or reflection operations.

Finalizer

An object that is in a queue, waiting for a finalizer to run.

Unfinalized

An object that has a finalize method, but was not finalized, and is not yet on the finalizer queue.

Unreachable

An object that is unreachable from any other root, but was marked as a root by Memory Analyzer so that the object can be included in an analysis.

Unreachable objects are often the result of optimizations in the garbage collection algorithm. For example, an object might be a candidate for garbage collection, but be so small that the garbage collection process would be too expensive. In this case, the object might not be garbage collected, and might remain as an unreachable object.

By default, unreachable objects are excluded when Memory Analyzer parses the heap dump. These objects are therefore not shown in the histogram, dominator tree, or query results. You can change this behavior by clicking File > Preferences... > IBM Diagnostic Tools for Java - Memory Analyzer, then selecting the Keep unreachable objects check box.

而https://www.dynatrace.com/resources/ebooks/javabook/how-garbage-collection-works/網站中給出的解釋如圖5-1-9所示。

 

圖5-1-9

最終我們總結歸納如下:

(1)JVM虛擬機棧中引用的實例對象。

(2)方法區中靜態屬性引用的對象(僅針對JDK1.8之前的JVM,JDK1.8及之后由於不存在方法區,靜態屬性直接存於Heap中)。

(3)方法區中靜態常量引用的對象(僅針對JDK1.8之前的JVM,JDK1.8及之后由於不存在方法區,靜態常量直接存於Heap中)。

(4)本地方法(native method,多用在JNI接口調用中)棧中引用的對象。

(5)JVM自身持有的對象,比如啟動類加載器、系統類加載器等。

下面講的其他GC算法基本都會引用根搜索算法這種概念。

本文作者:張永清,轉載請注明:https://www.cnblogs.com/laoqing/p/15950682.html 來源於博客園

2. 標記-清除算法

如圖5-1-10所示,標記-清除算法采用從GC ROOT進行掃描,對存活的對象節點進行標記,標記完成后再掃描整個內存區域中未被標記的對象進行直接回收。由於標記-清除算法標記完畢后不會對存活的對象進行移動和整理,因此很容易導致內存碎片空閑的連續內存空間比要申請的空間小,導致大量空閑的小內存塊不能被利用。但是由於僅對不存活的對象進行處理,在存活的對象較多、不存活的對象較少的情況下,標記清除-算法的性能極高。

 

圖5-1-10

3. 復制算法

復制算法同樣采用從GC ROOT根集合掃描,將存活的對象復制到空閑區間,當掃描完活動區間后,會將活動區間內存一次性全部回收,此時原來的活動區間就變成了空閑區域,如圖5-1-11所示。復制算法會將內存分為兩個區間,所有動態分配的實例對象都只能分配在其中一個區間(此時該區間就變成了活動區間),而另外一個區間則是空閑的,每次GC時都重復這樣的操作,每次總是會有一個區域是空閑的。

 

圖5-1-11

4. 標記-整理算法

采用標記-清除算法一樣的方式進行對象的標記、清除,但在回收不存活的對象占用的內存空間后,會將所有存活的對象往左端空閑空間移動,並更新對應的內存節點指針,如圖5-1-12所示。標記-整理算法是在標記-清除算法之上,又進行了對象的移動排序整理,雖然性能成本更高了,但卻解決了內存碎片的問題。如果不解決內存碎片的問題,一旦出現需要創建一個大的對象實例時,JVM可能無法給這個大的實例對象分配連續的大內存,從而導致發生Full GC。在垃圾回收中,Full GC 應該是需要盡量去避免的,因為一旦出現Full GC,一般會導致應用程序暫停很久以等待Full GC完成。

本文作者:張永清,轉載請注明:https://www.cnblogs.com/laoqing/p/15950682.html 來源於博客園

 

圖5-1-12

JVM為了優化垃圾回收的性能,使用了分代回收的方式。它對於新生代內存的回收(Minor GC)主要采用復制算法,而對於老年代的回收(Major GC/Full GC),大多采用標記-整理算法。在做垃圾回收優化時,最重要的一點就是減少老年代垃圾回收的次數,因為老年代垃圾回收耗時長,性能成本非常高,對應用程序的運行影響非常大。

4. 增量回收算法

增量回收算法把JVM內存空間划分為多個區域,每次僅對其中某一個區域進行垃圾回收,這樣做的好處就是減小應用程序的的中斷時間,使得用戶一般不能覺察到垃圾回收器正在工作

5.1.8  並行與並發

本文作者:張永清,轉載請注明:https://www.cnblogs.com/laoqing/p/15950682.html 來源於博客園

在並發程序開發中經常會提到並行與並發。在垃圾回收中並行和並發的區別如下:

l並行:JVM啟動多個垃圾回收線程並行工作,但此時用戶線程(應用程序的工作線程)需要一直處於等待狀態。

l並發:指用戶線程(應用程序的工作線程)與垃圾回收線程同時執行(但並不一定是並行的,可能會交替執行),用戶線程此時可以繼續運行,而垃圾回收線程運行於另一個CPU核上,彼此可以互不干擾。

未完待續,本文作者:張永清,轉載請注明:https://www.cnblogs.com/laoqing/p/15950682.html 來源於博客園。 本文摘選自《軟件性能測試分析與調優實踐之路》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM