使用自定义类加载器加载java.lang.String

前几天跟同事聊怎么用自定义类加载器加载java.lang.String的问题,正好又遇到一个类加载器的问题,决定花点时间研究一下。

在查看源码研究的过程中,我发现很多人都有个误区:双亲委派机制不能被打破,不能使用自定义类加载器加载java.lang.String,也是由于这个原因。

但是事实上并不是,只要重写ClassLoader的loadClass()方法,就能打破了。
如下是我写的一个简单的自定义类加载器:

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class MyClassLoader extends URLClassLoader {

    public MyClassLoader(URL[] urls) {
        super(urls);
    }
    
    @Override
    public Class loadClass(String name) throws ClassNotFoundException {
    	//只对MyClassLoader和String使用自定义的加载,其他的还是走双亲委派
        if(name.equals("MyClassLoader") || name.equals("java.lang.String")) {
            return super.findClass(name);
        } else {
            return getParent().loadClass(name);
        }
    }

    public static void main(String[] args) throws Exception {
    	//urls指定自定义类加载器的加载路径
        URL url = new File("J:/apps/demo/target/classes/").toURI().toURL();
        URL url3 = new File("C:/Program Files/Java/jdk1.8.0_191/jre/lib/rt.jar").toURI().toURL();
        URL[] urls = {
                url
                , url3
        };
        MyClassLoader myClassLoader = new MyClassLoader(urls);

        Class c1 = MyClassLoader.class.getClassLoader().loadClass("MyClassLoader");
        Class c2 = myClassLoader.loadClass("MyClassLoader");
        System.out.println(c1 == c2); //false
        System.out.println(c1.getClassLoader()); //AppClassLoader
        System.out.println(c2.getClassLoader()); //MyClassLoader

        System.out.println(myClassLoader.loadClass("java.lang.String")); //Exception 
    }

}

输出结果如下:

false
sun.misc.Launcher$AppClassLoader@18b4aac2
MyClassLoader@3feba861
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.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
	at MyClassLoader.loadClass(MyClassLoader.java:14)
	at MyClassLoader.main(MyClassLoader.java:35)

加载同一个类MyClassLoader,使用的类加载器不同,说明我这里是打破了双亲委派机制的,但是尝试加载String类的时候报错了。
使用自定义类加载器加载java.lang.String_第1张图片

看代码是ClassLoader类里面的限制,只要加载java开头的包就会报错。所以真正原因是JVM安全机制,并不是因为双亲委派。

https://www.cnblogs.com/idea360/p/12377464.html

这篇文章比较详细的讲解了类加载器的一些机制,但是最后在JVM安全机制这里就没下文了。难道真的就没办法了吗?

看代码既然是ClassLoader里面的代码做的限制,那把ClassLoader.class修改了不就好了吗。

抱着试一试的心态,我自己写了个java.lang.ClassLoader,把preDefineClass()方法里那段if直接删掉,再用编译后的class替换rt.jar里面的,直接通过命令jar uvf rt.jar java/lang/ClassLoader/class即可。

不过事与愿违,修改之后还是报错:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
	at com.example.demo.mini.test.MyClassLoader.loadClass(MyClassLoader.java:17)
	at com.example.demo.mini.test.MyClassLoader.main(MyClassLoader.java:31)

仔细看报错和之前的不一样了,这次是native方法报错了。
这就比较难整了,看来要自己重新编译个JVM才行了。理论上来说,编译JVM的时候把校验的代码去掉就行了。

正好之前自己编译过JVM,很多东西都是现成的,说干就干。找到defineClass1的native代码,进一步定位到systemDictionary.cpp的resolve_from_stream()方法:
使用自定义类加载器加载java.lang.String_第2张图片
直接把这里的if整个干掉。

顺手找到jdk/src/share/classes/java/lang/ClassLoader.java类,把这个里面的if也干掉。然后重新编译。

定位代码的过程比较繁琐,这里推荐一篇文章:
https://www.dazhuanlan.com/2019/11/19/5dd2e596bc024/

使用新编译的jdk测试MyClassLoader:
使用自定义类加载器加载java.lang.String_第3张图片
可以看到String类的classLoader是MyClassLoader了,验证成功。

结论:自定义类加载器加载java.lang.String,必须修改jdk的源码,自己重新编译个JVM才行。



顺便说下编译JVM过程中遇到的一个坑
编译的时候报错:

## Starting jdk
Compiling 9417 files for BUILD_JDK
Killed
CompileJavaClasses.gmk:316: recipe for target '/data/github/jdk-jdk8-b120/build/linux-x86_64-normal-server-release/jdk/classes/_the.BUILD_JDK_batch' failed
make[2]: *** [/data/github/jdk-jdk8-b120/build/linux-x86_64-normal-server-release/jdk/classes/_the.BUILD_JDK_batch] Error 137
BuildJdk.gmk:64: recipe for target 'classes-only' failed
make[1]: *** [classes-only] Error 2
/data/github/jdk-jdk8-b120//make/Main.gmk:115: recipe for target 'jdk-only' failed
make: *** [jdk-only] Error 2

本来我前几次编译都成功了,后面开始编译就一直报这个错,根据信息完全看不出是哪里问题。
http://freebsd.1045724.x6.nabble.com/Failure-compiling-java-openjdk8-td6093672.html
然后看到这篇文章说可能是内存不足,试着把虚拟机里面的vscode关掉,果然就编译通过了。

你可能感兴趣的:(备忘,技术研究,jvm)