前言
一開始不得不說說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
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;
}
是的,上面構造了很多的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
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
public class oompermtest {
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感興趣。首先是抽象類原型:
* myabsclass class
public abstract class myabsclass {
int less = -1;
int equal = 0;
int greater = 1;
abstract int absto(object o);
其次是自定義類加載器,實在沒法,classloader的defineclass方法都是protected的,要加載位元組數組形式(因為tobytearray了)的類隻有繼承一下自己再實作。
* myclassloader class
public class myclassloader extends classloader {
public class defineclass(string name, byte[] b) {
return defineclass(name, b, 0, b.length);
最後是測試類。
import java.util.arraylist;
import java.util.list;
import org.objectweb.asm.classwriter;
import org.objectweb.asm.opcodes;
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();
不一會兒,控制台就報錯了。
dumping heap to java_pid3023.hprof
heap dump file created [92593641 bytes in 2.405 secs]
打開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]”。
全文完。
參考資料
<a target="_blank" href="http://dev.eclipse.org/blogs/memoryanalyzer/">memoryanalyzer blog</a>
<a target="_blank" href="http://kenwublog.com/structure-of-java-class-loader">java類加載器體系結構</a>
<a target="_blank" href="http://mindprod.com/jgloss/classloader.html">classloader</a>