[转载]Java虚拟机类型卸载和类型更新解析头条 - 娱乐之横扫全球

[转载]Java虚拟机类型卸载和类型更新解析头条

2019-02-11 07:39:34 | 作者: 运凯 | 标签: 类型,实例,状况 | 浏览: 1792

【转载】Java虚拟机类型卸载和类型更新解析
衔接:https://www.blogjava.net/zhuxing/archive/2008/07/24/217285.html

【摘要】
        前面体系讨论过java类型加载(loading)的问题,在这篇文章中扼要剖析一下java类型卸载(unloading)的问题,并扼要剖析一下怎么处理怎么运转时加载newly compiled version的问题。

【相关标准摘要】
    首要看一下,关于java虚拟机标准中时怎么论述类型卸载(unloading)的:
    A class or interface may be unloaded if and only if its class loader is unreachable. The bootstrap class loader is always reachable; as a result, system classes may never be unloaded.
    Java虚拟机标准中关于类型卸载的内容就这么简略两句话,大致意思就是:只要当加载该类型的类加载器实例(非类加载器类型)为unreachable状况时,当时被加载的类型才被卸载.发动类加载器实例永久为reachable状况,由发动类加载器加载的类型或许永久不会被卸载.

    咱们再看一下Java言语标准供给的关于类型卸载的更详细的信息(部分摘抄):
    //摘自JLS 12.7 Unloading of Classes and Interfaces
    1、An implementation of the Java programming language may unload classes.
    2、Class unloading is an optimization that helps reduce memory use. Obviously,the semantics of a program should not depend  on whether and how a system chooses to implement an optimization such as class unloading.
    3、Consequently,whether a class or interface has been unloaded or not should be transparent to a program

    经过以上咱们能够得出定论: 类型卸载(unloading)仅仅是作为一种削减内存运用的功用优化办法存在的,详细和虚拟机完结有关,对开发者来说是通明的.

    纵观java言语标准及其相关的API标准,找不到显现类型卸载(unloading)的接口, 换句话说:
    1、一个现已加载的类型被卸载的几率很小至少被卸载的时刻是不断定的
    2、一个被特定类加载器实例加载的类型运转时能够认为是无法被更新的

【类型卸载进一步剖析】
     前面提到过,假如想卸载某类型,有必要确保加载该类型的类加载器处于unreachable状况,现在咱们再看看有 关unreachable状况的解说:
    1、A reachable object is any object that can be accessed in any potential continuing computation from any live thread.
    2、finalizer-reachable: A finalizer-reachable object can be reached from some finalizable object through some chain of references, but not from any live thread. An unreachable object cannot be reached by either means.

    某种程度上讲,在一个略微杂乱的java运用中,咱们很难精确判别出一个实例是否处于unreachable状况,所    以为了愈加精确的迫临这个所谓的unreachable状况,咱们下面的测验代码尽量简略一点.
   
    【测验场景一】运用自界说类加载器加载, 然后测验将其设置为unreachable的状况
    阐明:
    1、自界说类加载器(为了简略起见, 这儿就假定加载当时工程以外D盘某文件夹的class)
    2、假定现在有一个简略自界说类型MyClass对应的字节码存在于D:/classes目录下
   
public class MyURLClassLoader extends URLClassLoader {
   public MyURLClassLoader() {
      super(getMyURLs());
   }

   private static URL[] getMyURLs() {
    try {
       return new URL[]{new File ("D:/classes/").toURL()};
    } catch (Exception e) {
       e.printStackTrace();
       return null;
    }
  }
}

1 public class Main {
2     public static void main(String[] args) {
3       try {
4          MyURLClassLoader classLoader = new MyURLClassLoader();
5          Class classLoaded = classLoader.loadClass("MyClass");
6          System.out.println(classLoaded.getName());
7
8          classLoaded = null;
9          classLoader = null;
10
11          System.out.println("开端GC");
12          System.gc();
13          System.out.println("GC完结");
14        } catch (Exception e) {
15            e.printStackTrace();
16        }
17     }
18 }

        咱们添加虚拟机参数-verbose:gc来调查废物搜集的状况,对应输出如下:  
MyClass
开端GC
[Full GC[Unloading class MyClass]
207K- 131K(1984K), 0.0126452 secs]
GC完结

    【测验场景二】运用体系类加载器加载,可是无法将其设置为unreachable的状况
      阐明:将场景一中的MyClass类型字节码文件放置到工程的输出目录下,以便体系类加载器能够加载
       
1 public class Main {
2     public static void main(String[] args) {
3      try {
4       Class classLoaded =  ClassLoader.getSystemClassLoader().loadClass(
5 "MyClass");
6
7
8      System.out.printl(sun.misc.Launcher.getLauncher().getClassLoader());
9      System.out.println(classLoaded.getClassLoader());
10      System.out.println(Main.class.getClassLoader());
11
12      classLoaded = null;
13
14      System.out.println("开端GC");
15      System.gc();
16      System.out.println("GC完结");
17
18      //判别当时体系类加载器是否有被引证(是否是unreachable状况)
19      System.out.println(Main.class.getClassLoader());
20     } catch (Exception e) {
21         e.printStackTrace();
22     }
23   }
24 }
       
        咱们添加虚拟机参数-verbose:gc来调查废物搜集的状况, 对应输出如下:
sun.misc.Launcher$AppClassLoader@197d257
sun.misc.Launcher$AppClassLoader@197d257
sun.misc.Launcher$AppClassLoader@197d257
开端GC
[Full GC 196K- 131K(1984K), 0.0130748 secs]
GC完结
sun.misc.Launcher$AppClassLoader@197d257
        由于体系ClassLoader实例(AppClassLoader@197d257" sun.misc.Launcher$AppClassLoader@197d257)加载了许多类型,并且又没有清晰的接口将其设置为null,所以咱们无法将加载MyClass类型的体系类加载器实例设置为unreachable状况,所以经过测验成果咱们能够看出,MyClass类型并没有被卸载.(阐明: 像类加载器实例这种较为特别的目标一般在许多当地被引证, 会在虚拟机中呆比较长的时刻)

    【测验场景三】运用扩展类加载器加载, 可是无法将其设置为unreachable的状况

        阐明:将测验场景二中的MyClass类型字节码文件打包成jar放置到JRE扩展目录下,以便扩展类加载器能够加载的到。由于标志扩展ClassLoader实例(ExtClassLoader@7259da" sun.misc.Launcher$ExtClassLoader@7259da)加载了许多类型,并且又没有清晰的接口将其设置为null,所以咱们无法将加载MyClass类型的体系类加载器实例设置为unreachable状况,所以经过测验成果咱们能够看出,MyClass类型并没有被卸载.
       

1 public class Main {
2      public static void main(String[] args) {
3        try {
4          Class classLoaded = ClassLoader.getSystemClassLoader().getParent()
5 .loadClass("MyClass");
6
7          System.out.println(classLoaded.getClassLoader());
8
9          classLoaded = null;
10
11          System.out.println("开端GC");
12          System.gc();
13          System.out.println("GC完结");
14          //判别当时标准扩展类加载器是否有被引证(是否是unreachable状况)
15          System.out.println(Main.class.getClassLoader().getParent());
16       } catch (Exception e) {
17          e.printStackTrace();
18       }
19    }
20 }
        咱们添加虚拟机参数-verbose:gc来调查废物搜集的状况,对应输出如下:

sun.misc.Launcher$ExtClassLoader@7259da
开端GC
[Full GC 199K- 133K(1984K), 0.0139811 secs]
GC完结
sun.misc.Launcher$ExtClassLoader@7259da

    关于发动类加载器咱们就不需再做相关的测验了,jvm标准和JLS中现已有清晰的阐明晰.


    【类型卸载总结】
    经过以上的相关测验(尽管测验的场景较为简略)咱们能够大致这样归纳:
    1、有发动类加载器加载的类型在整个运转期间是不或许被卸载的(jvm和jls标准).
    2、被体系类加载器和标准扩展类加载器加载的类型在运转期间不太或许被卸载,由于体系类加载器实例或许标准扩展类的实例基本上在整个运转期间总能直接或许直接的拜访的到,其到达unreachable的或许性极小.(当然,在虚拟机快退出的时分能够,由于不论ClassLoader实例或许Class(java.lang.Class)实例也都是在堆中存在,相同遵从废物搜集的规矩).
    3、被开发者自界说的类加载器实例加载的类型只要在很简略的上下文环境中才干被卸载,并且一般还要借助于强制调用虚拟机的废物搜集功用才干够做到.能够料想,略微杂乱点的运用场景中(特别许多时分,用户在开发自界说类加载器实例的时分选用缓存的战略以进步体系功用),被加载的类型在运转期间也是简直不太或许被卸载的(至少卸载的时刻是不断定的).

      归纳以上三点,咱们能够默许前面的定论1, 一个现已加载的类型被卸载的几率很小至少被卸载的时刻是不断定的.一起,咱们能够看的出来,开发者在开发代码时分,不应该对虚拟机的类型卸载做任何假定的条件下来完结体系中的特定功用.

   
      【类型更新进一步剖析】
    前面现已清晰说过,被一个特定类加载器实例加载的特定类型在运转时是无法被更新的.留意这儿说的
         是一个特定的类加载器实例,而非一个特定的类加载器类型.
   
        【测验场景四】
        阐明:现在要删去前面现已放在工程输出目录下和扩展目录下的对应的MyClass类型对应的字节码
       
1 public class Main {
2      public static void main(String[] args) {
3        try {
4          MyURLClassLoader classLoader = new MyURLClassLoader();
5          Class classLoaded1 = classLoader.loadClass("MyClass");
6          Class classLoaded2 = classLoader.loadClass("MyClass");
7          //判别两次加载classloader实例是否相同
8           System.out.println(classLoaded1.getClassLoader() classLoaded2.getClassLoader());
9
10         //判别两个Class实例是否相同
11           System.out.println(classLoaded1 classLoaded2);
12       } catch (Exception e) {
13          e.printStackTrace();
14       }
15    }
16 }
        输出如下:
        true
        true

        经过成果咱们能够看出来,两次加载获取到的两个Class类型实例是相同的.那是不是的确是咱们的自界说
       类加载器真实意义上加载了两次呢(即从获取class字节码到界说class类型…整个进程呢)?
      经过对java.lang.ClassLoader的loadClass(String name,boolean resolve)办法进行调试,咱们能够看出来,第二
      次  加载并不是真实意义上的加载,而是直接回来了前次加载的成果.

       阐明:为了调试便利, 在Class classLoaded2 = classLoader.loadClass("MyClass");行设置断点,然后单步跳入, 能够看到第2次加载恳求回来的成果直接是前次加载的Class实例. 调试进程中的截图 最好能自己调试一下).
      
    
        【测验场景五】同一个类加载器实例重复加载同一类型
        阐明:首要要对已有的用户自界说类加载器做必定的修正,要掩盖已有的类加载逻辑, MyURLClassLoader.java类扼要修正如下:从头运转测验场景四中的测验代码
     
1 public class MyURLClassLoader extends URLClassLoader {
2     //省掉部分的代码和前面相同,仅仅新增如下掩盖办法
3     /*
4     * 掩盖默许的加载逻辑,假如是D:/classes/下的类型每次强制从头完好加载
5     *
6     * @see java.lang.ClassLoader#loadClass(java.lang.String)
7     */
8     @Override
9     public Class ? loadClass(String name) throws ClassNotFoundException {
10      try {
11        //首要调用体系类加载器加载
12         Class c = ClassLoader.getSystemClassLoader().loadClass(name);
13        return c;
14      } catch (ClassNotFoundException e) {
15       // 假如体系类加载器及其父类加载器加载不上,则调用本身逻辑来加载D:/classes/下的类型
16          return this.findClass(name);
17      }
18   }
19 }
阐明: this.findClass(name)会进一步调用父类URLClassLoader中的对应办法,其间触及到了defineClass(String name)的调用,所以说现在类加载器MyURLClassLoader会针对D:/classes/目录下的类型进行真实意义上的强制加载并界说对应的类型信息.

        测验输出如下:
        Exception in thread "main" java.lang.LinkageError: duplicate class definition: MyClass
       at java.lang.ClassLoader.defineClass1(Native Method)
       at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
       at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:124)
       at java.net.URLClassLoader.defineClass(URLClassLoader.java:260)
       at java.net.URLClassLoader.access$100(URLClassLoader.java:56)
       at java.net.URLClassLoader$1.run(URLClassLoader.java:195)
       at java.security.AccessController.doPrivileged(Native Method)
       at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
       at MyURLClassLoader.loadClass(MyURLClassLoader.java:51)
       at Main.main(Main.java:27)
     
       定论:假如同一个类加载器实例重复强制加载(含有界说类型defineClass动作)相同类型,会引起java.lang.LinkageError: duplicate class definition.
   
       【测验场景六】同一个加载器类型的不同实例重复加载同一类型
      
1 public class Main {
2     public static void main(String[] args) {
3       try {
4         MyURLClassLoader classLoader1 = new MyURLClassLoader();
5         Class classLoaded1 = classLoader1.loadClass("MyClass");
6         MyURLClassLoader classLoader2 = new MyURLClassLoader();
7         Class classLoaded2 = classLoader2.loadClass("MyClass");
8
9         //判别两个Class实例是否相同
10          System.out.println(classLoaded1 classLoaded2);
11       } catch (Exception e) {
12          e.printStackTrace();
13       }
14    }
15 }

      测验对应的输出如下:
      false
    
   
        【类型更新总结】  
     由不同类加载器实例重复强制加载(含有界说类型defineClass动作)同一类型不会引起java.lang.LinkageError过错, 可是加载成果对应的Class类型实例是不同的,即实践上是不同的类型(尽管包名+类名相同). 假如强制转化运用,会引起ClassCastException.(阐明: 头一段时刻那篇文章中解说过,为什么不同类加载器加载同名类型实践得到的成果其实是不同类型, 在JVM中一个类用其全名和一个加载类ClassLoader的实例作为仅有标识,不同类加载器加载的类将被置于不同的命名空间).


        运用场景:咱们在开发的时分或许会遇到这样的需求,就是要动态加载某指定类型class文件的不同版别,以便能动态更新对应功用.
         主张:
        1. 不要寄希望于等候指定类型的曾经版别被卸载,卸载行为对java开发人员通明的.
        2. 比较牢靠的做法是,每次创立特定类加载器的新实例来加载指定类型的不同版别,这种运用场景下,一般就要献身缓存特定类型的类加载器实例以带来功用优化的战略了.关于指定类型现已被加载的版别, 会在恰当机遇到达unreachable状况,被unload并废物收回.每次运用完类加载器特定实例后(断定不需要再运用时), 将其显现赋为null, 这样或许会比较快的到达jvm 标准中所说的类加载器实例unreachable状况, 增大现已不再运用的类型版别被赶快卸载的时机.
        3. 不得不提的是,每次用新的类加载器实例去加载指定类型的指定版别,的确会带来必定的内存耗费,一般类加载器实例会在内存中保存比较长的时刻. 在bea开发者网站上找到一篇相关的文章(有专门剖析ClassLoader的部分):http://dev2dev.bea.com/pub/a/2005/06/memory_leaks.html

           写的进程中参阅了jvm标准和jls, 并参阅了sun公司官方网站上的一些bug的剖析文档。

           欢迎我们批评指正!


本博客中的一切文章、漫笔除了标题中含有引证或许转载字样的,其他均为原创。转载请注明出处,谢谢!

 

版权声明
本文来源于网络,版权归原作者所有,其内容与观点不代表娱乐之横扫全球立场。转载文章仅为传播更有价值的信息,如采编人员采编有误或者版权原因,请与我们联系,我们核实后立即修改或删除。

猜您喜欢的文章