转自:http://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"); 行设置断点,然后单步跳入,   最好能自己调试一下 ). L 可以看到第二次加载请求返回的结果直接是上次加载的 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 的分析文档 。

欢迎大家批评指正!

http://blog.csdn.net/nomousewch/article/details/6336235

Java虚拟机类型卸载和类型更新解析(转)的更多相关文章

  1. 【转】Java虚拟机类型卸载和类型更新解析

    [摘要]         前面系统讨论过java类型加载(loading)的问题,在这篇文章中简要分析一下java类型卸载(unloading)的问题,并简要分析一下如何解决如何运行时加载newly ...

  2. 深入理解Java虚拟机--个人总结(持续更新)

    深入理解Java虚拟机--个人总结(持续更新) 每天按照书本学一点,会把自己的总结思考写下来,形成输出,持续更新,立帖为证 -- 2020年7月7日 开始第一次学习 -- 2020年7月8日 今天在百 ...

  3. Java 虚拟机 最易理解的 全面解析

    先上一个最容易理解的类实例化的内存模型案例截图: 转载自:https://www.zybuluo.com/Yano/note/321063 周志明著的<深入理解 Java 虚拟机>的干货~ ...

  4. 深入理解java虚拟机(十一) 方法调用-解析调用与分派调用

    方法调用过程是指确定被调用方法的版本(即调用哪一个方法),并不包括方法执行过程.我们知道,Class 文件的编译过程中并不包括传统编译中的连接步骤,一切方法调用在 Class 文件调用里面存储的都只是 ...

  5. 深入Java虚拟机——类型装载、连接(转)

    来自http://hi.baidu.com/holder/item/c38abf02de14c7d31ff046e0 Java虚拟机通过装载.连接和初始化一个Java类型,使该类型可以被正在运行的Ja ...

  6. 深入Java虚拟机读书笔记第五章Java虚拟机

    Java虚拟机 Java虚拟机之所以被称之为是虚拟的,就是因为它仅仅是由一个规范来定义的抽象计算机.因此,要运行某个Java程序,首先需要一个符合该规范的具体实现. Java虚拟机的生命周期 一个运行 ...

  7. 深入Java虚拟机

    第一章:Java体系结构介绍 1.Java为什么重要?       Java是为网络而设计的,而Java这种适合网络环境的能力又是由其体系结构决定的,可以保证安全健壮和平台无关的程序通过网络传播. 2 ...

  8. [转]JAVA虚拟机的生命周期

    JAVA虚拟机体系结构 JAVA虚拟机的生命周期 一个运行时的Java虚拟机实例的天职是:负责运行一个java程序.当启动一个Java程序时,一个虚拟机实例也就诞生了.当该程序关闭退出,这个虚拟机实例 ...

  9. 《深入Java虚拟机学习笔记》- 第5章 Java虚拟机

    一.JVM的生命周期 当启动一个Java程序时,一个Java虚拟机实例就诞生了:当该程序关闭退出时,这个Java虚拟机也就随之消亡: JVM实例通过调用某个初始类的main方法来运行一个Java程序: ...

随机推荐

  1. 远程方法调用(RMI)原理与示例 (转)

    RMI介绍 远程方法调用(RMI)顾名思义是一台机器上的程序调用另一台机器上的方法.这样可以大致知道RMI是用来干什么的,但是这种理解还不太确切.RMI是Java支撑分布式系统的基石,例如著名的EJB ...

  2. 0x00000000该内存不能为read

    0X000000存储器不能read解决方案 有这种现象方面,首先,在硬件,这有问题的内存,二,软件,其中有许多问题. 一:先说说硬件: 一般来说,电脑硬件不easy生病.内存故障的可能性并不大(非你的 ...

  3. Windows 8 应用开发 - 挂起与恢复

    原文:Windows 8 应用开发 - 挂起与恢复      Windows 8 应用通常涉及到两种数据类型:应用数据与会话数据.在上一篇提到的本地数据存储就是应用层面的数据,包括应用参数设置.用户重 ...

  4. #Windows Phone:在HTML5专案中,如何从Javascript传送字串到C#的APP端

    原文:#Windows Phone:在HTML5专案中,如何从Javascript传送字串到C#的APP端 #Windows Phone:在HTML5专案中,如何从Javascript传送字串到C#的 ...

  5. 【Android进阶】Android面试题目整理与讲解(二)

    1. ArrayList,Vector, LinkedList 的存储性能和特性 ArrayList 和 Vector 都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们 ...

  6. 双向链表实现简单的list

    双向链表结构: 定义一个如下结构体 struct Node { Object data; Node *next; Node *prev; }; 下面为list的具体实现: #include <i ...

  7. [转载][NAS] 使用win8的“存储池”功能~

    之前自己用DQ77KB搭建一个小存储系统(帖子链接:http://www.chiphell.com/thread-567753-1-1.html),一直使用intel主板带的软RAID功能构建RAID ...

  8. 轮播图片 高效图片轮播,两个imageView实现

    该轮播框架的优势: 文件少,代码简洁 不依赖任何其他第三方库,耦合度低 同时支持本地图片及网络图片 可修改分页控件位置,显示或隐藏 自定义分页控件的图片,就是这么个性 自带图片缓存,一次加载,永久使用 ...

  9. 10令人惊叹的模型的影响HTML5应用程序及源代码

    HTML5已经越来越流行起来了.尤其是移动互联网的发展,更是带动了HTML5的迅猛发展,我们也是时候学习HTML5了,以防到时候落伍.今天给大家介绍10款效果惊艳的HTML5应用.方便大家学习,也将应 ...

  10. vector和list删除元素

    #include <iostream> #include <vector> #include <list> using namespace std; int mai ...