JVM自定义类加载器加载指定classPath下的所有class及jar
一、JVM中的类加载器类型
从Java虚拟机的角度讲,只有两种不同的类加载器:启动类加载器和其他类加载器。
1.启动类加载器(Boostrap ClassLoader):这个是由c++实现的,主要负责JAVA_HOME/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入工作。
2.其他类加载器:由java实现,可以在方法区找到其Class对象。这里又细分为几个加载器
a).扩展类加载器(Extension ClassLoader):负责用于加载JAVA_HOME/lib/ext目录中的,或者被-Djava.ext.dirs系统变量指定所指定的路径中所有类库(jar),开发者可以直接使用扩展类加载器。java.ext.dirs系统变量所指定的路径的可以通过System.getProperty("java.ext.dirs")来查看。
b).应用程序类加载器(Application ClassLoader):负责java -classpath或-Djava.class.path所指的目录下的类与jar包装入工作。开发者可以直接使用这个类加载器。在没有指定自定义类加载器的情况下,这就是程序的默认加载器。
c).自定义类加载器(User ClassLoader):在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性。
这四个类加载器的层级关系,如下图所示。
二、为什么要自定义类加载器
- 区分同名的类:假定在tomcat 应用服务器,上面部署着许多独立的应用,同时他们拥有许多同名却不同版本的类。要区分不同版本的类当然是需要每个应用都拥有自己独立的类加载器了,否则无法区分使用的具体是哪一个。
- 类库共享:每个web应用在tomcat中都可以使用自己版本的jar。但存在如Servlet-api.jar,java原生的包和自定义添加的Java类库可以相互共享。
- 加强类:类加载器可以在 loadClass 时对 class 进行重写和覆盖,在此期间就可以对类进行功能性的增强。比如使用javassist对class进行功能添加和修改,或者添加面向切面编程时用到的动态代理,以及 debug 等原理。
- 热替换:在应用正在运行的时候升级软件,不需要重新启动应用。比如toccat服务器中JSP更新替换。
三、自定义类加载器
3.1 ClassLoader实现自定义类加载器相关方法说明
要实现自定义类加载器需要先继承ClassLoader,ClassLoader类是一个抽象类,负责加载classes的对象。自定义ClassLoader中至少需要了解其中的三个的方法: loadClass,findClass,defineClass。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
loadClass:JVM在加载类的时候,都是通过ClassLoader的loadClass()方法来加载class的,loadClass使用双亲委派模式。如果要改变双亲委派模式,可以修改loadClass来改变class的加载方式。双亲委派模式这里就不赘述了。
findClass:ClassLoader通过findClass()方法来加载类。自定义类加载器实现这个方法来加载需要的类,比如指定路径下的文件,字节流等。
definedClass:definedClass在findClass中使用,通过调用传进去一个Class文件的字节数组,就可以方法区生成一个Class对象,也就是findClass实现了类加载的功能了。
贴上一段ClassLoader中loadClass源码,见见真面目...
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) {
long t0 = System.nanoTime();
try {
if (parent != null) {
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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); // this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
源码说明...
/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/
翻译过来大概是:使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类: 调用findLoadedClass(String)方法检查这个类是否被加载过 使用父加载器调用loadClass(String)方法,如果父加载器为Null,类加载器装载虚拟机内置的加载器调用findClass(String)方法装载类, 如果,按照以上的步骤成功的找到对应的类,并且该方法接收的resolve参数的值为true,那么就调用resolveClass(Class)方法来处理类。 ClassLoader的子类最好覆盖findClass(String)而不是这个方法。 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)。
resolveClass:Class载入必须链接(link),链接指的是把单一的Class加入到有继承关系的类树中。这个方法给Classloader用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,这个类将被按照 Java™规范中的Execution描述进行链接。
3.2 自定义类加载器实现
按照3.1的说明,继承ClassLoader后重写了findClass方法加载指定路径上的class。先贴上自定义类加载器。
package com.chenerzhu.learning.classloader; import java.nio.file.Files;
import java.nio.file.Paths; /**
* @author chenerzhu
* @create 2018-10-04 10:47
**/
public class MyClassLoader extends ClassLoader {
private String path; public MyClassLoader(String path) {
this.path = path;
} @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClass(name);
if (result == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, result, 0, result.length);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
} private byte[] getClass(String name) {
try {
return Files.readAllBytes(Paths.get(path));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
以上就是自定义的类加载器了,实现的功能是加载指定路径的class。再看看如何使用。
package com.chenerzhu.learning.classloader; import org.junit.Test; /**
* Created by chenerzhu on 2018/10/4.
*/
public class MyClassLoaderTest {
@Test
public void testClassLoader() throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("src/test/resources/bean/Hello.class");
Class clazz = myClassLoader.loadClass("com.chenerzhu.learning.classloader.bean.Hello");
Object obj = clazz.newInstance();
System.out.println(obj);
System.out.println(obj.getClass().getClassLoader());
}
}
首先通过构造方法创建MyClassLoader对象myClassLoader,指定加载src/test/resources/bean/Hello.class路径的Hello.class(当然这里只是个例子,直接指定一个class的路径了)。然后通过myClassLoader方法loadClass加载Hello的Class对象,最后实例化对象。以下是输出结果,看得出来实例化成功了,并且类加载器使用的是MyClassLoader。
com.chenerzhu.learning.classloader.bean.Hello@2b2948e2
com.chenerzhu.learning.classloader.MyClassLoader@335eadca
四、类Class卸载
JVM中class和Meta信息存放在PermGen space区域(JDK1.8之后存放在MateSpace中)。如果加载的class文件很多,那么可能导致元数据空间溢出。引起java.lang.OutOfMemory异常。对于有些Class我们可能只需要使用一次,就不再需要了,也可能我们修改了class文件,我们需要重新加载 newclass,那么oldclass就不再需要了。所以需要在JVM中卸载(unload)类Class。
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
- 该类所有的实例都已经被GC。
- 该类的java.lang.Class对象没有在任何地方被引用。
- 加载该类的ClassLoader实例已经被GC。
很容易理解,就是要被卸载的类的ClassLoader实例已经被GC并且本身不存在任何相关的引用就可以被卸载了,也就是JVM清除了类在方法区内的二进制数据。
JVM自带的类加载器所加载的类,在虚拟机的生命周期中,会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象。因此这些Class对象始终是可触及的,不会被卸载。而用户自定义的类加载器加载的类是可以被卸载的。虽然满足以上三个条件Class可以被卸载,但是GC的时机我们是不可控的,那么同样的我们对于Class的卸载也是不可控的。
五、JVM自定义类加载器加载指定classPath下的所有class及jar
经过以上几个点的说明,现在可以实现JVM自定义类加载器加载指定classPath下的所有class及jar了。这里没有限制class和jar的位置,只要是classPath路径下的都会被加载进JVM,而一些web应用服务器加载是有限定的,比如tomcat加载的是每个应用classPath+“/classes”加载class,classPath+“/lib”加载jar。以下就是代码啦...
package com.chenerzhu.learning.classloader; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile; /**
* @author chenerzhu
* @create 2018-10-04 12:24
**/
public class ClassPathClassLoader extends ClassLoader{ private static Map<String, byte[]> classMap = new ConcurrentHashMap<>();
private String classPath; public ClassPathClassLoader() {
} public ClassPathClassLoader(String classPath) {
if (classPath.endsWith(File.separator)) {
this.classPath = classPath;
} else {
this.classPath = classPath + File.separator;
}
preReadClassFile();
preReadJarFile();
} public static boolean addClass(String className, byte[] byteCode) {
if (!classMap.containsKey(className)) {
classMap.put(className, byteCode);
return true;
}
return false;
} /**
* 这里仅仅卸载了myclassLoader的classMap中的class,虚拟机中的
* Class的卸载是不可控的
* 自定义类的卸载需要MyClassLoader不存在引用等条件
* @param className
* @return
*/
public static boolean unloadClass(String className) {
if (classMap.containsKey(className)) {
classMap.remove(className);
return true;
}
return false;
} /**
* 遵守双亲委托规则
*/
@Override
protected Class<?> findClass(String name) {
try {
byte[] result = getClass(name);
if (result == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, result, 0, result.length);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
} private byte[] getClass(String className) {
if (classMap.containsKey(className)) {
return classMap.get(className);
} else {
return null;
}
} private void preReadClassFile() {
File[] files = new File(classPath).listFiles();
if (files != null) {
for (File file : files) {
scanClassFile(file);
}
}
} private void scanClassFile(File file) {
if (file.exists()) {
if (file.isFile() && file.getName().endsWith(".class")) {
try {
byte[] byteCode = Files.readAllBytes(Paths.get(file.getAbsolutePath()));
String className = file.getAbsolutePath().replace(classPath, "")
.replace(File.separator, ".")
.replace(".class", "");
addClass(className, byteCode);
} catch (IOException e) {
e.printStackTrace();
}
} else if (file.isDirectory()) {
for (File f : file.listFiles()) {
scanClassFile(f);
}
}
}
} private void preReadJarFile() {
File[] files = new File(classPath).listFiles();
if (files != null) {
for (File file : files) {
scanJarFile(file);
}
}
} private void readJAR(JarFile jar) throws IOException {
Enumeration<JarEntry> en = jar.entries();
while (en.hasMoreElements()) {
JarEntry je = en.nextElement();
je.getName();
String name = je.getName();
if (name.endsWith(".class")) {
//String className = name.replace(File.separator, ".").replace(".class", "");
String className = name.replace("\\", ".")
.replace("/", ".")
.replace(".class", "");
InputStream input = null;
ByteArrayOutputStream baos = null;
try {
input = jar.getInputStream(je);
baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = input.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
addClass(className, baos.toByteArray());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (baos != null) {
baos.close();
}
if (input != null) {
input.close();
}
}
}
}
} private void scanJarFile(File file) {
if (file.exists()) {
if (file.isFile() && file.getName().endsWith(".jar")) {
try {
readJAR(new JarFile(file));
} catch (IOException e) {
e.printStackTrace();
}
} else if (file.isDirectory()) {
for (File f : file.listFiles()) {
scanJarFile(f);
}
}
}
} public void addJar(String jarPath) throws IOException {
File file = new File(jarPath);
if (file.exists()) {
JarFile jar = new JarFile(file);
readJAR(jar);
}
}
}
如何使用的代码就不贴了,和3.2节自定义类加载器的使用方式一样。只是构造方法的参数变成classPath了,篇末有代码。当创建MyClassLoader对象时,会自动添加指定classPath下面的所有class和jar里面的class到classMap中,classMap维护className和classCode字节码的关系,只是个缓冲作用,避免每次都从文件中读取。自定义类加载器每次loadClass都会首先在JVM中找是否已经加载className的类,如果不存在就会到classMap中取,如果取不到就是加载错误了。
六、最后
至此,JVM自定义类加载器加载指定classPath下的所有class及jar已经完成了。这篇博文花了两天才写完,在写的过程中有意识地去了解了许多代码的细节,收获也很多。本来最近仅仅是想实现Quartz控制台页面任务添加支持动态class,结果不知不觉跑到类加载器的坑了,在此也趁这个机会总结一遍。当然以上内容并不能保证正确,所以希望大家看到错误能够指出,帮助我更正已有的认知,共同进步。。。
本文的代码已经上传github:https://github.com/chenerzhu/learning/tree/master/classloader 欢迎下载和指正。
SpringBoot实现可视化动态操作Quartz定时任务:https://github.com/chenerzhu/quartz-console
参考文章:
JVM自定义类加载器加载指定classPath下的所有class及jar的更多相关文章
- JVM 自定义类加载器在复杂类情况下的运行分析
一.自定义类加载器在复杂类情况下的运行分析 1.使用之前创建的类加载器 public class MyTest16 extends ClassLoader{ private String classN ...
- (转)JVM——自定义类加载器
背景:为什么要自定义,如何自定义,实现过程 转载:http://blog.csdn.net/SEU_Calvin/article/details/52315125 0. 为什么需要自定义类加载器 网上 ...
- JVM——自定义类加载器
)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理.这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着 ...
- JVM学习二:JVM之类加载器之加载分析
前面一遍,我们对类的加载有了一个整体的认识,而这一节我们细节分析一下类加载器的第一步,即:加载. 一.概念 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区 ...
- java类加载器加载文件
例子:采用配置文件加反射的方式创建ArrayList和HashSet的实例对象. //第一种方式:类加载器加载文件 InputStream ips = ReflectTest2.class.getCl ...
- Java类加载器加载类顺序
java ClassLoader的学习 java是一门解释执行的语言,由开发人员编写好的java源文件先编译成字节码文件.class形式,然后由java虚拟机(JVM)解释执 行,.class字节码文 ...
- JVM 自定义类加载器
一.创建自定义类加载器 package com.example.jvm.classloader; import java.io.ByteArrayOutputStream; import java.i ...
- 使用类加载器加载配置文件/getClassLoader().getResourceAsStream()
加载配置文件的两种方式; 1. 2. 两者相比,第一种方式更为方便,因为文件目录发生改变就会找不到.
- 【深入理解JVM】类加载器与双亲委派模型
原文链接:http://blog.csdn.net/u011080472/article/details/51332866,http://www.cnblogs.com/lanxuezaipiao/p ...
随机推荐
- Java: war包的作用及使用方法,如何解压后缀名为war的文件
1.什么是war文件? 如果一个Web应用程序的目录和文件非常多,那么将这个Web应用程序部署到另一台机器上,就不是很方便了,我们可以将Web应用程序打包成Web归档(WAR)文件.这个过程和把Jav ...
- python-自定义异常,with用法
抛出异常 #coding=utf-8 def exceptionTest(num): if num<0: print "if num<0" raise Excepti ...
- Request 对象 response 对象 常见属性
请求和响应 Express 应用使用回调函数的参数: request 和 response 对象来处理请求和响应的数据. app.get('/', function (req, res) { // - ...
- redis删除单个key和多个key,ssdb会落地导致重启redis无法清除缓存
redis删除单个key和多个key,ssdb会落地导致重启redis无法清除缓存,需要针对单个key进行删除 删除单个:del key 删除多个:redis-cli -a pass(密码) keys ...
- java,url长链接生成短链接,短链接生成器,自定义字符串,对字符串md5混合KEY加密,根据短链接获得key值,不重复的随机数,不重复的随机字符串
java,url长链接生成短链接,短链接生成器,自定义字符串,对字符串md5混合KEY加密,根据短链接获得key值,不重复的随机数,不重复的随机字符串 package com.zdz.test; im ...
- AngularJs表单自动验证
angular-auto-validate 地址:https://github.com/jonsamwell/angular-auto-validate 引用: <script src=&quo ...
- jquery中的load方法加载页面无法缓存问题
在A页面中调用JQuery中的load方法,加载另一个B页面,B页面中的样式文件和JS文件无法从浏览器缓存中获取,每次都是实时获取.这是因为B页面的HTML经load方法处理后,会为每个样式和JS文件 ...
- 配置mysql主从数据库
来源地址:https://www.cnblogs.com/alvin_xp/p/4162249.html Mysql主从配置,实现读写分离 大型网站为了软解大量的并发访问,除了在网站实现分布式负载均衡 ...
- VIM 使用心得
序 到百度外卖任职以后,发现在我们部门无论 mac 还是 windows,程序员们清一色地都在使用 VIM 来编辑代码,期间穿插着各种插件.快捷键.眼花缭乱的命令.我在大学时只会极少的 VIM 命令, ...
- 20145118 《Java程序设计》第6周学习总结
20145118 <Java程序设计>第6周学习总结 教材学习内容总结 1.数据依靠串流在目的地与来源地之间传输,无论来去如何,只要取得InputStream或OutputStream的实 ...