Java里有如下几种类加载器
通过以下实例来了解各个类加载器:
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(Object.class.getClassLoader());
// java提供的与DNS服务交互的api
System.out.println(DNSNameService.class.getClassLoader());
System.out.println(ClassLoaderTest.class.getClassLoader());
}
}
运行结果如下:
null
sun.misc.Launcher$ExtClassLoader@6d6f6e28
sun.misc.Launcher$AppClassLoader@58644d46
启动类加载器是在有jvm底层创建的实例,所以在获取时为null,Object类是有启动类加载器进行加载的,所以获取其加载器时为null,而DNSNameService为JAVA_HOME/jre/lib目录下ext文件夹在的dnsns.jar包中的类,由扩展类加载器(ExtClassLoader)加载。而自己编写的类ClassLoaderTest 则由AppClassLoader进行加载。
在上面已经介绍过,java的类加载器也是普通的类,ExtClassLoader和AppClassLoader均是URLClassLoader的子类,而URL的继承关系如下:
那么AppClassLoader和ExtClassLoader为ClassLoader的子类。在上面已经已经介绍过在jvm启动时会通过sun.misc.Launcher的getLauncher方法从而获取Launcher的实例,那么在这个过程中Launcher会通过构造方法创建该类的实例。sun.misc.Launcher的构造方法如下:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 创建ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 创建AppClassPoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 省略代码 ....
}
}
通过分析sun.misc.Launcher构造方法我们知道在sun.misc.Launcher类的实例创建是会创建AppClassLoader实例和ExtClassLoader实例。同时由于两个类加载器均继承自ClassLoader,而ClassLoader中有一个ClassLoader的全局变量parent,该类类型也是ClassLoader:
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// 省略代码 ....
}
而在sun.misc.Launcher创建时,实例化ExtClassLoader和AppClassLoader时均指定其parent属性分别为null和ExtClassLoader。那么java中的类加载器的机构就如下:
自定义类加载器需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,大体逻辑
首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再 加载, 直接返回。
如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器, 则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用 Bootstrap类加载器来加载。
如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类 加载器 的findClass方法来完成类加载。还有一个方法是findClass,默认 实现是抛出异常,所以我们自定义类加载器主要是重写 findClass方法。
接下来看一个示例,首先我们编写需要自定义类加载器加载的类,如下:
package com.dp.jvm;
import java.io.PrintStream;
public class User
{
public void say()
{
System.out.println("hello");
}
}
需要注意的是,该类编写完成需要在工程中删除,避免AppClassLoader加载。编译完成后将该类的class文件放置指定的目录下:
然后编写自定义的类加载器,代码如下:
class MyClassLoader extends ClassLoader{
private final String path;
MyClassLoader(String path) {
this.path = path;
}
/**
* 重写ClassLoader的findClass方法,获取到类的Class对象
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] byteArrayFromClassName = getByteArrayFromClassName(name);
return defineClass(name, byteArrayFromClassName, 0, byteArrayFromClassName.length);
}
/**
* 通过类的全限定名称获取到类的二进制数据
* @param name
* @return
*/
private byte[] getByteArrayFromClassName(String name) {
String classPath = convertNameToPath(name);
byte[] data = null;
int off = 0;
int length = 0;
try(BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(classPath))) {
data = new byte[bufferedInputStream.available()];
while ((length = bufferedInputStream.read(data, off, data.length - off)) > 0) {
off += length;
}
} catch (Exception ex) {
ex.printStackTrace();
}
return data;
}
/**
* 通过类的全限定名称获取到对应类文件的的字节码文件路径
* @param name
* @return
*/
private String convertNameToPath(String name) {
String relativePath = name.replace(".", File.separator);
String absolutePath = path + File.separator + relativePath + ".class";
return absolutePath;
}
}
编写测试类,通过使用自定义的类加载将User加载并实例化,然后调用其say方法,如下:
public class CustomClassLoaderTest {
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("F:\\test");
Class<?> clazz = myClassLoader.loadClass("com.dp.jvm.User");
Object o = clazz.newInstance();
Method say = clazz.getDeclaredMethod("say");
say.invoke(o);
}
}
通过上面了实例,简单的实现了一个自定义的类加载器。接留下来了解一下类加载器的双亲委派机制。
JVM类加载器是有亲子层级结构的,如下图:
需要注意的是,这里的额亲子层级结构不是指的java中的继承关系,而是每一个类加载实现类都具有一个parent全局变量,而该全局变量的类型为ClassLoader。这里可能有一个疑问,在自定义类加载器中并未看到名称parent的全局变量。这是因为这个全局变量是在ClassLoader中定义声明的。
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// 省略代码 ....
还有一个问题也需要说明一下,我们自定义的类加载器的parent属性是如何设置的呢?怎么知道设置的为AppClassLoader呢?因为自定义的类加载器继承自ClassLoader,而ClassLoader中有一个无参的构造函数,如下:
protected ClassLoader() {
//调用有参构造函数
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
//设置父加载器
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
从ClassLoader的实现来看,通过getSystemClassLoader()方法获取系统类加载器然后将其赋值给parent属性。那么来看一下getSystemClassLoader()具体实现:
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
// 初始化系统类加载器
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
在getSystemClassLoader方法中获取sun.misc.Lanucher实例(单例),然后调用其getClassLoader方法获取系统类加载器,然后设置给parent方法。最后来看一下sun.misc.Lanucher的getClassLoader方法:
public ClassLoader getClassLoader() {
return this.loader;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
}
结合sun.misc.Lanucher的getClassLoader和构造方法可知系统类加载器就是AppClassLoader。
在了解了jvm中类加载器的组成结构后,我们再来看一下jvm中各个类加载器的组成的结构:
在了解了jvm中各个类加载器的层次结构之后,加下来来解析双亲委派机制就相对来说简单多了,首先从双亲委派的流程说起。
双亲委派流程如下:
加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
比如我们的PrintTest 类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托启动类加载器,顶层启动类加载器在自己的类加载路径里找了半天没找到PrintTest 类,则向下退回加载PrintTest 类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到PrintTest 类,又向下退回PrintTest 类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。
双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载。
那么为什么要设置双亲委派机制呢?
双亲委派的原理体现在ClassLoader的loadClass方法中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查类是够已经被加载
Class<?> c = findLoadedClass(name);
// 类还未加载则加载,使用双亲委派机制
if (c == null) {
long t0 = System.nanoTime();
try {
// 判断当前类加载器是否设置了父加载器,设置了则
// 调用父加载器的loadClass进行加载,如果父加载也是ClassLoader
// 的子类则会再次进入该方法,判断是否有父类加载器,依次递归
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 当类加载没有设置parent父加载,那么就使用启动类加载器加载
// 由于启动类加载器是底层创建的实例,所以该方法会调用本地
// native方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 向上委派所有父加载器仍然没有加载到参数类,那么调用当前
// 类加载器进行类的加载
long t1 = System.nanoTime();
c = findClass(name);
// ... 省略
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
通过了解双亲委派机制,知道了双亲委派机制的逻辑是在ClassLoader中的loadClass方法中实现的,如果要打破双亲委派机制,那么通过重载loadClass方法,不再通过parent父类加载器加载,而是由自己加载。实现打破双亲委派的类加载器如下:
public class BreakParentalDelegaClassLoader extends ClassLoader {
private final String sourcePath;
public BreakParentalDelegaClassLoader(String sourcePath) {
this.sourcePath = sourcePath;
}
/**
* 功能描述: 通过重载loadClass方法,变更原先的双签委派逻辑,
* 而是有当前类加载器直接去配置的路径中查找字节码文件并加载
* @param: [name, resolve]
* @return: java.lang.Class>
* @auther: binga
* @date: 2020/8/18 17:47
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先检查类是否加载过
Class<?> c = findLoadedClass(name);
/**
* 接下来不再像ClassLoader中的loadClass的逻辑一样,通过
* 父类加载器加载,而是通过自己的findClass进行类的查找和加载
*/
if (c == null) {
long t0 = System.nanoTime();
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 (c == null) {
throw new ClassNotFoundException();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
/**
* 功能描述: 重载CLassLoader的findClass方法,在该方法中通过指定的path,通过
* 指定的类的全限定名称转换为路径从而读取class文件,然后通defineClass方法
* 加载类并返回指定类的Class对象。
* @param: [name]
* @return: java.lang.Class>
* @auther: binga
* @date: 2020/8/18 17:03
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] byteCode = getByteArrayFromClassName(name);
return defineClass(name, byteCode, 0, byteCode.length);
}
/**
* 功能描述: 加载class文件返回字节数组
* @param: [name]
* @return: byte[]
* @auther: binga
* @date: 2020/8/18 17:05
*/
private byte[] getByteArrayFromClassName(String name) throws ClassNotFoundException {
String classPath = convertNameToPath(name);
byte[] data = null;
int off = 0;
int length;
try(BufferedInputStream bufferedInputStream =
new BufferedInputStream(new FileInputStream(classPath))) {
data = new byte[bufferedInputStream.available()];
while ((length = bufferedInputStream.read(data, off, data.length - off)) > 0) {
off += length;
}
} catch (Exception ex) {
ex.printStackTrace();
throw new ClassNotFoundException();
}
return data;
}
/**
* 功能描述: 类全限定名称转换为文件路径
* @param: [name]
* @return: java.lang.String
* @auther: binga
* @date: 2020/8/18 17:06
*/
private String convertNameToPath(String name) {
String classPath = name.replace(".", File.separator);
classPath = sourcePath + File.separator + classPath + ".class";
return classPath;
}
}
仍然沿用User类,测试代码如下:
public class BreakParentalDelegaTest {
public static void main(String[] args) throws Exception {
BreakParentalDelegaClassLoader classLoader = new BreakParentalDelegaClassLoader("F:\\test");
Class<?> userClass = classLoader.loadClass("com.binga.jvm.classloader.User", false);
Object user = userClass.newInstance();
Method say = userClass.getDeclaredMethod("say", null);
say.invoke(user, null);
}
}
将User.class方至F:/test/com/binga/jvm/classloader目录下运行测试结果如下:
java.io.FileNotFoundException: F:\test\java\lang\Object.class (系统找不到指定的路径。)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:131)
at java.io.FileInputStream.<init>(FileInputStream.java:87)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.getByteArrayFromClassName(BreakParentalDelegaClassLoader.java:90)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.findClass(BreakParentalDelegaClassLoader.java:72)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.loadClass(BreakParentalDelegaClassLoader.java:42)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.findClass(BreakParentalDelegaClassLoader.java:73)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.loadClass(BreakParentalDelegaClassLoader.java:42)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaTest.main(BreakParentalDelegaTest.java:15)
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.findClass(BreakParentalDelegaClassLoader.java:73)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.loadClass(BreakParentalDelegaClassLoader.java:42)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaTest.main(BreakParentalDelegaTest.java:15)
Caused by: java.lang.ClassNotFoundException
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.getByteArrayFromClassName(BreakParentalDelegaClassLoader.java:98)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.findClass(BreakParentalDelegaClassLoader.java:72)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.loadClass(BreakParentalDelegaClassLoader.java:42)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 6 more
可以看到抛出ClassNotFoundException,可能会纳闷,为什么加载的是User类,而抛出的是Object,因为Object是所有类的父类,那么在加载User类时会先加载User类,但是由于我们在自定义加载器中打破了双亲委派机制,所以有当前自定义类加载器加载Object类,但是在F:/test并没有Object的字节码文件,那么将JDK中的Object字节码文件拷贝过来进行测试:
再次运行代码如下:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.findClass(BreakParentalDelegaClassLoader.java:73)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.loadClass(BreakParentalDelegaClassLoader.java:42)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.findClass(BreakParentalDelegaClassLoader.java:73)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaClassLoader.loadClass(BreakParentalDelegaClassLoader.java:42)
at com.binga.jvm.classloader.brealparentaldelega.BreakParentalDelegaTest.main(BreakParentalDelegaTest.java:15)
可以看到这次不再是Object找不到了,而是禁止的包名称,JVM中对像java.lang等自有的包是禁止的,所以抛出安全异常,那么如何处理呢?解决方法就是通过在重载的loadClass中通过判断,只有自己指定的通过自己加载,如下修改loadClass方法:
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 不是自己指定的类则使用双亲委派
if (!"com.binga.jvm.classloader.User".equals(name))
return super.loadClass(name, false);
// 首先检查类是否加载过
Class<?> c = findLoadedClass(name);
/**
* 接下来不再像ClassLoader中的loadClass的逻辑一样,通过
* 父类加载器加载,而是通过自己的findClass进行类的查找和加载
*/
if (c == null) {
long t0 = System.nanoTime();
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 (c == null) {
throw new ClassNotFoundException();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
运行结果如下:
hello
Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
再看看我们的问题:
Tomcat 如果使用默认的双亲委派类加载机制行不行?