Java热替换

前面有提到过Tomcat的热部署,所谓热部署就是在应用运行时更新Java类文件以升级软件功能,升级过程不需要关停和重启应用。要进行热部署需要做class热替换。Class热替换实现了将修改的class再次加载到JVM中,以动态替换内存中原有的class字节码。

实现class的热替换就与Java类加载过程相关,关于Java类加载过程的文章或书籍早些年就已经很多了,这里从” 深入探讨 Java 类加载器(http://www.ibm.com/developerworks/cn/java/j-lo-classloader/)’’一文中摘录了部分内容说明Java类的加载过程。

 

类加载器基本概念

基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:

  •  引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。

  •  扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

  •  系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

 

除了引导类加载器之外,所有的类加载器都有一个父类加载器。通过getParent()方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器。

 

1、Bootstrap Loader(启动类加载器):加载System.getProperty("sun.boot.class.path")所指定的路径或jar。

2、Extended Loader(标准扩展类加载器ExtClassLoader):加载System.getProperty("java.ext.dirs")所指定的路径或jar。在使用Java运行程序时,也可以指定其搜索路径.

3、AppClass Loader(系统类加载器AppClassLoader):加载System.getProperty("java.class.path")所指定的路径或jar。在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置.        

 

类加载器的代理模式

类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。代理模式是为了保证 Java 核心库的类型安全。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。

 

加载类的过程

真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。

类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

 

类加载过程:

1、寻找jre目录,寻找jvm.dll,并初始化JVM;

2、产生一个Bootstrap Loader(启动类加载器);

3、Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为Bootstrap Loader。

4、Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。

5、最后由AppClass Loader加载应用程序类。

 

我们再来回顾一下类的加载过程:前面提到,某一特定类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成加载,则由该特定类加载器完成加载。这个机制就是所谓的双亲委派机制。

 

我们再来看一下Java 虚拟机是如何判定两个 Java 类是相同的。Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。具体的示例可以参见”深入探讨 Java 类加载器”一文。

 

了解了基本概念,我们知道要实现Java类的热替换,首先就需要继承java.lang.ClassLoader类实现自己的类加载器。

定义一个类加载器,如下:

public class MyClassLoader extends URLClassLoader {

    public static Map<String, Long> cacheLastModifyTimeMap = new HashMap<String, Long>();

    public static URL url = null;

 

    public MyClassLoader(URL url) {

        super(new URL[]{url});

        this.url = url;

    }

 

    public Class<?> loadClass(String name) throws ClassNotFoundException {

        return loadClass(name, false);

    }

 

    @Override

    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        Class clazz = null;

        //首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则继续。

        clazz = findLoadedClass(name); //查找名称为 name的已经被加载过的类

        if (clazz != null) {

            if (resolve) {

                resolveClass(clazz); //链接指定的 Java 类

            }

            //如果class类被修改过,则重新加载

            if (isModify(name)) {

                MyClassLoader hcl = new MyClassLoader(url);

                clazz = customLoad(name, hcl);

            }

            return (clazz);

        }

 

        //如果类的包名为"java."开始,则有系统默认加载器加载

        if (!name.startsWith("org.jevo.")) {

            try {

                //得到系统默认的加载cl

                ClassLoader system = ClassLoader.getSystemClassLoader();

                clazz = system.loadClass(name);  //加载名称为 name的类

                if (clazz != null) {

                    if (resolve)

                        resolveClass(clazz);

                    return (clazz);

                }

            } catch (ClassNotFoundException e) {

                // Ignore

            }

        }

 

        return customLoad(name, this);

    }

 

    public Class customLoad(String name, ClassLoader cl) throws ClassNotFoundException {

        return customLoad(name, false, cl);

    }

 

    public Class customLoad(String name, boolean resolve, ClassLoader cl)

            throws ClassNotFoundException {

        // //调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;否则抛出异常

        Class clazz = ((MyClassLoader) cl).findClass(name); //查找名称为 name的类

        if (resolve)

            ((MyClassLoader) cl).resolveClass(clazz);

        //缓存加载class文件的最后修改时间

        long lastModifyTime = getClassLastModifyTime(name);

        cacheLastModifyTimeMap.put(name, lastModifyTime);

        return clazz;

    }

 

    private boolean isModify(String name) {

        long lastmodify = getClassLastModifyTime(name);

        long previousModifyTime = cacheLastModifyTimeMap.get(name);

        if (lastmodify > previousModifyTime) {

            return true;

        }

        return false;

    }

 

    private long getClassLastModifyTime(String name) {

        String path = getClassCompletePath(name);

        File file = new File(path);

        if (!file.exists()) {

            throw new RuntimeException(new FileNotFoundException(name));

        }

        return file.lastModified();

    }

 

    private String getClassCompletePath(String name) {

        String simpleName = name.replaceAll("\\.", "/");

        return url.getPath() + simpleName + ".class";

    }

}

 

测试代码如下:

public class DyService {

    public String doBusiness() {

//        return "do something here..";

        return "do otherthings here..";

    }

}

 

public class Main {

    static ClassLoader cl;

    static Object server;

    static Class hotClazz = null;

 

    public static void loadNewVersionOfServer() throws Exception {

        synchronized (Main.class) {

            if (cl == null)

                cl = new MyClassLoader(new URL("file://D:/Project/test/out/"));

        }

        hotClazz = cl.loadClass("org.jevo.hotswap.sample.DyService");

        server = hotClazz.newInstance();

    }

 

    public static void test() throws Exception {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        loadNewVersionOfServer();

        while (true) {

            System.out.println("Enter DOBUS, RELOAD, or QUIT: ");

            String cmdRead = br.readLine();

            String cmd = cmdRead.toUpperCase();

            if (cmd.equals("QUIT")) {

                return;

            } else if (cmd.equals("DOBUS")) {

                Method m = hotClazz.getMethod("doBusiness");

                System.out.println(m.invoke(server, null)); //这里使用反射机制来执行事务。

            } else if (cmd.equals("RELOAD")) {

                loadNewVersionOfServer();

            }

        }

    }

 

    public static void main(String[] args) {

        try {

            test();

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

}

 

在上面的代码中,使用了反射机制来执行事务。这里不能将hotClazz.newInstance()得到的实例强制转换成DyService对象实例来使用doBussiness方法,否则会抛出java.lang.ClassCastException异常。抛出ClassCastException异常的原因就在于前面提到的“Java 虚拟机如何判定两个 Java 类是相同的”,这里例子中的hotClazz 是由MyClassLoader加载的,而server变量类型声明和类是由loadNewVersionOfServer方法所属的类的加载器加载的,因此属于不同的两个类型,转换时并不兼容,所以会抛出ClassCastException异常。

同样,在上面例子中,当class类被修改过后重新加载时,我们是通过重新new一个MyClassLoader来加载被修改类的,MyClassLoader hcl = new MyClassLoader(url);

在前面我们提到:对于一个类加载器实例来说,相同全名的类只加载一次。所以同一个ClassLoader实例只能加载Class一次,一个class被一个ClassLoader实例加载过的话,就不能再被这个ClassLoader实例再次加载,即不再重复调defineClass()方法关联字节码,重复装载将抛出重复类定义异常。同样,系统默认的ClassLoader加载器内部会缓存加载过的class,重新加载的话,就直接取缓存。因此这里只能重新创建一个ClassLoader,然后再去加载已经被加载过的class文件。

 

AppClass Loader(系统类加载器AppClassLoader):加载System.getProperty("java.class.path")所指定的路径或jar

 

前面我们通过模拟loadClass方法的过程来加载Java类,我们也提到” 真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。”同时我们也知道系统类加载器一般加载ClassPath所指定的路径下的class,所以我们可以使用类定义加载器来加载ClassPath指定路径下的class来实现热替换,代码如下:

        String classPath = System.getProperty("java.class.path");

        List classRepository = new ArrayList();

 

        if ((classPath != null) && !(classPath.equals(""))) {

            StringTokenizer tokenizer = new StringTokenizer(classPath,

                    File.pathSeparator);

            while (tokenizer.hasMoreTokens()) {

                classRepository.add(tokenizer.nextToken());

            }

        }

        Iterator dirs = classRepository.iterator();

        byte[] classBytes = null;

 

        while (dirs.hasNext()) {

            String dir = (String) dirs.next();

            //replace '.' in the class name with File.separatorChar & append .class to the name

            String classFileName = className.replace('.', File.separatorChar);

            classFileName += ".class";

            try {

                File file = new File(dir + File.separatorChar + classFileName);

                if (file.exists()) {

                    InputStream is = new FileInputStream(file);

 

                    classBytes = new byte[is.available()];

                    is.read(classBytes);

                    break;

                }

            }

            catch (IOException ex) {

                System.out.println("IOException raised while reading class file data");

                ex.printStackTrace();

                return null;

            }

        }

        return this.defineClass(className, classBytes, 0, classBytes.length);

 

我们知道,JVM中class和Meta信息存放在PermGen space区域。在上面的代码中,如果加载的class文件很多,那么可能导致PermGen space区域空间溢出,即java.lang.OutOfMemoryErrorPermGen space. 异常。

‘Java虚拟机类型卸载和类型更新解析(http://www.blogjava.net/zhuxing/archive/2008/07/24/217285.html)’中作了一些说明,摘录部分内容如下:

首先看一下,关于java虚拟机规范中时如何阐述类型卸载(unloading)的:
    A class or interface may be unloaded if and only if its class loader is unreachable. The bootstrap class loader is always reachable; as a result, system classes may never be unloaded.
Java虚拟机规范中关于类型卸载的内容就这么简单两句话,大致意思就是:只有当加载该类型的类加载器实例(非类加载器类型)为unreachable状态时,当前被加载的类型才被卸载.启动类加载器实例永远为reachable状态,由启动类加载器加载的类型可能永远不会被卸载.
我们再看一下Java语言规范提供的关于类型卸载的更详细的信息(部分摘录):
    //摘自JLS 12.7 Unloading of Classes and Interfaces
    1、An implementation of the Java programming language may unload classes.
    2、Class unloading is an optimization that helps reduce memory use. Obviously,the semantics of a program should not depend  on whether and how a system chooses to implement an optimization such as class unloading.
    3、Consequently,whether a class or interface has been unloaded or not should be transparent to a program
通过以上我们可以得出结论: 类型卸载(unloading)仅仅是作为一种减少内存使用的性能优化措施存在的,具体和虚拟机实现有关,对开发者来说是透明的.
纵观java语言规范及其相关的API规范,找不到显示类型卸载(unloading)的接口, 换句话说: 
    1、一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的
    2、一个被特定类加载器实例加载的类型运行时可以认为是无法被更新的

有 关unreachable状态的解释:
    1、A reachable object is any object that can be accessed in any potential continuing computation from any live thread.
    2、finalizer-reachable: A finalizer-reachable object can be reached from some finalizable object through some chain of references, but not from any live thread. An unreachable object cannot be reached by either means.

某种程度上讲,在一个稍微复杂的java应用中,我们很难准确判断出一个实例是否处于unreachable状态.

关于类型卸载大致概括为:

    1、有启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范).

    2、被系统类加载器和标准扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者标准扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小.

    3、被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到.

综合以上三点,我们可以默认前面的结论 一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的..

关于java.lang.OutOfMemoryErrorPermGen space异常,网上曾有过相关的讨论,可通过搜索找到更多的内容。

通过上面的说明,我们在自定义ClassLoader中实现加载过程时,就可以将先将原加载器设置为null,相关的加载信息也都设置为null后再调用System.gc();作一次GC.

 

关于类加载的说明,有几篇可作参考:

Java类加载原理解析 http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html

入探讨 Java 类加载器 http://www.ibm.com/developerworks/cn/java/j-lo-classloader/

深入研究Java类加载机制  http://lavasoft.blog.51cto.com/62575/184547/

http://nonopo.iteye.com/blog/208007

http://nonopo.iteye.com/blog/208012

 

posted @ 2013-04-22 22:49  Jevo  阅读(3850)  评论(0编辑  收藏  举报