动态加载和卸载Java类-据说可以成功-尚未验证

在开发Java服务器应用时,我们最希望开发的应用能够支持热部署,即不需要重启系统就可以用新的应用替换旧的应用。 
如果使用动态语言,这些功能比较容易实现。但Java是静态语言,是否也可实现动态热部署呢? 

首先,我们要深入了解一下Java的类装载(Class Loading)机制,和垃圾回收(Garbage Collection)机制。其中class loading 将负责装载新的应用包;GC将负责卸载旧的应用包。 
装载新应用包的方法比较简单,只需要定制一个ClassLoader,从指定路径装载.jar文件即可。 
要卸载一个ClassLoader,则必须要同时卸载通过该ClassLoader 载入的类的所有实例, 简单将ClassLoader的引用置为null并希望GC回收的做法是无效的。然而,要想统计并记录所有该ClassLoader载入的类实例是不现实的。而且,应用的装载和卸载功能,是服务器Framework的一部分,而不是应用业务功能的一部分。因此,framework也无法知道业务应用中何时载入和创建了多少对象。 

解决方法还是有的。因为GC回收Heap中那些unreachable的对象,追根溯源,所有对象的创建都是由活动线程中发起的, 即所谓的Root set of references. 因此一旦活动线程结束运行,则可以说,线程中所有对象的根引用不可用了,则由根应用创建的所有对象也变为unreachable. 

因此可以做如下设计: 
动态加载和卸载Java类-据说可以成功-尚未验证_第1张图片  

其中Server负责service的deploy和undeploy。 
其中deploy的过程可简单描述为:创建一个daemon线程,设置其context classloader为Service Jar包的ClassLoader,起动该Daemon线程。 
undeploy的过程可简单描述为:停止服务(service daemon thread),设置线程context classloader为null, 等带线程彻底结束后手动执行GC来回收对象和ClassLoader. 

Service接口如下: 
Java代码   收藏代码
  1. public interface Service {  
  2.   
  3.     public final static String ATTR_SERVICE_ID = "SERVICE-ID";  
  4.     public final static String ATTR_SERVICE_CLASS = "SERVICE-CLASS";  
  5.   
  6.     public void install() throws ServiceException;  
  7.   
  8.     public void start() throws ServiceException;  
  9.   
  10.     public void stop() throws ServiceException;  
  11.   
  12.     public void uninstall() throws ServiceException;  
  13.   
  14.     public String getId();  
  15. }  


服务管理器如下: 
Java代码   收藏代码
  1. /** 
  2.  *  
  3.  * @author less 
  4.  * Responsible for deploy and undeploy Services. 
  5.  */  
  6. public class ServiceManager {  
  7.   
  8.     private final static HashMap installedServices = new HashMap();  
  9.   
  10.     public final synchronized static String install(File jarService) throws Exception {  
  11.         MyJarLoader loader = new MyJarLoader(jarService, MyJarLoader.class.getClassLoader());  
  12.         Manifest manifest = loader.getManifest();  
  13.         Attributes attrs = manifest.getAttributes("Service");  
  14.         String svcId = attrs.getValue(Service.ATTR_SERVICE_ID);  
  15.         if (installedServices.containsKey(svcId)) {  
  16.             uninstall(svcId);  
  17.         }  
  18.         ServiceThread t = new ServiceThread();  
  19.         t.setContextClassLoader(loader);  
  20.         t.setDaemon(true);  
  21.         t.start();  
  22.         loader = null;  
  23.         return svcId;  
  24.     }  
  25.   
  26.     public final static void uninstall(File jarService) throws Exception {  
  27.         MyJarLoader loader = new MyJarLoader(jarService, MyJarLoader.class.getClassLoader());  
  28.         Manifest manifest = loader.getManifest();  
  29.         Attributes attrs = manifest.getAttributes("Service");  
  30.         String svcId = attrs.getValue(Service.ATTR_SERVICE_ID);  
  31.         loader = null;  
  32.         uninstall(svcId);  
  33.     }  
  34.   
  35.     public final static void uninstall(String svcId) throws Exception {  
  36.         ServiceThread t = installedServices.get(svcId);  
  37.         if (t.getStatus() == ServiceThread.Status.RUNNING) {  
  38.             t.stopService();  
  39.         }  
  40.         ServiceManager.getInstalledServices().remove(svcId);  
  41.         t.destroyService();  
  42.         t.setContextClassLoader(null);  
  43.         while (t.isAlive()) {  
  44.             try {  
  45.                 Thread.sleep(200);  
  46.             } catch (InterruptedException e) {  
  47.             }  
  48.         }  
  49.         t = null;  
  50.         System.gc();  
  51.         try {  
  52.             Thread.sleep(200);  
  53.         } catch (InterruptedException e) {  
  54.         }  
  55.         System.gc();  
  56.     }  
  57.   
  58.     static HashMap getInstalledServices() {  
  59.         return installedServices;  
  60.     }  
  61.   
  62. }  
  63.   
  64. /** 
  65.  *  
  66.  * @author less 
  67.  * A Customer Service carrier. 
  68.  */  
  69. class ServiceThread extends Thread {  
  70.   
  71.     public static enum Status {  
  72.         RUNNING, STOPPED  
  73.     }  
  74.   
  75.     private Status status = Status.STOPPED;  
  76.     private Service service = null;  
  77.   
  78.     public Status getStatus() {  
  79.         return this.status;  
  80.     }  
  81.   
  82.     @Override  
  83.     public void run() {  
  84.         try {  
  85.             Manifest manifest = ((MyJarLoader) getContextClassLoader()).getManifest();  
  86.             Attributes attrs = manifest.getAttributes("Service");  
  87.             String svcId = attrs.getValue(Service.ATTR_SERVICE_ID);  
  88.             this.setName(svcId);  
  89.             String svcClass = attrs.getValue(Service.ATTR_SERVICE_CLASS);  
  90.             Class c = (Class) getContextClassLoader().loadClass(svcClass);  
  91.             service = c.newInstance();  
  92.             c = null;  
  93.             service.install();  
  94.             ServiceManager.getInstalledServices().put(svcId, this);  
  95.             startService();  
  96.             stopService();  
  97.         } catch (Exception e) {  
  98.             e.printStackTrace();  
  99.         }  
  100.     }  
  101.   
  102.     public void stopService() {  
  103.         if (this.status != Status.STOPPED) {  
  104.             try {  
  105.                 service.stop();  
  106.                 this.status = Status.STOPPED;  
  107.             } catch (Exception e) {  
  108.                 e.printStackTrace();  
  109.                 this.status = Status.RUNNING;  
  110.             }  
  111.         }  
  112.     }  
  113.   
  114.     public void startService() {  
  115.         if (this.status == Status.STOPPED) {  
  116.             this.status = Status.RUNNING;  
  117.             try {  
  118.                 service.start();  
  119.             } catch (Exception e) {  
  120.                 e.printStackTrace();  
  121.                 this.status = Status.STOPPED;  
  122.             }  
  123.         }  
  124.     }  
  125.   
  126.     public void destroyService() {  
  127.         try {  
  128.             service.uninstall();  
  129.             service = null;  
  130.         } catch (Exception e) {  
  131.             e.printStackTrace();  
  132.         }  
  133.     }  
  134. }  
  135.   
  136. /** 
  137.  *  
  138.  * @author less 
  139.  *  
  140.  */  
  141. class MyJarLoader extends JarLoader {  
  142.   
  143.     public MyJarLoader(File file, ClassLoader parent) throws Exception {  
  144.         super(file, parent);  
  145.         System.out.println(this + " is created.");  
  146.     }  
  147.   
  148.     @Override  
  149.     protected void finalize() throws Throwable {  
  150.         destroy();  
  151.         System.out.println(this + " is finalized.");  
  152.         super.finalize();  
  153.     }  
  154. }  



示例服务代码,测试修改编译后重新部署: 
Java代码   收藏代码
  1. /** 
  2.  * @author less 
  3.  *  
  4.  */  
  5. public class ExampleService1 implements Service {  
  6.     private boolean bRun = true;  
  7.   
  8.     @Override  
  9.     public void install() throws ServiceException {  
  10.         System.out.println("== " + this + " is installed.");  
  11.     }  
  12.   
  13.     @Override  
  14.     public void start() throws ServiceException {  
  15.         System.out.println("== " + this + " is started.");  
  16.   
  17.         //int i = 10000;  
  18.         int i=0;  
  19.         while (bRun) {  
  20.             //System.out.println(this + "  " + i--);  
  21.             System.out.println(this + "  " + i++);  
  22.             try {  
  23.                 Thread.sleep(5000);  
  24.             } catch (InterruptedException ie) {  
  25.             }  
  26.         }  
  27.         System.out.println("== " + this + " is stopped.");  
  28.     }  
  29.   
  30.     @Override  
  31.     public void stop() throws ServiceException {  
  32.         System.out.println("== Trying to stop " + this);  
  33.         bRun = false;  
  34.         Thread.currentThread().interrupt();  
  35.     }  
  36.   
  37.     @Override  
  38.     public void uninstall() throws ServiceException {  
  39.         System.out.println("== " + this + " is uninstalled.");  
  40.     }  
  41.   
  42.     @Override  
  43.     public String getId() {  
  44.         // TODO Auto-generated method stub  
  45.         return null;  
  46.     }  
  47.   
  48.     @Override  
  49.     protected void finalize() throws Throwable {  
  50.         System.out.println(this + " - is finalized.");  
  51.         super.finalize();  
  52.     }  
  53. }  


控制台输出如下: 
Server started. 
org.lucky.server.MyJarLoader@27b15692 is created. 
== org.lucky.service.example.ExampleService1@4e76fba0 is installed. 
== org.lucky.service.example.ExampleService1@4e76fba0 is started. 
org.lucky.service.example.ExampleService1@4e76fba0  10000 
org.lucky.service.example.ExampleService1@4e76fba0  9999 
org.lucky.service.example.ExampleService1@4e76fba0  9998 
org.lucky.service.example.ExampleService1@4e76fba0  9997 
org.lucky.server.MyJarLoader@2f833eca is created. 
== Trying to stop org.lucky.service.example.ExampleService1@4e76fba0 
== org.lucky.service.example.ExampleService1@4e76fba0 is uninstalled. 
== org.lucky.service.example.ExampleService1@4e76fba0 is stopped. 
org.lucky.service.example.ExampleService1@4e76fba0 - is finalized. 
org.lucky.server.MyJarLoader@27b15692 is finalized. 
== org.lucky.service.example.ExampleService1@7b36a43c is installed. 
== org.lucky.service.example.ExampleService1@7b36a43c is started. 
org.lucky.service.example.ExampleService1@7b36a43c  0 
org.lucky.service.example.ExampleService1@7b36a43c  1 
org.lucky.service.example.ExampleService1@7b36a43c  2 
org.lucky.service.example.ExampleService1@7b36a43c  3 

由上述测试可见,通过这种方式,可以实现类动态加载和卸载以及热部署。 

这个方法为Java类的动态加载/卸载提供了一个思路。由于它需要为每一个需要部署的服务起动一个线程,虽然该线程只负责装载和卸载服务,服务运行时并不消耗CPU,但会固定占用一定大小的内存。测试值为每线程约占用350k内存。因此服务多时,存在StackOverflowError的风险。 

在JDK7中据说提供了anonymous classloading 机制来支持动态类加载/卸载。需要使用的同学可以关注一下。



回复 @yangzhiyong : 那说明这个热加载做得不完善,或者你们的程序通过某种方式长期持有了想要被热替换的类实例;因为热加载的原理是,将原来加载的类,连同其classloader一起扔掉(也就是断掉所有与其的引用); 这时候如果热加载机制不完善,这种引用扔不干净,那么老的类就会还会继续存在于内存中,然后同样一个类被不同的(注意,是“不同的”)classloader加载到内存中,实际上这个类被两个不同的classloader加载了共2份到内存中;这种情况一多,就会慢慢消耗掉permGen,导致永生代OOM; 至于具体是哪个类在内存中占据大量资源,以及哪里长期持有了这些类的引用,你可以使用jmap -heap命令观察永生代大小,在它快要OOM的时候,用jmap -dump命令导出其内存,然后使用MAT来分析其引用情况,从而找出问题

你可能感兴趣的:(java)