【摘要】
        前面系统讨论过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目录下

  1. public class MyURLClassLoader extends URLClassLoader {
  2. public MyURLClassLoader() {
  3. super(getMyURLs());
  4. }
  5.  
  6. private static URL[] getMyURLs() {
  7. try {
  8. return new URL[]{new File ("D:/classes/").toURL()};
  9. } catch (Exception e) {
  10. e.printStackTrace();
  11. return null;
  12. }
  13. }
  14. }
  1. public class Main {
  2. 2 public static void main(String[] args) {
  3. 3 try {
  4. 4 MyURLClassLoader classLoader = new MyURLClassLoader();
  5. 5 Class classLoaded = classLoader.loadClass("MyClass");
  6. 6 System.out.println(classLoaded.getName());
  7. 7
  8. 8 classLoaded = null;
  9. 9 classLoader = null;
  10. 10
  11. 11 System.out.println("开始GC");
  12. 12 System.gc();
  13. 13 System.out.println("GC完成");
  14. 14 } catch (Exception e) {
  15. 15 e.printStackTrace();
  16. 16 }
  17. 17 }
  18. 18 }

我们增加虚拟机参数-verbose:gc来观察垃圾收集的情况,对应输出如下:

  1. MyClass
  2. 开始GC
  3. [Full GC[Unloading class MyClass]
  4. 207K->131K(1984K), 0.0126452 secs]
  5. GC完成

测试场景二】使用系统类加载器加载,但是无法将其设置为unreachable的状态
      
说明:将场景一中的MyClass类型字节码文件放置到工程的输出目录下,以便系统类加载器可以加载

  1. 1 public class Main {
  2. 2 public static void main(String[] args) {
  3. 3 try {
  4. 4 Class classLoaded = ClassLoader.getSystemClassLoader().loadClass(
  5. 5 "MyClass");
  6. 6
  7. 7
  8. 8 System.out.printl(sun.misc.Launcher.getLauncher().getClassLoader());
  9. 9 System.out.println(classLoaded.getClassLoader());
  10. 10 System.out.println(Main.class.getClassLoader());
  11. 11
  12. 12 classLoaded = null;
  13. 13
  14. 14 System.out.println("开始GC");
  15. 15 System.gc();
  16. 16 System.out.println("GC完成");
  17. 17
  18. 18 //判断当前系统类加载器是否有被引用(是否是unreachable状态)
  19. 19 System.out.println(Main.class.getClassLoader());
  20. 20 } catch (Exception e) {
  21. 21 e.printStackTrace();
  22. 22 }
  23. 23 }
  24. 24 }

我们增加虚拟机参数-verbose:gc来观察垃圾收集的情况, 对应输出如下:

  1. sun.misc.Launcher$AppClassLoader@197d257
  2. sun.misc.Launcher$AppClassLoader@197d257
  3. sun.misc.Launcher$AppClassLoader@197d257
  4. 开始GC
  5. [Full GC 196K->131K(1984K), 0.0130748 secs]
  6. GC完成
  7. 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. 1 public class Main {
  2. 2 public static void main(String[] args) {
  3. 3 try {
  4. 4 Class classLoaded = ClassLoader.getSystemClassLoader().getParent()
  5. 5 .loadClass("MyClass");
  6. 6
  7. 7 System.out.println(classLoaded.getClassLoader());
  8. 8
  9. 9 classLoaded = null;
  10. 10
  11. 11 System.out.println("开始GC");
  12. 12 System.gc();
  13. 13 System.out.println("GC完成");
  14. 14 //判断当前标准扩展类加载器是否有被引用(是否是unreachable状态)
  15. 15 System.out.println(Main.class.getClassLoader().getParent());
  16. 16 } catch (Exception e) {
  17. 17 e.printStackTrace();
  18. 18 }
  19. 19 }
  20. 20 }

我们增加虚拟机参数-verbose:gc来观察垃圾收集的情况,对应输出如下:

  1. sun.misc.Launcher$ExtClassLoader@7259da
  2. 开始GC
  3. [Full GC 199K->133K(1984K), 0.0139811 secs]
  4. GC完成
  5. sun.misc.Launcher$ExtClassLoader@7259da

关于启动类加载器我们就不需再做相关的测试了,jvm规范和JLS中已经有明确的说明了.

类型卸载总结】
    
通过以上的相关测试(虽然测试的场景较为简单)我们可以大致这样概括:
    1、有启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范).
    2、被系统类加载器和标准扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者标准扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小.(当然,在虚拟机快退出的时候可以,因为不管ClassLoader实例或者Class(java.lang.Class)实例也都是在堆中存在,同样遵循垃圾收集的规则).
    3、被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到.可以预想,稍微复杂点的应用场景中(尤其很多时候,用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的).

综合以上三点,我们可以默认前面的结论1, 一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的.同时,我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下来实现系统中的特定功能.

类型更新进一步分析】
    
前面已经明确说过,被一个特定类加载器实例加载的特定类型在运行时是无法被更新的.注意这里说的
         是一个特定的类加载器实例,而非一个特定的类加载器类型.
    
        【测试场景四】
        说明:现在要删除前面已经放在工程输出目录下和扩展目录下的对应的MyClass类型对应的字节码

  1. 1 public class Main {
  2. 2 public static void main(String[] args) {
  3. 3 try {
  4. 4 MyURLClassLoader classLoader = new MyURLClassLoader();
  5. 5 Class classLoaded1 = classLoader.loadClass("MyClass");
  6. 6 Class classLoaded2 = classLoader.loadClass("MyClass");
  7. 7 //判断两次加载classloader实例是否相同
  8. 8 System.out.println(classLoaded1.getClassLoader() == classLoaded2.getClassLoader());
  9. 9
  10. 10 //判断两个Class实例是否相同
  11. 11 System.out.println(classLoaded1 == classLoaded2);
  12. 12 } catch (Exception e) {
  13. 13 e.printStackTrace();
  14. 14 }
  15. 15 }
  16. 16 }

输出如下:
        true
        true

通过结果我们可以看出来,两次加载获取到的两个Class类型实例是相同的.那是不是确实是我们的自定义
       类加载器真正意义上加载了两次呢(即从获取class字节码到定义class类型…整个过程呢)?
      通过对java.lang.ClassLoader的loadClass(String name,boolean resolve)方法进行调试,我们可以看出来,第二
      次  加载并不是真正意义上的加载,而是直接返回了上次加载的结果.

说明:为了调试方便, 在Class classLoaded2 =
classLoader.loadClass("MyClass");行设置断点,然后单步跳入,
可以看到第二次加载请求返回的结果直接是上次加载的Class实例. 调试过程中的截图 最好能自己调试一下).

测试场景五】同一个类加载器实例重复加载同一类型
        说明:首先要对已有的用户自定义类加载器做一定的修改,要覆盖已有的类加载逻辑, MyURLClassLoader.java类简要修改如下:重新运行测试场景四中的测试代码

  1. 1 public class MyURLClassLoader extends URLClassLoader {
  2. 2 //省略部分的代码和前面相同,只是新增如下覆盖方法
  3. 3 /*
  4. 4 * 覆盖默认的加载逻辑,如果是D:/classes/下的类型每次强制重新完整加载
  5. 5 *
  6. 6 * @see java.lang.ClassLoader#loadClass(java.lang.String)
  7. 7 */
  8. 8 @Override
  9. 9 public Class<?> loadClass(String name) throws ClassNotFoundException {
  10. 10 try {
  11. 11 //首先调用系统类加载器加载
  12. 12 Class c = ClassLoader.getSystemClassLoader().loadClass(name);
  13. 13 return c;
  14. 14 } catch (ClassNotFoundException e) {
  15. 15 // 如果系统类加载器及其父类加载器加载不上,则调用自身逻辑来加载D:/classes/下的类型
  16. 16 return this.findClass(name);
  17. 17 }
  18. 18 }
  19. 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. 1 public class Main {
  2. 2 public static void main(String[] args) {
  3. 3 try {
  4. 4 MyURLClassLoader classLoader1 = new MyURLClassLoader();
  5. 5 Class classLoaded1 = classLoader1.loadClass("MyClass");
  6. 6 MyURLClassLoader classLoader2 = new MyURLClassLoader();
  7. 7 Class classLoaded2 = classLoader2.loadClass("MyClass");
  8. 8
  9. 9 //判断两个Class实例是否相同
  10. 10 System.out.println(classLoaded1 == classLoaded2);
  11. 11 } catch (Exception e) {
  12. 12 e.printStackTrace();
  13. 13 }
  14. 14 }
  15. 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虚拟机类型卸载和类型更新解析的更多相关文章

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

    转自:http://www.blogjava.net/zhuxing/archive/2008/07/24/217285.html [摘要]          前面系统讨论过java 类型加载(loa ...

  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内存区域-学习笔记

    Java运行时内存区域 Java虚拟机在运行Java程序的时候会将它所管理的内存区域划分为多个不同的区域.每个区域都有自己的用途,创建以及销毁的时间.有的随着虚拟机的启动而存在,有的则是依赖用户线程来 ...

  8. 深入理解java虚拟机系列初篇(一):为什么要学习JVM?

    前言 本来想着关于写JVM这个专栏,直接写知识点干货的,但是想着还是有必要开篇讲一下为什么要学习JVM,这样的话让一些学习者心里有点底的感觉比较好... 原因一:面试 不得不说,随着互联网门槛越来越高 ...

  9. 深入Java虚拟机

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

随机推荐

  1. TimesTen启动停止命令

    ttDaemonAdmin –start 启动 ttDaemonAdmin –stop 停止 或打开服务cmd-serviers.msc,找到相关服务启动或停止.

  2. matlab中如何根据t检验参数查找t检验值

    这个问题花了一些时间.先看图 这个是t检验里面的公式,但是如何在matlab中找到该式子对应的值,我现在才知道. 就是这样:x=tinv(1-α/2,n-1)----t(n)分布的上侧α分位数     ...

  3. js 常用事件句柄总结

    HTML 4.0 的新特性之一是有能力使 HTML 事件触发浏览器中的动作(action),比如当用户点击某个 HTML 元素时启动一段 JavaScript.下面是一个属性列表,这些属性可插入 HT ...

  4. Java网络编程三--基于TCP协议的网络编程

    ServerSocket对象用于监听来自客户端的Socket连接,如果没有连接,它将一直处于等待状体 Socket accept():如果接收到客户端的连接请求,该方法返回一个与客户端对应Socket ...

  5. Vue1.0基础学习笔记整理

    最近一直在使用Vue.js开发项目,现将在学习过程中遇到的一些学习小细节总结如下: 1.只处理单次插值,今后的数据变化就不会再引起插值更新了 <span>This will never c ...

  6. redis的有序集合(Sorted Sets)数据类型

    和Sets相比,Sorted Sets增加了一个权重参数score,使得集合中的元素能够按score进行有序排列,比如一个存储全班同学成绩的Sorted Sets,其集合value可以是同学的学号,而 ...

  7. struts quick start

    1.create a dynamic web project 2.import the needed jar(about 11) 3. request page(index.jsp) <%@ p ...

  8. 数据结构复习之C语言malloc()动态分配内存概述

    #include <stdio.h> #include <malloc.h> int main(void) { ] = {, , , , }; // 计算数组元素个数 ]); ...

  9. Java设计模式—访问者模式

    原文地址:http://www.cnblogs.com/java-my-life/archive/2012/06/14/2545381.html 总结的太棒啦,导致自己看了都不想总结了...... 在 ...

  10. Java中去除字符串中空格的方法

    昨天写了一个关于Excel文件处理的脚本,在字符串匹配功能上总是出现多余不正确的匹配,debug调试之后,发现一个坑. ------->代码中字符串使用了replaceAll()方法,去除了所有 ...