1. 使用自定义Classloader机制,实现xlass的加载
1.1 类加载流程
BootStrap 加载路径
System.getProperty("sun.boot.class.path")
输出结果如下:
xxx/jre/lib/resources.jar:
xxx/jre/lib/rt.jar:
xxx/jre/lib/sunrsasign.jar:
xxx/jre/lib/jsse.jar:
xxx/jre/lib/jce.jar:
xxx/jre/lib/charsets.jar:
xxx/jre/lib/jfr.jar:
xxx/jre/classes
ExtClassLoader 加载路径
System.getProperty("java.ext.dirs")
输出结果如下:
/Users/xxx/Library/Java/Extensions:
xxx/jre/lib/ext:
/Library/Java/Extensions:
/Network/Library/Java/Extensions:
/System/Library/Java/Extensions:
/usr/lib/java
AppClassLoader 加载路径
System.out.println("自定义类加载路径:");
System.out.println(AXClassLoader.class.getClassLoader());
System.out.println("对应Parent ClassLoader:");
System.out.println(AXClassLoader.getSystemClassLoader().getParent());
System.out.println("对应父类的父类 ClassLoader:");
System.out.println(AXClassLoader.getSystemClassLoader().getParent().getParent());
1.2 Java Resource 路径
private static void printResourcePath() {
System.out.println("AXClassLoader.class.getResource("")):
" + AXClassLoader.class.getResource(""));
System.out.println("AXClassLoader.class.getResource("/")):
" + AXClassLoader.class.getResource("/"));
System.out.println("AXClassLoader.class.getClassLoader().getResource("")):
" + AXClassLoader.class.getClassLoader().getResource(""));
System.out.println("ClassLoader.getSystemResource("))
" + ClassLoader.getSystemResource(""));
System.out.println("Thread.currentThread().getContextClassLoader().getResource("")
" + Thread.currentThread().getContextClassLoader().getResource(""));
}
输出结果:
AXClassLoader.class.getResource("")):
file:/xxx/java-p7-in-action/jvm-base/target/classes/com/holddie/jvm/classloader/v1/
AXClassLoader.class.getResource("/")):
file:/xxx/java-p7-in-action/jvm-base/target/classes/
AXClassLoader.class.getClassLoader().getResource("")):
file:/xxx/java-p7-in-action/jvm-base/target/classes/
ClassLoader.getSystemResource("))
file:/xxx/java-p7-in-action/jvm-base/target/classes/
Thread.currentThread().getContextClassLoader().getResource("")
file:/xxx/java-p7-in-action/jvm-base/target/classes/
1.3 实现 AXClassLoader 定义
public class AXClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = Files.readAllBytes(Paths.get(getFileName(name)));
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) (255 - bytes[i]);
}
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if (index == -1) {
return ClassLoader.getSystemResource("").getPath() + name + ".xlass";
} else {
return ClassLoader.getSystemResource("").getPath() + name.substring(index + 1) + ".xlass";
}
}
}
1.4 调用加载
AXClassLoader axClassLoader = new AXClassLoader();
try {
Class<?> hello = axClassLoader.loadClass("Hello");
if (hello != null) {
Object obj = hello.newInstance();
Method method = hello.getDeclaredMethod("hello");
method.invoke(obj);
}
} catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException e) {
e.printStackTrace();
}
2. 实现xlass打包的xar(类似class文件打包的jar)的加载
2.1 生成 xar 包
private static void createHelloXar() {
try {
XarSink xarSink = new XarSink();
final XarFileSource fileSource;
final File fileToCompress = getClasspathResourceAsFile(CLASS_FILE_NAME);
assert fileToCompress != null;
fileSource = new XarFileSource(fileToCompress);
xarSink.addSource(fileSource);
URL resource = Thread.currentThread().getContextClassLoader().getResource("");
assert resource != null;
xarSink.write(FileUtils.openOutputStream(new File(resource.getPath(), XAR_FILE_NAME)));
} catch (Exception e) {
e.printStackTrace();
}
}
2.2 解析 xar 包
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
XarSource xar = new FileXarSource(getClasspathResourceAsFile(name));
XarEntry entry = xar.getEntry(CLASS_FILE_NAME);
byte[] bytes = entry.getBytes();
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) (255 - bytes[i]);
}
return defineClass(CLASS_NAME, bytes, 0, bytes.length);
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
}
return super.findClass(name);
}
2.3 加载 xar 包
/*
*自定义加载 xar 文件
*/
AXClassLoader axClassLoader = new AXClassLoader();
try {
Class<?> hello = axClassLoader.loadClass(XAR_FILE_NAME);
if (hello != null) {
Object obj = hello.newInstance();
Method method = hello.getDeclaredMethod(METHOD_NAME);
method.invoke(obj);
}
} catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException e) {
e.printStackTrace();
}
3. 基于自定义Classloader实现类的动态加载和卸载
3.1 理论支撑
如何实现类的卸载,需要满足哪些条件:
- 该类的所有实例对象不可达
- 该类的Class对象不可达
- 该类的ClassLoader不可达
使用什么方式监控JVM的加载和卸载过程?
-verbose:class
:同时追踪类的加载和卸载-XX:+TraceClassLoading
:单独跟踪类的加载-XX:+TranceUnloading
:单独跟踪累的卸载
类加载器特征
- 每个ClassLoader都维护了一份自己的名称空间,同一个名称空间里不能出现两个同名的类;
- 为了实现Java安全沙箱模型的类加载安全机制,Java默认采用了“双亲委派的加载链”结构;
3.2 动态加载
这里我们清楚,类的动态加载,或者说热加载,就是我们程序从一个固定路径读取 class 文件,同样类名称,前后修改方法实现,使用替换手法,就可以立即使用替换类的方法。
public class AXClassLoader extends ClassLoader {
/**
* 基础包路径
*/
private String basePackagePath;
/**
* 需要该类加载器直接加载的类文件的基目录
*/
private String basedir;
/**
* 需要由该类加载器直接加载的类名
*/
private HashSet<Object> dynaclazns;
public AXClassLoader(String basedir, String[] clazns, String packagePath) {
super(null);
this.basePackagePath = packagePath;
this.basedir = basedir;
dynaclazns = new HashSet<>();
loadClassByMe(clazns);
}
@SneakyThrows
private void loadClassByMe(String[] clazns) {
for (int i = 0; i < clazns.length; i++) {
String fileNamePath = getFileName(clazns[i]);
byte[] bytes = Files.readAllBytes(Paths.get(fileNamePath));
defineClass(this.basePackagePath + "." + clazns[i], bytes, 0, bytes.length);
dynaclazns.add(clazns[i]);
}
}
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if (index == -1) {
return this.basedir + name + ".class";
} else {
return this.basedir + name.substring(index + 1) + ".class";
}
}
@Override
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class cls = findLoadedClass(name);
if (!this.dynaclazns.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) {
String packagePath = "com.holddie.jvm.classloader.v3";
new Timer("timer - 1")
.schedule(
new TimerTask() {
@Override
public void run() {
try {
// 每次都创建出一个新的类加载器
AXClassLoader cl =
new AXClassLoader(
ClassLoader.getSystemResource("").getPath()
+ "v3/",
new String[] {"Foo"},
packagePath);
Class cls = cl.loadClass(packagePath + ".Foo");
Object foo = cls.newInstance();
Method m = foo.getClass().getMethod("sayHello", new Class[] {});
m.invoke(foo, new Object[] {});
} catch (Exception ex) {
ex.printStackTrace();
}
}
},
2000,
3000);
}
}
对应这里,我们使用 resources 文件夹下的 v3_2 的 Foo.class 文件替换之前文件夹下的 Foo.class 文件。注意我们为什么要使用一个 Schedule 定时加载,主要目的模拟程序在频繁类加载,方便前后两次热替换之后,方法输出结果展示;
3.3 卸载
上述介绍了满足类卸载的三个条件,code show as flow:
public static void main(String[] args) {
String packagePath = "com.holddie.jvm.classloader.v3";
new Timer("timer - 1")
.schedule(
new TimerTask() {
@Override
public void run() {
try {
AXClassLoader cl =
new AXClassLoader(
ClassLoader.getSystemResource("").getPath()
+ "v3/",
new String[] {"Foo"},
packagePath);
Class cls = cl.loadClass(packagePath + ".Foo");
Object foo = cls.newInstance();
Method m = foo.getClass().getMethod("sayHello", new Class[] {});
m.invoke(foo, new Object[] {});
foo = null;
cls = null;
cl = null;
System.gc();
System.out.println("GC over");
} catch (Exception ex) {
ex.printStackTrace();
}
}
},
2000,
3000);
}
此处注意我们,将对应的引用都置为空,之后手动执行了 System.gc()
方法,对此我们要观察类的加载和卸载需要在程序启动的时候设置JVM参数 -verbose:class
即可看到对应 loading 和 unloading 日志;
4. 基于自定义 Classloader 实现模块化机制:需要设计模块化机制。
主要思想:
- 已经熟悉基于一个类的加载和卸载,在实际使用原理上大同小异;
- 一个基础模块,提供抽象定义;
- 两个实现模块,每个模块内部都有自己 module.properties 文件定义自己模块版本、模块入口等,以作为生成jar包运行使用;
- 一个运行模块,使用两种模块化加载方式;
4.1 multi-skd 基础模块
定义模块抽象方法
public interface IFun {
void sayHello(String name);
}
4.2 multi-fz 实现模块
依赖基础模块,fz 自定义实现
public class FzFun implements IFun {
@Override
public void sayHello(String name) {
System.out.println("Hi " + name + ", i am FzFun");
}
}
模块元数据声明
# 位置在 resources/META-INF/module.properties
module.appCode=fz
module.jarVersion=1.0
module.fun.location=com.holddie.jvm.FzFun
4.3 multi-tz 实现模块
依赖基础模块,tz 自定义实现类
public class TzFun implements IFun {
@Override
public void sayHello(String name) {
System.out.println("Hi " + name + ", i am TzFun");
}
}
模块元数据声明
# 位置在 resources/META-INF/module.properties
module.appCode=tz
module.jarVersion=1.0
module.fun.location=com.holddie.jvm.TzFun
4.4 multi-server 运行动态加载模块
使用两种方式,一种是JDK基于接口的动态代理,一种原生反射实现方法调用,详情在第五小节;
5. 使用 jar 作为模块,实现xar动态加载和卸载,综合应用前面的内容;
我们首先需要把实现模块的两个Jar包,打包OK,为下文加载提供方便,此处我们同时加载两个模块,并且指定了加载jar的名称,也可以使用自定义的目录区分;
5.1 ModuleClassLoader
public class ModuleClassLoader extends URLClassLoader {
private final List<String> excludePackages;
public ModuleClassLoader(URL[] urls, List<String> excludePackages) {
super(urls);
this.excludePackages = excludePackages;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (isExcludePackage(name)) {
return super.loadClass(name);
}
synchronized (getClassLoadingLock(name)) {
Class cls = findLoadedClass(name);
if (cls == null) {
try {
cls = findClass(name);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
if (cls != null) {
return cls;
}
}
return super.loadClass(name);
}
private boolean isExcludePackage(String name) {
if (StringUtils.isEmpty(name)) {
return false;
}
return this.excludePackages.stream().anyMatch(name::startsWith);
}
}
5.2 DynamicProxy
public class DynamicProxy implements InvocationHandler {
private Object target;
public Object bind(Object target) {
this.target = target;
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
Long end = System.currentTimeMillis();
String format = String.format("执行: class => %s, 方法=> %s, 参数 => %s, 耗时 => %sms, 执行结果 => %s, 当前路径 => %s",
target.getClass().getName(),
method.getName(),
JSON.toJSON(args),
end - start,
JSON.toJSON(result),
target.getClass().getResource("").getPath());
System.out.println(format);
return result;
}
}
5.3 ModuleJar
public class ModuleJar {
private String jarVersion;
private String appCode;
private String moduleJarUrl;
private ModuleClassLoader moduleClassLoader;
private String moduleFunLocation;
public String getJarVersion() {
return jarVersion;
}
public void setJarVersion(String jarVersion) {
this.jarVersion = jarVersion;
}
public String getAppCode() {
return appCode;
}
public void setAppCode(String appCode) {
this.appCode = appCode;
}
public String getModuleJarUrl() {
return moduleJarUrl;
}
public void setModuleJarUrl(String moduleJarUrl) {
this.moduleJarUrl = moduleJarUrl;
}
public ModuleClassLoader getModuleClassLoader() {
return moduleClassLoader;
}
public void setModuleClassLoader(ModuleClassLoader moduleClassLoader) {
this.moduleClassLoader = moduleClassLoader;
}
public String getModuleFunLocation() {
return moduleFunLocation;
}
public void setModuleFunLocation(String moduleFunLocation) {
this.moduleFunLocation = moduleFunLocation;
}
}
5.4 MultiModuleLoaderMain
public class MultiModuleLoaderMain {
private final static String APP_CODE = "module.appCode";
private final static String JAR_VERSION = "module.jarVersion";
private final static String MODULE_FUN_LOCATION = "module.fun.location";
public static void main(String[] args) {
HashMap<String, ModuleJar> moduleCache = new HashMap<String, ModuleJar>();
List<String> excludePackages = Arrays.asList("java", "com.holddie.jvm.sdk");
List<String> jarUrls = Arrays.asList(
"file:" + ClassLoader.getSystemResource("").getPath() + "multi-fz-1.0.0-jar-with-dependencies.jar",
"file:" + ClassLoader.getSystemResource("").getPath() + "multi-tz-1.0.0-jar-with-dependencies.jar");
jarUrls.forEach(url -> {
try {
URL moduleJarUrl = new URL(url);
ModuleClassLoader moduleClassLoader = new ModuleClassLoader(new URL[]{moduleJarUrl}, excludePackages);
Properties properties = getProperties(moduleClassLoader.getResourceAsStream("META-INF/module.properties"));
ModuleJar moduleJar = new ModuleJar();
moduleJar.setAppCode(properties.getProperty(APP_CODE));
moduleJar.setJarVersion(properties.getProperty(JAR_VERSION));
moduleJar.setModuleFunLocation(properties.getProperty(MODULE_FUN_LOCATION));
moduleJar.setModuleClassLoader(moduleClassLoader);
moduleJar.setModuleJarUrl(moduleJarUrl.getPath());
moduleCache.put(moduleJar.getAppCode(), moduleJar);
} catch (MalformedURLException e) {
e.printStackTrace();
}
});
ModuleJar fzModuleJar = moduleCache.get("fz");
ModuleJar tzModuleJar = moduleCache.get("tz");
try {
ModuleClassLoader fzModuleJarModuleClassLoader = fzModuleJar.getModuleClassLoader();
Object fzInstance = fzModuleJarModuleClassLoader.loadClass(fzModuleJar.getModuleFunLocation()).newInstance();
Method fzSayHello = fzInstance.getClass().getMethod("sayHello", new Class[]{String.class});
fzSayHello.invoke(fzInstance, new Object[]{"4456"});
fzSayHello = null;
fzInstance = null;
fzModuleJarModuleClassLoader = null;
fzModuleJar = null;
moduleCache.put("fz", null);
System.gc();
IFun tzFun = (IFun) new DynamicProxy().bind(tzModuleJar.getModuleClassLoader().loadClass(tzModuleJar.getModuleFunLocation()).newInstance());
tzFun.sayHello("123");
tzFun.toString();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
System.out.println("----------------------");
System.out.println();
}
public static Properties getProperties(InputStream inputStream) {
Properties properties = new Properties();
try {
properties.load(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
return properties;
}
}
5.6 运行日志
[Opened /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.CharSequence from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.String from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from
[Loaded java.util.stream.Sink from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.stream.MatchOps$BooleanTerminalSink from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.function.Supplier from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.stream.MatchOps$$Lambda$3/511754216 from java.util.stream.MatchOps]
[Loaded java.util.stream.MatchOps$1MatchSink from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.holddie.jvm.sdk.IFun from file:/xxx/java-p7-in-action/jvm-base/modularization/multi-sdk/target/classes/]
[Loaded com.holddie.jvm.FzFun from file:/xxx/java-p7-in-action/jvm-base/modularization/multi-server/target/classes/multi-fz-1.0.0-jar-with-dependencies.jar]
Hi 4456, i am FzFun
[Unloading class com.holddie.jvm.FzFun 0x00000007c0086828]
[Loaded java.lang.reflect.InvocationHandler from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.holddie.jvm.DynamicProxy from file:/xxx/java-p7-in-action/jvm-base/modularization/multi-server/target/classes/]
[Loaded com.holddie.jvm.TzFun from file:/xxx/java-p7-in-action/jvm-base/modularization/multi-server/target/classes/multi-tz-1.0.0-jar-with-dependencies.jar]
[Loaded java.lang.reflect.Proxy from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.reflect.WeakCache from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
.....
[Loaded java.lang.reflect.WeakCache$CacheValue from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.reflect.UndeclaredThrowableException from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
Hi 123, i am TzFun
[Loaded com.alibaba.fastjson.JSONStreamAware from file:/Users/zeyangg/.m2/repository/com/alibaba/fastjson/1.2.4/fastjson-1.2.4.jar]
[Loaded com.alibaba.fastjson.JSONAware from file:/xxx/.m2/repository/com/alibaba/fastjson/1.2.4/fastjson-1.2.4.jar]
[Loaded java.util.HashMap$KeyIterator from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.alibaba.fastjson.serializer.ListSerializer from file:/Users/zeyangg/.m2/repository/com/alibaba/fastjson/1.2.4/fastjson-1.2.4.jar]
[Loaded com.alibaba.fastjson.serializer.SerialContext from file:/Users/zeyangg/.m2/repository/com/alibaba/fastjson/1.2.4/fastjson-1.2.4.jar]
执行: class => com.holddie.jvm.TzFun, 方法=> sayHello, 参数 => ["123"], 耗时 => 0ms, 执行结果 => null, 当前路径 => /xxx/java-p7-in-action/jvm-base/modularization/multi-server/target/classes/com/holddie/jvm/
执行: class => com.holddie.jvm.TzFun, 方法=> toString, 参数 => null, 耗时 => 0ms, 执行结果 => com.holddie.jvm.TzFun@6ed3ef1, 当前路径 => /xxx/java-p7-in-action/jvm-base/modularization/multi-server/target/classes/com/holddie/jvm/
----------------------
[Loaded sun.misc.VMSupport from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.Hashtable$KeySet from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.nio.cs.ISO_8859_1$Encoder from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
注意,日志中的一些路径,自己做了一些脱敏使用 /xxx
代替,自己在看的时候,切勿当真以为是 /xxx
,主要想表达的是一种数据脱敏;
最终代码参考实现:https://github.com/HoldDie/java-p7-in-action/tree/master/jvm-base/