Java HotSwap Ⅳ-Reloading Java Classes Series 2-How do ClassLoader leaks happen

Java HotSwap Ⅳ-Reloading Java Classes Series 2-How do ClassLoader leaks happen

Reloading Java Classes 201: How do ClassLoader leaks happen?

If you have programmed in Java for some time you know that memory leaks do happen. Usually it’s the case of a collection somewhere with references to objects (e.g. listeners) that should have been cleared, but never were. Classloaders are a very special case of this, and unfortunately, with the current state of the Java platform, these leaks are both inevitable and costly: routinely causing OutOfMemoryError’s in production applications after just a few redeploys.

Let’s get started. Recalling RJC101: to reload a class we threw away the old classloader and created a new one, copying the object graph as best we could:



Every object had a reference to its class, which in turn had a reference to its classloader. However we didn’t mention that every classloader in turn has a reference to each of the classes it has loaded, each of which holds static fields defined in the class:


This means that

  1. If a classloader is leaked it will hold on to all its classes and all their static fields. Static fields commonly hold caches, singleton objects, and various configuration and application states. Even if your application

    doesn’t have any large static caches, it doesn’t mean that the framework you use doesn’t hold them for you (e.g. Log4J is a common culprit as it’s often put in the server classpath). This explains why leaking a

    classloader can be so expensive.

  2. To leak a classloader it’s enough to leave a reference to any object, created from a class, loaded by that classloader. Even if that object seems completely harmless (e.g. doesn’t have a single field), it will still hold on

    to its classloader and all the application state. A single place in the application that survives the redeploy and doesn’t do a proper cleanup is enough to sprout the leak. In a typical application there will be several such

    places, some of them almost impossible to fix due to the way third-party libraries are built. Leaking a classloader is therefore, quite common.

To examine this from a different perspective let’s return to the code example from our previous article. Breeze through it to quickly catch up.

Introducing the Leak

We will use the exact same Main class as before to show what a simple leak could look like:

public   class  Main {

  
private   static  IExample example1;

  
private   static  IExample example2;

  
public   static   void  main(String[] args)  {

    example1  =  ExampleFactory.newInstance().copy();

    
while  ( true ) {

      example2  =  ExampleFactory.newInstance().copy();

      System.out.println( " 1)  "   +

        example1.message()  +   "  =  "   +  example1.plusPlus());

      System.out.println( " 2)  "   +

        example2.message()  +   "  =  "   +  example2.plusPlus());

      System.out.println();

      Thread.currentThread().sleep( 3000 );
    }
  }
}

The ExampleFactory class is also exactly the same, but here’s where things get leaky. Let’s introduce a new class called Leak and a corresponding interface ILeak:

interface  ILeak {
}

public   class  Leak  implements  ILeak {

  
private  ILeak leak;

  
public  Leak(ILeak leak) {

    
this .leak  =  leak;
  }
}

As you can see it’s not a terribly complicated class: it just forms a chain of objects, with each doing nothing more than holding a reference to the previous one. We will modify the Exampleclass to include a reference to the Leak object and throw in a large array to take up memory (it represents a large cache). Let’s omit some methods shown in the previous article for brevity:

public   class  Example  implements  IExample {

  
private   int  counter;

  
private  ILeak leak;

  
private   static   final   long [] cache  =   new   long [ 1000000 ];

  
/*  message(), counter(), plusPlus() impls  */

  
public  ILeak leak() {

    
return   new  Leak(leak);
  }

  
public  IExample copy(IExample example) {

    
if  (example  !=   null ) {

      counter  =  example.counter();

      leak  =  example.leak();
    }
    
return   this ;
  }
}

The important things to note about Example class are:

  1. Example holds a reference to Leak, but Leak has no references to Example.

  2. When Example is copied (method copy() is called) a new Leak object is created holding a reference to the previous one.

If you try to run this code an OutOfMemoryError will be thrown after just a few iterations:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 	at example.Example.<clinit>(Example.java:8)

With the right tools, we can look deeper and see how this happens.

Post Mortem

Since Java 5.0, we’ve been able to use the jmap command line tool included in the JDK distribution to dump the heap of a running application (or for that matter even extract the Java heap from a core dump). However, since our application is crashing we will need a feature that was introduced in Java 6.0: dumping the heap on OutOfMemoryError. To do that we only need to add -XX:+HeapDumpOnOutOfMemoryError to the JVM command line:

java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid37266.hprof ... Heap dump file created [57715044 bytes in 1.707 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 	at example.Example.<clinit>(Example.java:8)

After we have the heap dump we can analyze it. There are a number of tools (including jhat, a small web-based analyzer included with the JDK), but here we will use the more sophisticatedEclipse Memory Analyzer (EMA).

After loading the heap dump into the EMA we can look at the Dominator Tree analysis. It is a very useful analysis that will usually reliably identify the biggest memory consumers in the heap and what objects hold a reference to them. In our case it seems quite obvious that the Leakclass is the one that consumes most of the heap:


Now let’s run a search for all of the Leak objects and see what are they holding to. To do that we run a search List objects -> with outgoing references for “example.Leak”:


The results include several Leak objects. Expanding the outgoing references we can see that each of them holds on to a separate instance of Example through a bunch of intermediate objects:


You may notice that one of the intermediate objects is ExampleFactory$1, which refers to the anonymous subclass of URLClassLoader we created in the ExampleFactory class. In fact what is happening is exactly the situation we described in the beginning of the article:

  • Each Leak object is leaking. They are holding on to their classloaders

  • The classloaders are holding onto the Example class they have loaded:


Conclusions

Though this example is slightly contrived, the main idea to take away is that it’s easy to leak a single object in Java. Each leak has the potential to leak the whole classloader if the application is redeployed or otherwise a new classloader is created. Since preventing such leaks is very challenging, it’s a better idea to use Eclipse Memory Analyzer and your understanding of classloaders to hunt them down after you get an OutOfMemoryError on redeploy.

This article addressed the following questions:

  • How does reloading a class cause the classloader to leak?

  • What are some consequences of leaking classloaders?

  • What tools can be used to troubleshoot these memory leaks



    ___________________________________________________________________________________________________________________

    笔记:
        1.每一个对象都持有一个指向自己class的引用---指向加载自己的classloader

        2.每一个classloader持有所有加载的class中的静态字段

    _______________________________________________________________________________________________________________________

    本地代码测试:

    package  com.mavsplus.example.java.rjc;

    import  java.util.concurrent.TimeUnit;

    /**
     * <a href= " http://www.zeroturnaround.com/blog/rjc201/ " >rjc series 2</>
     * 
     * 测试因为reload引起的"内存泄露"问题 --->注:LeakExample.class和Leak.class均是由urlclassloader加载
     * 
     * <pre>
     *     泄露原因分析:

     *     1.主循环中每次reload example2的时候,新实例的leak引用都会指向旧的实例leak引用,从而形成一个"链"

     *     2.每一个leak实例都会有一个指向自己class的引用,即Leak-->Leak.class

     *     3.因为Leak.class是由匿名的urlclassloader加载->即ExampleFactory$3->class是有一个指向加载自己的classloader的引用的->

     *         -->即Leak.class->ExampleFactory$3(匿名urlclassloader)->

     *     4.而ExampleFactory$3(匿名urlclassloader)则持有其加载的所有class,而class则包括所有的静态字段则本例中则是指LeakExample中的cache

     *         即ExampleFactory$3->LeakExample.class

     *     5.即文档中的描述    

     *  LeakExample.class            LeakExample.class            LeakExample.class    
     *           /|\                            /|\                            /|\
     *     ExampleFactory$3            ExampleFactory$3            ExampleFactory$3
     *           /|\                            /|\                            /|\
     *         Leak.class                    Leak.class                    Leak.class
     *           /|\                           /|\                         /|\
     *         Leak------------------------->Leak------------------------->Leak

     *     6.如何避免reload引起的泄露问题

     *         1.状态复制时,不要引起类似本例的"链式引用"问题

     *         2.状态复制时要用深度克隆,则不会引起问题.

             {@link RJCAvoidLeakTest}

     * </pre>
     * 
     * <pre>
     *     vm-arguments:

     *         -Xms32m -Xmx32m -XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=C:\Users\pc
     * </pre>
     * 
     * <pre>

     *     输出:

     *     1)Version 1 = 0

     *     2)Version 1 = 0
     * 
     *     1)Version 1 = 1

     *     2)Version 1 = 1
     * 
     *     java.lang.OutOfMemoryError: Java heap space
     *     Dumping heap to C:\Users\pc\java_pid7564.hprof 

     *     Heap dump file created [25453917 bytes in 0.033 secs]

     *     Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

     *         at com.mavsplus.example.java.rjc.LeakExample.<clinit>(LeakExample.java:13)

     *         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.mavsplus.example.java.rjc.ExampleFactory.newInstance3(ExampleFactory.java:69)

     *         at com.mavsplus.example.java.rjc.RJCLeakTest.main(RJCLeakTest.java:37)
     * </pre>
     * 
     *  @author  landon
     *  @since  1.8.0_25
     
    */
    public   class  RJCLeakTest {

        
    private   static  ILeakExample example1;

        
    private   static  ILeakExample example2;

        
    public   static   void  main(String[] args)  throws  Exception {

            example1  =  ExampleFactory.newInstance3();

            
    while  ( true ) {
                example2  =  ExampleFactory.newInstance3().copy(example2);

                System.out.println( " 1) "   +  example1.message()  +   "  =  "   +  example1.plusPlus());

                System.out.println( " 2) "   +  example2.message()  +   "  =  "   +  example2.plusPlus());

                System.out.println();


                
    //  加速"泄露",每5ms执行一次

                TimeUnit.MILLISECONDS.sleep( 5 );
            }
        }
    }




    package  com.mavsplus.example.java.rjc;

    import  java.io.Serializable;

    /**
     * "泄露对象"接口,用来测试reload引起的"内存泄露"
     * 
     *  @author  landon
     *  @since  1.8.0_25
     
    */
    public   interface  ILeak  extends  Serializable{
        
        
    public  Object deepCopy();
    }




    package  com.mavsplus.example.java.rjc;

    import  java.io.ByteArrayInputStream;

    import  java.io.ByteArrayOutputStream;

    import  java.io.IOException;

    import  java.io.ObjectInputStream;

    import  java.io.ObjectOutputStream;

    /**
     * "泄露"对象
     * 
     *  @author  landon
     
    */
    public   class  Leak  implements  ILeak {

        
    private  ILeak leak;

        
    public  Leak(ILeak leak) {

            
    this .leak  =  leak;

        }


        
    //  用序列化方式实现了深度克隆

        
    //  因为本例的特殊性,Leak中套了一个leak-->所以用clone方法还是会有"链式引用"的问题,所以采用此种方式

        
    //  --->但是依然会报错
        @Override

        
    public  Object deepCopy() {
            
    try  {
                ByteArrayOutputStream bo  =   new  ByteArrayOutputStream();

                ObjectOutputStream oo  =   new  ObjectOutputStream(bo);

                oo.writeObject( this );

                ByteArrayInputStream bi  =   new  ByteArrayInputStream(bo.toByteArray());

                ObjectInputStream oi  =   new  ObjectInputStream(bi);

                
    return  (oi.readObject());

            }  catch  (IOException  |  ClassNotFoundException e) {

                e.printStackTrace();
            }

            
    return   null ;
        }
    }



    package  com.mavsplus.example.java.rjc;

    /**
     * 可"泄露"接口
     * 
     * 
    @author  landon
     * 
    @since  1.8.0_25
     
    */
    public   interface  ILeakExample {

        
    public  String message();

        
    public   int  plusPlus();

        
    public   int  counter();

        
    public  ILeakExample copy(ILeakExample example);

        
    public  ILeak leak();
        
        
    public  ILeakExample deepCopy(ILeakExample example);
    }




    package  com.mavsplus.example.java.rjc;

    /**
     * "泄露的"Example对象
     * 
     *  @author  landon
     
    */
    public   class  LeakExample  implements  ILeakExample {

        
    private   int  counter;

        
    //  表示一个大的cache-用来"加快内存泄露"--->注意是static字段,表示属于"类"的
         private   static   final   long [] cache  =   new   long [ 1000000 ];

        
    //  增加一个"泄露"对象
         private  ILeak leak;

        @Override

        
    public  ILeak leak() {

            
    return   new  Leak(leak);

        }

        @Override

        
    public  String message() {

            
    return   " Version 1 " ;

            
    //  return "Version 2";
        }

        @Override

        
    public   int  plusPlus() {

            
    return  counter ++ ;
        }

        @Override

        
    public   int  counter() {

            
    return  counter;
        }

        @Override

        
    public  ILeakExample copy(ILeakExample example) {

            
    if  (example  !=   null ) {

                counter  =  example.counter();

                
    //  注意这块的代码很关键.每次调用copy的时候,调用leak方法->而leak方法会传入之前实例对象的leak引用.

                
    //  也就是说形成一个"链式",所有LeakExample中的leak引用都指向之前构造的实例中的leak引用.
                leak  =  example.leak();
            }

            
    return   this ;
        }

        
        @Override
        
    public  ILeakExample deepCopy(ILeakExample example) {

            
    if  (example  !=   null ) {

                counter  =  example.counter();

                leak  =  (ILeak)example.leak().deepCopy();
            }

            
    return   this ;
        }
    }




    package  com.mavsplus.example.java.rjc;

    import  java.util.concurrent.TimeUnit;

    /**
     * <a href= " http://www.zeroturnaround.com/blog/rjc201/ " >rjc series 2</>
     * 
     * 测试因为reload引起的"内存泄露"问题-->注,这个test中的urlclassloader只加载LeakExample.class而不加载
     * 
     * <pre>
     *     泄露原因分析:

     *     1.首先说明该示例代码和RJCLeakTest有一点不同,上一个例子urlclassloader不进加载LeakExample,还加载了Leak.class,泄露原因已分析.但是本示例的urlclassloader并未

     *       加载Leak.class,但是从堆栈看到,还是发生了内存溢出,why

     *  2.主要原因还是Leak对象的链式引用问题,可以看到链式引用的数目已经到了5373个-->

     *      -->从EMA分析工具的dominator_tree中可以看到,每一个Leak对象的Shallow Heap为16,最后加载的LeakExample持有的Leak引用的Retained Heap(所有的链式引用)为

     *          --->16 * 5373 = 85968
     *          --->而测试例子中最大堆是有限的-->于是便内存溢出了
     * </pre>
     * 
     * <pre>
     *     vm-arguments:

     *         -Xms32m -Xmx32m -XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=C:\Users\pc
     * </pre>
     * 
     * <pre>
     *     输出:
     *     
     *     1)Version 1 = 5370
     *     2)Version 1 = 5370
     * 
     *     1)Version 1 = 5371
     *     2)Version 1 = 5371
     * 
     *     1)Version 1 = 5372
     *     2)Version 1 = 5372
     * 
     *     1)Version 1 = 5373
     *     2)Version 1 = 5373
     * 
     *     java.lang.OutOfMemoryError: Java heap space
     *     Dumping heap to C:\Users\pc\java_pid7652.hprof 

     *     Heap dump file created [25576205 bytes in 0.033 secs]

     *     Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

     *         at com.mavsplus.example.java.rjc.LeakExample.<clinit>(LeakExample.java:13)

     *         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.mavsplus.example.java.rjc.ExampleFactory.newInstance2(ExampleFactory.java:49)

     *         at com.mavsplus.example.java.rjc.RJCLeakTest2.main(RJCLeakTest2.java:34)
     * </pre>
     * 
     *  @author  landon
     *  @since  1.8.0_25
     
    */
    public   class  RJCLeakTest2 {

        
    private   static  ILeakExample example1;

        
    private   static  ILeakExample example2;

        
    public   static   void  main(String[] args)  throws  Exception {

            example1  =  ExampleFactory.newInstance2();

            
    while  ( true ) {

                example2  =  ExampleFactory.newInstance2().copy(example2);

                System.out.println( " 1) "   +  example1.message()  +   "  =  "   +  example1.plusPlus());

                System.out.println( " 2) "   +  example2.message()  +   "  =  "   +  example2.plusPlus());

                System.out.println();

                
    //  加速"泄露",每5ms执行一次

                TimeUnit.MILLISECONDS.sleep( 5 );
            }
        }
    }





    package  com.mavsplus.example.java.rjc;

    import  java.util.concurrent.TimeUnit;

    /**
     * <a href= " http://www.zeroturnaround.com/blog/rjc201/ " >rjc series 2</>
     * 
     * 测试如何【避免】因为reload引起的"内存泄露"问题 --->注:LeakExample.class和Leak.class均是由urlclassloader加载
     * 
     * <pre>
     *     1.本例复制状态时采取了深拷贝的方式,避免了因"链式引用"引起的持有classloader无法gc的问题

     *     2.但是因为本例的特殊性,这个"链式"引用是无法解决的->所以造成序列化的时候会出现StackOverflowError
     * </pre>
     * 
     * <pre>
     *     vm-arguments:
     *         -Xms64m -Xmx64m -XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=C:\Users\pc
     * </pre>
     * 
     * <pre>
     *     输出:
     *     1)Version 1 = 3300
        2)Version 1 = 3300

        1)Version 1 = 3301
        2)Version 1 = 3301
        
        1)Version 1 = 3302
        2)Version 1 = 3302
        
        1)Version 1 = 3303
        2)Version 1 = 3303
        
        1)Version 1 = 3304
        2)Version 1 = 3304
        
        Exception in thread "main" java.lang.StackOverflowError

            at java.io.ObjectInputStream.readObject0(Unknown Source)

            at java.io.ObjectInputStream.defaultReadFields(Unknown Source)

            at java.io.ObjectInputStream.readSerialData(Unknown Source)

            at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)

            at java.io.ObjectInputStream.readObject0(Unknown Source)

            at java.io.ObjectInputStream.defaultReadFields(Unknown Source)

            at java.io.ObjectInputStream.readSerialData(Unknown Source)
     * </pre>
     * 
     *  @author  landon
     *  @since  1.8.0_25
     
    */
    public   class  RJCAvoidLeakTest {

        
    private   static  ILeakExample example1;

        
    private   static  ILeakExample example2;

        
    public   static   void  main(String[] args)  throws  Exception {

            example1  =  ExampleFactory.newInstance3();

            
    while  ( true ) {

                example2  =  ExampleFactory.newInstance3().deepCopy(example2);

                System.out.println( " 1) "   +  example1.message()  +   "  =  "   +  example1.plusPlus());

                System.out.println( " 2) "   +  example2.message()  +   "  =  "   +  example2.plusPlus());

                System.out.println();

                
    //  加速"泄露",每5ms执行一次

                TimeUnit.MILLISECONDS.sleep( 5 );
            }
        }
    }














    _____________________________________________________________________________________________

    关于EMA中shallow heap、retained heap,gc root的知识,请参考:

        
    1. https://www.yourkit.com/docs/java/help/sizes.jsp


    2. http://www.yourkit.com/docs/80/help/gc_roots.jsp











你可能感兴趣的:(Java HotSwap Ⅳ-Reloading Java Classes Series 2-How do ClassLoader leaks happen)