前言的前言
寫blog就是好,在大前提下可以想說(shuō)什么寫什么,不像投稿那么字字斟酌。上周末回了趟成都辦事,所以本文來(lái)遲了。K117從達(dá)州經(jīng)由達(dá)成線往成都方向走的時(shí)候,發(fā)現(xiàn)鐵路邊有條河,盡管我現(xiàn)在也不知道其名字,但已被其深深的陶醉。河很寬且水流平緩,河邊山丘森林密布,民房星星點(diǎn)點(diǎn)的分布在河邊,河里偶爾些小船。當(dāng)時(shí)我就在想,在這里生活是多么的愜意,夏天還可以下去暢游一番,閑來(lái)無(wú)事也可垂釣。唉,越來(lái)越討厭北漂了。
前言
在使用Memory Analyzer tool(MAT)分析內(nèi)存泄漏(一)中,我介紹了內(nèi)存泄漏的前因后果。在本文中,將介紹MAT如何根據(jù)heap dump分析泄漏根源。由于測(cè)試范例可能過(guò)于簡(jiǎn)單,很容易找出問(wèn)題,但我期待借此舉一反三。
一開(kāi)始不得不說(shuō)說(shuō)ClassLoader,本質(zhì)上,它的工作就是把磁盤上的類文件讀入內(nèi)存,然后調(diào)用java.lang.ClassLoader.defineClass方法告訴系統(tǒng)把內(nèi)存鏡像處理成合法的字節(jié)碼。Java提供了抽象類ClassLoader,所有用戶自定義類裝載器都實(shí)例化自ClassLoader的子類。system class loader在沒(méi)有指定裝載器的情況下默認(rèn)裝載用戶類,在Sun Java 1.5中既sun.misc.Launcher$AppClassLoader。更詳細(xì)的內(nèi)容請(qǐng)參看下面的資料。
準(zhǔn)備heap dump
請(qǐng)看下面的Pilot類,沒(méi)啥特殊的。
然后再看OOMHeapTest類,它是如何撐破heap dump的。
是的,上面構(gòu)造了很多的Pilot類實(shí)例,向數(shù)組和map中放。由于是Strong Ref,GC自然不會(huì)回收這些對(duì)象,一直放在heap中直到溢出。當(dāng)然在運(yùn)行前,先要在Eclipse中配置VM參數(shù)-XX:+HeapDumpOnOutOfMemoryError。好了,一會(huì)兒功夫內(nèi)存溢出,控制臺(tái)打出如下信息。
java_pid3600.hprof既是heap dump,可以在OOMHeapTest類所在的工程根目錄下找到。
MAT安裝
話分兩頭說(shuō),有了heap dump還得安裝MAT。可以在http://www.eclipse.org/mat/downloads.php選擇合適的方式安裝。安裝完成后切換到Memory Analyzer視圖。在Eclipse的左上角有Open Heap Dump按鈕,按照剛才說(shuō)的路徑找到j(luò)ava_pid3600.hprof文件并打開(kāi)。解析hprof文件會(huì)花些時(shí)間,然后會(huì)彈出向?qū)В苯覨inish即可。稍后會(huì)看到下圖所示的界面。

MAT工具分析了heap dump后在界面上非常直觀的展示了一個(gè)餅圖,該圖深色區(qū)域被懷疑有內(nèi)存泄漏,可以發(fā)現(xiàn)整個(gè)heap才64M內(nèi)存,深色區(qū)域就占了99.5%。接下來(lái)是一個(gè)簡(jiǎn)短的描述,告訴我們main線程占用了大量?jī)?nèi)存,并且明確指出system class loader加載的"java.lang.Thread"實(shí)例有內(nèi)存聚集,并建議用關(guān)鍵字"java.lang.Thread"進(jìn)行檢查。所以,MAT通過(guò)簡(jiǎn)單的兩句話就說(shuō)明了問(wèn)題所在,就算使用者沒(méi)什么處理內(nèi)存問(wèn)題的經(jīng)驗(yàn)。在下面還有一個(gè)"Details"鏈接,在點(diǎn)開(kāi)之前不妨考慮一個(gè)問(wèn)題:為何對(duì)象實(shí)例會(huì)聚集在內(nèi)存中,為何存活(而未被GC)?是的——Strong Ref,那么再走近一些吧。

點(diǎn)擊了"Details"鏈接之后,除了在上一頁(yè)看到的描述外,還有Shortest Paths To the Accumulation Point和Accumulated Objects部分,這里說(shuō)明了從GC root到聚集點(diǎn)的最短路徑,以及完整的reference chain。觀察Accumulated Objects部分,java.util.HashMap和java.lang.Object[1000000]實(shí)例的retained heap(size)最大,在上一篇文章中我們知道retained heap代表從該類實(shí)例沿著reference chain往下所能收集到的其他類實(shí)例的shallow heap(size)總和,所以明顯類實(shí)例都聚集在HashMap和Object數(shù)組中了。這里我們發(fā)現(xiàn)一個(gè)有趣的現(xiàn)象,既Object數(shù)組的shallow heap和retained heap竟然一樣,通過(guò)Shallow and retained sizes一文可知,數(shù)組的shallow heap和一般對(duì)象(非數(shù)組)不同,依賴于數(shù)組的長(zhǎng)度和里面的元素的類型,對(duì)數(shù)組求shallow heap,也就是求數(shù)組集合內(nèi)所有對(duì)象的shallow heap之和。好,再來(lái)看org.rosenjiang.bo.Pilot對(duì)象實(shí)例的shallow heap為何是16,因?yàn)閷?duì)象頭是8字節(jié),成員變量int是4字節(jié)、String引用是4字節(jié),故總共16字節(jié)。

接著往下看,來(lái)到了Accumulated Objects by Class區(qū)域,顧名思義,這里能找到被聚集的對(duì)象實(shí)例的類名。org.rosenjiang.bo.Pilot類上頭條了,被實(shí)例化了290,325次,再返回去看程序,我承認(rèn)是故意這么干的。還有很多有用的報(bào)告可用來(lái)協(xié)助分析問(wèn)題,只是本文中的例子太簡(jiǎn)單,也用不上。以后如有用到,一定撰文詳細(xì)敘述。
又是perm gen
我們?cè)谏弦黄恼轮兄溃琾erm gen是個(gè)異類,里面存儲(chǔ)了類和方法數(shù)據(jù)(與class loader有關(guān))以及interned strings(字符串駐留)。在heap dump中沒(méi)有包含太多的perm gen信息。那么我們就用這些少量的信息來(lái)解決問(wèn)題吧。
看下面的代碼,利用interned strings把perm gen撐破了。
控制臺(tái)打印如下的信息,然后把java_pid1824.hprof文件導(dǎo)入到MAT。其實(shí)在MAT里,看到的狀況應(yīng)該和“OutOfMemoryError: Java heap space”差不多(用了數(shù)組),因?yàn)閔eap dump并沒(méi)有包含interned strings方面的任何信息。只是在這里需要強(qiáng)調(diào),使用intern()方法的時(shí)候應(yīng)該多加注意。
倒是在思考如何把class loader撐破廢了些心思。經(jīng)過(guò)嘗試,發(fā)現(xiàn)使用ASM來(lái)動(dòng)態(tài)生成類才能達(dá)到目的。ASM(http://asm.objectweb.org)的主要作用是處理已編譯類(compiled class),能對(duì)已編譯類進(jìn)行生成、轉(zhuǎn)換、分析(功能之一是實(shí)現(xiàn)動(dòng)態(tài)代理),而且它運(yùn)行起來(lái)足夠的快和小巧,文檔也全面,實(shí)屬居家必備之良品。ASM提供了core API和tree API,前者是基于事件的方式,后者是基于對(duì)象的方式,類似于XML的SAX、DOM解析,但是使用tree API性能會(huì)有損失。既然下面要用到ASM,這里不得不啰嗦下已編譯類的結(jié)構(gòu),包括:
1、修飾符(例如public、private)、類名、父類名、接口和annotation部分。
2、類成員變量聲明,包括每個(gè)成員的修飾符、名字、類型和annotation。
3、方法和構(gòu)造函數(shù)描述,包括修飾符、名字、返回和傳入?yún)?shù)類型,以及annotation。當(dāng)然還包括這些方法或構(gòu)造函數(shù)的具體Java字節(jié)碼。
4、常量池(constant pool)部分,constant pool是一個(gè)包含類中出現(xiàn)的數(shù)字、字符串、類型常量的數(shù)組。

已編譯類和原來(lái)的類源碼區(qū)別在于,已編譯類只包含類本身,內(nèi)部類不會(huì)在已編譯類中出現(xiàn),而是生成另外一個(gè)已編譯類文件;其二,已編譯類中沒(méi)有注釋;其三,已編譯類沒(méi)有package和import部分。
這里還得說(shuō)說(shuō)已編譯類對(duì)Java類型的描述,對(duì)于原始類型由單個(gè)大寫字母表示,Z代表boolean、C代表char、B代表byte、S代表short、I代表int、F代表float、J代表long、D代表double;而對(duì)類類型的描述使用內(nèi)部名(internal name)外加前綴L和后面的分號(hào)共同表示來(lái)表示,所謂內(nèi)部名就是帶全包路徑的表示法,例如String的內(nèi)部名是java/lang/String;對(duì)于數(shù)組類型,使用單方括號(hào)加上數(shù)據(jù)元素類型的方式描述。最后對(duì)于方法的描述,用圓括號(hào)來(lái)表示,如果返回是void用V表示,具體參考下圖。


下面的代碼中會(huì)使用ASM core API,注意接口ClassVisitor是核心,F(xiàn)ieldVisitor、MethodVisitor都是輔助接口。ClassVisitor應(yīng)該按照這樣的方式來(lái)調(diào)用:visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*( visitInnerClass | visitField | visitMethod )* visitEnd。就是說(shuō)visit方法必須首先調(diào)用,再調(diào)用最多一次的visitSource,再調(diào)用最多一次的visitOuterClass方法,接下來(lái)再多次調(diào)用visitAnnotation和visitAttribute方法,最后是多次調(diào)用visitInnerClass、visitField和visitMethod方法。調(diào)用完后再調(diào)用visitEnd方法作為結(jié)尾。
注意ClassWriter類,該類實(shí)現(xiàn)了ClassVisitor接口,通過(guò)toByteArray方法可以把已編譯類直接構(gòu)建成二進(jìn)制形式。由于我們要?jiǎng)討B(tài)生成子類,所以這里只對(duì)ClassWriter感興趣。首先是抽象類原型:
其次是自定義類加載器,實(shí)在沒(méi)法,ClassLoader的defineClass方法都是protected的,要加載字節(jié)數(shù)組形式(因?yàn)閠oByteArray了)的類只有繼承一下自己再實(shí)現(xiàn)。
最后是測(cè)試類。
不一會(huì)兒,控制臺(tái)就報(bào)錯(cuò)了。
打開(kāi)java_pid3023.hprof文件,注意看下圖的Classes: 88.1k和Class Loader: 87.7k部分,從這點(diǎn)可看出class loader加載了大量的類。

更進(jìn)一步分析,點(diǎn)擊上圖中紅框線圈起來(lái)的按鈕,選擇Java Basics——Class Loader Explorer功能。打開(kāi)后能看到下圖所示的界面,第一列是class loader名字;第二列是class loader已定義類(defined classes)的個(gè)數(shù),這里要說(shuō)一下已定義類和已加載類(loaded classes)了,當(dāng)需要加載類的時(shí)候,相應(yīng)的class loader會(huì)首先把請(qǐng)求委派給父class loader,只有當(dāng)父class loader加載失敗后,該class loader才會(huì)自己定義并加載類,這就是Java自己的“雙親委派加載鏈”結(jié)構(gòu);第三列是class loader所加載的類的實(shí)例數(shù)目。

在Class Loader Explorer這里,能發(fā)現(xiàn)class loader是否加載了過(guò)多的類。另外,還有Duplicate Classes功能,也能協(xié)助分析重復(fù)加載的類,在此就不再截圖了,可以肯定的是MyAbsClass被重復(fù)加載了N多次。
最后
其實(shí)MAT工具非常的強(qiáng)大,上面故弄玄虛的范例代碼根本用不上MAT的其他分析功能,所以就不再描述了。其實(shí)對(duì)于OOM不只我列舉的兩種溢出錯(cuò)誤,還有多種其他錯(cuò)誤,但我想說(shuō)的是,對(duì)于perm gen,如果實(shí)在找不出問(wèn)題所在,建議使用JVM的-verbose參數(shù),該參數(shù)會(huì)在后臺(tái)打印出日志,可以用來(lái)查看哪個(gè)class loader加載了什么類,例:“[Loaded org.rosenjiang.test.MyAbsClass from org.rosenjiang.test.MyClassLoader]”。
全文完。
參考資料
memoryanalyzer Blog
java類加載器體系結(jié)構(gòu)
ClassLoader
寫blog就是好,在大前提下可以想說(shuō)什么寫什么,不像投稿那么字字斟酌。上周末回了趟成都辦事,所以本文來(lái)遲了。K117從達(dá)州經(jīng)由達(dá)成線往成都方向走的時(shí)候,發(fā)現(xiàn)鐵路邊有條河,盡管我現(xiàn)在也不知道其名字,但已被其深深的陶醉。河很寬且水流平緩,河邊山丘森林密布,民房星星點(diǎn)點(diǎn)的分布在河邊,河里偶爾些小船。當(dāng)時(shí)我就在想,在這里生活是多么的愜意,夏天還可以下去暢游一番,閑來(lái)無(wú)事也可垂釣。唉,越來(lái)越討厭北漂了。
前言
在使用Memory Analyzer tool(MAT)分析內(nèi)存泄漏(一)中,我介紹了內(nèi)存泄漏的前因后果。在本文中,將介紹MAT如何根據(jù)heap dump分析泄漏根源。由于測(cè)試范例可能過(guò)于簡(jiǎn)單,很容易找出問(wèn)題,但我期待借此舉一反三。
一開(kāi)始不得不說(shuō)說(shuō)ClassLoader,本質(zhì)上,它的工作就是把磁盤上的類文件讀入內(nèi)存,然后調(diào)用java.lang.ClassLoader.defineClass方法告訴系統(tǒng)把內(nèi)存鏡像處理成合法的字節(jié)碼。Java提供了抽象類ClassLoader,所有用戶自定義類裝載器都實(shí)例化自ClassLoader的子類。system class loader在沒(méi)有指定裝載器的情況下默認(rèn)裝載用戶類,在Sun Java 1.5中既sun.misc.Launcher$AppClassLoader。更詳細(xì)的內(nèi)容請(qǐng)參看下面的資料。
準(zhǔn)備heap dump
請(qǐng)看下面的Pilot類,沒(méi)啥特殊的。
/**
* 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;
}
}
* 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 Object[1000000];
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;
}
}
}
* 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 Object[1000000];
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;
}
}
}
是的,上面構(gòu)造了很多的Pilot類實(shí)例,向數(shù)組和map中放。由于是Strong Ref,GC自然不會(huì)回收這些對(duì)象,一直放在heap中直到溢出。當(dāng)然在運(yùn)行前,先要在Eclipse中配置VM參數(shù)-XX:+HeapDumpOnOutOfMemoryError。好了,一會(huì)兒功夫內(nèi)存溢出,控制臺(tái)打出如下信息。
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










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安裝
話分兩頭說(shuō),有了heap dump還得安裝MAT。可以在http://www.eclipse.org/mat/downloads.php選擇合適的方式安裝。安裝完成后切換到Memory Analyzer視圖。在Eclipse的左上角有Open Heap Dump按鈕,按照剛才說(shuō)的路徑找到j(luò)ava_pid3600.hprof文件并打開(kāi)。解析hprof文件會(huì)花些時(shí)間,然后會(huì)彈出向?qū)В苯覨inish即可。稍后會(huì)看到下圖所示的界面。

MAT工具分析了heap dump后在界面上非常直觀的展示了一個(gè)餅圖,該圖深色區(qū)域被懷疑有內(nèi)存泄漏,可以發(fā)現(xiàn)整個(gè)heap才64M內(nèi)存,深色區(qū)域就占了99.5%。接下來(lái)是一個(gè)簡(jiǎn)短的描述,告訴我們main線程占用了大量?jī)?nèi)存,并且明確指出system class loader加載的"java.lang.Thread"實(shí)例有內(nèi)存聚集,并建議用關(guān)鍵字"java.lang.Thread"進(jìn)行檢查。所以,MAT通過(guò)簡(jiǎn)單的兩句話就說(shuō)明了問(wèn)題所在,就算使用者沒(méi)什么處理內(nèi)存問(wèn)題的經(jīng)驗(yàn)。在下面還有一個(gè)"Details"鏈接,在點(diǎn)開(kāi)之前不妨考慮一個(gè)問(wèn)題:為何對(duì)象實(shí)例會(huì)聚集在內(nèi)存中,為何存活(而未被GC)?是的——Strong Ref,那么再走近一些吧。

點(diǎn)擊了"Details"鏈接之后,除了在上一頁(yè)看到的描述外,還有Shortest Paths To the Accumulation Point和Accumulated Objects部分,這里說(shuō)明了從GC root到聚集點(diǎn)的最短路徑,以及完整的reference chain。觀察Accumulated Objects部分,java.util.HashMap和java.lang.Object[1000000]實(shí)例的retained heap(size)最大,在上一篇文章中我們知道retained heap代表從該類實(shí)例沿著reference chain往下所能收集到的其他類實(shí)例的shallow heap(size)總和,所以明顯類實(shí)例都聚集在HashMap和Object數(shù)組中了。這里我們發(fā)現(xiàn)一個(gè)有趣的現(xiàn)象,既Object數(shù)組的shallow heap和retained heap竟然一樣,通過(guò)Shallow and retained sizes一文可知,數(shù)組的shallow heap和一般對(duì)象(非數(shù)組)不同,依賴于數(shù)組的長(zhǎng)度和里面的元素的類型,對(duì)數(shù)組求shallow heap,也就是求數(shù)組集合內(nèi)所有對(duì)象的shallow heap之和。好,再來(lái)看org.rosenjiang.bo.Pilot對(duì)象實(shí)例的shallow heap為何是16,因?yàn)閷?duì)象頭是8字節(jié),成員變量int是4字節(jié)、String引用是4字節(jié),故總共16字節(jié)。

接著往下看,來(lái)到了Accumulated Objects by Class區(qū)域,顧名思義,這里能找到被聚集的對(duì)象實(shí)例的類名。org.rosenjiang.bo.Pilot類上頭條了,被實(shí)例化了290,325次,再返回去看程序,我承認(rèn)是故意這么干的。還有很多有用的報(bào)告可用來(lái)協(xié)助分析問(wèn)題,只是本文中的例子太簡(jiǎn)單,也用不上。以后如有用到,一定撰文詳細(xì)敘述。
又是perm gen
我們?cè)谏弦黄恼轮兄溃琾erm gen是個(gè)異類,里面存儲(chǔ)了類和方法數(shù)據(jù)(與class loader有關(guān))以及interned strings(字符串駐留)。在heap dump中沒(méi)有包含太多的perm gen信息。那么我們就用這些少量的信息來(lái)解決問(wèn)題吧。
看下面的代碼,利用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;
}
}
}
* 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;
}
}
}
控制臺(tái)打印如下的信息,然后把java_pid1824.hprof文件導(dǎo)入到MAT。其實(shí)在MAT里,看到的狀況應(yīng)該和“OutOfMemoryError: Java heap space”差不多(用了數(shù)組),因?yàn)閔eap dump并沒(méi)有包含interned strings方面的任何信息。只是在這里需要強(qiáng)調(diào),使用intern()方法的時(shí)候應(yīng)該多加注意。
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










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撐破廢了些心思。經(jīng)過(guò)嘗試,發(fā)現(xiàn)使用ASM來(lái)動(dòng)態(tài)生成類才能達(dá)到目的。ASM(http://asm.objectweb.org)的主要作用是處理已編譯類(compiled class),能對(duì)已編譯類進(jìn)行生成、轉(zhuǎn)換、分析(功能之一是實(shí)現(xiàn)動(dòng)態(tài)代理),而且它運(yùn)行起來(lái)足夠的快和小巧,文檔也全面,實(shí)屬居家必備之良品。ASM提供了core API和tree API,前者是基于事件的方式,后者是基于對(duì)象的方式,類似于XML的SAX、DOM解析,但是使用tree API性能會(huì)有損失。既然下面要用到ASM,這里不得不啰嗦下已編譯類的結(jié)構(gòu),包括:
1、修飾符(例如public、private)、類名、父類名、接口和annotation部分。
2、類成員變量聲明,包括每個(gè)成員的修飾符、名字、類型和annotation。
3、方法和構(gòu)造函數(shù)描述,包括修飾符、名字、返回和傳入?yún)?shù)類型,以及annotation。當(dāng)然還包括這些方法或構(gòu)造函數(shù)的具體Java字節(jié)碼。
4、常量池(constant pool)部分,constant pool是一個(gè)包含類中出現(xiàn)的數(shù)字、字符串、類型常量的數(shù)組。

已編譯類和原來(lái)的類源碼區(qū)別在于,已編譯類只包含類本身,內(nèi)部類不會(huì)在已編譯類中出現(xiàn),而是生成另外一個(gè)已編譯類文件;其二,已編譯類中沒(méi)有注釋;其三,已編譯類沒(méi)有package和import部分。
這里還得說(shuō)說(shuō)已編譯類對(duì)Java類型的描述,對(duì)于原始類型由單個(gè)大寫字母表示,Z代表boolean、C代表char、B代表byte、S代表short、I代表int、F代表float、J代表long、D代表double;而對(duì)類類型的描述使用內(nèi)部名(internal name)外加前綴L和后面的分號(hào)共同表示來(lái)表示,所謂內(nèi)部名就是帶全包路徑的表示法,例如String的內(nèi)部名是java/lang/String;對(duì)于數(shù)組類型,使用單方括號(hào)加上數(shù)據(jù)元素類型的方式描述。最后對(duì)于方法的描述,用圓括號(hào)來(lái)表示,如果返回是void用V表示,具體參考下圖。


下面的代碼中會(huì)使用ASM core API,注意接口ClassVisitor是核心,F(xiàn)ieldVisitor、MethodVisitor都是輔助接口。ClassVisitor應(yīng)該按照這樣的方式來(lái)調(diào)用:visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*( visitInnerClass | visitField | visitMethod )* visitEnd。就是說(shuō)visit方法必須首先調(diào)用,再調(diào)用最多一次的visitSource,再調(diào)用最多一次的visitOuterClass方法,接下來(lái)再多次調(diào)用visitAnnotation和visitAttribute方法,最后是多次調(diào)用visitInnerClass、visitField和visitMethod方法。調(diào)用完后再調(diào)用visitEnd方法作為結(jié)尾。
注意ClassWriter類,該類實(shí)現(xiàn)了ClassVisitor接口,通過(guò)toByteArray方法可以把已編譯類直接構(gòu)建成二進(jìn)制形式。由于我們要?jiǎng)討B(tài)生成子類,所以這里只對(duì)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);
}
* @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);
}
其次是自定義類加載器,實(shí)在沒(méi)法,ClassLoader的defineClass方法都是protected的,要加載字節(jié)數(shù)組形式(因?yàn)閠oByteArray了)的類只有繼承一下自己再實(shí)現(xiàn)。
/**
* @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
* 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);
}
}
最后是測(cè)試類。
/**
* @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();
}
}
}
* @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();
}
}
}
不一會(huì)兒,控制臺(tái)就報(bào)錯(cuò)了。
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



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



打開(kāi)java_pid3023.hprof文件,注意看下圖的Classes: 88.1k和Class Loader: 87.7k部分,從這點(diǎn)可看出class loader加載了大量的類。

更進(jìn)一步分析,點(diǎn)擊上圖中紅框線圈起來(lái)的按鈕,選擇Java Basics——Class Loader Explorer功能。打開(kāi)后能看到下圖所示的界面,第一列是class loader名字;第二列是class loader已定義類(defined classes)的個(gè)數(shù),這里要說(shuō)一下已定義類和已加載類(loaded classes)了,當(dāng)需要加載類的時(shí)候,相應(yīng)的class loader會(huì)首先把請(qǐng)求委派給父class loader,只有當(dāng)父class loader加載失敗后,該class loader才會(huì)自己定義并加載類,這就是Java自己的“雙親委派加載鏈”結(jié)構(gòu);第三列是class loader所加載的類的實(shí)例數(shù)目。

在Class Loader Explorer這里,能發(fā)現(xiàn)class loader是否加載了過(guò)多的類。另外,還有Duplicate Classes功能,也能協(xié)助分析重復(fù)加載的類,在此就不再截圖了,可以肯定的是MyAbsClass被重復(fù)加載了N多次。
最后
其實(shí)MAT工具非常的強(qiáng)大,上面故弄玄虛的范例代碼根本用不上MAT的其他分析功能,所以就不再描述了。其實(shí)對(duì)于OOM不只我列舉的兩種溢出錯(cuò)誤,還有多種其他錯(cuò)誤,但我想說(shuō)的是,對(duì)于perm gen,如果實(shí)在找不出問(wèn)題所在,建議使用JVM的-verbose參數(shù),該參數(shù)會(huì)在后臺(tái)打印出日志,可以用來(lái)查看哪個(gè)class loader加載了什么類,例:“[Loaded org.rosenjiang.test.MyAbsClass from org.rosenjiang.test.MyClassLoader]”。
全文完。
參考資料
memoryanalyzer Blog
java類加載器體系結(jié)構(gòu)
ClassLoader
請(qǐng)注意!引用、轉(zhuǎn)貼本文應(yīng)注明原作者:Rosen Jiang 以及出處: http://www.aygfsteel.com/rosen