文章目录
概述
-
Class Loader定义
:当运行Java程序时,首先运行JVM,然后再把Java class加载到JVM里面运行,负责加载Java class的这部分就叫做Class Loader。概括来说就是将编译后的class装载、加载到机器内存中,为了以后的程序的执行提供前提条件。 -
Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节码(.class 文件)。类加载器负责读取 Java 字节码,并转换成 java.lang.Class类的一个实例,每个这样的实例用来表示一个Java 类。
-
类加载器使得 Java 类可以被动态加载到JVM中并执行,在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性.
-
类加载器的特性
- 每个ClassLoader都维护了一份自己的名称空间, 同一个名称空间里不能出现两个同名的类。确切的说,只有由同一个ClassLoader实例装载的class才具有Package访问权限,只有由同一个ClassLoader实例装载的class才共享一个static的副本。
- 为了实现java安全沙箱模型顶层的类加载器安全机制, java默认采用了"双亲委派的加载链" 结构.
- 同一个命名空间内的类是相互可见的。子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。比如系统类加载器加载的类能看见根类加载器加载的类。父加载器加载的类不能看见子加载器加载的类。如果两个加载器之间没有直接或间接的父子关系,各自加载的类互不可见。
-
为什么能访问字节码?当编译一个JAVA文件时,编译器都会在其字节码文件中嵌入一个被public, static, final修饰、类型为java.lang.Class、名称为class的静态变量。因为使用了public修饰,所以我们可以采用如下的形式对其访问:
java.lang.Class c1 = String.class; 编译器编译时即加载 Class.forName("java.lang.String"); 通过配置文件由JVM运行时加载 java.lang.Class c2 = int[].class; java.lang.Class c3 = int.class;
-
数组类本身不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。如果数组中的元素类型是引用类型,数组类的类加载器与数组中元素的类加载器是一样的。如果数组中的元素类型是原生类型,则数组类是没有类加载器的
public class ArrayTypeClassLoader { public static void main(String[] args) { String[] str = new String[]{"1", "2"}; //null 表示根类加载器 System.out.println(str[0].getClass().getClassLoader()); //null System.out.println(String[].class.getClassLoader()); ArrayTypeClassLoader[] arr = new ArrayTypeClassLoader[]{ new ArrayTypeClassLoader() }; //sun.misc.Launcher$AppClassLoader@7f31245a System.out.println(arr.getClass().getClassLoader()); //sun.misc.Launcher$AppClassLoader@7f31245a System.out.println(arr[0].getClass().getClassLoader()); int[] ints = {1}; // null 此处表示的是没有类加载器的 System.out.println(ints.getClass().getClassLoader()); } }
类加载
-
隔离机制
-
在JAVA中,一个类用其完全匹配类名作为标识(包名+类名)
-
但在JVM中一个类用其完全匹配类名和一个加载类ClassLoader的实例ID作为唯一标识(包名+类名+ClassLoaderId)。即在同一个虚拟机中,可以有两个类,它们的类名和包名都是相同的。例如,浏览器为每个web页都使用了一个独立的Applet类加载器类(
sun.applet.AppletClassLoader
)的实例,这样,虚拟机就能区分来自不同web页的各个类,而不用管它们的类名是什么 -
在Java虚拟机中,一个命名空间是一个由Java虚拟机维护的一组不重复的被加载类,不同类加载器加载的类被不同的命名空间所分割。一个类只能访问同一个命名空间中的其他类。不同命名中的类甚至都不能相互察觉,除非你提供一种途径容许他们相互影响
/** * jvm中存在两个ClassLoaderTest类(但是来自同一个Class文件) * 1. 一个是由系统应用程序类加载器加载 * 2. 一个是由自定义的类加载器加载 */ public class ClassLoaderTest { public static void main(String[] args) throws Exception { Class<ClassLoaderTest> clazz = ClassLoaderTest.class; ClassLoader classLoader = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream resourceAsStream = getClass().getResourceAsStream(fileName); if (resourceAsStream == null) { return super.loadClass(name); } try { byte[] bytes = new byte[resourceAsStream.available()]; resourceAsStream.read(bytes); return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { throw new ClassNotFoundException(name); } } }; Object o = classLoader.loadClass("classloader.ClassLoaderTest").newInstance(); System.out.println(o.getClass()); //false 表示不是同一个Class对象 System.out.println(clazz == o.getClass()); //sun.misc.Launcher$AppClassLoader@18b4aac2 System.out.println(clazz.getClassLoader()); //classloader.ClassLoaderTest$1@5451c3a8 System.out.println(o.getClass().getClassLoader()); //false System.out.println(o instanceof classloader.ClassLoaderTest); } }
-
-
什么时候JVM会使用ClassLoader加载一个类呢?
- 当你使用java去执行一个类,JVM使用
ApplicationClassLoader
加载这个类;然后如果类A引用了类B,不管是直接引用还是用Class.forName()
引用,JVM就会找到加载类A的ClassLoader,并用这个ClassLoader来加载类B。JVM按照运行时的有效执行语句,来决定是否需要装载新类,从而装载尽可能少的类,这一点和编译类是不相同的。
- 当你使用java去执行一个类,JVM使用
-
为什么创建自己的类加载器?JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,编写自己的加载器可以实现
- 在执行非置信代码之前,自动验证数字签名
- 动态地创建符合用户特定需要的定制化构建类
- 特定的场所取得java class,例如数据库、远程服务器等。当使用Applet的时候,就用到了特定的ClassLoader,因为这时需要从网络上加载java class,并且要检查相关的安全信息。
-
JDK中的类加载器
- 本地编译好的class中直接加载
- 网络加载:java.net.URLClassLoader可以加载url指定的类
- 从jar、zip等等压缩文件加载类,自动解析jar文件找到class文件去加载util类
- 从java源代码文件动态编译成为class文件
-
怎么获取类加载
public class ObtainClassloader { public static void main(String[] args) { //获取当前类的类加载 System.out.println(ObtainClassloader.class.getClassLoader()); ObtainClassloader obtainClassloader = new ObtainClassloader(); System.out.println(obtainClassloader.getClass().getClassLoader()); //获取上下文类加载器 System.out.println(Thread.currentThread().getContextClassLoader()); //获取系统类加载器 System.out.println(ClassLoader.getSystemClassLoader()); /** * * Reflection.getCallerClass()返回调用此方法的方法调用者的类,忽略关联的框架及其实现。 * JVM将跟踪@CallerSensitive这个注解,该方法只能在使用该注释标记方法时才报告方法的调用方。 * 只有特权代码才能使用这个注释。如果代码通过引导类装入器或扩展类装入器装入,则具有特权。否则会抛出: * java.lang.InternalError: CallerSensitive annotation expected at frame 1 * * Oracle不建议开发人员调用sun.*下的方法 */ System.out.println(sun.reflect.Reflection.getCallerClass().getClassLoader()); }
类加载器组织结构
- JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
Bootstrap ClassLoader
-
Bootstrap ClassLoader(引导类加载器)
主要负责JDK_HOME/lib
目录下的核心 api 或-Xbootclasspath
选项指定的jar包装入工作.引导类加载器负责加载Java的核心类以及java语言编写的类加载器(ClassLoader的实现类),如/jre/lib/rt.jar与/jre/lib/sse.jar等。这个加载器非常特殊,它实际上是JVM整体的一部分,通常是由C语言实现的,并不是 java.lang.ClassLoader 的实例。例如该方法:System.class.getClassLoader()将返回 null系统在开始就将这些文件加载进内存,避免以后的多次 IO 操作,从而提高程序执行效率。public class BootStrapClassLoader { public static void main(String[] args) { // 获得引导类加载的内容 String paths = System.getProperty("sun.boot.class.path"); //处理 String[] pathArr = paths.split(";"); for (String p : pathArr) { System.out.println(p); } // 结论:java在jre中的rt.jar //null 说明加载java的类加载器是通过引导类加载器加载的 System.out.println(URLClassLoader.class.getClassLoader()); //null System.out.println(ClassLoader.class.getClassLoader()); //null System.out.println(Launcher.class.getClassLoader()); } /** D:\install\develop\Java\jdk1.8.0_65\lib\resources.jar D:\install\develop\Java\jdk1.8.0_65\lib\rt.jar D:\install\develop\Java\jdk1.8.0_65\lib\sunrsasign.jar D:\install\develop\Java\jdk1.8.0_65\lib\jsse.jar D:\install\develop\Java\jdk1.8.0_65\lib\jce.jar D:\install\develop\Java\jdk1.8.0_65\lib\charsets.jar D:\install\develop\Java\jdk1.8.0_65\lib\jfr.jar D:\install\develop\Java\jdk1.8.0_65\classes **/ }
Extension ClassLoader
-
Extension ClassLoader(扩展类加载器)
主要负责JDK_HOME/lib/ext
目录下的jar包或-Djava.ext.dirs
指定目录下的jar包装入工作(扩展类加载器只能通过jar的形式来加载,不能直接加载class文件)。扩展类加载器专门用来加载系统属性java.ext.dirs
或者JDK_HOME/lib/ext
目录下所有的类文件。在这个加载器实例上调用方法getParent()总是返回空值null,因为引导类加载器Bootstrap ClassLoader(引导类加载器)
不是一个真正的ClassLoader实例。这是一种无需在类路径中添加条目即可扩展 JDK 的便捷方法。但扩展目录中的所有内容都必须是自包含的,且只能引用扩展目录中的类或JDK 类。public class ExtClassLoader { public static void main(String[] args) { // 扩展类加载器 ,加载的内容 String paths = System.getProperty("java.ext.dirs"); String[] pathArr = paths.split(";"); for (String p : pathArr) { System.out.println(p); } /** * D:\install\develop\Java\jdk1.8.0_65\lib\ext * C:\windows\Sun\Java\lib\ext */ } }
System ClassLoader
-
System ClassLoader(系统类加载器)
主要负责java -classpath/-Djava.class.path
所指的目录下的类与jar包装入工作.系统类加载器,它负责在JVM被启动时,加载来自在命令java中的-classpath属性或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径。我们在程序中需要使用自己定义的类的时候就要使用依需求加载方法( load-on-demand ),就是在 Java 程序需要用到的时候再加载,以减少内存的消耗,因为 Java 语言的设计初衷就是面向嵌入式领域的。 总能通过静态方法ClassLoader.getSystemClassLoader()
找到该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。public class AppClassLoader { public static void main(String[] args) { // 应用类加载器 加载的内容 String paths = System.getProperty("java.class.path"); String[] pathArr = paths.split(";"); for (String p : pathArr) { System.out.println(p); } //null 未自己指定系统类加载,如果想要用自己的类加载器替换掉系统类加载器, // 可以在程序启动时通过此参数指定 System.out.println(System.getProperty("java.system.class.loader")); //D:\new\javase-parent\javase-classloader\target\classes } }
类加载机制
-
类加载器加载类时用的是全盘负责委托机制。即是当一个
Classloader
加载一个Class的时候,这个Class所依赖的和引用的所有Class也由这个Classloader
负责载入,除非是显式的使用另外一个Classloader
载入;委托机制则是先让parent(父)类加载器 (而不是super,它与Parent Classloader类不是继承关系)寻找,只有在parent找不到的时候才从自己的类路径中去寻找。此外类加载还采用了cache机制,也就是如果cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么我们修改了Class但是必须重新启动JVM才能生效的原因。 -
扩展类加载器实际上是
sun.misc.Launcher$ExtClassLoader
类的一个实例;系统类加载器实际上是sun.misc.Launcher$AppClassLoader
类的一个实例,并且都是java.net.URLClassLoader
的子类 -
类加载器的加载类的顺序是:
- 先是
Bootstrap Classloader
- 然后是
Extension Classloader
- 最后才是
System classloader
。加载的Class越是重要的越在靠前面
- 先是
-
采用父类委托机制的原因
- 比如对于
Demo demo = new Demo();
。如果不采用父类委托机制,内存中会出现两份字节码,可能demo是由Demo.class
这份字节码创建的,而new Demo()
是由另一个Demo.class
的字节码创建的,此时会出现类型转换异常,因为两份字节码不是同一个类加载器加载的,所以会出现类型转换异常 - 安全性:如果
System Classloader
亲自加载了一个具有破坏性的java.lang.System
类的后果吧。这种委托机制保证了用户即使具有一个这样的类,也把它加入到了类路径中,但是它永远不会被载入,因为这个类总是由Bootstrap Classloader
来加载的。
- 比如对于
-
类加载的过程(每个classloader加载类Class的过程)
- (1)检测此Class是否载入过(即在缓存cache中是否有此Class),如果有到8,如果没有到2(不重复从磁盘中加载)
- (2)如果parent classloader不存在(没有parent,那parent一定是bootstrap classloader了),到4
- (3)请求parent classloader载入,如果成功到8,不成功到5
- (4)请求jvm从bootstrap classloader中载入,如果成功到8
- (5)寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到7.
- (6)从文件中载入Class,到8.
- (7)抛出ClassNotFoundException.
- (8)返回Class.
- 其中(5)和(6)步我们可以通过覆盖ClassLoader的findClass方法来实现自己的载入策略。甚至覆盖loadClass方法来实现自己的载入过程
// 检查类是否已被装载过 Class c = findLoadedClass(name); if (c == null ) { // 指定类未被装载过 try { if (parent != null ) { // 如果父类加载器不为空, 则委派给父类加载 c = parent.loadClass(name, false ); } else { // 如果父类加载器为空, 则委派给启动类加载加载 c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 启动类加载器或父类加载器抛出异常后, 当前类加载器将其 // 捕获, 并通过findClass方法, 由自身加载 c = findClass(name); } }
自定义类加载器
-
一般来说自己开发的类加载器只需要覆写findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了父类委托的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写loadClass()方法,而是覆写findClass()方法。
-
ClassLoader常用的方法
方法名称 描述 getParent() 返回该类加载器的父类加载器 loadClass(String name) 加载名称为name的类,返回结果为java.lang.Class类的实例 findClass(String name) 查找名称为name的类,返回结果为java.lang.Class类的实例 findLoadedClass(String name) 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例 defineClass(String name,byte[] b,int off,int len) 把字节数组b中的内容转换为一个java类,返回结果为java.lang.Class类的实例 -
loadClass默认实现按以下顺序搜索类
- 调用findLoadedClass(String)以检查类是否已经被加载。
- 在父类加载器上调用loadClass方法。 如果父级是null ,则使用虚拟机内置的类加载器。
- 调用findClass(String)方法来查找该类,若还加载不了就返回ClassNotFoundException,不交给发起请求的加载器的子加载器
-
用户自定义类加载器(java.lang.ClassLoader的子类)
自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器(system classloader),如果将父类加载器强制设置为null,那么会自动将引导类加载器设置为当前用户自定义类加载器的父类加载器。- 获取类的class文件的字节数组,如loadClassData方法
- 将字节数组转换为Class类的实例,重写findClass中调用的defineClass方法
-
改变父类委托机制的办法,覆写loadClass,先从当前类加载器加载,如果加载不到,再从父类加载器加载
public class NotParentClassLoader extends ClassLoader { private String classLoaderName; private String rootDir; public NotParentClassLoader(String rootDir, String classLoaderName) { //super()会指定当前类加载器的父加载器,AppClassLoader加载 super(); this.rootDir = rootDir; this.classLoaderName = classLoaderName; } /* * 覆盖了父类的findClass,执行自己的加载逻辑 */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); InputStream ins = null; ByteArrayOutputStream baos = null; try { ins = new FileInputStream(path); baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { //ignore } finally { if (ins != null) { try { ins.close(); } catch (IOException e) { } } if (baos != null) { try { baos.close(); } catch (IOException e) { } } } return null; } public String getClassLoaderName() { return classLoaderName; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } /** * 重写loadClass方法先从当前类加载器加载再从父类加载器加载。 * (如项目中的某类的版本可能和web容器中的不一致的时候,若还从container加载就会报jar包冲突的异常) */ @Override public Class<?> loadClass(String name) throws ClassNotFoundException { Class<?> c = findLoadedClass(name); if (c == null) { try { c = findClass(name); } catch (ClassNotFoundException e) { return super.loadClass(name); } } return c; } }
输出
public static void testNotParentClassLoader() throws Exception { String rootDir = "/Volumes/O/codeRepository/git-repository/gitee/usagoole/java/java-classloader/out/production/classes"; NotParentClassLoader notParentClassLoader = new NotParentClassLoader(rootDir, "notParentClassLoader"); Class<?> parent = notParentClassLoader.loadClass("init.Parent"); Class<?> parent2 = notParentClassLoader.loadClass("init.Parent"); System.out.println(parent); //define.NotParentClassLoader@330bedb4 System.out.println(parent.getClassLoader()); //true System.out.println(parent.hashCode() == parent2.hashCode()); } define.NotParentClassLoader@330bedb4
如果注释掉覆写的loadClass方法,输出
sun.misc.Launcher$AppClassLoader@7f31245a
-
案例:自定义文件类加载
public class FileSystemClassLoader extends ClassLoader { private String classLoaderName; private String rootDir; public FileSystemClassLoader(String rootDir, String classLoaderName) { //不能指定当前类加载器的父加载器,否则父类委托机制会先使用AppClassLoader加载 super(null); this.rootDir = rootDir; this.classLoaderName = classLoaderName; } /* * 覆盖了父类的findClass,执行自己的加载逻辑 */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); InputStream ins = null; ByteArrayOutputStream baos = null; try { ins = new FileInputStream(path); baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } finally { if (ins != null) { try { ins.close(); } catch (IOException e) { e.printStackTrace(); } } if (baos != null) { try { baos.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } public String getClassLoaderName() { return classLoaderName; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } } package define; public class DefinelassLoaderTest { public static void main(String[] args) throws ClassNotFoundException { String rootDir = "/Volumes/O/java-classloader/out/production/classes"; FileSystemClassLoader fileSystemClassLoader = new FileSystemClassLoader(rootDir, "FileSystemClassLoader"); Class<?> parent = fileSystemClassLoader.loadClass("init.Parent"); Class<?> parent2 = fileSystemClassLoader.loadClass("init.Parent"); System.out.println(parent); //define.FileSystemClassLoader@330bedb4 System.out.println(parent.getClassLoader()); //true System.out.println(parent.hashCode() == parent2.hashCode()); } }
-
系统默认的
AppClassLoader
加载器,内部会缓存加载过的class,重新加载的话,就直接取缓存。对于热加载的话,只能重新创建一个ClassLoader,然后再去加载已经被加载过的class文件public class HotSwapURLClassLoader extends URLClassLoader { //缓存加载class文件的最后最新修改时间 public static Map<String, Long> cacheLastModifyTimeMap = new HashMap<String, Long>(); //类的包路径 public String packagePath = null; public URL[] urls = null; public HotSwapURLClassLoader(String packagePath, URL[] urls) { super(urls, null); this.packagePath = packagePath; this.urls = urls; } @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class clazz = findLoadedClass(name); if (clazz != null) { if (resolve) { resolveClass(clazz); } //如果class类被修改过,则重新加载 if (isModify(name)) { HotSwapURLClassLoader hcl = new HotSwapURLClassLoader(this.packagePath, this.urls); clazz = customLoad(name, false, hcl); } return clazz; } //如果类的包名为"java.xx"开始,则有系统默认加载器AppClassLoader加载 if (name.startsWith("java.")) { try { //得到系统默认的加载cl,即AppClassLoader ClassLoader system = ClassLoader.getSystemClassLoader(); clazz = system.loadClass(name); if (clazz != null) { if (resolve) { resolveClass(clazz); } return (clazz); } } catch (ClassNotFoundException e) { throw new RuntimeException(e.getMessage(), e); } } return customLoad(name, false, this); } /** * 自定义加载 * @param name * @param resolve * @return * @throws ClassNotFoundException */ protected Class customLoad(String name, boolean resolve, HotSwapURLClassLoader cl) throws ClassNotFoundException { //findClass()调用的是URLClassLoader里面重载了ClassLoader的findClass()方法 Class clazz = cl.findClass(name); if (resolve) { cl.resolveClass(clazz); } //缓存加载class文件的最后修改时间 long lastModifyTime = getClassLastModifyTime(name); cacheLastModifyTimeMap.put(name, lastModifyTime); return clazz; } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } /** * @param name * @return .class文件最新的修改时间 */ private long getClassLastModifyTime(String name) { String path = getClassCompletePath(name); File file = new File(path); if (!file.exists()) { throw new RuntimeException(new FileNotFoundException(name)); } return file.lastModified(); } /** * 判断这个文件跟上次比是否修改过 * @param name * @return */ private boolean isModify(String name) { long lastmodify = getClassLastModifyTime(name); long previousModifyTime = cacheLastModifyTimeMap.get(name); if (lastmodify > previousModifyTime) { return true; } return false; } /** * @param name * @return .class文件的完整路径 */ private String getClassCompletePath(String name) { String classPackagePath = name.replace('.', File.separatorChar); return packagePath + File.separatorChar + classPackagePath + ".class"; } }
监听类
class MonitorHotSwap implements Runnable { private String className = null; private HotSwapURLClassLoader hotSwapURLClassLoader = null; public MonitorHotSwap(String packagePath, URL[] url, String className) { this.className = className; this.hotSwapURLClassLoader = new HotSwapURLClassLoader(packagePath, url); } @Override public void run() { while (true) { try { // 如果类被修改了,那么会重新加载,class也会返回新的 Class clazz = hotSwapURLClassLoader.loadClass(className); //替换新的classLoader ClassLoader classLoader = clazz.getClassLoader(); if (classLoader instanceof HotSwapURLClassLoader && !classLoader.equals(this.hotSwapURLClassLoader)) { this.hotSwapURLClassLoader = (HotSwapURLClassLoader) classLoader; } System.out.println(clazz.hashCode()); Object hot = clazz.newInstance(); Method m = clazz.getMethod("toString"); m.invoke(hot, null); // 每隔5秒重新加载一次,此处可以优化监听文件变化,然后重现加载 Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); } } } }
测试用例,不断修改Person并重新编译
public static void testURLClassLoader() throws Exception { String packagePath = "/Volumes/O/java-classloader/out/production/classes/"; URL url = new File(packagePath).toURI().toURL(); HotSwapURLClassLoader hotSwapURLClassLoader = new HotSwapURLClassLoader(packagePath, new URL[]{url}); Class<?> personClazz = hotSwapURLClassLoader.loadClass("define.Person"); System.out.println(personClazz); System.out.println(personClazz.getClassLoader()); //开启线程,如果class文件有修改,就热替换 Thread t = new Thread(new MonitorHotSwap(packagePath, new URL[]{url}, "define.Person")); t.start(); LockSupport.park(); } public class Person { private String name = "jannal"; public Person() { } public Person(String name) { this.name = name; } @Override public String toString() { System.out.println(" version : " + this.getClass().getClassLoader()); return "Person{" + "name='" + name + '\'' + '}'; } }
输出
...省略... 1472183692 version : define.HotSwapURLClassLoader@5d47d6fc 1472183692 version : define.HotSwapURLClassLoader@5d47d6fc 1472183692 version : define.HotSwapURLClassLoader@5d47d6fc 1472183692 version : define.HotSwapURLClassLoader@5d47d6fc //Person被修改后,新的classloader实例重新加载class [Loaded define.Person from file:/Volumes/O/java-classloader/out/production/classes/] 593852800 version : define.HotSwapURLClassLoader@12bd291f 593852800 version : define.HotSwapURLClassLoader@12bd291f 593852800 version : define.HotSwapURLClassLoader@12bd291f ...省略...
可以强制执行垃圾回收,此时可以看到控制台会输出类卸载的信息。一个已经加载的类是无法被更新的,如果你试图用同一个ClassLoader再次加载同一个类,就会得到异常(java.lang.LinkageError: duplicate class definition),我们只能够重新创建一个新的ClassLoader实例来再次加载新类。至于原来已经加载的类,不必去管它,因为它可能还有实例正在被使用,只要相关的实例都被内存回收了,那么JVM就会在适当的时候把不会再使用的类卸载。
...省略... [Unloading class define.Person 0x00000007c0061028] ...省略...
类加载器与Web容器
- 对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
- 绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则: 每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在
WEB-INF/classes
和WEB-INF/lib
目录下面。多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。 - tomcat为什么要自定义类加载器? tomcat为每个应用都分配了一个专属类加载器,主要原因:
- 为了不同webapp加载不同版本的jar包: 在现在的web应用中,第三方框架的使用随处可见,但是如果两个webapp都使用了同一个jar包但是版本不同,那么就非常有必要进行隔离。我们知道一个Classloader实例对同一个class文件仅能加载一次,在加载某class文件一个版本之后,如果再次搜索到同名的class文件是会抛出异常的。这样对于不同版本的jar包加载还是隔离开比较好。
- 为了保证安全(避免类加载器的内存泄露让webapp互相影响)
- 热部署
线程上下文类加载器
-
类加载器的委托模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。SPI 接口中的代码经常需要加载具体的实现类。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的(ServiceLoader是由BootstrapClassLoader加载的);SPI 实现的 Java 类一般是由系统类加载器(AppClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库,它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。即类加载器的委托模式无法解决这个问题
-
线程上下文类加载器正好解决以上的问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。
public static <S> ServiceLoader<S> load(Class<S> service) { // 获取当前调用线程的类加载器,默认就是AppClassLoader ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
-
线程上下文类加载器(Context ClassLoader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法
getContextClassLoader()
和setContextClassLoader(ClassLoader cl)
用来获取和设置线程的上下文类加载器。如果没有通过setContextClassLoader(ClassLoader cl)
方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。 -
默认情况下继承父线程的上下文类加载器,即系统类加载器
/** * 准备: * 1. 引入mysql-connector-java的jar包到classpath下 * 输出: * driver: class com.mysql.jdbc.Driverloader: sun.misc.Launcher$AppClassLoader@7f31245a * driver: class com.mysql.fabric.jdbc.FabricMySQLDriverloader: sun.misc.Launcher$AppClassLoader@7f31245a * 当前线程上下文类加载器: sun.misc.Launcher$AppClassLoader@7f31245a * ServiceLoader的类加载器: null */ public static void defaultContextClassLoader() { ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class); Iterator<Driver> iterator = serviceLoader.iterator(); while (iterator.hasNext()) { Driver driver = iterator.next(); System.out.println("driver: " + driver.getClass() + "loader: " + driver.getClass().getClassLoader()); } System.out.println("当前线程上下文类加载器: " + Thread.currentThread().getContextClassLoader()); System.out.println("ServiceLoader的类加载器: " + ServiceLoader.class.getClassLoader()); }
-
设置自定义的线程上下文类加载器
/** * 输出: * 当前线程上下文类加载器: sun.misc.Launcher$ExtClassLoader@45ee12a7 * ServiceLoader的类加载器: null * 解析: 因为扩展类加载器无法加载项目classpath下jar包,所以找不到 * @param args */ public static void main(String[] args) { Thread.currentThread().setContextClassLoader(FileSystemClassLoader.class.getClassLoader().getParent()); defaultContextClassLoader(); }
-
线程上下文类加载器,一般使用模式(获取-使用-还原)
ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try{ Thread.currnetThread().setContextClassLoader(targetTccl); //fooMethod里面则调用了Thread.currentThread().getContextClassLoader(),获取当前线程的上下文加载器做某些事情。 fooMethod(); }finally{ Thread.currentThread().setContextClassLoader(classLoader); }
Class.forName与ClassLoader的区别
-
Java反射中Class.forName()加载类和使用ClassLoader加载类的区别?
- 在java中Class.forName()和ClassLoader都可以对类进行加载,Class.forName()方法实际上也是调用的CLassLoader来实现的。
- Class.forName()默认加载类会初始化类,而ClassLoader仅仅是加载类,并不会初始化类
-
案例
public class ClassForNameTest { /** * 输出: * car 静态块 * end */ public static void main(String[] args) throws Exception { Class.forName("init.Car", true, ClassForNameTest.class.getClassLoader()); /** * Class<?> caller = Reflection.getCallerClass(); * //默认初始化为true,类加载器就是调用者的类加载器 * return forName0(className, true, ClassLoader.getClassLoader(caller), caller); */ Class.forName("init.Car"); ClassLoader.getSystemClassLoader().loadClass("init.Bike"); System.out.println("end"); } } class Car { static { System.out.println("car 静态块"); } public Car() { System.out.println("car 构造方法"); } } class Bike { static { System.out.println("Bike 静态块"); } public Bike() { System.out.println("Bike 构造方法"); } }
Jar Hell
-
有时classpath中不同的JAR包会包含限定名完全相同的类。造成这种现象的原因有很多,例如一个类库存在两个不同的版本,例如一个包含了所有依赖的fat jar(fat JAR是将所有依赖和资源文件都打包成一个jar包)又被当成standalone的jar来使用,例如一个类库被重命名后又再次被加到classpath中。JVM总是从classpath中第一个包含某个类的JAR包里加载该类,这个被加载的类将会“屏蔽”该类的其他版本,使这些类变得不可用。如果这些不同版本的类在语义上有所区别,将会导致各类问题,从难以发现的不正常行为到非常严重的错误都可能发生。更糟糕的是这类问题的表现形式很可能是不确定。这取决于JVM查找JAR包的顺序。因此,问题的表现形式很可能因环境的不同而有差别。典型的例子就是开发者使用的IDE和生产环境的不同将可能让相同的代码产生不同的行为。
-
如果某个类库的两个不同版本都存在于classpath中,那么应用程序的行为将变得无法预测。首先,由于存在屏蔽问题,两个版本中都存在的类只能从其中一个类库加载。更糟糕的是,如果应用程序访问了一个只存在某个版本类库的类,那么该类也会被加载。这就意味着调用该类库的程序会混合着使用这两个版本类库的代码。正由于应用程序需要访问同一类库的不同版本,因此当其中一个版本不存在的时候,应用程序极有可能无法正常工作。要么是程序行为不符合期望,要么就抛出NoClassDefFoundErrors异常。
-
诊断
public class JarHell { public static void main(String[] args) throws Exception { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); String resourceName = "net/sf/cglib/proxy/MethodInterceptor.class"; Enumeration<URL> urls = classLoader.getResources(resourceName); while (urls.hasMoreElements()) { System.out.println(urls.nextElement()); } } }