java热更新机制
1. 类加载实现方式
Java虚拟机的类加载是指通过一个类的全限定名来获取描述此类的二进制字节流,将其转化为方法区的运行时数据结构,最终形成可以被虚拟机直接使用的java类型的过程。实现类加载功能的代码模块就称为“类加载模型”。
- 类加载限制
每一个类加载器,都拥有一个独立的类名空间,确保任意一个类都只能被同一个类加载器加载一次。
-
类加载模型
双亲委托加载模型
: 某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。双亲委托加载模型如图所示:
- 启动(Bootstrap)类加载器:负责将 %JAVA_HOME%/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
- 标准扩展(Extension)类加载器:是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将%JAVA_HOME%/lib/ext 或者由系统变量java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
- 应用程序(Application)类加载器:是由Sun的AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般称为系统(System)加载器。
双亲委派模式的实现:
{
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 加载类的行为委托parent类加载器执行
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// parent类加载器无法加载类的时候,自己才会去加载类
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
双亲委派模式的类加载机制的优点是java类它的类加载器一起具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,保证了java程序的安全、稳定运行。
类加载方式实现Java热更新
在双亲委派加载模型中,每一个类都需要确保被对应的类加载器所加载。如果多次加载,除了第一次会正真加载外,之后都是通过在其独有的类名空间中查找。那么怎么实现热更新,替换相同全限定名的类呢?
打破双亲委派模型
,让自定义类加载器加载需要热更新的类。具体实现如下:
{
public class MyClassLoader extends ClassLoader {
private String classPath;
private Set<String> hotswapClassSet;
public MyClassLoader(String classPath, Set<String> hotswapClassSet) {
super(Thread.currentThread().getContextClassLoader());
this.classPath = classPath;
this.hotswapClassSet = hotswapClassSet;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (hotswapClassSet != null && hotswapClassSet.contains(name)) {
byte[] byteCode = getByteCode(name);
return this.defineClass(name, byteCode, 0, byteCode.length);
}
return super.loadClass(name);
}
public byte[] getByteCode(String name) {
name = name.replace(".", "\\");
String filePath = classPath + "\\" + name + ".class";
try {
InputStream inputStream = new FileInputStream(filePath);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int n = -1;
byte[] buf = new byte[2048];
while ((n = inputStream.read(buf)) != -1) {
baos.write(buf, 0, n);
}
baos.flush();
inputStream.close();
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
}
在loadClass()方法中,只有需要动更的类由自定义的类加载器加载,而其他的类交给父加载器完成加载。
Main及其他类如下所示:
public class Main {
public static void main(String[] args) throws Exception {
Set<String> hotswapClassSet = new HashSet<>();
hotswapClassSet.add("com.wy.loader.TestService");
Random random = new Random();
while(true) {
Class<?> clazz = null;
if (random.nextBoolean()) {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
clazz = contextClassLoader.loadClass("com.wy.loader.TestService");
} else {
MyClassLoader loader = new MyClassLoader("E:\\java", hotswapClassSet);
clazz = loader.loadClass("com.wy.loader.TestService");
}
Constructor<?> constructor = clazz.getConstructor(new Class<?>[] {});
ITestService service = (ITestService)constructor.newInstance(new Object[] {});
ServiceManager.getInstance().setService(service);
ServiceManager.getInstance().getService().sayHello("limei");
Thread.sleep(2000);
}
}
}
ITestService.java
{
public interface ITestService {
public void sayHello(String name);
}
}
TestService.java
{
public class TestService implements ITestService {
public void sayHello(String name) {
System.out.println(name + " say Hello");
}
}
}
ServiceManager.java
{
public class ServiceManager {
private static ServiceManager instance = new ServiceManager();
private ServiceManager() {}
public static ServiceManager getInstance() {
return instance;
}
private ITestService service;
public ITestService getService() {
return service;
}
public void setService(ITestService service) {
this.service = service;
}
}
}
运行结果:
limei say Hello
limei say Hi
limei say Hello
limei say Hi
limei say Hi
limei say Hello
2. JVM代理实现方式
- Instrumentation介绍
java.lang.instrument包的具体实现,依赖于JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由Java虚拟机提供的, 为JVM相关的工具提供的本地编程接口集合。在 Instrumentation的实现当中,存在一个JVMTI的代理程序,通过调用JVMTI当中Java类相关的函数来完成Java类的动态操作。
- JDK代理的两种方式:
- premain方式: JavaSE5开始就提供的代理方式,但其必须在命令行指定代理jar,并且代理类必须在main方法前启动,它要求开发者在应用启动前就必须确认代理的处理逻辑和参数内容等等。该代理方法必须实现以下方法:
public static void premain(String agentArgs, Instrumentation inst); [1]
public static void premain(String agentArgs); [2]
- agentmain方式:JavaSE6开始提供,它可以在应用程序的VM启动后再动态添加代理的方式。该代理方法必须实现以下方法:
public static void agentmain (String agentArgs, Instrumentation inst); [1]
public static void agentmain (String agentArgs);[2]
- 代理的实现
目前,虚拟机内部实现类代码更新有两种api,ClassDefinition(1.5)和ClassFileTransformer(1.6),其具体实现方法如下:
{
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException, ClassNotFoundException {
ClassDefinition cd = new ClassDefinition(TestService.class, new byte[]{});
inst.redefineClasses(new ClassDefinition[] { cd });
}
}
{
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException, ClassNotFoundException {
MyClassTransformer transformer = new MyClassTransformer("E:\\java");
inst.addTransformer(transformer, true);
inst.retransformClasses(new Class<?>[] { TestService.class });
}
}
其中,MyClassTransformer实现ClassFileTransformer接口,完成类方法修改操作。实现如下:
public class MyClassTransformer implements ClassFileTransformer {
private String classPath = null;
public MyClassTransformer(String classPath) {
this.classPath = classPath;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (!className.endsWith("TestService")) {
return null;
}
return getByteCode(className);
}
public byte[] getByteCode(String name) {
name = name.replace(".", "\\");
String filePath = classPath + "\\" + name + ".class";
try {
InputStream inputStream = new FileInputStream(filePath);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int n = -1;
byte[] buf = new byte[2048];
while ((n = inputStream.read(buf)) != -1) {
baos.write(buf, 0, n);
}
baos.flush();
inputStream.close();
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
Agent类实现本地连接虚拟机,发起代理请求的逻辑:
public class Agent implements Runnable {
private String pid;
private String agent;
private String args;
public Agent(String pid, String agent, String args) {
this.pid = pid;
this.agent = agent;
this.args = args;
}
@Override
public void run() {
try {
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agent, args);
vm.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
if (args.length < 3) {
System.out.println("params length error!");
return;
}
StringBuilder sb = new StringBuilder();
for (int i = 2; i < args.length; i ++) {
if (sb.length() > 0) {
sb.append(" ");
}
sb.append(args[i]);
}
Agent agent = new Agent(args[0], args[1], sb.toString());
new Thread(agent).start();
}
}
最后,需要单独对Agent.java打包,生成Agent.jar,并在MANIFEST文件中添加以下内容:
Manifest-Version: 1.0
Premain-Class: Premain
命令行运行以下命令:
java -javaagent:TestInstrument1.jar -cp TestInstrument1.jar TestMainInJar