在开发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.
因此可以做如下设计:
其中Server负责service的deploy和undeploy。
其中deploy的过程可简单描述为:创建一个daemon线程,设置其context classloader为Service Jar包的ClassLoader,起动该Daemon线程。
undeploy的过程可简单描述为:停止服务(service daemon thread),设置线程context classloader为null, 等带线程彻底结束后手动执行GC来回收对象和ClassLoader.
Service接口如下:
服务管理器如下:
示例服务代码,测试修改编译后重新部署:
控制台输出如下:
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的风险。
如果使用动态语言,这些功能比较容易实现。但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.
因此可以做如下设计:
其中Server负责service的deploy和undeploy。
其中deploy的过程可简单描述为:创建一个daemon线程,设置其context classloader为Service Jar包的ClassLoader,起动该Daemon线程。
undeploy的过程可简单描述为:停止服务(service daemon thread),设置线程context classloader为null, 等带线程彻底结束后手动执行GC来回收对象和ClassLoader.
Service接口如下:
- public interface Service {
- public final static String ATTR_SERVICE_ID = "SERVICE-ID";
- public final static String ATTR_SERVICE_CLASS = "SERVICE-CLASS";
- public void install() throws ServiceException;
- public void start() throws ServiceException;
- public void stop() throws ServiceException;
- public void uninstall() throws ServiceException;
- public String getId();
- }
服务管理器如下:
- /**
- *
- * @author less
- * Responsible for deploy and undeploy Services.
- */
- public class ServiceManager {
- private final static HashMap<String, ServiceThread> installedServices = new HashMap<String, ServiceThread>();
- public final synchronized static String install(File jarService) throws Exception {
- MyJarLoader loader = new MyJarLoader(jarService, MyJarLoader.class.getClassLoader());
- Manifest manifest = loader.getManifest();
- Attributes attrs = manifest.getAttributes("Service");
- String svcId = attrs.getValue(Service.ATTR_SERVICE_ID);
- if (installedServices.containsKey(svcId)) {
- uninstall(svcId);
- }
- ServiceThread t = new ServiceThread();
- t.setContextClassLoader(loader);
- t.setDaemon(true);
- t.start();
- loader = null;
- return svcId;
- }
- public final static void uninstall(File jarService) throws Exception {
- MyJarLoader loader = new MyJarLoader(jarService, MyJarLoader.class.getClassLoader());
- Manifest manifest = loader.getManifest();
- Attributes attrs = manifest.getAttributes("Service");
- String svcId = attrs.getValue(Service.ATTR_SERVICE_ID);
- loader = null;
- uninstall(svcId);
- }
- public final static void uninstall(String svcId) throws Exception {
- ServiceThread t = installedServices.get(svcId);
- if (t.getStatus() == ServiceThread.Status.RUNNING) {
- t.stopService();
- }
- ServiceManager.getInstalledServices().remove(svcId);
- t.destroyService();
- t.setContextClassLoader(null);
- while (t.isAlive()) {
- try {
- Thread.sleep(200);
- } catch (InterruptedException e) {
- }
- }
- t = null;
- System.gc();
- try {
- Thread.sleep(200);
- } catch (InterruptedException e) {
- }
- System.gc();
- }
- static HashMap<String, ServiceThread> getInstalledServices() {
- return installedServices;
- }
- }
- /**
- *
- * @author less
- * A Customer Service carrier.
- */
- class ServiceThread extends Thread {
- public static enum Status {
- RUNNING, STOPPED
- }
- private Status status = Status.STOPPED;
- private Service service = null;
- public Status getStatus() {
- return this.status;
- }
- @Override
- public void run() {
- try {
- Manifest manifest = ((MyJarLoader) getContextClassLoader()).getManifest();
- Attributes attrs = manifest.getAttributes("Service");
- String svcId = attrs.getValue(Service.ATTR_SERVICE_ID);
- this.setName(svcId);
- String svcClass = attrs.getValue(Service.ATTR_SERVICE_CLASS);
- Class<Service> c = (Class<Service>) getContextClassLoader().loadClass(svcClass);
- service = c.newInstance();
- c = null;
- service.install();
- ServiceManager.getInstalledServices().put(svcId, this);
- startService();
- stopService();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- public void stopService() {
- if (this.status != Status.STOPPED) {
- try {
- service.stop();
- this.status = Status.STOPPED;
- } catch (Exception e) {
- e.printStackTrace();
- this.status = Status.RUNNING;
- }
- }
- }
- public void startService() {
- if (this.status == Status.STOPPED) {
- this.status = Status.RUNNING;
- try {
- service.start();
- } catch (Exception e) {
- e.printStackTrace();
- this.status = Status.STOPPED;
- }
- }
- }
- public void destroyService() {
- try {
- service.uninstall();
- service = null;
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- /**
- *
- * @author less
- *
- */
- class MyJarLoader extends JarLoader {
- public MyJarLoader(File file, ClassLoader parent) throws Exception {
- super(file, parent);
- System.out.println(this + " is created.");
- }
- @Override
- protected void finalize() throws Throwable {
- destroy();
- System.out.println(this + " is finalized.");
- super.finalize();
- }
- }
示例服务代码,测试修改编译后重新部署:
- /**
- * @author less
- *
- */
- public class ExampleService1 implements Service {
- private boolean bRun = true;
- @Override
- public void install() throws ServiceException {
- System.out.println("== " + this + " is installed.");
- }
- @Override
- public void start() throws ServiceException {
- System.out.println("== " + this + " is started.");
- //int i = 10000;
- int i=0;
- while (bRun) {
- //System.out.println(this + " " + i--);
- System.out.println(this + " " + i++);
- try {
- Thread.sleep(5000);
- } catch (InterruptedException ie) {
- }
- }
- System.out.println("== " + this + " is stopped.");
- }
- @Override
- public void stop() throws ServiceException {
- System.out.println("== Trying to stop " + this);
- bRun = false;
- Thread.currentThread().interrupt();
- }
- @Override
- public void uninstall() throws ServiceException {
- System.out.println("== " + this + " is uninstalled.");
- }
- @Override
- public String getId() {
- // TODO Auto-generated method stub
- return null;
- }
- @Override
- protected void finalize() throws Throwable {
- System.out.println(this + " - is finalized.");
- super.finalize();
- }
- }
控制台输出如下:
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来分析其引用情况,从而找出问题