为什么要实现热加载?

希望依赖的jar在需要用的时候依赖,不用的时候就不依赖,项目打包部署就不会过于庞大

为什么需要自定义ClassLoader?

每次加载类都会消耗一些内存,加载越来越多,如果不释放可能会引发OOM
由于每个对象都有相应的Class对象,所以当该类仍有实例的时候,是无法卸载的,因为此时Class对象仍可达;
对于ClassLoader对象,留意双亲委托机制中,每个ClassLoader都会记录自身已加载的类信息,所以如果ClassLoader可达,那么Class对象仍是可达的,这就解释了为什么我们为什么需要自定义ClassLoader
因为系统的ClassLoader永远是可达的,他们加载的类在运行时永远不会被卸载

自定义类加载器

通过jar的路径实现jar的热加载,继承URLClassLoader

  1. public class MyURLClassLoader extends URLClassLoader {
  2. private JarURLConnection cachedJarFile;
  3. /**
  4. * 定位基于当前上下文的父类加载器
  5. *
  6. * @return 返回可用的父类加载器.
  7. */
  8. private static ClassLoader findParentClassLoader() {
  9. ClassLoader parent = MyURLClassLoader.class.getClassLoader();
  10. if (parent == null) {
  11. parent = Thread.currentThread().getContextClassLoader();
  12. }
  13. return parent;
  14. }
  15. public MyURLClassLoader() {
  16. super(new URL[]{}, findParentClassLoader());
  17. }
  18. /**
  19. * 将指定的文件url添加到类加载器的classpath中去,并缓存jar connection,方便以后卸载jar
  20. *
  21. * @param file 一个可想类加载器的classpath中添加的文件url
  22. */
  23. public void addURLFile(URL file) {
  24. try {
  25. // 打开并缓存文件url连接
  26. URLConnection uc = file.openConnection();
  27. if (uc instanceof JarURLConnection) {
  28. uc.setUseCaches(true);
  29. ((JarURLConnection) uc).getManifest();
  30. cachedJarFile = (JarURLConnection) uc;
  31. }
  32. } catch (Exception e) {
  33. System.err.println("Failed to cache plugin JAR file: " + file.toExternalForm());
  34. }
  35. //加载jar包文件 父类提供
  36. addURL(file);
  37. }
  38. /**
  39. * jar包卸载
  40. */
  41. public void removeJarFile() {
  42. if (cachedJarFile == null) {
  43. return;
  44. }
  45. try {
  46. System.err.println("Unloading plugin JAR file " + cachedJarFile.getJarFile().getName());
  47. cachedJarFile.getJarFile().close();
  48. } catch (Exception e) {
  49. System.err.println("Failed to unload JAR file\n" + e);
  50. }
  51. }
  52. }

类加载器管理工具类

向外提供加载jar,加载类,获取目标对象的方法

  1. public class MyClassLoaderManager {
  2. /**
  3. * 加载jar包的缓存 一个jar包对应一个类加载器对象 key为jar包名
  4. */
  5. private static final ConcurrentHashMap<String, MyURLClassLoader> LOADER_CACHE = new ConcurrentHashMap<>();
  6. /**
  7. * jar包所在父目录
  8. */
  9. private String jarPath;
  10. public MyClassLoaderManager(String jarPath) {
  11. this.jarPath = jarPath;
  12. }
  13. /**
  14. * 加载jar包
  15. *
  16. * @param jarName jar包名称 包括后缀
  17. */
  18. public void loadJar(String jarName) {
  19. MyURLClassLoader urlClassLoader = LOADER_CACHE.get(jarName);
  20. if (urlClassLoader != null) {
  21. return;
  22. }
  23. try {
  24. MyURLClassLoader classLoader = new MyURLClassLoader();
  25. URL jarUrl = new URL("jar:file:/" + jarPath + "/" + jarName + "!/");
  26. classLoader.addURLFile(jarUrl);
  27. LOADER_CACHE.put(jarName, classLoader);
  28. } catch (MalformedURLException e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. /**
  33. * 根据jar包名和类路径获取实例化对象
  34. *
  35. * @param jarName jar包名称 包括后缀
  36. * @param classPackage 包括包名的类完整路径
  37. */
  38. public <T> T getInstance(String jarName, String classPackage) {
  39. Class<?> loadClass = loadClass(jarName, classPackage);
  40. try {
  41. return (T) loadClass.newInstance();
  42. } catch (IllegalAccessException e) {
  43. throw new IllegalArgumentException(e.getMessage());
  44. } catch (InstantiationException e) {
  45. throw new IllegalArgumentException("实例化失败");
  46. }
  47. }
  48. /**
  49. * 根据jar包名和类路径获取实例化对象
  50. *
  51. * @param jarName jar包名称 包括后缀
  52. * @param classPackage 包括包名的类完整路径
  53. */
  54. public Class<?> loadClass(String jarName, String classPackage) {
  55. MyURLClassLoader urlClassLoader = LOADER_CACHE.get(jarName);
  56. if (urlClassLoader == null) {
  57. return null;
  58. }
  59. try {
  60. return urlClassLoader.loadClass(classPackage);
  61. } catch (ClassNotFoundException e) {
  62. throw new IllegalArgumentException("类没有找到");
  63. }
  64. }
  65. /**
  66. * 移除jar包
  67. *
  68. * @param jarName jar包名称 包括后缀
  69. */
  70. public void removeJarFile(String jarName) {
  71. MyURLClassLoader urlClassLoader = LOADER_CACHE.get(jarName);
  72. if (urlClassLoader == null) {
  73. return;
  74. }
  75. urlClassLoader.removeJarFile();
  76. try {
  77. urlClassLoader.close();
  78. LOADER_CACHE.remove(jarName);
  79. } catch (IOException e) {
  80. e.printStackTrace();
  81. }
  82. }
  83. }

用来加载的类

希望达到的目的是classloader项目实现热加载business的jar包,上述写的自定义类加载器都在classloader项目中
image.png
在classloader项目中创建一个接口,非常简单的一个接口

public interface Plugin {

    void doSome();

}

在business项目中引入classloader的依赖,注意scope为provided,依赖不参与打包,只是为了写Plugin插件接口的实现类时编译不报错

<dependency>
    <groupId>com.halayang</groupId>
    <artifactId>classloader</artifactId>
    <version>1.0-SNAPSHOT</version>
    <scope>provided</scope>
    <optional>true</optional>
</dependency>

在business项目中写Plugin接口实现类

public class MyPlugin implements Plugin {
    @Override
    public void doSome() {
        System.out.println("hahahahaha");
    }
}

使用maven将business打成jar包image.png

测试

public static void main(String[] args)  {
    //指定jar包所在目录
    MyClassLoaderManager classLoader = new MyClassLoaderManager("E:/JavaStudy/ZZZZZZ StudyingCode/classloaderhotdeploy");
    String jarName = "business.jar";
    //先加载jar包再加载类再获取实例
    classLoader.loadJar(jarName);
    Plugin plugin =  classLoader.getInstance(jarName, "com.halayang.service.MyPlugin");
    plugin.doSome();
    //卸载jar包
    classLoader.removeJarFile(jarName);
}

运行结果
image.png

监控

为了查看类卸载有没有成功,在测试方法中加死循环

MyClassLoaderManager classLoader = new MyClassLoaderManager("E:/JavaStudy/ZZZZZZ StudyingCode/classloaderhotdeploy");
while (true) {
    String jarName = "business.jar";
    //先加载jar包再加载类
    classLoader.loadJar(jarName);
    Plugin plugin1 = classLoader.getInstance(jarName, "com.halayang.service.MyPlugin");
    plugin1.doSome();
    Plugin plugin2 = classLoader.getInstance(jarName, "com.halayang.service.HaHaPlugin");
    plugin2.doSome();
    Thread.sleep(2000);
    classLoader.removeJarFile(jarName);
    //手动gc
    //System.gc();
}

运行jvm参数把堆调小一点,调成10m
image.png
jdk提供了一个监控工具jvisualvm,可以实时观察线程 ,类加载、卸载, 堆内存的使用情况等
手动gc时 观察到的类卸载会比较明显
image.png