classloader
任何一本java相关的书都有介绍classloader相关的知识,最近工作中确实用到了一些classloader相关的东西,做个总结以免自己忘记。
定义
classloader负责将应用中用到的==所有class==加载到虚拟机中供应用使用,并且确保类能够被==正确的以符合开发者想法的方式加载,保证所有类能够被正确的使用==。这里对应的是类加载的第一阶段。
虚拟机设计团队在保证类能够被正确加载的情况下,尽可能的给了应用开发者更大的自由,我们来看一下深入理解java虚拟机中对classloader的定义。
虚拟机设计团队把类加载阶段中的”通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何取获取所需要的类。实现这个动作的代码模块称为”类加载器”。
划重点:
- 通过全限定名获取此类的二进制流,并没有限定二进制流的来源,可以是文件、字符串、或者网络流。
- 放到虚拟机外部实现,开发者可以自己定义自己的classloader,并且自由决定加载的时机。
以上两点设计的非常精妙,使得classloader在热部署等领域大放异彩。
双亲委派模型
被嚼烂的知识点,保证了一个类在一套命名空间中仅能被加载一次,这点非常重要。
- 除了启动类加载器(Bootstrap ClassLoader),每个类加载起都有一个父加载器(不是父类)。
- 加载器在加载类的时候,首先交给父加载器加载,如果父加载器不能加载,则再由自己加载。
- 如果在自己和自己父加载器组成的命名空间中已经有相同全限定名的类,则直接返回。
- 这点保证了一个类在一套类加载器体系中仅能被加载一次,防止程序运行中由类加载器导致的不稳定性,也防止用户自定义的类和系统类冲突。
- 启动类加载器(Bootstrap ClassLoader),加载JAVA_HOME\lib下和被-Xbootclasspath标记的目录下的类。
- 扩展类加载器(Extension ClassLoader),加载JAVA_HOME\lib\ext或者被java.ext.dirs标记的目录,父加载器为启动类加载器。
- 总之以上两个加载器用户自己一般都不会用,这两个加载器的优先级高于应用类加载器,主要是为了确保虚拟机正确稳定的启动。
- 应用程序类加载器(Application ClassLoader),用来加载classpath指定的类库,应用程序中自定义的类大都是由这个加载器加载。
- 自定义加载器,自定义的加载器如非特别指定,父加载器一般都是应用程序加载器。
- 类加载器有两个方法:loadClass和findClass,其中loadClass一般不建议覆盖,这个方法内包含了双亲委派模型的实现,覆盖了这个方法将会导致双亲委派模型受到破坏,对程序稳定性造成影响。
- 类加载的时候,先查看自己加载过的类列表中有没有这个类,如果没有则交给父加载器,如果父加载器不能加载,则通过findClass方法获取class,因此继承findClass不会造成双亲委派模型被破坏。
热加载
需求
我们有时候会有这样的需求,应用程序不能停机,但是需要更新其中的某些实现,比如换一个排序器加快性能,换一个解析器来解析新的字段,这些都是很正常的需求,但是如果每次都重启一下应用程序,就比较麻烦而且影响系统稳定了,因此就有了热加载的需求。
关键点
- 每个类加载器和父加载器共享一套命名系统,如果链式的加载所有类,那么很难在不影响服务的情况下完成类的替换(无法做到原子操作)。
- 因此必须考虑采用旁支的方式,我们从应用程序类加载器这里分两个叉出来,一个叉表示当前正在使用的一套类加载和命名空间,另一套则用来加载新的类。
- 这样能够保证,应用程序类加载器及其以上的类不会被重复加载,仅有分支加载器加载的类能够被加载,而且对每个分支加载器对别的分支加载器完全无感知,每一套都是完整的类加载系统。
- 每次应用在创建对象的时候都通过当前的类加载起获得class对象,然后通过接口实例化对象。
- 在新的类加载器加载完所有需要的类之后,我们通过原子替换将加载器换掉,这样后面使用新的加载器构造的对象就是新的对象了。
- 旧的加载起由于失去了引用,会被gc掉,当一个类的class对象、加载器、和实力对象都全部被gc掉时,这个类会被卸载掉,从而完成了整个热加载的过程。
实现
删除了业务部分代码,大概逻辑还是比较清楚的。
classloader
public class ComponentLoader extends URLClassLoader {
private static final Logger LOGGER = LoggerFactory.getLogger("SEARCHER_ARTS");
private List<JarURLConnection> cachedJarFiles = new ArrayList<>();
public ComponentLoader() {
super(new URL[] {}, findParentClassLoader());
}
/**
* 将指定的url所有jar添加到类加载器的classpath中去,并缓存jar connection,方便以后卸载jar
* @param file 一个可向类加载器的classpath中添加的文件url
*/
protected boolean addDir(File file) throws MalformedURLException {
boolean res = true;
if (file.isDirectory()) {
File[] childFiles = file.listFiles();
for (File childFile : childFiles) {
res = res & addDir(childFile);
}
}
else if (file.getName().endsWith(".jar")) {
try {
res = addComponent(file.toURI().toURL());
} catch (MalformedURLException e) {
LOGGER.error("Failed add custom jar, jarPath:{}", file.getAbsolutePath());
res = false;
}
}
return res;
}
/**
* 将指定的Jar添加到类加载器的classpath中去,并缓存jar connection,方便以后卸载jar
* @param file 一个可向类加载器的classpath中添加的文件url
*/
protected boolean addComponent(URL file) {
LOGGER.info("begin add JAR file! fileUrl: {}", file.getPath());
try {
// 打开并缓存文件url连接
URLConnection uc = file.openConnection();
if (uc instanceof JarURLConnection) {
uc.setUseCaches(true);
((JarURLConnection) uc).getManifest();
cachedJarFiles.add((JarURLConnection)uc);
}
} catch (Exception e) {
LOGGER.error("Failed to add JAR file! fileUrl: {}", file.getPath(), e);
}
addURL(file);
return true;
}
/**
* 卸载jar包,已被加载的类不会被卸载
*/
public void unloadJarFiles() {
for (JarURLConnection jarURLConnection : cachedJarFiles) {
try {
LOGGER.info("begin remove JAR file! fileUrl: {}", jarURLConnection.getJarFileURL().getPath());
jarURLConnection.getJarFile().close();
jarURLConnection = null;
} catch (Exception e) {
LOGGER.error("Failed to unload JAR file! fileUrl: {}", jarURLConnection.getJarFileURL().getPath(), e);
}
}
}
/**
* 定位基于当前上下文的父类加载器
* @return 返回可用的父类加载器.
*/
private static ClassLoader findParentClassLoader() {
ClassLoader parent = ComponentService.class.getClassLoader();
if (parent == null) {
parent = ComponentLoader.class.getClassLoader();
}
if (parent == null) {
parent = ClassLoader.getSystemClassLoader();
}
return parent;
}
}
service
public class ComponentService {
private static volatile boolean loading = false;
private volatile ComponentLoader currentLoader = null;
private ExecutorService componentReloadExecutor = null;
private static final ComponentService instance = new ComponentService();
private ComponentService() {
this.componentReloadExecutor = Executors.newSingleThreadExecutor();
try {
currentLoader.addDir(rootPath);
} catch (Exception e) {
LOGGER.error("load local component failed", e);
}
}
public static ComponentService getInstance() {
return instance;
}
public boolean loadComponent(String loaderPath) {
CustomComponentLoadTask customReloadTask = new CustomComponentLoadTask(loaderPath);
componentReloadExecutor.submit(customReloadTask);
return true;
}
public JSONObject reloadComponent(String loaderPath) {
loadComponent(loaderPath);
}
private class CustomComponentLoadTask implements Runnable {
private final String loaderPath;
@Override
public void run() {
try {
loading = true;
ComponentLoader componentLoader = new ComponentLoader();
componentLoader.addDir(loaderPath);
currentLoader = componentLoader;
} finally {
loading = false;
}
}
}
}