jvm如何打破双亲委托机制

x33g5p2x  于2021-12-18 转载在 其他  
字(5.3k)|赞(0)|评价(0)|浏览(323)

打破双亲委托机制

重写父类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对象。

相关文章