JVM內存泄漏跟蹤

什麼是內存泄漏: 無用的對象持續的佔用內存或無用對象的內存得不到釋放。(也可以說是: 一個對象已不再被任何程序邏輯所引用、需要, 但是還存在着跟對象GCRoots的引用)

內存泄漏的根本原因: 長生命週期的對象持有短生命週期的對象的引用, 儘管短生命週期的對象已經不在需要了, 但因爲長生命週期持有它的引用而導致不可回收。

案例分析:

程序運行越來越慢, 或者出現OOM, 初步懷疑是出現了內存泄漏現象。

 

測試代碼:

package test;

import java.util.HashMap;
import java.util.Map;


public class demo {
	public static class TestMemory {
		
		public byte[] M_64Array =new byte[64*1024];
	}
	
	
	    //聲明緩存對象
	    private static final Map map = new HashMap();
	    public static void main(String args[]){
	        try {
	            Thread.sleep(10000);//給打開visualvm時間
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	        System.out.println("開始");
	        //循環添加對象到緩存
	        for(int i=0; i<100000;i++){
	        	System.out.println(i);
	            TestMemory t = new TestMemory();
	            map.put("key"+i,t);
	        }
	        System.out.println("first");
	        //爲dump出堆提供時間
	        try {
	            Thread.sleep(10000);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	        for(int i=0; i<100000;i++){
	        	System.out.println(i);
	            TestMemory t = new TestMemory();
	            map.put("key"+i,t);
	        }
	        System.out.println("second");
	        try {
	            Thread.sleep(10000);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	        for(int i=0; i<300000;i++){
	        	System.out.println(i);
	            TestMemory t = new TestMemory();
	            map.put("key"+i,t);
	        }
	        System.out.println("third");
	        try {
	            Thread.sleep(10000);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	        for(int i=0; i<400000;i++){
	        	System.out.println(i);
	            TestMemory t = new TestMemory();
	            map.put("key"+i,t);
	        }
	        System.out.println("forth");
	        try {
	            Thread.sleep(Integer.MAX_VALUE);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	        System.out.println("qqqq");
	    }
}

1.使用命令行排查

(1)使用jps指令, 查看虛擬機的當前進程

jps -l

得到進程的ID爲 12592

 

(2)使用jstat指令查看該進程gc情況

jstat -gcutil 12592 250 7

發生Full GC的次數較多,共4次

 

(3)使用jmap指令, 查看各個類佔用內存的狀況

jmap -histo:live 12592

可以看出, 某個類佔的內存特別大, 這很不正常, 接下來就是找到這個類

 

(4)生成heap dump文件

方法一: jmap -dump:format=b,file=heap.bin 12592 ( b代表文件格式爲二進制, file代表文件名)

方法二: 使用JDK /bin目錄下的jvisualvm 可視化工具生成

 

(5)使用MAT 對heap dump文件分析

藍色代表被佔用空間, 發現99%的空間被佔用了, 繼續往下追蹤, 看看到底是哪個類

 

 

最後發現是test.demo下的 HashMap中的類發生內存泄漏

for(int i=0; i<10000;i++){
	        	System.out.println(i);
	            TestMemory t = new TestMemory();
	            map.put("key"+i,t);//此處對象發生內存泄漏
	        }

 

 

不用命令行也可以用其他工具來判斷是否發生內存泄漏, 如JDK /bin目錄下的 可視化工具 jconsole和jvisualvm

堆內存使用情況:

 

Old區使用情況:

 

 

通過多次觀察發現, 一般來說堆內存圖像如上圖所示(呈上升趨勢折線圖), 同時出現GC掉的對象越來越少的情況, 則很有可能發生了內存泄漏

 

 

內存泄漏帶來的問題:

1.應用可用的內存減小, 增加了對內存的壓力。

2.降低了應用的性能, 比如觸發更頻繁的GC。

3.嚴重的時候可能會導致內存溢出錯誤, 即OOM。

對於程序員來說, GC基本是透明的, 不可見的。 不同的JVM的實現可能使用不同的算法管理GC。通常, GC線程的優先級較低, JVM調用的GC策略也有很多種, 有的是內存使用達一定程度而GC, 也有的是定時執行, 有的是平緩執行, 還有中斷式GC。

 

內存泄漏發生的場景:

1.靜態集合類引發的內存泄漏

static vector c=new vector(10);
for(int i=0;i<100;i++){
    Object o=new Object();
    v.add(o);
    o=null; //即使將對象o置空, 但是對象依然被集合v所引用, 故對象不可回收, 發生內存泄漏
}

解決辦法, 將對象從集合v中移除, 或者直接將集合置空 v=null

 

2.當集合裏的對象屬性被修改(對象的hashcode()發生了改變)後, 在調用remove()方法無效

public static void main(String[] args){
    Set<Person> set=new HashSet<Person>;
    Person p1=new Person("劉昊然",25);
    Person p2=new Person("李鍾碩",27);
    Person p3=new Person("陳秋婷",20);
    set.add(p1);
    set.add(p2);
    System.out.println("共:"+set.size()+" 個元素!"); //結果:2
    p3.setAge(24); //修改p3的年齡,此時p3元素對應的hashcode值發生改變

    set.remove(p3); //此時remove不掉,造成內存泄漏

    set.add(p3); //重新添加,居然添加成功
    System.out.println("共:"+set.size()+" 個元素!"); //結果:4 
    for (Person person : set)
    {
        System.out.println(person);
    }
}

 

3.監聽器, 開了沒關閉

 

4.各種連接, 數據庫連接, 網絡連接等等, 打開了沒關閉, 除非顯式的調用了close()方法, 否則是不會被GC掉的

 

5. 單例模式

什麼是單例模式?  確保一個類只有一個實例, 自行提供這個實例並向整個系統提供這個實例。

特點: (1) 一個類只能有一個實例

         (2) 自己創建這個實例

         (3) 整個系統都使用這個實例

注意: 不正確的使用單例模式是引起內存泄漏的常見問題, 單例對象在初始化後將在JVM的整個生命週期中存在; 比如單例中引用了外部對象;

以上是Java後臺的

 

下面這些是安卓的(ps: 沒整過安卓, 查資料看到安卓其實有好多種內存泄漏的方式, 但是我能理解並記住的就下面三種(T T))

1. 集合類泄漏

    集合類僅有添加元素, 而無刪除元素; 集合類是全局性變量, 無刪除機制;

2. 單例(單例的靜態特性, 使其生命週期和應用的生命週期一樣長)

    單例中調用了其他對象, 則其他對象的生命週期和應用的一樣長;

3. 儘量避免使用static 成員變量

    如果成員變量被聲明爲static , 那我們都知道其生命週期與整個app 進程的生命週期一樣; 如果你的app 進程設計上是長駐內 存,   那即使app切到後臺, 這部分內存也不會被釋放;