有个同事提高个jrebel的工具,提起tomcat的热部署方案。
jrebel是一款收费的JVM级的热部署工具包。
JVM级的热部署也就是说,可以不重启JVM,让修改或添加的类加载到JVM中。
加载器:启动类加载器-》扩展类加载器-》应用程序类加载器-》自定义类加载器。
对JVM来说只有一种启动类加载器(Bootstrap ClassLoader)是C++写的。
这个类加载器负责将存放在<JAVA_HOME>lib目录中的,
或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
Java实现的加载器的:扩展类加载器,应用程序类加载器,自定义类加载器。
什么是双亲委派模型?
按这个顺序:启动类加载器-》扩展类加载器-》应用程序类加载器-》自定义类加载器。去加载类。也就是当你用自定义类加载器加载某类时,一定要从启动类加载器(<JAVA_HOME>lib)这个目录中的jar中找先。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
打破双亲委派模型
如果基础类又要调用回用户的代码,那该怎么办?这并非是不可能的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的Class Path下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能“认识”这些代码,因为启动类加载器的搜索范围中找不到用户应用程序类,那该怎么办?为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置
Tomcat的类加载器架构
在Tomcat目录结构中,有3组目录(“/common/*”、“/server/*”和“/shared/*”)可以存放Java类库,另外还可以加上Web应用程序自身的目录“/WEB-INF/*”,一共4组,把Java类库放置在这些目录中的含义分别如下:
①放置在/common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
②放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。
③放置在/shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
④放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*、/server/*、/shared/*和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。Tomcat 6.x顺理成章地把/common、/server和/shared三个目录默认合并到一起变成一个/lib目录,这个目录里的类库相当于以前/common目录中类库的作用。
Tomcat和server.xml中的 reloadble=true时
<Context docBase="" path="" reloadable="true" source=""/></Host>
Tomcat的加载是怎样的呢?看看tomcaat源码。
public void backgroundProcess() {
if (reloadable && modified()) {
try {
Thread.currentThread().setContextClassLoader
(WebappLoader.class.getClassLoader());
if (context != null) {
context.reload();
}
} finally {
if (context != null && context.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(context.getLoader().getClassLoader());
}
}
}
}
有个后台线程扫.class文件,如果有修改就使用(Appclassloader)应用程序类加载器去动态加载被修改过的class到JVM中:context.reload()重启上下文。也是就是重启当前的WEB应用程序;。而不是重启jvm去静态加载.class。
请看这个if:if (reloadable && modified()) ,如果reloadble=true而且有.class文件被修改过。就会重启上下文件。那么添加.class会不会也重启上下文呢?请看下面代码:
public boolean modified() {
if (log.isDebugEnabled())
log.debug("modified()");
for (Entry<String,ResourceEntry> entry : resourceEntries.entrySet()) {
long cachedLastModified = entry.getValue().lastModified;
long lastModified = resources.getClassLoaderResource(
entry.getKey()).getLastModified();
if (lastModified != cachedLastModified) {
if( log.isDebugEnabled() )
log.debug(sm.getString("webappClassLoader.resourceModified",
entry.getKey(),
new Date(cachedLastModified),
new Date(lastModified)));
return true;
}
}
// Check if JARs have been added or removed
WebResource[] jars = resources.listResources("/WEB-INF/lib");
// Filter out non-JAR resources
int jarCount = 0;
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
jarCount++;
Long recordedLastModified = jarModificationTimes.get(jar.getName());
if (recordedLastModified == null) {
// Jar has been added
log.info(sm.getString("webappClassLoader.jarsAdded",
resources.getContext().getName()));
return true;
}
if (recordedLastModified.longValue() != jar.getLastModified()) {
// Jar has been changed
log.info(sm.getString("webappClassLoader.jarsModified",
resources.getContext().getName()));
return true;
}
}
}
if (jarCount < jarModificationTimes.size()){
log.info(sm.getString("webappClassLoader.jarsRemoved",
resources.getContext().getName()));
return true;
}
// No classes have been modified
return false;
}
看到//jar has been added没有,所以肯定也会重启加载。
Jrebel热部署的原理就很清晰了,就是只加载调用appclassloader加载.class到jvm中,而不context.reload()重启上下文就行了。