Java 中classloader 无时无刻都在使用,但是其中一些细节一直没有思考清楚。
jdk默认classloader
-
Bootstrap ClassLoader(加载JDK的/lib目录下的类)
-Xbootclasspath:<path> 替换bootclasspath为<path>
-Xbootclasspath/a:<path> 将<path> 追加到bootclasspath之后
-Xbootclasspath/p:<path> 将<path> 附加到bootclasspath之前 -
Extension ClassLoader(加载JDK的/lib/ext目录下的类)
-
Application ClassLoader(程序自己classpath下的类)
也就是-cp --classpath指定的路径的类
自定义classloader
自定义classloader 的话主要是实现 findClass 这个模板方法。
类似
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
其中一般主要区别是这个byte[] b的获取,可以是网络流中读取,亦或者是磁盘文件、hdfs等等。
打破双亲委派
先说为什么要有双亲委派,因为安全性,不想默认的bootstrap path下的类被用户自定义的同限定名类覆盖,比如java.lang.String这个常用类。
确保获取到的String.class总是bootstrapPath下面的那个String
打破双亲委派很简单,不是去实现findClass这个模板模式的方法,转而去实现loadClass这个主调入口方法。
为什么要打破双亲委派
这个一般出现在开发框架,然后去加载用户上传的jar包中,比如spark flink此类都有自己的类加载器,如果不用自定义类加载器,以spark为例,他的一个executor启动时候用默认的classloader,你要这样去启动
java -cp $spark.jar:...:$user.jar... Main.class
一般来说,spark中和用户程序可能会存在相同的类库的不同版本,比如他们包名相同,此时加载时候,写在命令行前面的就会覆盖后面的。
spark中自定义了类加载器,就能解决这个问题,其中就打破了parent优先。
具体如何设置,就是通过
Thread.currentThread().setContextClassLoader()
这样task线程执行用户代码获取到的class,就与框架运行所需的同名class来自不同的类加载器。就算有同包名的类也不会冲突。
instanceOf
instanceOf 用来判断两个对象的类的继承关系,如果是不同类加载器加载的类,也是会返回false。
类似的情况还包括我们在类型强转出现ClassCastException,用==比较class实例的时候。
一般这时候就要排查下是否是类加载器的问题,可以打印
Class.getClassLoader()
来确认。
框架中的坑
某些解析框架使用时候有这样一组方法,比如SnakeYaml
Yaml.parse(String input,String clazz,ClassLoader classloader)
如果用户自定义classloader,SnakeYaml内部就无法根据类限定名获取Class实例,因为他只能获取当前线程的classLoader。
网友评论