`
helloscan
  • 浏览: 788 次
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

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

阅读更多

【转载】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");行设置断点,然后单步跳入, 可以看到第二次加载请求返回的结果直接是上次加载的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的分析文档。

           欢迎大家批评指正!


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

 

分享到:
评论

相关推荐

    深入Java虚拟机(原书第2版).pdf【附光盘内容】

    8.1.1 解析和动态扩展 8.1.2 类装载器与双亲委派模型 8.1.3 常量池解析 8.1.4 解析constant_class_info入口 8.1.5 解析constant_fieldref_info入口 s.1.6 解析constant_methodref_info入口 ...

    Java虚拟机工作原理详解

    Java虚拟机工作原理详解 Java虚拟机工作原理详解是 Java 程序执行的核心组件之一。了解 Java 虚拟机的工作原理对 Java 开发人员来说非常重要。本文将详细介绍 Java 虚拟机工作原理的详细过程和类加载器的工作机理。...

    深入Java虚拟机

    8.1.1 解析和动态扩展 8.1.2 类装载器与双亲委派模型 8.1.3 常量池解析 8.1.4 解析CONSTANT_Class_info入口 8.1.5 解析CONSTANT_Fieldref_info 入口 S.1.6 解析CONSTANT_Methodref_info 入口 ...

    深入理解Java虚拟机笔记(带目录).docx

    Java 虚拟机(JVM)是 Java 语言的运行环境,它负责解释和执行 Java 字节码。下面是 Java 虚拟机相关的知识点: 虚拟机内存结构 Java 虚拟机的内存结构主要包括以下几个部分: * 方法区(Method Area):用于存储...

    深入java虚拟机第二版

    8.1.1 解析和动态扩展 8.1.2 类装载器与双亲委派模型 8.1.3 常量池解析 8.1.4 解析CONSTANT_Class_info入口 8.1.5 解析CONSTANT_Fieldref_info 入口 S.1.6 解析CONSTANT_Methodref_info 入口 8.1.7 ...

    深入JAVA虚拟机(第2版)

    8.1.1 解析和动态扩展 8.1.2 类装载器与双亲委派模型 8.1.3 常量池解析 8.1.4 解析CONSTANT_Class_info入口 8.1.5 解析CONSTANT_Fieldref_info入口 S.1.6 解析CONSTANT_Methodref_info入口 ...

    Java虚拟机JVM类加载初始化

    由Java虚拟机自带的默认加载器(根加载器、扩展加载器、系统加载器)所加载的类在JVM生命周期中始终不被卸载。所以这些类的Class对象(我称其为实例的模板对象)始终能被触及!而由用户自定义的类加载器所加载的类会...

    Java虚拟机类加载机制浅谈

     虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验、准备、解析和初始化,终会形成可以被虚拟机使用的Java类型,这是一个虚拟机的类加载机制。Java中的类是动态加载的,只有在运行期间使用到该类的...

    深入理解java类加载机制

    在类加载方面,我们将深入探讨Java程序的类加载原理和流程,包括加载、验证、准备、解析和初始化等五个环节的详细解析,并对其强调点进行详细讲解。我们将详细介绍Java虚拟机中类的生命周期并探讨类加载时的各种问题...

    虚拟机LINUX与主机WINDOWS文件共享

    在虚拟机 LINUX 和主机 WINDOWS 之间实现文件共享需要使用 SAMBA 服务器。 SAMBA 是一个功能强大的开源软件,可以实现 LINUX 和 WINDOWS 之间的文件共享。下面将详细介绍如何实现虚拟机 LINUX 和主机 WINDOWS 之间的...

    关于JVM的总结

    类被加载到虚拟机内存开始,到卸载出内存为止,生命周期包含: 加载,验证,准备,解析,初始化,使用,卸载 7个阶段,加载,验证,准备,初始化和卸载这5个顺序是确定的,解析阶段则不一定,他在某些情况下可以在...

    详解JAVA类加载机制(推荐)

    JAVA源码编译由三个过程组成: 1、源码编译机制。 2、类加载机制 ...系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类,当运行某个java程序时,会启动一个java虚拟机进程,两次运行

    阿里巴巴面试题总结

    答:7个阶段,分别是:加载、验证、准备、解析、初始化、使用和卸载。 2. java有多少个类加载器?分别的作用是什么?一个Class文件是怎么被加载 到JVM里的,描述一下加载流程。 答:java有三个类加载器,分别为:...

    12.虚拟机的加载机制1

    1. 加载 2. 验证 【连接】 3. 准备 【连接】 4. 解析 【连接】 5. 初始化 6. 使 7. 卸载 1. 遇到new、getstatic、puts

    JVM—类加载过程学习

    其实,整个生命周期是7步,类从被加载到虚拟机内存中开始,到卸载出内存为止,分为:加载-&gt;验证-&gt;准备-&gt;解析-&gt;初始化-&gt;使用-&gt;卸载。 2 加载   加载分为三步: 1)通过全类名获取定义此类的二进制字节流; 2)将...

    Linux系统(Centos6.5以上)安装jdk教程解析

    ①,Java JDK区分32位和64位系统,在安装的之前先通过命令查看Centos系统的版本号,具体查看命令:#uname -a 解释:如果有x86_64就是64位的,没有就是32位的。后面是X686或X86_64则内核是64位的,i686或i386则内核...

    2010年谢彦的android笔记

    1.9 安装和卸载应用程序(apk包) 23 1.10 系统升级 26 1.11 android系统支持app2sd(修改boot.img) 27 2 基本概念 30 2.1 Android组件 31 2.1.1 基本组件 31 2.1.2 组件间的通讯 32 2.1.3 intent使用方法 33 2.2 ...

    新版Android开发教程.rar

    程序可以采用 JAVA 开发,但是因为它的虚拟机 (Virtual Machine) Dalvik ,是将 JAVA 的 bytecode 转成 自 己的格式,回避掉需要付给 SUN 有关 JAVA 的授权费用。 对手机制造者的影响 � Android 是款开源的移动计算...

    RED HAT LINUX 6大全

    本书内容翔实、涉及领域广泛,并且提供了详细的例子和大量的参考资料(包括书籍、电子文档和Internet站点),是一本学习、使用和管理Linux不可多得的好书。 目 录 译者序 前言 第一部分 Red Hat Linux的介绍与安装 ...

    P13_Blazor项目模板讲解ThreeBlazor2020_1_11.rar

    许多语言,包括 C、C#、Go 和 Rust,都可以编译成面向基于栈的 WebAssembly 虚拟机的代码。 .NET 代码可以在任何地方运行,包括浏览器内部。 Blazor 是一个客户端库,它在 WebAssembly 上使用.NET 来支持借助 ...

Global site tag (gtag.js) - Google Analytics