一、打破双亲委派机制的方法
双亲委派机制的核心思想是:当一个类加载器接收到加载类的请求时,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器(Bootstrap ClassLoader)中去,只有当父类加载器无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过再由顶向下进行加载。
双亲委派机制是Java类加载器的重要特性,但在某些情况下,可能需要打破这种机制。以下是打破双亲委派机制的方法:
- 自定义类加载器:在Java中,可以通过继承ClassLoader并重写其loadClass方法来创建自定义类加载器。通过这种方式,可以打破双亲委派机制,实现类的隔离。例如,在Tomcat中,每个Web应用都有自己的类加载器,从而实现了应用之间的类隔离。当两个Web应用中有相同限定名的类时,如Servlet类,Tomcat通过自定义类加载器保证它们是不同的类。
- 线程上下文类加载器:在Java中,每个线程都有一个关联的上下文类加载器。通过设置线程的上下文类加载器,可以实现类的加载。例如,JDBC和JNDI等就是利用线程上下文类加载器来加载类的。
- Osgi框架的类加载器:Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载。在Osgi中,每个Bundle都有自己的类加载器,当需要加载类时,会先在自己的存储空间中查找,如果找不到,则委托给父级Bundle的类加载器进行查找。这种机制打破了传统的双亲委派模型。
二、自定义类加载器
1.Tomcat自定义类加载器案例
在Tomcat环境中,一个显著的特点是其能够同时运行多个Web应用。这就引出了一个重要的问题:如果两个应用中存在相同限定名的类,例如Servlet类,那么Tomcat如何保证这两个类能够被正确加载,并且它们实际上是不同的类。
在传统的类加载机制中,双亲委派机制(Parent Delegation Mechanism)是核心。这个机制规定,当一个类加载器收到类加载请求时,它首先不会自己去加载,而是把这个请求委派给父类加载器去执行。这就形成了一个从上到下的“类加载委托层次”。然而,在多Web应用环境下,这种机制可能会导致类加载的问题。比如,当Web应用1中的MyServlet已经被其应用类加载器加载后,由于双亲委派机制的存在,Web应用2中相同限定名的MyServlet类可能就无法被其应用类加载器加载。
为了解决这个问题,Tomcat采用了一种自定义类加载器的策略。每个Web应用都有其独立的类加载器,负责加载该应用中的类。这样,即使两个应用中有相同名称的类,由于它们是由不同的类加载器加载的,因此它们实际上是不同的类。
在同一个Java虚拟机中也同理,两个自定义类加载器加载相同限定名的类不会冲突,只有相同类加载器和相同的类限定名才会被认为是同一个类。
2.自定义类加载器详解
ClassLoader中包含了4个核心方法,双亲委派机制的核心代码就位于loadClass方法中。
- loadClass()是类加载的入口,提供了双亲委派机制,内部会调用findClass:
public Class<?> loadClass(String var1)
loadClass()源码:
public Class<?> loadClass(String var1) throws ClassNotFoundException { return this.loadClass(var1, false); } protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException { synchronized(this.getClassLoadingLock(var1)) { Class var4 = this.findLoadedClass(var1); if (var4 == null) { long var5 = System.nanoTime(); try { if (this.parent != null) { var4 = this.parent.loadClass(var1, false); } else { var4 = this.findBootstrapClassOrNull(var1); } } catch (ClassNotFoundException var10) { } if (var4 == null) { long var7 = System.nanoTime(); var4 = this.findClass(var1); PerfCounter.getParentDelegationTime().addTime(var7 - var5); PerfCounter.getFindClassTime().addElapsedTimeFrom(var7); PerfCounter.getFindClasses().increment(); } } if (var2) { this.resolveClass(var4); } return var4; } }
- findClass()由类加载器的子类实现,其核心功能是获取二进制数据并调用defineClass ,比如URLClassLoader会根据文件路径去获取类文件中的二进制数据。
protected Class<?> findClass(String var1)
- defineClass()会做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中。
protected final Class<?> defineClass(String var1, byte[] var2, int var3, int var4)
- resolveClass()会执行类生命周期中的连接阶段。
protected final void resolveClass(Class<?> var1)
loadClass()核心代码解析:
parent等于null说明父类加载器是启动类加载器,直接调用findBootstrapClassOrNull,否则调用父类加载器的加载方法。
if (this.parent != null) { var4 = this.parent.loadClass(var1, false); } else { var4 = this.findBootstrapClassOrNull(var1); }
父类加载器无法找到所需的类时,当前类加载器将承担起加载的责任。
if (var4 == null) { ... var4 = this.findClass(var1); ... }
在实际开发中,为了正确地实现一个自定义类加载器,并确保不破坏双亲委派机制,应当重写findClass()方法。这样的做法确保了类加载请求的正确委派,同时允许开发者根据特定需求定制类加载的行为。
3.案例解析
自定义Test类:
public class Test { public static void main(String[] args) { System.out.println("自定义Test类"); } }
将Test类放到相应的目录下:
自定义类加载器:
public class BreakClassLoader extends ClassLoader { private String basePath; private final static String FILE_EXT = ".class"; public void setBasePath(String basePath) { this.basePath = basePath; } private byte[] loadClassData(String name) { try { String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator)); FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT); try { return IOUtils.toByteArray(fis); } finally { IOUtils.closeQuietly(fis); } } catch (Exception e) { System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage()); return null; } } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if(name.startsWith("java.")){ return super.loadClass(name); } byte[] data = loadClassData(name); return defineClass(name, data, 0, data.length); } public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException { BreakClassLoader classLoader1 = new BreakClassLoader(); classLoader1.setBasePath("D:\\Test\\com\\rye\\"); Class<?> aClass = classLoader1.loadClass("Test"); System.out.println(aClass.getClassLoader()); } }
运行结果(获取自定义类加载器):
需要注意的是加载的类名不能以java.开头,源码解析:
private ProtectionDomain preDefineClass(String var1, ProtectionDomain var2) { if (!this.checkName(var1)) { throw new NoClassDefFoundError("IllegalName: " + var1); } else if (var1 != null && var1.startsWith("java.")) { throw new SecurityException("Prohibited package name: " + var1.substring(0, var1.lastIndexOf(46))); } else { if (var2 == null) { var2 = this.defaultDomain; } if (var1 != null) { this.checkCerts(var1, var2.getCodeSource()); } return var2; } }
获取自定义类加载器的父类加载器:
public class BreakClassLoader extends ClassLoader { private String basePath; private final static String FILE_EXT = ".class"; public void setBasePath(String basePath) { this.basePath = basePath; } private byte[] loadClassData(String name) { try { String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator)); FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT); try { return IOUtils.toByteArray(fis); } finally { IOUtils.closeQuietly(fis); } } catch (Exception e) { System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage()); return null; } } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if(name.startsWith("java.")){ return super.loadClass(name); } byte[] data = loadClassData(name); return defineClass(name, data, 0, data.length); } public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException { BreakClassLoader classLoader1 = new BreakClassLoader(); // classLoader1.setBasePath("D:\\Test\\com\\rye\\"); // // Class<?> aClass = classLoader1.loadClass("Test"); // System.out.println(aClass.getClassLoader()); ClassLoader parent = classLoader1.getParent(); System.out.println(parent); } }
运行结果:
解析:
ClassLoader类中提供了构造方法设置parent的内容(JDK8中):
private ClassLoader(Void var1, ClassLoader var2) { this.classes = new Vector(); this.defaultDomain = new ProtectionDomain(new CodeSource((URL)null, (Certificate[])null), (PermissionCollection)null, this, (Principal[])null); this.packages = new HashMap(); this.nativeLibraries = new Vector(); this.defaultAssertionStatus = false; this.packageAssertionStatus = null; this.classAssertionStatus = null; this.parent = var2; if (ClassLoader.ParallelLoaders.isRegistered(this.getClass())) { this.parallelLockMap = new ConcurrentHashMap(); this.package2certs = new ConcurrentHashMap(); this.domains = Collections.synchronizedSet(new HashSet()); this.assertionLock = new Object(); } else { this.parallelLockMap = null; this.package2certs = new Hashtable(); this.domains = new HashSet(); this.assertionLock = this; } }
这个构造方法由另外一个构造方法调用,其中父类加载器由getSystemClassLoader方法设置,该方法返回的是AppClassLoader。
protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); }
验证getSystemClassLoader方法返回的是AppClassLoader:
public static void main(String[] args){ System.out.println(getSystemClassLoader()); }
运行结果:
总结
JVM是Java程序的运行环境,负责字节码解释、内存管理、安全保障、多线程支持、性能监控和跨平台运行。本文主要介绍了打破双亲委派机制的方法、自定义类加载器等内容,希望对大家有所帮助。