一、问题背景
itask中线上20几个task,这些task的大多数都有这么一些特点:
1.基于quartz和spring框架,配置信息已经硬编码在xml配置中
2.执行的时候一个任务启动一个jvm线程,然后加载spring容器,quartz配置在spring容器中
3.实际执行处理数据的时候大概需要512M空间或者更多
4.这些任务每天只触发执行数据处理几次,一般在6-10次左右,每次执行的时间大都在5-10分左右。
为了更好的利用机器的内存资源,个人认为可以将业务相关的一组任务在执行时间不相互冲突的情况下合并到一个jvm中。有些同学反映,合并后同一个jvm中的任务就不能单独重启或者关闭了。
二、初步方案
系统中现有一个合并方案,该合并方案是将所有的任务放到一个spring容器中了,然后配置多个quartz的trigger来实现的。显然这种情况先任务是不可以单独重启或者关闭的。尤其是当其中一个任务有新的发布更新的时候必须将这一组任务全部重启。这样就带了不必要的麻烦。
因此,我们仔细考虑了这个问题,并提出了三个解决方案(现在还没有确定最终方案,不过最终方案于本文的分享内容没有关系)。在讨论这个方案的时候发现一个非常好玩的技术点。在此跟大家分享下。
其中一个轻量级的方案是:将现有的一个任务进程转换成同一个jvm中的一个线程,然后在该线程内加载并启动该任务的spring容器。同时为了能够支持单独启动或者关闭某个任务,我们需要在这个jvm线程开启一个socket服务端口,主要用来侦听其他进程发来的启动关闭命令。由于遗留任务较多,这一解决方案可以最大化的减少遗留任务的改造。
三、方案难点解决
但是这个方案上有一个难点,就是当其中一个任务需要用新发布的jar包时,如果jvm不重启,是无法重新加载这个更新的jar包的。当然这也是jvm规范对一个well-behaved的classloader所要求的(见jvm规范5.3节末尾,参考链接http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.3)。同样我们在jdk的classLoader的实现代码也能验证这一点(见下图291行处)。
四、解决办法:
找到了问题所在也就基本解决了这个问题,可以从两个地方分别找到这个问题的答案。
一是jvm的规范中。jvm规范在提到“At run time, a class or interface is determined not by its name alone, but by a pair: its binary name (§4.2.1) and its defining class loader. ”也就是说我们只需要是使用不同的classloader再去装载这个class,那么这个class就可以被重新从磁盘读入了。
我们知道默认的java类在jvm中的加载是通过启动器类加载器、扩展类加载器、系统类加载器以双亲委托的方式完成的。而这些类加载器在同一个jvm中一般以单例的形式实现的。所以我们必须编写自己的类加载器。而该类加载器游必须与默认的系统类加载器的功能完全一样。在我们所使用的jvm中系统类加载器实际上一般就等同于URLClassloarder所以我们可以直接继承该类编写我们自己的类加载器。代码如下:
class MyClassLoader extends URLClassLoader {//为了防止被滥用,此类的访问权限控制的越强越好
private static URLClassLoader scl = (URLClassLoader) getSystemClassLoader();
private MyClassLoader() {
super(scl.getURLs(), null);//大家可以想想此处的parent为什么指定为null
}
static ClassLoader getAnotherSysClassLoader() {
return new MyClassLoader();
}
}
完整的参考代码见链接:http://taft.iteye.com/blog/1839825