Java类重新加载101:对象、类和类加载器

在这篇文章中,我们会回顾如何使用动态类加载器去重新加载一个Java类。在这个过程中,我们会看到对象、类和类加载器是怎么在一起配合的过程是怎样的。我们先来大概看一下问题,解释一下重新加载的过程,然后再以具体的示例来阐述典型的问题和解决方案。这个系列中的其他文章包括:

RJC101: 对象,类和类加载器
RJC201: 类加载器泄漏是怎么发生的?
RJC301: Web开发中的类加载器 — Tomcat, GlassFish, OSGi, Tapestry 5等等
RJC401: 深入HotSwap和JRebel
RJC501: Turnaround的损耗?

一个Jevgeni Kabanov的关于“你真的理解class loader吗?”的展示视频

概述

当说到重新加载Java代码时,首先需要理清的是类和对象之间的关系。所有Java代码都跟类中的方法关联。很简单,你可以把类当成接收”this”为第一个参数的一系列方法的集合。这个类和它所有的方法被加载到内存,并且被分配得到一个唯一的标识符。在Java API中,这个标识符由java.lang.Class的一个实例来表示,而你可以通过MyObject.class表达式来访问它。

每一个创建的对象都可以通过Object.getClass()方法来获取指向这个标识的引用。当调用对象方法时,JVM会查看类引用并调用特定类的方法。实际上就是,当你调用mo.method()(mo是MyObject的实例),JVM会调用mo.getClass().getDeclaredMethod("method").invoke(mo)(实际上JVM当然不是这样做的,但结果是一样的)。

每一个Class对象和它的相应的classloader(MyObject.class.getClassLoader())关联。类加载器的最主要作用就是定义一个类作用域 — 在哪里可见,哪里不可见。这个域概念允许相同名称的类存在,只要它们是通过不同的类加载器加载。它同样也允许不同的类加载器加载一个更新版本的类。

最主要的问题是:在Java的代码重新加载中,尽管你可以加载一个更新版本的类,但会得到一个完全不同的标识,而已存在的对象依然会指向之前版本的类。因此当调用那些对象方法时,它仍然执行的是老版本的。

让我们来假设一下,我们加载了一个新版本的MyObject类。假设老版本的为MyObject_1,新的那个为MyObject_2,同时假设MyObject_1中的MyObject.method()返回”1″,MyObject_2的返回”2″。如果mo2是MyObject_2的实例:

1
2
3
mo.getClass() != mo2.getClass()
mo.getClass().getDeclaredMethod( "method" ).invoke(mo)
!= mo2.getClass().getDeclaredMethod( "method" ).invoke(mo2)

mo.getClass().getDeclaredMethod("method").invoke(mo2) 抛出ClassCastException,因为momo2Class的标识不匹配。

这意味着任何有效的方法都必须创建一个新的mo2实例,而这个实例必须是mo的完全拷贝,并替换掉所有原有的mo的引用。想知道有多困难,试回想一下,你上次不得不换你的手机号码的时候。换号码本身很简单,但麻烦的是你需要确保你认识的每个人都知道用这个新号码。这跟对象一样复杂(实际上,除非你自己控制对象创建,否则是不可能的),而这里说的是你需要在同一个时间内更新很多个对象。

深入并尝试

让我们来看看代码层面会是怎样的。请记住,我们这里做的是通过另外一个类加载器加载一个新版本的类。我们使用的Example看起来就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
public class Example implements IExample {
   private int counter;
   public String message() {
     return "Version 1" ;
   }
   public int plusPlus() {
     return counter++;
   }
   public int counter() {
     return counter;
   }
}

我们使用无限循环并打印出Example类信息的的main()方法。我们同样需要Example类的两个实例:example1在开始的时候创建一次,example2在每一次循环都重新创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
   private static IExample example1;
   private static IExample example2;
  
   public static void main(String[] args)  {
     example1 = ExampleFactory.newInstance();
  
     while ( true ) {
       example2 = ExampleFactory.newInstance();
  
       System.out.println( "1) " +
         example1.message() + " = " + example1.plusPlus());
       System.out.println( "2) " +
         example2.message() + " = " + example2.plusPlus());
       System.out.println();
  
       Thread.currentThread().sleep( 3000 );
     }
   }
}

IExample是一个拥有Example所有方法的接口。这是必须的,因为我们通过另外一个隔离的类加载器加载Example,所以Main不能直接使用它(否则会有ClassCastException)。

1
2
3
4
public interface IExample {
   String message();
   int plusPlus();
}  

从这个例子中,你也许会惊讶创建动态类加载器怎么这么简单。如果我们去掉异常处理代码,精简后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ExampleFactory {
   public static IExample newInstance() {
     URLClassLoader tmp =
       new URLClassLoader( new URL[] {getClassPath()}) {
         public Class loadClass(String name) {
           if ( "example.Example" .equals(name))
             return findClass(name);
           return super .loadClass(name);
         }
       };
  
     return (IExample)
       tmp.loadClass( "example.Example" ).newInstance();
   }
}

由于例子需要,方法getClassPath()可以返回一个固定的类路径(hardcoded classpath)。然而,在完整的代码中(参见后面的资源章节),你可以看到我们怎么使用ClassLoader.getResource()来做到自动适配的。

现在让我们来执行Main.main,在几个循环后,我们可以看到输出:

1
2
1) Version 1 = 3
2) Version 1 = 0

如我们所期望的,虽然第一个实例的counter被更新了,但第二个还是维持为”0″。如果我们修改Exampler.message()方法,使它返回”Version 2″。输出如下:

1
2
1) Version 1 = 4
2) Version 2 = 0   

我们看到,第一个实例继续增加counter,但使用旧版本的类来输出版本信息。而第二个实例已经被更新,但所有的状态都已经丢失了。

为了改进这个,我们尝试为第二个实例重新构造状态。我们只需从前一个迭代中复制它即可。

首先我们为Example类添加一个新的copy方法(和对象的接口方法):

1
2
3
4
5
public IExample copy(IExample example) {
   if (example != null )
     counter = example.counter();
   return this ;
}

接着我们更新Main.main()方法中创建第二个对象的那行代码:

1
example2 = ExampleFactory.newInstance().copy(example2);

等几个迭代后可以看到:

1
2
1) Version 1 = 3
2) Version 1 = 3

修改Example.message()方法使它返回”Version 2″输出:

1
2
1) Version 1 = 4
2) Version 2 = 4

你可以看到尽管用户可以看到第二个实例被更新,并且所有状态都被保留,但它涉及到手动进行状态的管理。不幸的是,Java API中没办法只更新现存的对象或者可以保存它的状态,因此我们只能求助于这种复制的解决方案了。

在接下来的文章中,我们会回顾OSGi, Tapestry 5, Grails和其他的web容器是怎么面对在重新加载类时的状态处理问题的,之后我们会深入HotSwap, 动态语言和Instrumentation API是怎么工作的,同样也会深入JRebel。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值