打破双亲委托机制
重写父类ClassLoader的loadClass方法
package com.morris.jvm.classloader;
public class BreakDelegateClassLoader extends MyClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 根据类的全限定名进行加锁,确保一个类在多线程下只被加载一次
synchronized (getClassLoadingLock(name)) {
// 到已加载类的缓存中查看是否已经加载过,如果已加载则直接返回
Class<?> c = findLoadedClass(name);
if (null == c) {
// 尝试使用自定义类加载器加载
c = this.findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
是否可以加载自定义java.lang.String类
当我们尝试加载自定义的java.lang.String类时,会抛出如下异常:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.morris.jvm.load.MyClassLoader.findClass(MyClassLoader.java:33)
at com.morris.jvm.load.BreakDelegateClassLoader.loadClass(BreakDelegateClassLoader.java:14)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at com.morris.jvm.load.StringClassLoaderTest.main(StringClassLoaderTest.java:7)
JVM出于安全考虑,禁止使用以java开头的包名。以下为相关源码:
摘自jdk1.8 java.lang.ClassLoader
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
// 以java.开头直接抛出异常
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
强制使用根加载器加载自定义java.lang.String类
从JDK中复制String.java的源码,添加hello()方法后编译,放入D:\classloader\java\lang目录,在Test方法中调用hello()方法。
public class Test {
public static void main(String[] args) {
String str = new java.lang.String();
str.hello();
}
}
使用-Xbootclasspath参数指定自定义java.lang.String的class文件的路径,具体如下:
$ java "-Xbootclasspath/p:D:\classloader" Test
hello string
在调试过程中可以添加虚拟机参数-XX:+TraceClassLoading来监控类的加载情况。
注意一定要复制JDK中的String.java类,并在其基础上添加方法,否则运行会报错,因为jvm在启动过程中会调用String类的方法,如果自定义的String类没有原生类的方法就会报错。
另外如果我们要加载自定义的java.util.HashMap(不在java.lang包下)类,可以使用endorsed技术,将自己的HashMap类,打包成一个jar包,然后放到-Djava.endorsed.dirs指定的目录中。注意类名和包名,应该和JDK自带的是一样的。
命名空间与运行时包
每个类加载器都有自己的命名空间。同一个命名空间内的类是相互可见的,命名空间由该加载器及所有父加载器所加载的类组成。
子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器的类。例如系统类加载器加载的类能看见根类加载器加载的类。由父亲加载器加载的类不能看见子加载器加载的类。如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载类相互不可见。
在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
在类的加载过程中,所有参与的类加载器,即时没有亲自加载过该类,都会被标识为该类的初始类加载器,实际加载类的加载器被称为定义类加载器。
运行时包:由类加载器的命名空间和类的全限定名组成。
package com.morris.jvm.classloader;
// 自定义类加载器必须继承ClassLoader
public class CustomerClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
}
}
package com.morris.jvm.classloader;
public class CustomerClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
CustomerClassLoader myClassLoader = new CustomerClassLoader();
Class<?> clazz = myClassLoader.loadClass("java.lang.String");
System.out.println(clazz.getClassLoader());
}
}
在上面例子中,java.lang.String依次经过了CustomerClassLoader类加载器、系统类加载器、扩展类加载器、根类加载器,这些加载器都是java.lang.String的初始类加载器,而根类加载是java.lang.String的定义类加载器,JVM会在每一个类加载器维护的列表中添加该类型。如下图所示:
同一个类加载器实例加载同一个class
package com.morris.jvm.classloader;
public class NameSpaceTest1 {
public static void main(String[] args) throws ClassNotFoundException {
// 获取系统类加载器
ClassLoader classLoader = NameSpaceTest1.class.getClassLoader();
Class<?> aClass = classLoader.loadClass("com.morris.jvm.classloader.HelloWorld");
Class<?> bClass = classLoader.loadClass("com.morris.jvm.classloader.HelloWorld");
System.out.println(aClass == bClass); // true
}
}
使用同一个类加载器实例不论load多少次,都只会返回同一个class对象。
不同类加载器加载同一个class
package com.morris.jvm.classloader;
public class NameSpaceTest2 {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = new MyClassLoader();
ClassLoader classLoader2 = new BreakDelegateClassLoader();
Class<?> aClass = classLoader.loadClass("com.morris.jvm.classloader.HelloWorld");
Class<?> bClass = classLoader2.loadClass("com.morris.jvm.classloader.HelloWorld");
System.out.println(aClass == bClass); // false
}
}
使用不同的类加载器实例加载同一个class,会在堆内存产生多个class对象。
相同类加载器加载同一个class
package com.morris.jvm.classloader;
public class NameSpaceTest3 {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = new MyClassLoader();
ClassLoader classLoader2 = new MyClassLoader();
Class<?> aClass = classLoader.loadClass("com.morris.jvm.classloader.HelloWorld");
System.out.println(aClass.getClassLoader());
Class<?> bClass = classLoader2.loadClass("com.morris.jvm.classloader.HelloWorld");
System.out.println(bClass.getClassLoader());
System.out.println(aClass == bClass); // false
}
}
使用相同的类加载器实例加载同一个class,会在堆内存产生多个class对象。
更多精彩内容关注本人公众号:架构师升级之路