JVM中类的热替换

JVM类的热替换的主要作用是在系统升级的时候,系统不停止也能进行类或者对象的升级替换

其实就是在程序在运行的时候,对内存方法区的类定义进行替换。(在类加载的过程中,类的结构信息会存在JVM的方法区中,具体对象在堆中)

类的热替换其实就是重新进行类的加载过程,在加载的过程中,通过Hook方法,修改类的字节码,并基于修改后的字节码,重新初始化类。

关于JVM中的类加载机制见我的另一篇文章《深入理解java虚拟机之类加载机制》

先说一下java中的类实例化的流程分为类的加载和类的实例化。
类的加载分为显式加载和隐式加载
一般我们在new创建类的实例的时候就是隐式的包含了类的加载过程。显式加载常用的就是:Class.forName或者ClassLoader的loadClass方法。
其实他们都是通过调用ClassLoader类的loadClass方法进行类的实际的加载工作的。

ClassLoader类:

抽象类;
该类的实例能将读入的java字节码类装载到JVM中;
可以定制,满足不同的字节码流获取方式;
负责类装载过程中的加载阶段。

根据JVM中的类加载机制,每个类加载器有自己的名字空间,**对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。**不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。

要想实现Java类的热替换,首先必须要让系统中同名类的不同版本实例的共存,要想实现同一个类的不同版本的共存,必须要通过不同的类加载器来加载该类的不同版本。另外,为了能够绕过Java类的既定加载过程,需要实现自己的类加载器。

自定义的类加载器CustomLoader

ClassLoader中和热替换有关的方法

1.findLoadedClass()

该方法会在对应加载器的名字空间中寻找指定的类是否已存在,如果存在就返回给类的引用,否则就返回null。**每个类加载器都维护有自己的一份已加载类名字空间,其中不能出现两个同名的类。**凡是通过该类加载器加载的类,无论是直接的还是间接的,都保存在自己的名字空间中,这里的直接是指,存在于该类加载器的加载路径上并由该加载器完成加载,间接是指,由该类加载器把类的加载工作委托给其他类加载器完成类的实际加载。

2.getSystemClassLoader()

该方法返回系统使用的 ClassLoader。可以在自定义的类加载器中通过该方法把一部分工作转交给系统类加载器去处理。

3.defineClass()

该方法接收以字节数组表示的类字节码,并把它转换成Class实例。该方法转换一个类的同时,会先要求装载该类的父类以及实现的接口类。

4. loadClass()

加载类的入口方法,调用该方法完成类的显式加载。通过对该方法的重写,可以完全控制和管理类的加载过程。执行loadClass方法,只是单纯的把类加载到内存,并不是对类的主动使用,不会引起类的初始化。

5.resolveClass()

链接一个指定的类。这是一个在某些情况下确保类可用的必要方法。

自定义加载器的代码实现

了解了上面的这些方法,接下来实现一个自定义的类加载器来实现热替换,在给出示例代码前,再重申两点内容:

(1)、要想实现同一个类的不同版本的共存,那么这些不同版本必须由不同的类加载器进行加载,因此就不能把这些类的加载工作委托给系统加载器来完成,因为它们只有一份。

(2)、为了做到这一点,就不能采用系统默认的类加载器委托规则,也就是说我们定制的类加载器的父加载器必须设置为null

定制的类加载器:

public class CustomClassLoader extends ClassLoader {

private String basedir; // 需要该类加载器直接加载的类文件的基目录
private HashSet className; // 需要由该类加载器直接加载的类名

public CustomClassLoader(String basedir, String[] clazns) throws Exception {
	super(null); // 指定父类加载器为 null
	this.basedir = basedir;
	className = new HashSet();
	loadClassByMe(clazns);
}

private void loadClassByMe(String[] clazns) throws Exception {
	for (int i = 0; i < clazns.length; i++) {
		loadDirectly(clazns[i]);
		className.add(clazns[i]);
	}
}

private Class loadDirectly(String name) throws Exception, Exception {
	Class cls = null;
	StringBuffer sb = new StringBuffer(basedir);
	String classname = name.replace('.', File.separatorChar) + ".class";
	sb.append(File.separator + classname);
	System.out.println(sb.toString());
	File classF = new File(sb.toString());
	cls = instantiateClass(name, new FileInputStream(classF), classF.length());
	return cls;
}

private Class instantiateClass(String name, InputStream fin, long len) throws Exception {
	byte[] raw = new byte[(int) len];
	fin.read(raw);
	fin.close();
	return defineClass(name, raw, 0, raw.length);
}

protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
	Class cls = null;
	cls = findLoadedClass(name);
	if (!this.className.contains(name) && cls == null)
		cls = getSystemClassLoader().loadClass(name);
	if (cls == null)
		throw new ClassNotFoundException(name);
	if (resolve)
		resolveClass(cls);
	return cls;
}

public static void main(String[] args) throws FileNotFoundException, IOException {
	new Timer().schedule(new TimerTask() {

		@Override
		public void run() {
			try {
				// 每次都创建出一个新的类加载器
				CustomClassLoader customClassLoader = new CustomClassLoader(
						CustomClassLoader.class.getResource("").getFile(), new String[] { "Foo" });
				Class<?> cls = customClassLoader.loadClass("Foo");
				Object foo = cls.newInstance();

				Method m = foo.getClass().getMethod("sayHi", new Class[] {});
				m.invoke(foo, new Object[] {});
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
	}, 0, 1000L);
}}

在main方法中:编写一个定时器任务,每隔1秒钟执行一次。其中,程序会创建新的类加载器实例加载Foo类,生成实例,并调用sayHi()方法。此处第一次加载的事Foo.java文件,该文件内容如下:

	public class Foo implements FooInterface {

		@Override
		public void sayHi() {
			// TODO Auto-generated method stub
			System.out.println("hi\tv1");
		}
	}

对应接口文件为:

public interface FooInterface {
		public void sayHi();
	}

在这里插入图片描述
这里需要分析的是:

如果把main函数中的代码改为:Foo foo = (Foo)cls.newInstance(); 会发现会抛出 ClassCastException 异常。这是因为
在上面的例子中 cls 是由CustomClassLoader 加载的,而 foo 变量类型声名类却是由 run 方法所属的类的加载器(默认为 AppClassLoader)加载的,因此是完全不同的类型。

如果把main函数中的代码改为:FooInterface foo = (FooInterface )cls.newInstance(); 会发现还会抛出 ClassCastException 异常。这是因为外部声名和转型部分的 FooInterface 是由 run 方法所属的类加载器加载的,而 Foo 类定义中 implements FooInterface 中的 FooInterface 是由 CustomClassLoader 加载的,因此属于不同的类型转型还是会抛出异常的,但是由于我们在实例化 CustomClassLoader 时是这样的:

String path = CustomClassLoader.class.getResource("").getFile();
CustomClassLoader ccl  = new CustomClassLoader (path, new String[]{"Foo"});

其中 仅仅指定 Foo 类由 CustomClassLoader 加载(因为在Foo用javac编译的时候,需要用到它实现的接口,但在拷贝Foo.class文件的时候,只拷贝了Foo.class一个文件,并没有拷贝它的接口文件),而其实现的 FooInterface 接口文件会委托给系统类加载器加载,因此转型成功,采用接口调用的代码如下:

Object foo = ccl.newInstance();
FooInterface foo = (FooInterface )ccl.newInstance(); 
foo.sayHello(); 

其实上面的程序可以写的再完美一点,在进行替换后,可以把老的Class给卸载掉,但需要注意的是: 只有自定义类加载器加载的类才可以卸载。卸载的办法很简单,把类对象,Class对象,classloader对象的引用设置为null,JVM就会把它们当作是垃圾(此处可以了解JVM的垃圾回收机制),会在适当的时候,卸载掉内存方法区中的二进制数据。

类加载器的命名空间:

(1)、命名空间由加载器和所有的父加载器所加载的类构成;
(2)、在同一个命名空间中,不可能出现类名相同的两个类;
(3)、在不同的命名空间中,可能出现类名相同的两个类(类名指类全称);
(4)、由子加载器加载的类能看见父加载器加载的类,反之不可以;(比如java.lang.String类,我们自己写的类肯定能看见,但是父加载器肯定看不见我们自己定义的类)
(5)、如果两个加载器之间没有直接或者间接的父子关系,那么两个加载器加载的类是相互不可见的;

https://blog.csdn.net/u011130752/article/details/51768020
https://blog.csdn.net/qq_41701956/article/details/84929729

猜你喜欢

转载自blog.csdn.net/mulinsen77/article/details/89034972