一、 解决方案
1) 自定义类加载器。
首先需要明白一点,class相等的判断条件不仅仅是类名相同,还需要加载它的ClassLoader相同。JVM内部规定一个ClassLoader不可以重复定义类,也就是说想要重定义一个类,就必须使用一个全新的ClassLoader。
JVM内部class被卸载的条件及其苛刻,甚至没有明确的方法可以直接调用,只有当加载该类型的类加载器实例为unreachable状态时,也就是没有任何实例,class才有可能被卸载。(启动类加载器实例永远为reachable状态,由启动类加载器加载的类型可能永远不会被卸载)
<span style="white-space:pre"> </span>public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class<?> clazz = null; // 首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则继续。 if (name.startsWith("com.wafer") || name.contains("Service")) { if (resolve) { resolveClass(clazz); // 链接指定的 Java 类 } // 如果class类被修改过,则重新加载 MoeLoader hcl = new MoeLoader(url); clazz = customLoad(name, hcl); return (clazz); } // 如果类的包名为"java."开始,则有系统默认加载器加载 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); }
此范例的核心在于缓存自己已经加载的class,当再次需要加载时,如果发生变更,则可以new一个ClassLoader,这样新的字节码便可以即时生效。
JRbel是一种热更新的方案,它实现的方式是通过在启动参数中添加javaagent,即JVM底层提供的Instrumentation技术,来改变生成对象的方式。
2) JVMTI虚拟机工具接口
JPDA是 Java 平台调试体系结构的缩写。通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。 JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI)、Java 调试线协议(JDWP),以及 Java 调试接口(JDI)。参考资源(http://www.ibm.com/developerworks/cn/views/java/libraryview.jsp?search_by=深入+Java+调试体系)
<span style="white-space:pre"> </span>List<Connector> connectors = Bootstrap.virtualMachineManager().allConnectors(); SocketAttachingConnector sac = null; for (Connector connector : connectors) { if (connector instanceof SocketAttachingConnector) { sac = (SocketAttachingConnector) connector; } } if (sac != null) { Map<String, Connector.Argument> defaultArguments = sac.defaultArguments(); Connector.Argument hostArg = defaultArguments.get("hostname"); Connector.Argument portArg = defaultArguments.get("port"); hostArg.setValue("localhost"); portArg.setValue("8787"); VirtualMachine vm = sac.attach(defaultArguments); List<ReferenceType> rtList = vm.classesByName("com.wafer.demo.jdi.Foo"); ReferenceType rt = rtList.get(0); Map<ReferenceType, byte[]> newByteCodeMap = new HashMap<ReferenceType, byte[]>(1); //获取特定类的字节码,发送到虚拟机 byte[] newByteCode = genNewByteCodeUsingJavassist(); newByteCodeMap.put(rt, newByteCode); if (vm.canRedefineClasses()) { vm.redefineClasses(newByteCodeMap); } }
Eclipse的热更新实现的方式便是如此,以Debug启动时,Eclipse会在JVM参数中添加-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:63597,之后检测到代码变更时,将变更的字节码通过jdwp发送到应用。
BTrace可以发送字节码到应用,对拦截的类进行监控,其实现的方式同样用到JVMTI。使用VirtualMachine.attach(pid)连接到应用。