記憶體分析工具講解
阿新 • • 發佈:2019-02-07
前言
在使用Memory Analyzer tool(MAT)分析記憶體洩漏(一)中,我介紹了記憶體洩漏的前因後果。在本文中,將介紹MAT如何根據heap dump分析洩漏根源。由於測試範例可能過於簡單,很容易找出問題,但我期待藉此舉一反三。
一開始不得不說說ClassLoader,本質上,它的工作就是把磁碟上的類檔案讀入記憶體,然後呼叫java.lang.ClassLoader.defineClass方法告訴系統把記憶體映象處理成合法的位元組碼。Java提供了抽象類ClassLoader,所有使用者自定義類裝載器都例項化自ClassLoader的子類。system class loader在沒有指定裝載器的情況下預設裝載使用者類,在Sun Java 1.5中既sun.misc.Launcher$AppClassLoader。更詳細的內容請參看下面的資料。
準備heap dump
請看下面的Pilot類,沒啥特殊的。
/**
* Pilot class
* @author rosen jiang
*/
package org.rosenjiang.bo;
public class Pilot{
String name;
int age;
public Pilot(String a, int b){
name = a;
age = b;
}
}
然後再看OOMHeapTest類,它是如何撐破heap dump的。
/**
* OOMHeapTest class
* @author rosen jiang
package org.rosenjiang.test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.rosenjiang.bo.Pilot;
public class OOMHeapTest {
public static void main(String[] args){
oom();
}
private static void oom(){
Map<String, Pilot> map = new HashMap<String, Pilot>();
Object[] array = new
for(int i=0; i<1000000; i++){
String d = new Date().toString();
Pilot p = new Pilot(d, i);
map.put(i+"rosen jiang", p);
array[i]=p;
}
}
}
是的,上面構造了很多的Pilot類例項,向陣列和map中放。由於是Strong Ref,GC自然不會回收這些物件,一直放在heap中直到溢位。當然在執行前,先要在Eclipse中配置VM引數-XX:+HeapDumpOnOutOfMemoryError。好了,一會兒功夫記憶體溢位,控制檯打出如下資訊。
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3600.hprof
Heap dump file created [78233961 bytes in 1.995 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
java_pid3600.hprof既是heap dump,可以在OOMHeapTest類所在的工程根目錄下找到。
MAT安裝
話分兩頭說,有了heap dump還得安裝MAT。可以在http://www.eclipse.org/mat/downloads.php選擇合適的方式安裝。安裝完成後切換到Memory Analyzer檢視。在Eclipse的左上角有Open Heap Dump按鈕,按照剛才說的路徑找到java_pid3600.hprof檔案並開啟。解析hprof檔案會花些時間,然後會彈出嚮導,直接Finish即可。稍後會看到下圖所示的介面。
MAT工具分析了heap dump後在介面上非常直觀的展示了一個餅圖,該圖深色區域被懷疑有記憶體洩漏,可以發現整個heap才64M記憶體,深色區域就佔了99.5%。接下來是一個簡短的描述,告訴我們main執行緒佔用了大量記憶體,並且明確指出system class loader載入的"java.lang.Thread"例項有記憶體聚集,並建議用關鍵字"java.lang.Thread"進行檢查。所以,MAT通過簡單的兩句話就說明了問題所在,就算使用者沒什麼處理記憶體問題的經驗。在下面還有一個"Details"連結,在點開之前不妨考慮一個問題:為何物件例項會聚集在記憶體中,為何存活(而未被GC)?是的——Strong Ref,那麼再走近一些吧。
點選了"Details"連結之後,除了在上一頁看到的描述外,還有Shortest Paths To the Accumulation Point和Accumulated Objects部分,這裡說明了從GC root到聚集點的最短路徑,以及完整的reference chain。觀察Accumulated Objects部分,java.util.HashMap和java.lang.Object[1000000]例項的retained heap(size)最大,在上一篇文章中我們知道retained heap代表從該類例項沿著reference chain往下所能收集到的其他類例項的shallow heap(size)總和,所以明顯類例項都聚集在HashMap和Object陣列中了。這裡我們發現一個有趣的現象,既Object陣列的shallow heap和retained heap竟然一樣,通過Shallow and retained sizes一文可知,陣列的shallow heap和一般物件(非陣列)不同,依賴於陣列的長度和裡面的元素的型別,對陣列求shallow heap,也就是求陣列集合內所有物件的shallow heap之和。好,再來看org.rosenjiang.bo.Pilot物件例項的shallow heap為何是16,因為物件頭是8位元組,成員變數int是4位元組、String引用是4位元組,故總共16位元組。
接著往下看,來到了Accumulated Objects by Class區域,顧名思義,這裡能找到被聚集的物件例項的類名。org.rosenjiang.bo.Pilot類上頭條了,被例項化了290,325次,再返回去看程式,我承認是故意這麼幹的。還有很多有用的報告可用來協助分析問題,只是本文中的例子太簡單,也用不上。以後如有用到,一定撰文詳細敘述。
又是perm gen
我們在上一篇文章中知道,perm gen是個異類,裡面儲存了類和方法資料(與class loader有關)以及interned strings(字串駐留)。在heap dump中沒有包含太多的perm gen資訊。那麼我們就用這些少量的資訊來解決問題吧。
看下面的程式碼,利用interned strings把perm gen撐破了。
/**
* OOMPermTest class
* @author rosen jiang
*/
package org.rosenjiang.test;
public class OOMPermTest {
public static void main(String[] args){
oom();
}
private static void oom(){
Object[] array = new Object[10000000];
for(int i=0; i<10000000; i++){
String d = String.valueOf(i).intern();
array[i]=d;
}
}
}
控制檯列印如下的資訊,然後把java_pid1824.hprof檔案匯入到MAT。其實在MAT裡,看到的狀況應該和“OutOfMemoryError: Java heap space”差不多(用了陣列),因為heap dump並沒有包含interned strings方面的任何資訊。只是在這裡需要強調,使用intern()方法的時候應該多加註意。
java.lang.OutOfMemoryError: PermGen space
Dumping heap to java_pid1824.hprof
Heap dump file created [121273334 bytes in 2.845 secs]
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
倒是在思考如何把class loader撐破廢了些心思。經過嘗試,發現使用ASM來動態生成類才能達到目的。ASM(http://asm.objectweb.org)的主要作用是處理已編譯類(compiled class),能對已編譯類進行生成、轉換、分析(功能之一是實現動態代理),而且它執行起來足夠的快和小巧,文件也全面,實屬居家必備之良品。ASM提供了core API和tree API,前者是基於事件的方式,後者是基於物件的方式,類似於XML的SAX、DOM解析,但是使用tree API效能會有損失。既然下面要用到ASM,這裡不得不囉嗦下已編譯類的結構,包括:
1、修飾符(例如public、private)、類名、父類名、介面和annotation部分。
2、類成員變數宣告,包括每個成員的修飾符、名字、型別和annotation。
3、方法和建構函式描述,包括修飾符、名字、返回和傳入引數型別,以及annotation。當然還包括這些方法或建構函式的具體Java位元組碼。
4、常量池(constant pool)部分,constant pool是一個包含類中出現的數字、字串、型別常量的陣列。
已編譯類和原來的類原始碼區別在於,已編譯類只包含類本身,內部類不會在已編譯類中出現,而是生成另外一個已編譯類檔案;其二,已編譯類中沒有註釋;其三,已編譯類沒有package和import部分。
這裡還得說說已編譯類對Java型別的描述,對於原始型別由單個大寫字母表示,Z代表boolean、C代表char、B代表byte、S代表short、I代表int、F代表float、J代表long、D代表double;而對類型別的描述使用內部名(internal name)外加字首L和後面的分號共同表示來表示,所謂內部名就是帶全包路徑的表示法,例如String的內部名是java/lang/String;對於陣列型別,使用單方括號加上資料元素型別的方式描述。最後對於方法的描述,用圓括號來表示,如果返回是void用V表示,具體參考下圖。
下面的程式碼中會使用ASM core API,注意介面ClassVisitor是核心,FieldVisitor、MethodVisitor都是輔助介面。ClassVisitor應該按照這樣的方式來呼叫:visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*( visitInnerClass | visitField | visitMethod )* visitEnd。就是說visit方法必須首先呼叫,再呼叫最多一次的visitSource,再呼叫最多一次的visitOuterClass方法,接下來再多次呼叫visitAnnotation和visitAttribute方法,最後是多次呼叫visitInnerClass、visitField和visitMethod方法。呼叫完後再呼叫visitEnd方法作為結尾。
注意ClassWriter類,該類實現了ClassVisitor介面,通過toByteArray方法可以把已編譯類直接構建成二進位制形式。由於我們要動態生成子類,所以這裡只對ClassWriter感興趣。首先是抽象類原型:
/**
* @author rosen jiang
* MyAbsClass class
*/
package org.rosenjiang.test;
public abstract class MyAbsClass {
int LESS = -1;
int EQUAL = 0;
int GREATER = 1;
abstract int absTo(Object o);
}
其次是自定義類載入器,實在沒法,ClassLoader的defineClass方法都是protected的,要載入位元組陣列形式(因為toByteArray了)的類只有繼承一下自己再實現。
/**
* @author rosen jiang
* MyClassLoader class
*/
package org.rosenjiang.test;
public class MyClassLoader extends ClassLoader {
public Class defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}
最後是測試類。
/**
* @author rosen jiang
* OOMPermTest class
*/
package org.rosenjiang.test;
import java.util.ArrayList;
import java.util.List;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
public class OOMPermTest {
public static void main(String[] args) {
OOMPermTest o = new OOMPermTest();
o.oom();
}
private void oom() {
try {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT,
"org/rosenjiang/test/MyAbsClass", null, "java/lang/Object",
new String[] {});
cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "LESS", "I",
null, new Integer(-1)).visitEnd();
cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "EQUAL", "I",
null, new Integer(0)).visitEnd();
cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "GREATER", "I",
null, new Integer(1)).visitEnd();
cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "absTo",
"(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd();
byte[] b = cw.toByteArray();
List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
while (true) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.defineClass("org.rosenjiang.test.MyAbsClass", b);
classLoaders.add(classLoader);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
不一會兒,控制檯就報錯了。
java.lang.OutOfMemoryError: PermGen space
Dumping heap to java_pid3023.hprof
Heap dump file created [92593641 bytes in 2.405 secs]
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
開啟java_pid3023.hprof檔案,注意看下圖的Classes: 88.1k和Class Loader: 87.7k部分,從這點可看出class loader載入了大量的類。
更進一步分析,點選上圖中紅框線圈起來的按鈕,選擇Java Basics——Class Loader Explorer功能。開啟後能看到下圖所示的介面,第一列是class loader名字;第二列是class loader已定義類(defined classes)的個數,這裡要說一下已定義類和已載入類(loaded classes)了,當需要載入類的時候,相應的class loader會首先把請求委派給父class loader,只有當父class loader載入失敗後,該class loader才會自己定義並載入類,這就是Java自己的“雙親委派載入鏈”結構;第三列是class loader所載入的類的例項數目。
在Class Loader Explorer這裡,能發現class loader是否載入了過多的類。另外,還有Duplicate Classes功能,也能協助分析重複載入的類,在此就不再截圖了,可以肯定的是MyAbsClass被重複載入了N多次。
最後
其實MAT工具非常的強大,上面故弄玄虛的範例程式碼根本用不上MAT的其他分析功能,所以就不再描述了。其實對於OOM不只我列舉的兩種溢位錯誤,還有多種其他錯誤,但我想說的是,對於perm gen,如果實在找不出問題所在,建議使用JVM的-verbose引數,該引數會在後臺打印出日誌,可以用來檢視哪個class loader載入了什麼類,例:“[Loaded org.rosenjiang.test.MyAbsClass from org.rosenjiang.test.MyClassLoader]”。
全文完。
參考資料
memoryanalyzer Blog
java類載入器體系結構
ClassLoader