使用ReferenceQueue实现对ClassLoader垃圾回收过程的观察、以及由此引发的ClassLoader内存泄露的场景及排查过程
来源:互联网 发布:米格31知乎 编辑:程序博客网 时间:2024/06/08 17:11
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 FooSet class null and execute gc!Set classloader null and execute gc!GC回收了:Class classloader.test.FooGC回收了: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回收以后执行一些订制的业务逻辑@Overridepublic void finalizeReferent() {System.out.println("GC回收了:ClassLoader URLClassLoader");}}; Reference<Class> weakReference1 = new FinalizableWeakReference<Class>(cls, rq) { //在引用对象被GC回收以后执行一些订制的业务逻辑@Overridepublic void finalizeReferent() {System.out.println("GC回收了:Class classloader.test.Foo");}}; Reference<Object> weakReference2 = new FinalizableWeakReference<Object>(obj, rq) { //在引用对象被GC回收以后执行一些订制的业务逻辑@Overridepublic 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 FooSet class null and execute gc!Set classloader null and execute gc!GC回收了:ClassLoader URLClassLoaderGC回收了: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.classpublic 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回收。
/** 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库
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: java.net.URLClassLoader@7ea987acJust do something!Thread[main,5,main]:开始执行GCGC回收了:Instance of ExecuteSomethingByFinalizableReferenceQueueThread[main,5,main]:再次执行GCGC回收了:Class(ExecuteSomethingByFinalizableReferenceQueue)GC回收了:Application ClassLoaderThread[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: java.net.URLClassLoader@7ea987acJust do something!Thread[main,5,main]:开始执行GCGC回收了:Instance of ExecuteSomethingByFinalizableReferenceQueueThread[main,5,main]:再次执行GCThread[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终止。
4 使用MAT分析dump文件,找出ClassLoader内存泄露的可达路径
MAT介绍不再多说,自行搜索,使用MAT查找ClassLoader内存泄露的相关资料参考自http://java.jiderhamn.se/2011/12/11/classloader-leaks-i-how-to-find-classloader-leaks-with-eclipse-memory-analyser-mat/
首先在程序运行时候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(URLClassLoader@0x708e09408)加载的,而Finalizer是由新创建独立的ClassLoader(URLClassLoader@0x708e092f0)加载的。这个行为符合我们的预期:由于System ClassLoader中并没有Guava库,所以无法加载Finalizer,因此按顺序应该是新建一个URLClassLoader并加载之。接着我们要查看Application ClassLoader(URLClassLoader@0x708e09408)为什么没有被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两节。
5 解决方案
知道了问题所在,我们接下来就考虑解决方案。其实从日常使用的角度讲,该极端问题一般不会遇到,另外如果在相关的reference统统被处理掉(finalized)以后显式的去掉对FinalizableReferenceQueue的强引用,或者执行其close方法就肯定会达到FinalizableReferenceQueue的回收和Finalizer Thread的停止。但是假如就在该极端情况下,又不取消强引用及执行其close方法,有办法让GC自动回收掉FinalizableReferenceQueue从而实现Finalizer Thread的停止和Application ClassLoader的卸载吗?
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: java.net.URLClassLoader@548c4f57----------FinalizableReferenQueue ClassLoader:java.net.URLClassLoader@548c4f57----------Finalizer ClassLoader:java.net.URLClassLoader@1b28cdfafinalizer thread's classloader is:java.net.URLClassLoader@548c4f57Just do something!Thread[com.common.finalizablereference.Finalizer,5,main] start run a deamon thread!Thread[main,5,main]:开始执行GCGC回收了:Instance of ExecuteSomethingByFinalizableReferenceQueueThread[main,5,main]:再次执行GCGC回收了: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 ClassLoaderThread[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内存泄露的场景及排查过程
- ClassLoader的加载过程及分析一
- ClassLoader—观察程序运行时类加载的过程-verbose:class
- zz~ ClassLoader的加载过程及分析一
- ClassLoader加载Class的过程 解析
- ClassLoader加载Class的过程 解析
- ClassLoader加载Class的过程 解析
- Android应用的ClassLoader创建过程
- Flex的垃圾回收机理及预防内存泄露
- Flex的垃圾回收机理及预防内存泄露
- ClassLoader的一个应用场景
- Android的垃圾回收与内存泄露
- Linux是这样泄露内存的:Linux内存泄露过程观察
- Linux是这样泄露内存的:Linux内存泄露过程观察
- 自定义classloader的实现
- 内存泄露的排查
- 关于ClassLoader的使用
- classloader内存引出的mem leak(eg tomcat使用场景)
- 函数实现指定位置搜索
- 【Unity Shader入门精要】— 开始Unity Shader之旅
- PHP--ajax
- Elimination Round 2-A. Search for Pretty Integers
- PAT-Head of Hangs
- 使用ReferenceQueue实现对ClassLoader垃圾回收过程的观察、以及由此引发的ClassLoader内存泄露的场景及排查过程
- POJ_Butterfly
- 作业10.15
- 勇敢迈出第一步
- Linux系统不同主机之间的时间同步
- 进程间通信——有名管道(FIFO)
- 【Leetcode】【python】Set Matrix Zeroes
- Linux系统主机和虚拟机的外网通信、网络管理
- 最优方向法(MOD)