在定位JVM性能問(wèn)題時(shí)可能會(huì)遇到內(nèi)存泄露導(dǎo)致JVM OutOfMemory的情況,在使用Tomcat容器時(shí)如果設(shè)置了reloadable=”true”這個(gè)參數(shù),在頻繁熱部署應(yīng)用時(shí)也有可能會(huì)遇到內(nèi)存溢出的情況。Tomcat的熱部署原理是檢測(cè)到WEB-INF/classes或者WEB-INF/lib目錄下的文件發(fā)生了變更后會(huì)把應(yīng)用先停止然后再啟動(dòng),由于Tomcat默認(rèn)給每個(gè)應(yīng)用分配一個(gè)WebAppClassLoader,熱替換的原理就是創(chuàng)建一個(gè)新的ClassLoader來(lái)加載類,由于JVM中一個(gè)類的唯一性由它的class文件和它的類加載器來(lái)決定,因此重新加載類可以達(dá)到熱替換的目的。當(dāng)熱部署的次數(shù)比較多會(huì)導(dǎo)致JVM加載的類比較多,如果之前的類由于某種原因(比如內(nèi)存泄露)沒(méi)有及時(shí)卸載就可能導(dǎo)致永久代或者M(jìn)etaSpace的OutOfMemory。這篇文章通過(guò)一個(gè)Demo來(lái)簡(jiǎn)要介紹下ThreadLocal和ClassLoader導(dǎo)致內(nèi)存泄露最終OutOfMemory的場(chǎng)景。
類的卸載
在類使用完之后,滿足下面的情形,會(huì)被卸載:
1.該類在堆中的所有實(shí)例都已被回收,即在堆中不存在該類的實(shí)例對(duì)象。
2.加載該類的classLoader已經(jīng)被回收。
3.該類對(duì)應(yīng)的Class對(duì)象沒(méi)有任何地方可以被引用,通過(guò)反射訪問(wèn)不到該Class對(duì)象。
如果類滿足卸載條件,JVM就在GC的時(shí)候,對(duì)類進(jìn)行卸載,即在方法區(qū)清除類的信息。
場(chǎng)景介紹
上一篇文章我介紹了ThreadLocal的原理,每個(gè)線程有個(gè)ThreadLocalMap,如果線程的生命周期比較長(zhǎng)可能會(huì)導(dǎo)致ThreadLocalMap里的Entry沒(méi)法被回收,那ThreadLocal的那個(gè)對(duì)象就一直被線程持有強(qiáng)引用,由于實(shí)例對(duì)象會(huì)持有Class對(duì)象的引用,Class對(duì)象又會(huì)持有加載它的ClassLoader的引用,這樣就會(huì)導(dǎo)致Class無(wú)法被卸載了,當(dāng)加載的類足夠多時(shí)就可能出現(xiàn)永久代或者M(jìn)etaSpace的內(nèi)存溢出,如果該類有大對(duì)象,比如有比較大的字節(jié)數(shù)組,會(huì)導(dǎo)致Java堆區(qū)的內(nèi)存溢出。
源碼介紹
這里定義了一個(gè)內(nèi)部類Inner,Inner類有個(gè)靜態(tài)的ThreadLocal對(duì)象,主要用于讓線程持有Inner類的強(qiáng)引用導(dǎo)致Inner類無(wú)法被回收,定義了一個(gè)自定義的類加載器去加載Inner類,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
public class MemoryLeak { public static void main(String[] args) { //由于線程一直在運(yùn)行,因此ThreadLocalMap里的Inner對(duì)象一直被Thread對(duì)象強(qiáng)引用 new Thread( new Runnable() { @Override public void run() { while ( true ) { //每次都新建一個(gè)ClassLoader實(shí)例去加載Inner類 CustomClassLoader classLoader = new CustomClassLoader ( "load1" , MemoryLeak. class .getClassLoader(), "com.ezlippi.MemoryLeak$Inner" , "com.ezlippi.MemoryLeak$Inner$1" ); try { Class<?> innerClass = classLoader.loadClass( "com.ezlippi.MemoryLeak$Inner" ); innerClass.newInstance(); //幫助GC進(jìn)行引用處理 innerClass = null ; classLoader = null ; Thread.sleep( 10 ); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | InterruptedException e) { e.printStackTrace(); } } } }).start(); } //為了更快達(dá)到堆區(qū) public static class Inner { private byte [] MB = new byte [ 1024 * 1024 ]; static ThreadLocal<Inner> threadLocal = new ThreadLocal<Inner>() { @Override protected Inner initialValue() { return new Inner(); } }; //調(diào)用ThreadLocal.get()才會(huì)調(diào)用initialValue()初始化一個(gè)Inner對(duì)象 static { threadLocal.get(); } public Inner() { } } //源碼省略 private static class CustomClassLoader extends ClassLoader {} |
堆區(qū)內(nèi)存溢出
為了觸發(fā)堆區(qū)內(nèi)存溢出,我在Inner類里面設(shè)置了一個(gè)1MB的字節(jié)數(shù)組,同時(shí)要在靜態(tài)塊中調(diào)用threadLocal.get(),只有調(diào)用才會(huì)觸發(fā)initialValue()來(lái)初始化一個(gè)Inner對(duì)象,不然只是創(chuàng)建了一個(gè)空的ThreadLocal對(duì)象,ThreadLocalMap里并沒(méi)有數(shù)據(jù)。
JVM參數(shù)如下:
1
|
-Xms100m -Xmx100m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintClassHistogram -XX:+HeapDumpOnOutOfMemoryError |
最后執(zhí)行了814次后JVM堆區(qū)內(nèi)存溢出了,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid11824.hprof ... Heap dump file created [ 100661202 bytes in 1.501 secs] Heap par new generation total 30720K, used 30389K [ 0x00000000f9c00000 , 0x00000000fbd50000 , 0x00000000fbd50000 ) eden space 27328K, 99 % used [ 0x00000000f9c00000 , 0x00000000fb6ad450 , 0x00000000fb6b0000 ) from space 3392K, 90 % used [ 0x00000000fb6b0000 , 0x00000000fb9b0030 , 0x00000000fba00000 ) to space 3392K, 0 % used [ 0x00000000fba00000 , 0x00000000fba00000 , 0x00000000fbd50000 ) concurrent mark-sweep generation total 68288K, used 67600K [ 0x00000000fbd50000 , 0x0000000100000000 , 0x0000000100000000 ) Metaspace used 3770K, capacity 5134K, committed 5248K, reserved 1056768K class space used 474K, capacity 578K, committed 640K, reserved 1048576K Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space at com.ezlippi.MemoryLeak$Inner.<clinit>(MemoryLeak.java: 34 ) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source) at java.lang.reflect.Constructor.newInstance(Unknown Source) at java.lang.Class.newInstance(Unknown Source) at com.ezlippi.MemoryLeak$ 1 .run(MemoryLeak.java: 20 ) at java.lang.Thread.run(Unknown Source) |
可以看到JVM已經(jīng)沒(méi)有內(nèi)存來(lái)創(chuàng)建新的Inner對(duì)象,因?yàn)槎褏^(qū)存放了很多個(gè)1MB的字節(jié)數(shù)組,這里我把類的直方圖打印出來(lái)了(下圖是堆大小為1024M的場(chǎng)景),省略了一些無(wú)關(guān)緊要的類,可以看出字節(jié)數(shù)組占了855M的空間,創(chuàng)建了814個(gè) com.ezlippi.MemoryLeak$CustomClassLoader 的實(shí)例,和字節(jié)數(shù)組的大小基本吻合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
num #instances #bytes class name ---------------------------------------------- 1 : 6203 855158648 [B 2 : 13527 1487984 [C 3 : 298 700560 [I 4 : 2247 228792 java.lang.Class 5 : 8232 197568 java.lang.String 6 : 3095 150024 [Ljava.lang.Object; 7 : 1649 134480 [Ljava.util.HashMap$Node; 11 : 813 65040 com.ezlippi.MemoryLeak$CustomClassLoader 12 : 820 53088 [Ljava.util.Hashtable$Entry; 15 : 817 39216 java.util.Hashtable 16 : 915 36600 java.lang.ref.SoftReference 17 : 543 34752 java.net.URL 18 : 697 33456 java.nio.HeapCharBuffer 19 : 817 32680 java.security.ProtectionDomain 20 : 785 31400 java.util.TreeMap$Entry 21 : 928 29696 java.util.Hashtable$Entry 22 : 1802 28832 java.util.HashSet 23 : 817 26144 java.security.CodeSource 24 : 814 26048 java.lang.ThreadLocal$ThreadLocalMap$Entry |
Metaspace溢出
為了讓Metaspace溢出,那就必須把MetaSpace的空間調(diào)小一點(diǎn),要在堆溢出之前加載足夠多的類,因此我調(diào)整了下JVM參數(shù),并且把字節(jié)數(shù)組的大小調(diào)成了1KB,如下所示:
1
2
|
private byte [] KB = new byte [ 1024 ]; -Xms100m -Xmx100m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintClassHistogram -XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m |
從 GC日志可以看出在Meraspace達(dá)到GC閾值(也就是MaxMetaspaceSize配置的大小時(shí))會(huì)觸發(fā)一次FullGC:
1
2
3
4
5
6
7
8
9
10
11
12
|
java.lang.OutOfMemoryError: Metaspace <<no stack trace available>> {Heap before GC invocations= 20 (full 20 ): par new generation total 30720K, used 0K [ 0x00000000f9c00000 , 0x00000000fbd50000 , 0x00000000fbd50000 ) eden space 27328K, 0 % used [ 0x00000000f9c00000 , 0x00000000f9c00000 , 0x00000000fb6b0000 ) from space 3392K, 0 % used [ 0x00000000fb6b0000 , 0x00000000fb6b0000 , 0x00000000fba00000 ) to space 3392K, 0 % used [ 0x00000000fba00000 , 0x00000000fba00000 , 0x00000000fbd50000 ) concurrent mark-sweep generation total 68288K, used 432K [ 0x00000000fbd50000 , 0x0000000100000000 , 0x0000000100000000 ) Metaspace used 1806K, capacity 1988K, committed 2048K, reserved 1056768K class space used 202K, capacity 384K, committed 384K, reserved 1048576K [Full GC (Metadata GC Threshold) [CMS Process finished with exit code 1 |
通過(guò)上面例子可以看出如果類加載器和ThreadLocal使用的不當(dāng)確實(shí)會(huì)導(dǎo)致內(nèi)存泄露的問(wèn)題,完整的源碼在github
原文鏈接:https://www.ezlippi.com/blog/2017/12/java-memory-leak-example.html?utm_source=tuicool&utm_medium=referral