1. 程式人生 > >使用ReferenceQueue實現對ClassLoader垃圾回收過程的觀察、以及由此引發的ClassLoader記憶體洩露的場景及排查過程

使用ReferenceQueue實現對ClassLoader垃圾回收過程的觀察、以及由此引發的ClassLoader記憶體洩露的場景及排查過程

1 使用Reference/ReferenceQueue觀察Class和ClassLoader的解除安裝

在java中,存在著強引用(=),軟引用(SoftReference),弱引用(WeakReference),虛引用(PhantomReference)這4種引用型別。如果一個物件強引用可達,就一定不會被GC回收;而如果一個物件只有軟引用可達,則虛機會保證在out of memory之前會回收該物件;如果一個物件只是弱引用或者虛引用可達,則下一次GC時候就會將其回收。弱引用和虛引用的區別是,當一個物件只是弱引用可達時,它在下一次GC來臨之前還有搶救的餘地,也就是說弱引用仍可以獲得該被引用的物件,然後將其賦值給某一個GC Root可達的變數,此時它就“得救”了;而當一個物件只是虛引用可達時候,它已經無法被搶救了,因為虛引用無法得到被引用物件。


Java還提供了ReferenceQueue用於在一個物件被gc回收掉的時候可以進行額外的處理。ReferenceQueue即是這樣的一個佇列,當一個物件被gc回收之後,其相應的包裝類,即ref物件會被放入佇列中。我們可以從queue中獲取到相應的物件資訊,同時進行額外的處理。比如反向操作,資料清理等。


由上面的介紹可以得知,我們可以使用WeakReference或者PhantomReference來觀察ClassLoader和Class的解除安裝動作,而不會影響到他們的生命週期。先上一段程式碼:SimpleMonitorClassLoader.java

public class SimpleMonitorClassLoader {
	public static void main(String args[]) throws Exception{
		final ReferenceQueue<Object> rq = new ReferenceQueue<Object>();
		final Map<Object, Object> map = new HashMap<>();
		Thread thread = new Thread(() -> {
		    try {
		        WeakReference<byte[]> k;
		        while((k = (WeakReference) rq.remove()) != null) {
		            System.out.println("GC回收了:" + map.get(k));
		        }
		    } catch(InterruptedException e) {
		        //結束迴圈
		    }
		});
		thread.setDaemon(true);
		thread.start();
		
		ClassLoader cl = newLoader();
		Class cls = cl.loadClass("classloader.test.Foo");
		Object obj = cls.newInstance();
		
		
		Object value = new Object();
		
		WeakReference<ClassLoader> weakReference = new WeakReference<ClassLoader>(cl, rq);
		map.put(weakReference, "ClassLoader URLClassLoader");
		WeakReference<Class> weakReference1 = new WeakReference<Class>(cls, rq);
		map.put(weakReference1, "Class classloader.test.Foo");
		WeakReference<Object> weakReference2 = new WeakReference<Object>(obj, rq);
		map.put(weakReference2, "Instance of Foo");
		
		obj=null;
		System.out.println("Set instance null and execute gc!");
		System.gc();
		Thread.sleep(3000);
		cls=null;
		System.out.println("Set class null and execute gc!");
		System.gc();
		Thread.sleep(3000);
		cl=null;
		System.out.println("Set classloader null and execute gc!");
		System.gc();
		Thread.sleep(3000);
	}
	
	static URLClassLoader newLoader() throws Exception{
		URL url = new File("/home/wangd/work/test/wangd/target/classes").toURI().toURL();
		URLClassLoader ucl = new URLClassLoader(new URL[] {url}, null);
	    return ucl;
	}
}
執行結果如下:
Set instance null and execute gc!
GC回收了:Instance of Foo
Set class null and execute gc!
Set classloader null and execute gc!
GC回收了:Class classloader.test.Foo
GC回收了:ClassLoader URLClassLoader

從上面的結果可以看出,當heap中的例項物件例項失去了引用以後,會在GC時立刻被回收。而ClassLoader及其載入的Class是同時被回收的,因為ClassLoader及其載入的Class之間是相互引用的關係,要麼同時GC Root可達,要麼同時不可達也就是被回收。當外部沒有任何引用到一個ClassLoader本身以及其載入的所有Class(也就是GC root不可達)時,GC會將其同時回收。

2 Guava FinalizableReference/FinalizableReferenceQueue對Reference/ReferenceQueue的封裝

Guava是一種基於開源的Java庫,對Java進行了封裝,谷歌很多專案使用了它的核心庫。這個庫是為了方便編碼,並減少人為的編碼錯誤。Guava中提供了對Reference/ReferenceQueue的封裝,使其使用起來變得非常簡單。上面的例子使用Guava FinalizableReferen實現的程式碼如下:SimpleMonitorClassLoaderByGuava.class

public class SimpleMonitorClassLoaderByGuava {
	public static void main(String args[]) throws Exception{
		final FinalizableReferenceQueue rq = new FinalizableReferenceQueue();
		ClassLoader cl = newLoader();
		Class cls = cl.loadClass("classloader.test.Foo");
		Object obj = cls.newInstance();
		
		Reference<ClassLoader> weakReference = new FinalizableWeakReference<ClassLoader>(cl, rq) {
	    	//在引用物件被GC回收以後執行一些訂製的業務邏輯
			@Override
			public void finalizeReferent() {
				System.out.println("GC回收了:ClassLoader URLClassLoader");
			}
		};
	    
	    Reference<Class> weakReference1 = new FinalizableWeakReference<Class>(cls, rq) {
	    	//在引用物件被GC回收以後執行一些訂製的業務邏輯
			@Override
			public void finalizeReferent() {
				System.out.println("GC回收了:Class classloader.test.Foo");
			}
		};
	    
	    Reference<Object> weakReference2 = new FinalizableWeakReference<Object>(obj, rq) {
	    	//在引用物件被GC回收以後執行一些訂製的業務邏輯
			@Override
			public void finalizeReferent() {
				System.out.println("GC回收了:Instance of Foo");
			}
		};
	
		obj=null;
		System.out.println("Set instance null and execute gc!");
		System.gc();
		Thread.sleep(3000);
		cls=null;
		System.out.println("Set class null and execute gc!");
		System.gc();
		Thread.sleep(3000);
		cl=null;
		System.out.println("Set classloader null and execute gc!");
		System.gc();
		Thread.sleep(3000);
	}
	
	static URLClassLoader newLoader() throws Exception{
		URL url = new File("/home/wangd/work/test/wangd/target/classes").toURI().toURL();
		URLClassLoader ucl = new URLClassLoader(new URL[] {url}, null);
	    return ucl;
	}
}

執行結果如下:

Set instance null and execute gc!
GC回收了:Instance of Foo
Set class null and execute gc!
Set classloader null and execute gc!
GC回收了:ClassLoader URLClassLoader
GC回收了:Class classloader.test.Foo
與上面的效果完全一致,Guava FinalizableReferenceQueue是對Java ReferenceQueue進行了一層封裝,並在內部啟動守護執行緒監控ReferenceQueue,當發現有FinalizableReference物件被enqueue以後,對該物件執行其finalizeReferent方法。無需我們自己編寫程式碼啟動執行緒,並管理隨後可能出現的各種問題。

3 FinalizableReferenceQueue原始碼分析及其引出的Application ClassLoader無法解除安裝的問題

FinalizableReferenceQueue內部持有一個ReferenceQueue<Object> queue,並通過PhantomReference將自己也關聯到queue上。然後在初始化的時候啟動一個Finalizer Thread並將queue、對自己的PhantomReference以及FinalizableReference.class傳遞給Finalizer Thread。程式碼如下:FinalizableReferenceQueue.class
public class FinalizableReferenceQueue implements Closeable {
	.............................
	
  final ReferenceQueue<Object> queue;
  final PhantomReference<Object> frqRef;
  final boolean threadStarted;
  
  public FinalizableReferenceQueue() {
    queue = new ReferenceQueue<Object>();
    frqRef = new PhantomReference<Object>(this, queue);
    boolean threadStarted = false;
    try {
      startFinalizer.invoke(null, FinalizableReference.class, queue, frqRef);
      threadStarted = true;
    } catch (IllegalAccessException impossible) {
      throw new AssertionError(impossible); // startFinalizer() is public
    } catch (Throwable t) {
      logger.log(
          Level.INFO,
          "Failed to start reference finalizer thread."
              + " Reference cleanup will only occur when new references are created.",
          t);
    }

    this.threadStarted = threadStarted;
  }
  ....................
}
其中startFinalizer是一個通過反射獲得的Method,代表了Finalizer類的startFinalizer靜態方法,其作用是啟動守護執行緒。該方法程式碼如下:
public static void startFinalizer(Class<?> finalizableReferenceClass,
      ReferenceQueue<Object> queue,
      PhantomReference<Object> frqReference) {
    ..............
    Finalizer finalizer = new Finalizer(finalizableReferenceClass, queue, frqReference);
    Thread thread = new Thread(finalizer);
    thread.setName(Finalizer.class.getName());
    thread.setDaemon(true);

    try {
      if (inheritableThreadLocals != null) {
        inheritableThreadLocals.set(thread, null);
      }
    } catch (Throwable t) {
      logger.log(
          Level.INFO,
          "Failed to clear thread local values inherited by reference finalizer thread.",
          t);
    }

    thread.start();
  }
Finalizer內部持有對FinalizableReferenceQueue傳遞來的queue和PhantomReference的引用,並建立一個WeakReference來監控FinalizableReference.class,其建構函式如下所示:
private Finalizer(
      Class<?> finalizableReferenceClass,
      ReferenceQueue<Object> queue,
      PhantomReference<Object> frqReference) {
    this.queue = queue;

    this.finalizableReferenceClassReference =
        new WeakReference<Class<?>>(finalizableReferenceClass);

    this.frqReference = frqReference;
  }
Finalizer Thread是一個守護執行緒,啟動後在正常迴圈下,從queue中獲得被回收物件的引用包裝,並執行其finalizeReferent方法。而當FinalizableReference.class被解除安裝,或者是queue中入列(enqueue)了FinalizableReferenceQueue本身時,迴圈就會被打破,Finalizer Thread就會退出。程式碼如下:Finalizer.class
public void run() {
    while (true) {
      try {
        if (!cleanUp(queue.remove())) {
          break;
        }
      } catch (InterruptedException e) {
        // ignore
      }
    }
  }

  private boolean cleanUp(Reference<?> reference) {
    Method finalizeReferentMethod = getFinalizeReferentMethod();
    if (finalizeReferentMethod == null) {
      return false;
    }
    do {
      reference.clear();

      if (reference == frqReference) {
        return false;
      }

      try {
        finalizeReferentMethod.invoke(reference);
      } catch (Throwable t) {
        logger.log(Level.SEVERE, "Error cleaning up after reference.", t);
      }

    } while ((reference = queue.poll()) != null);
    return true;
  }

在FinalizableReferenceQueue原始碼的註釋中提到,如果應用客戶端(client)是通過動態載入或者osgi等載入的則其ClassLoader為Application ClassLoader(與預設的System ClassLoader相區別),在這種情況下假如client有一個static變數指向FinalizableReferenceQueue例項的話,而FinalizableReferenceQueue在啟動Finalizer Thread的時候直接使用預設的Application Classloader,這樣會形成一個Finalizer Thread->Finalizer.class->Application Classloader->ClientClass.class->FinalizableReferenceQueue的引用關係,導致FinalizableReferenceQueue無法被GC回收,從而導致Finalizer Thread無法停止,最終導致Application Classloader無法被GC回收。

因此如果Guava庫是被Application ClassLoader載入的話,Finalizer Thread中不應該持有對Application Classloader的引用,對此Guava的解決方案是,在FinalizableReferenceQueue中載入Finalizer類時依次嘗試使用SystemClassLoader、新建立的URLClassLoader、以及Application ClassLoader來執行。除了最後一種情況,其他兩者應該都可以打破Finalizer Thread對Application Classloader的引用。原始碼如下:
/** Reference to Finalizer.startFinalizer(). */
  private static final Method startFinalizer;

  static {
    Class<?> finalizer =
        loadFinalizer(new SystemLoader(), new DecoupledLoader(), new DirectLoader());
    startFinalizer = getStartFinalizer(finalizer);
  }
...............................
  private static Class<?> loadFinalizer(FinalizerLoader... loaders) {
    for (FinalizerLoader loader : loaders) {
      Class<?> finalizer = loader.loadFinalizer();
      if (finalizer != null) {
        return finalizer;
      }
    }

    throw new AssertionError();
  }
其中SystemLoader、DecoupledLoader、DirectLoader都實現了介面FinalizerLoader,並實現其loadFinalizer方法,在其中各自使用ClassLoader.getSystemClassLoader()、new URLClassLoader(new URL[] {base}, null)以及直接使用Class.forName方式(也就是當前的Application ClassLoader)來實現對Finalizer.class的載入,具體程式碼可檢視FinalizableReferenceQueue.class這裡就不再一一貼出。
上面的解決方案看上去沒有任何問題,然而實際執行結果如何呢?我們可以實驗模擬一下這種情況,程式碼如下:GuavaQueueUseByDynamicApp.java
public class GuavaQueueUseByDynamicApp {
	public static void main(String args[]) throws Exception{
		final ReferenceQueue<Object> rq = new ReferenceQueue<Object>();
		final Map<Object, Object> map = new HashMap<>();
		Thread thread = new Thread(() -> {
		    try {
		        WeakReference<byte[]> k;
		        while((k = (WeakReference) rq.remove()) != null) {
		            System.out.println("GC回收了:" + map.get(k));
		        }
		    } catch(InterruptedException e) {
		        //結束迴圈
		    }
		});
		thread.setDaemon(true);
		thread.start();
		
		ClassLoader cl = newLoader();
		System.out.println("Set application contextclassloader as: "+cl);
		Thread.currentThread().setContextClassLoader(cl);	//如果到時候不取消contextclassloader對cl的引用,則cl無法被解除安裝
		
		Class cls = cl.loadClass("jdktest.reference.ExecuteSomethingByFinalizableReferenceQueue");
		Object obj = cls.newInstance();
		Method method = cls.getMethod("doSomething");
        method.invoke(obj);
		
		WeakReference<ClassLoader> weakReference = new WeakReference<ClassLoader>(cl, rq);
		map.put(weakReference, "Application ClassLoader");
		WeakReference<Class> weakReference1 = new WeakReference<Class>(cls, rq);
		map.put(weakReference1, "Class(ExecuteSomethingByFinalizableReferenceQueue)");
		WeakReference<Object> weakReference2 = new WeakReference<Object>(obj, rq);
		map.put(weakReference2, "Instance of ExecuteSomethingByFinalizableReferenceQueue");
		
		obj=null;
		method=null;
		cls=null;
		cl=null;
		System.out.println(Thread.currentThread()+":開始執行GC");
		System.gc();
		Thread.sleep(10000);
		
		Thread.currentThread().setContextClassLoader(null);
		
		System.out.println(Thread.currentThread()+":再次執行GC");
		System.gc();
		Thread.sleep(10000);
		System.out.println(Thread.currentThread()+":第三次執行GC");
		System.gc();
		Thread.sleep(600000);
	}
	
	static URLClassLoader newLoader() throws Exception{
		URL url1 = new File("/home/wangd/data/java/guava-23.0.jar").toURI().toURL();
		URL url2 = new File("/home/wangd/work/test/wangd/target/classes").toURI().toURL();
		URLClassLoader ucl = new URLClassLoader(new URL[] {url1, url2}, null);
	      return ucl;
	}
}
為了更真實的模擬應用模組動態載入的情形,GuavaQueueUseByDynamicApp的執行環境中並不載入guava庫
模擬的動態載入的應用模組程式碼如下:ExecuteSomethingByFinalizableReferenceQueue.class
public class ExecuteSomethingByFinalizableReferenceQueue{
	public FinalizableReferenceQueue rq = new FinalizableReferenceQueue();
	public void doSomething() throws Exception{
		System.out.println("Just do something!");
	}
}
注意public FinalizableReferenceQueue rq = new FinalizableReferenceQueue();這一句,當其修飾符變為static時會導致結果的不一樣。
執行結果如下:

Set application contextclassloader as: [email protected]
Just do something!
Thread[main,5,main]:開始執行GC
GC回收了:Instance of ExecuteSomethingByFinalizableReferenceQueue
Thread[main,5,main]:再次執行GC
GC回收了:Class(ExecuteSomethingByFinalizableReferenceQueue)
GC回收了:Application ClassLoader
Thread[main,5,main]:第三次執行GC
我們可見,第一次GC時,只回收了例項物件,因為此時main thread的contextClassLoader引用了Application ClassLoader,所以Application ClassLoader以及其載入的ExecuteSomethingByFinalizableReferenceQueue.class都無法被回收。當第二次GC的時候,由於執行了Thread.currentThread.setContextClassLoader(null),因此Application ClassLoader和ExecuteSomethingByFinalizableReferenceQueue.class都變得GC Root不可達,GC就將他們都回收了。

假如將ExecuteSomethingByFinalizableReferenceQueue中的變數rq新增static修飾,也就是在FinalizableReferenceQueue原始碼註釋中提到的情況時,執行結果如下:

Set application contextclassloader as: [email protected]
Just do something!
Thread[main,5,main]:開始執行GC
GC回收了:Instance of ExecuteSomethingByFinalizableReferenceQueue
Thread[main,5,main]:再次執行GC
Thread[main,5,main]:第三次執行GC
這次居然在執行了Thread.currentThread.setContextClassLoader(null)以後也沒有回收Application ClassLoader及其載入的ExecuteSomethingByFinalizableReferenceQueue.class。這是為什麼呢,結合兩者的不同,可以推測問題很有可能是守護執行緒Finalizer依然持有著Application ClassLoader和ExecuteSomethingByFinalizableReferenceQueue.class的引用,因此當ExecuteSomethingByFinalizableReferenceQueue中的變數FinalizableReferenceQueue rq是例項變數時,該變數指向的物件可被GC回收(Class類並不持有其例項化物件的引用),而rq被回收會導致其啟動的守護執行緒Finalizer Thread終止,當Finalizer Thread終止後,Application ClassLoader和ExecuteSomethingByFinalizableReferenceQueue.class就徹底不可達了,因此會在下一次GC的時候被回收。

而當FinalizableReferenceQueue rq是類變數的時候,該變數指向的物件不可被GC回收(Class類直接持有靜態變數的引用),形成了一個Finalizer Thread->Finalizer.class->etc.->Application Classloader->ExecuteSomethingByFinalizableReferenceQueue.class->FinalizableReferenceQueue的引用路徑,阻止了FinalizableReferenceQueue物件被回收,因此也阻止了Finalizer Thread終止。

按照Guava原始碼中的說法,已經考慮到並處理了這個問題了啊,為什麼還是會有這種結果呢?如果仍然存在記憶體洩露那麼可達路徑又是什麼呢?另外,ClassLoader記憶體洩露還什麼其他的方式?如何有效查詢ClassLoader的記憶體洩露路徑呢?

4 使用MAT分析dump檔案,找出ClassLoader記憶體洩露的可達路徑

首先在程式執行時候dump出記憶體快照:

$ jps
$ jmap -dump:format=b,file=/home/data/testdump.bin <pid>

然後使用MAT開啟該dump檔案,選擇open query browser->java basics->classloader explorer
然後在彈出的框中直接點確定,可得到ClassLoader的列表,

一一點開可看到FinalizableReferenceQueue是由Application ClassLoader([email protected])載入的,而Finalizer是由新建立獨立的ClassLoader([email protected])載入的。這個行為符合我們的預期:由於System ClassLoader中並沒有Guava庫,所以無法載入Finalizer,因此按順序應該是新建一個URLClassLoader並載入之。接著我們要檢視Application ClassLoader([email protected])為什麼沒有被GC回收,在URLClassLoader(@0x708e09408)上右鍵->ClassLoader->Path to GC Roots->exclude all reference

從結果列表中可以檢視到該ClassLoader的GC Roots可達路徑:


我們發現居然還有三條可達路徑,第一條是Finalizer Thread的contextClassLoader是Application ClassLoader;第二條和第三條略微複雜,都是跟訪問控制上下文(AccessControlContext)相關:其中一條是Finalizer Thread從建立它的執行緒中繼承過來的訪問控制上下文(AccessControllContext)中持有父執行緒呼叫棧對應的保護域(ProtectionDomain)陣列,其中一個保護域對應的ClassLoader就是Application ClassLoader;另外一條是Finalizer.class的類載入器持有的訪問控制上下文(AccessControllContext)中持有的一個保護域的ClassLoader屬性就是Application ClassLoader。

其中,AccessControlContext、ProtectionDomain都是Java內建安全體系中的概念,關於AccessControlContext、ProtectionDomain的介紹,以及線上程和類載入器建立時為何必須繼承它們建立者的AccessControlContext的討論,請參照《Java Security Architecture--Java安全體系技術文件翻譯(四)》4.3,4.4兩節。

由此可見僅僅使用Guava中的解決方案,並不能完全阻止Finalizer Thread持有對Application ClassLoader的強引用。

5 解決方案

知道了問題所在,我們接下來就考慮解決方案。其實從日常使用的角度講,該極端問題一般不會遇到,另外如果在相關的reference統統被處理掉(finalized)以後顯式的去掉對FinalizableReferenceQueue的強引用,或者執行其close方法就肯定會達到FinalizableReferenceQueue的回收和Finalizer Thread的停止。但是假如就在該極端情況下,又不取消強引用及執行其close方法,有辦法讓GC自動回收掉FinalizableReferenceQueue從而實現Finalizer Thread的停止和Application ClassLoader的解除安裝嗎?

從上面MAT找出的結果來看,我們需要破壞掉那三條從Finalizer Thread到Application ClassLoader的可達路徑。翻看上面列出的startFinalizer方法中建立Finalizer Thread時contextClassLoader和inheritedAccessControlContext統統是使用預設處理,因此導致了對Application ClassLoader的關聯,因此修改startFinalizer方法如下:Finalizer.java
public static void startFinalizer(
	      Class<?> finalizableReferenceClass,
	      ReferenceQueue<Object> queue,
	      PhantomReference<Object> frqReference) {
	    if (!finalizableReferenceClass.getName().equals(FINALIZABLE_REFERENCE)) {
	      throw new IllegalArgumentException("Expected " + FINALIZABLE_REFERENCE + ".");
	    }

	    Finalizer finalizer = new Finalizer(finalizableReferenceClass, queue, frqReference);
	    Thread thread = new Thread(finalizer);
	    thread.setName(Finalizer.class.getName());
	    thread.setDaemon(true);

	    try {
	      if (inheritableThreadLocals != null) {
	        inheritableThreadLocals.set(thread, null);
	      }
	      
	      //消除本執行緒與啟動本執行緒的類的類載入器之間的關聯
	      Field inheritedAccessControlContext = Thread.class.getDeclaredField("inheritedAccessControlContext");
	      inheritedAccessControlContext.setAccessible(true);
	      inheritedAccessControlContext.set(thread, null);
	      
	      
	    } catch (Throwable t) {
	      logger.log(
	          Level.INFO,
	          "Failed to clear thread local values inherited by reference finalizer thread.",
	          t);
	    }
	    //消除本執行緒的contextClassLoader與啟動本執行緒的類的類載入器之間的關聯
	    thread.setContextClassLoader(null);

	    thread.start();
	  }

其中打了註釋的部分為我們新增的程式碼,目的就是切斷上一步找出來的第一條和第二條路徑。我們使用反射機制是因為Thread並沒有提供公開的API來取消繼承父執行緒中訪問控制上下文的行為。

另外,修改FinalizableReferenceQueue的內部靜態類DecoupledLoader中的方法newLoader(),使得新建立的URLClassLoader與Application ClassLoader不再關聯,程式碼如下:

URLClassLoader newLoader(URL base) {
	    	URLClassLoader cl = new URLClassLoader(new URL[] {base}, null);
	    	
	    	try{
	    		//消除新建立的類載入器與本類的類載入器之間的關聯
	    		Field acc = URLClassLoader.class.getDeclaredField("acc");
	    		acc.setAccessible(true);
	    		acc.set(cl, null);
	    	}catch(Exception e){
	    		e.printStackTrace();
	    	}
	      return cl;
	    }
其中註釋部分為切斷上面找出來的第三條路徑,也就是新建立的類載入器與Application ClassLoader之間的關聯。同樣使用反射機制是因為URLClassLoader並沒有提供公開的API來取消儲存訪問控制上下文(包括父載入器訪問控制上下文)的行為。

修改完畢使用重新打包後的Guava庫,執行結果如下:

Set application contextclassloader as: [email protected]
----------FinalizableReferenQueue ClassLoader:[email protected]
----------Finalizer ClassLoader:[email protected]
finalizer thread's classloader is:[email protected]
Just do something!
Thread[com.common.finalizablereference.Finalizer,5,main] start run a deamon thread!
Thread[main,5,main]:開始執行GC
GC回收了:Instance of ExecuteSomethingByFinalizableReferenceQueue
Thread[main,5,main]:再次執行GC
GC回收了:Class(ExecuteSomethingByFinalizableReferenceQueue)
Thread[com.common.finalizablereference.Finalizer,5,main] Class has been unloaded!!
Thread[com.common.finalizablereference.Finalizer,5,main] thread loop break;
Thread[com.common.finalizablereference.Finalizer,5,main] run out a deamon thread!
GC回收了:Application ClassLoader
Thread[main,5,main]:第三次執行GC

為了方便檢視,在Finalizer Thread的run方法中添加了一些輸出,這樣可以方便的看到,當第一次執行GC的時候只回收了instance,因為main thread的contextClassLoader還關聯著Application ClassLoader,因此大家都不會被回收,Finalizer Thread也不會停止。而第二次GC的時候,由於執行Thread.currentThread().setContextClassLoader(null);因此Application ClassLoader,ExecuteSomethingByFinalizableReferenceQueue.class以及static屬性指向的FinalizableReferenceQueue物件都被回收了,因此Finalizer Thread的迴圈也被打破,執行緒也成功關閉了。

6 總結

上面的解決方案肯定不是想說Guava的FinalizableReferenQueue應該像第5章解決方案中那麼去用,其實更自然的使用方式就是在沒必要存活的時候主動去除引用,不要讓物件擁有不必要的存活範圍。本文旨在通過這樣的一個案例來說明ClassLoader洩露的可能原因、如何觀察ClassLoader是否被解除安裝、如何查詢ClassLoader記憶體洩露的路徑以及如何解決這一類問題。另外,除了由Thread引發的ClassLoader記憶體洩露問題外,不同ClassLoader之間我們未預期到的相互引用導致的記憶體洩露也是相當隱蔽及棘手的問題(例如:通過AccessControllContext裡的ProtectionDomain[])。但是有了上面的一整套查詢及解決的方法,相信在實際情況中遇到的這些問題也都可以一一解決。

本文的程式碼及測試結果均為作者在自己的環境中實驗得出,結論也只是一家之言並沒有嚴格考證,若有不當之處,還請大家多多指正。

相關推薦

使用ReferenceQueue實現ClassLoader垃圾回收過程觀察以及由此引發ClassLoader記憶體洩露場景排查過程

1 使用Reference/ReferenceQueue觀察Class和ClassLoader的解除安裝在java中,存在著強引用(=),軟引用(SoftReference),弱引用(WeakReference),虛引用(PhantomReference)這4種引用型別。如果

G1垃圾回收器的理解

版權宣告:本文為博主原創文章,未經博主允許不得轉載。    https://blog.csdn.net/u012904383/article/details/79202893 1:瞭解G1 G1的第一篇paper(附錄1)發表於2004年,在2012年才在jdk1.7u

垃圾回收算法內存管理

style if語句 進行 class lob 準備 define glob red  餵雞百科 翻譯:   追蹤垃圾回收是一種自動內存管理,這種機制決定了什麽對象應該被回收,除了從根作用域開始的引用鏈上可到達的對象外,其余對象一律被認為是“垃圾”而且應該要回收。垃圾回

通過jQuery和C#分別實現.NET Core Web Api的訪問以及文件上傳

補充 param 詳細 ace lin col mage exp n) 準備工作:    建立.NET Core Web Api項目    新建一個用於Api請求的UserInfo類 public class UserInfo { publ

JVM垃圾回收--年輕代年老點和持久代

就會 為什麽 比例 生命 system 碎片 根據 請求 min 年輕代:   一般情況下,所有新生成的對象首先都是放在年輕代的。年輕代的目的就是盡可能快速的收集掉那些生命周期短的對象。年輕代分三個區。一個Eden區,兩個 Survivor區(分別叫from和to)Eden

大數據學習——java代碼實現HDFS文件的readappendwrite操作

導入 () 學習 ioe java 1.8 todo ever col 在之前的環節進行了HDFS 的搭建過程,接下來學習的內容是通過java代碼實現對HDFS中文件進行操作。 這部分學習是為了之後在使用到的MapRedce對HDFS 文件進行操作。 在eclipse上編寫

利用java實現文字的去除停用詞以及分詞處理

功能: 對txt文件進行分詞處理,並去除停用詞。 工具: IDEA,java,hankcs.hanlp.seg.common.Term等庫。 程式: import java.util.*; import java.io.*; import java.lang.String; imp

C/C++中實現輸入到EOF的判斷鍵盤手動輸入檔案結尾符EOFPython中輸入EOF判斷

C/C++中實現對輸入到EOF的判斷: 在C/C++中,EOF是一個定義在標頭檔案 stdio.h 中的常量,等於-1。 在C/C++中實現遇到檔案結尾符停止讀取: int data; while(scanf("%d",&data)!=EOF){ //EOF即檔案結尾符,-1

【java HanNLP】HanNLP 利用java實現文字的去除停用詞以及分詞處理

HanNLP 功能很強大,利用它去停用詞,加入使用者自定義詞庫,中文分詞等,計算分詞後去重的個數、 maven pom.xml 匯入 <dependency> <groupId>com.hankcs</g

第18課:JVM垃圾回收器序列並行併發垃圾回收器概述

內容: 1.JVM中不同的垃圾回收器 2.穿行、並行、併發垃圾回收器概述 一、JVM中不同的垃圾回收器     1.按照分代收集的方式,把垃圾回收器做如下的劃分:         a)新生代收集器:Serial 、ParNew、Parallel Scavenge   

淺談C#的垃圾回收----關於GC解構函式Disposeand Finalize

    對於.Net CLR的垃圾自動回收,這兩日有興致小小研究了一下。查閱資料,寫程式碼測試,發現不研究還罷,越研究越不明白了。在這裡sban寫下自己的心得以拋磚引玉,望各路高手多多指教。    近日瀏覽Msdn2,有一段很是費解,引於此處: 實現 Finalize 方法

C#垃圾回收和解構函式以及弱引用

在程式中,當我們每建立一個物件,就會在記憶體中開闢一個空間,用以存放這個物件。如果建立的物件多了,記憶體就會出現不夠用的情況。這時我們就要把記憶體中不再使用的物件釋放掉,避免記憶體的佔用及程式的異常。這個過程就是垃圾回收。手動進行垃圾回收的方法是:GC.Collect();

Spring框架開發實現商品列表的增刪改查以及批量刪除和批量修改

花了一週時間重新熟悉了一下SSM三大框架,也經歷了一次由複雜及簡單的過程,從剛開始的使用Mybatis操作資料就可以比較其和JDBC的不同,相應的java程式碼減少了,但是相對的配置檔案的內容越來越多,程式碼以後的維護性也得到了越來越高的提升。後面使用SpringMVC後,

垃圾回收演算法——複製演算法 以及eden和survivor

複製(Copying)演算法說到底也是為了解決標記-清除演算法產生的那些碎片。首先將記憶體分為大小相等的兩部分(假設A、B兩部分),每次呢只使用其中的一部分(這裡我們假設為A區),等這部分用完了,這時候就將這裡面還能活下來的物件複製到另一部分記憶體(這裡設為B區)中,然後把A

java對於垃圾回收機制[GC垃圾回收機制] 為什麼有GC還會有記憶體溢位呢?

java垃圾回收機制 來源於書本和工作中的總結。 記憶體洩露 如果分配出去的記憶體得不到釋放,及時回收,就會引起系統執行速度下降,甚至導致程式癱瘓,這就是記憶體洩露 GC機制 java記憶體分配和回收 都是jre後臺進行, 簡稱GC機制, JRE在

一個SQL導致整個資料庫很卡的問題排查過程

問題:我執行了一個sql,五六分鐘沒有執行成功,然後我就ctrl +c,沒成功,然後我就kill,之後顯示成功,但是處於killed狀態,事務還在。這是一個從庫,那之後從庫應用的sql,也就是一個很簡單的插入sql,跟我執行的sql沒有任何關聯關係,也執行不了了,主從也就發生

說一說垃圾回收的原理,講一下過程

升級 遍歷 無需 情況 容量 blog 變化 變量 調用 垃圾回收:只回收托管堆中的內存資源,不回收其他資源(數據庫連接、文件句柄、網絡端口等) 什麽樣的對象才會被回收?答:沒有變量引用的對象 什麽時間回收? 系統回收,具體何時回收垃圾由系統自行確定,並不是即時執行回

垃圾回收象的引用

finalize 物理 tst 測試 div 是我 出現異常 AI .net 垃圾回收 當程序創建對象、數組等引用類型實體時,系統就會在對內存中為之分配一塊內存區,對象就保存在這塊內存區中,當這塊內存不再被任何引用變量引用時,這塊內存就變成垃圾,等待垃圾回收機制進行回收。

JVM原理(Java代碼編譯和執行的整個過程+JVM內存管理垃圾回收機制)

變化 並行 colspan 同時 簡單的 table 目前 動態 中心 轉載註明出處: http://blog.csdn.net/cutesource/article/details/5904501 JVM工作原理和特點主要是指操作系統裝入JVM是通過jdk中Java.ex