Java虚拟机类型卸载和类型更新解析(转)

转自: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

 

时间: 2024-11-05 02:48:25

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

Java虚拟机类型卸载和类型更新解析

首先看一下,关于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虚拟机规范中关于类型卸载的内容就这么简单两句

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

来自http://hi.baidu.com/holder/item/c38abf02de14c7d31ff046e0     Java虚拟机通过装载.连接和初始化一个Java类型,使该类型可以被正在运行的Java程序所使用.其中,装载就是把二进制形式的Java类型读入Java虚拟机中:而连接就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去.连接阶段分为三个子步骤--验证.准备和解析."验证"步骤确保了Java类型数据格式正确并且适于Java虚拟机使用而&quo

Windows系统巧用Rundll32卸载Java虚拟机

在Windows系统中安装和卸载程序是件非常容易的事情,一般用户通过安装和卸载程序向导就能很轻松地完成.但安装和卸载Windows程序还有些鲜为人知的小技巧,使用Rundll32命令安装和卸载Windows程序就是其中之一,可能你从来还没接触过,下面我们就一起领略它的神奇功能吧! 提示:Rundll32.exe是 Windows系统提供的一个命令,它用来调用32位的DLL函数(16位的DLL文件用Rundll.exe来调用 .DLL文件是Windows的基础,所有的API函数都是在DLL中实现的

java虚拟机构造原理

Java虚拟机的生命周期 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序.程序开始执行时他才运行,程序结束时他就停止.你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机. Java虚拟机总是开始于一个main()方法,这个方法必须是公有.返回void.直接受一个字符串数组.在程序执行时,你必须给Java虚拟机指明这个包换main()方法的类名. Main()方法是程序的起点,他被执行的线程初始化为程序的初始线程.程序中其他的线程都由他来启动.Java中的线程分为两种:

Java虚拟机体系结构

JAVA虚拟机的生命周期 一个运行时的Java虚拟机实例的天职是:负责运行一个java程序.当启动一个Java程序时,一个虚拟机实例也就诞生了.当该程序关闭退出,这个虚拟机实例也就随之消亡.如果同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例.每个Java程序都运行于它自己的Java虚拟机实例中. Java虚拟机实例通过调用某个初始类的main()方法来运行一个Java程序.而这个main()方法必须是共有的(public).静态的(static).返回值为void,并且接受

深入Java虚拟机读书笔记[5]

第五章 Java虚拟机 1. Java虚拟机内部体系结构 在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配.当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用. 堆内存用来存放由new创建的对象和数组.在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理. 2. 数据类型 Java虚拟机中的数据类型分为基本类型和引用类型.Java语言中的所有基本类型同样为

java虚拟机在进行类型强制转换的时候究竟做了什么

问题描述 java虚拟机在进行类型强制转换的时候究竟做了什么 打个比方 A a = new A(): B b = (B)a: 这两行代码究竟是怎样在Jvm中实现的? 解决方案 子类继承父类之后,在new子类的时候,实际上父类也被构造出来了,因此子类实际上包含着父类的完整信息,也就因此可以完成向上转型,. 至于具体怎么实现的,我查了一下好像是堆分配的过程,贴两个链接,不误导了.http://blog.csdn.net/anjayxc/article/details/6063210http://ww

全面解析Java支持的数据类型及Java的常量和变量类型_java

基本数据类型变量就是用来储存值而保留的内存位置.这就意味着当你创建一个变量时就会在内存中占用一定的空间. 基于变量的数据类型,操作系统会进行内存分配并且决定什么将被储存在保留内存中.因此,通过给变量分配不同的数据类型,你可以在这些变量中存储整数,小数或者字字母. Java 中有两种有效地数据类型: 原始数据类型 引用数据类型 原始数据类型 Java 支持 8 种原始数据类型.原始数据类型是由该语言预先定义的并用关键词命名的.下面让我们深入学习一下这 8 种数据类型. 字节型(byte) 字节型是

《深入解析Android 虚拟机》——2.2 Java虚拟机概述

2.2 Java虚拟机概述 Java虚拟机(JVM)是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能模拟来实现的.Java虚拟机有自己完善的硬件架构,如处理器.堆栈.寄存器等,还具有相应的指令系统.JVM虚拟机的运作结构如图2-1所示. 从该图中可以看到,JVM是运行在操作系统之上的,与硬件没有直接的交互.JVM的具体组成部分如图2-2所示. (1)使用JVM的原因. Java语言的一个非常重要的特点就是与平台的无关性.而使用JVM是实现这一特点的关键.一般的高级语言如果要在