一、文章来由
来阿里玩Java也有一个多月了,一直对Java虚拟机比较感兴趣,而ClassLoader是整个class载入过程中很重要的组件。而classloader有个双亲委派模型,师兄说这个模型不能破坏,于是打赌一试。
相信如果问:为什么要双亲委派,可能有人可以侃侃而谈,但是说到为什么要这么分层,为什么要分三层,如何绕过双亲委派模型。。。
这就不是那么容易了,这个时候就需要一些专研了。
二、classloader的作用
这个问题我问了师兄:加载+连接的所有过程,但是深入理解Java虚拟机说的不太一样(所以有待考证)
请原谅我贴图,但下面两张图字字珠玑(p228):
classloader虽然只用于实现类的加载动作,但在Java程序中作用却远远不限于类加载阶段,也就是后面说的可以决定类。
三、为什么要3个classloader
我个人认为有两个原因,当然可能不止。
1、是为了安全
http://stackoverflow.com/questions/28011224/what-is-the-reason-for-having-3-class-loaders-in-java
The reason for having the three basic class loaders (Bootstrap, extension, system) is mostly security.
A key concept is the fact that the JVM will not grant package access (the access that methods and fields have if you didn’t specifically mention private, public or protected) unless the class that asks for this access comes from the same class loader that loaded the class it wishes to access.
So, suppose a user calls his class java.lang.MyClass. Theoretically, it could get package access to all the fields and methods in the java.lang package and change the way they work. The language itself doesn’t prevent this. But the JVM will block this, because all the real java.lang classes were loaded by bootstrap class loader. Not the same loader = no access.
There are other security features built into the class loaders that make it hard to do certain types of hacking.
So why three class loaders? Because they represent three levels of trust. The classes that are most trusted are the core API classes. Next are installed extensions, and then classes that appear in the classpath, which means they are local to your machine.
For a more extended explanation, refer to Bill Venners’s “Inside the Java Virtual Machine”.
2、另外,这个帖子没有提到的应该是隔离。
java的所有类都是由classloader加载的,不同classloader之间加载的类彼此是不可见的。tomcat加载了log4j,容器里servlet也加载了log4j,servlet是看不见tomcat加载的log4j类的,反之亦然。
在深入理解Java虚拟机,p278,提到:
tomcat为了支持权限目录结构,对目录中的类库进行加载和隔离,tomcat自定义了多个类加载器。
也说明了这点。
四、尝试绕过双亲委派
深入理解Java虚拟机,p231,写到:
双亲委派模型在jdk1.2引入,但它不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载方式。
换句话说,也就是可以不用这个模型的,自己实现类加载就可以,毕竟类加载器的原始作用就是:“通过类的全限定名得到类的二进制码流”
来看看双亲委派模型是如何实现的:
/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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 (resolve) {
resolveClass(c);
}
return c;
}
}
逻辑很清晰,优先给自己的parent加载器加载,特别注意,这里的父加载器,不是类的继承,因为三个classloader都是内存对象,所以他们只是逻辑上的父子关系。
(1)bootstrap classloader是native实现的
(2)extclassloader 和 APPclassloader 都是 URLclassloader 的子类对象
其实我想做的事情很简单,就是尝试自己写一个完全不依靠双亲委派的classloader,但是因为编码量比较大,所以我只尝试绕过APPclassloader,让自己的加载器继承(再次说明是逻辑上继承)于extclassloader
上面的代码已经显示,如果parent加载器没办法加载,就找子classloader的findclass方法,但是我想破坏这个模型,就必须重写classloader的loadclass方法
上代码:
package classloader;
import java.io.*;
/**
* Created by hupo.wh on 2016/7/18.
*/
public class OverwriteClassLoader extends ClassLoader {
private String rootDir = "d:\\";
public Class<?> loadClass(String name)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findClass(name);
return c;
}
}
private byte[] getClassData(String className) {
//String path = classNameToPath(className);
String path = "D:\\xiaohua\\WhTest\\target\\classes\\helloworld\\HelloWorld.class";
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
System.out.println("name == "+name);
return defineClass(name, classData, 0, classData.length);
}
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
public OverwriteClassLoader(ClassLoader classLoader) {
super(classLoader);
}
protected final Class<?> whdefineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass("helloworld.HelloWorld", b, off, len, null);
}
}
/Main.java
class Main{
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader extcl =new Object(){}.getClass().getEnclosingClass().getClassLoader();
while(extcl.getParent()!=null){
extcl=extcl.getParent();
}
System.out.println("extcl == "+extcl);
System.out.println("overwriteclassloader == "+OverwriteClassLoader.class.getClassLoader());
OverwriteClassLoader cl = new OverwriteClassLoader(extcl);
Class<?> clazz = cl.loadClass("helloworld.HelloWorld");
System.out.println(clazz.getClassLoader());
}
}
然而我的程序止步于一个地方:
详细跟断点进去,发现这个地方本来我要加载,helloworld.HelloWorld类,但是加载java.lang.Object类的时候,一个native方法又调回了我的classloader,进入第三部分的第1小部分。
SecurityException: Prohibited package name: java.lang
- 这又说明了一个问题,classloader去load这个类的父类,也是找我这个类,但是我这个类的loadclass没有双亲委派,同时安全检查又是用的classloader这个类内置的,所以通不过。
后来发现这段代码实际上是有问题的,因为我把InputStream写死了,下面代码才是正确的
其实这个时候,我自己加载的类已经绕过双亲委派了,因为自己这个类是没有去查父亲的,于是有了下面这个更极端的测试~~
package classloader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Created by hupo.wh on 2016/7/20.
*/
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) {
try {
InputStream is = null;
if(name == "helloworld.HelloWorld") {
is = new FileInputStream("D:\\xiaohua\\WhTest\\target\\classes\\helloworld\\HelloWorld.class");
}
else {
is = new FileInputStream("D:\\lang\\Object.class");
//return super.loadClass(name);
}
byte [] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
};
Class<?> clazz = myLoader.loadClass("helloworld.HelloWorld");
System.out.println(clazz.getClassLoader());
}
}
我将rt.jar中的java.lang解压在d盘了,当然还是会报那个错误。。。
于是这样也会报错:
package java.lang;
/**
* Created by hupo.wh on 2016/7/20.
*/
public class sayHello {
public static void main(String[] args) {
System.out.println("hello");
}
}
当然也是有方法的,就是完全自己实现classloader这个类,不继承于任何东西,这样的话,jvm也拿你没办法了。
附上深入理解Java虚拟机正确运行源码(p228)
package classloader;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* Created by hupo.wh on 2016/7/20.
*/
public class ClassLoaderTest2 {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) {
try {
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte [] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
};
Class<?> clazz = myLoader.loadClass("helloworld.HelloWorld");
System.out.println(clazz.getClassLoader());
Class<?> clazz1 = myLoader.loadClass("org.omg.CORBA.Any");
System.out.println(clazz1.getClassLoader());
}
}
参考资料
[1] 深入理解Java虚拟机