起源来自于网易面试官的一个问题,一个类的静态块是否可能被执行两次。众所周知类加载的初始化阶段会自动收集类中所有类变量的赋值动作与静态语句块中的语句生成一个()方法,这个方法只会被执行一次。因此通常的理解类的静态语句块只会被执行一次。但感觉事情应该不会那么简单,在虚拟机中区分类是由类本身与加载类的类加载器决定的,猜想,同一个类被不同的类加载器加载,会执行两次静态块吗.?
失败的尝试
由于需要使用不同的类加载器加载类,所以自定义了一个类加载器从指定的目录下加载类。代码如下:
public class TestStaticBlock {
static{
System.out.println("static block init");
}
@Test
public void test(){
//new TestStaticBlock();
Class<?> class0 = TestStaticBlock.class;
try {
System.out.println(class0.getClassLoader() instanceof MyClassLoader);
Class<?> class1 = class0.getClassLoader().loadClass("classloader.TestStaticBlock");
ClassLoader classLoader = new MyClassLoader();
Class<?> class2 = classLoader.loadClass("TestStaticBlock");
System.out.println(class1.hashCode());
System.out.println(class2.hashCode());
System.out.println(class1.equals(class2));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//自定义一个类加载器从指定磁盘目录加载类
public class MyClassLoader extends ClassLoader {
//不破坏双亲委派模型
@Override
protected Class<?> findClass(String name) {
String myPath = "D:/myeclipseworkspace/class/" + name.replace(".","/") + ".class";
System.out.println(myPath);
byte[] classBytes = null;
FileInputStream in = null;
try {
File file = new File(myPath);
in = new FileInputStream(file);
classBytes = new byte[(int) file.length()];
in.read(classBytes);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
Class<?> clazz = defineClass(name, classBytes, 0, classBytes.length);
return clazz;
}
}
}
System.out.println(class1.equals(class2));猜猜输出的结果是什么?答案居然是true!这不是违背了我们平时的认知吗?被不同的类加载器加载的类不应该是不同的类吗?机智的博主很快想到了可能是双亲委派模型在作祟,先让我们看一看ClassLoader中loadClass()的源码:
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;
}
}
看看打印的结果
System.out.println(classLoader.getParent());
System.out.println(class0.getClassLoader());
sun.misc.Launcher$AppClassLoader@6d06d69c
sun.misc.Launcher$AppClassLoader@6d06d69c
当当当当,发现我们虽然重写了ClassLoader的findClass()方法,但是并没有打破双亲委派模型。使用自定义类加载器加载TestStaticBlock最后还是被转发到了父类加载器,而从输出结果可以看出这个父类加载器就是class0.getClassLoader()。当然加载出来的类也会是同一个类。
打破双亲委派模型
那么就没有办法打破双亲委派模型吗?结果当然是false。只需要重写ClassLoader类的loadClass()方法。
//破坏双亲委派模型
@Override
public Class<?> loadClass(String name)
throws ClassNotFoundException {
String myPath = "D:/myeclipseworkspace/class/" + name.replace(".","/") + ".class";
System.out.println(myPath);
byte[] classBytes = null;
FileInputStream in = null;
try {
File file = new File(myPath);
in = new FileInputStream(file);
classBytes = new byte[(int) file.length()];
in.read(classBytes);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println();
Class<?> clazz = defineClass(name, classBytes, 0, classBytes.length);
return clazz;
}
以为这就结束了吗?naive。让我们看看运行结果:
java.io.FileNotFoundException: D:\myeclipseworkspace\class\java\lang\Object.class (系统找不到指定的路径。)
由于我们打破了双亲委派模型,所以父类的加载(Object)也会交由我们自自定义的类加载器加载。而很明显在我们自定义的加载目录下是不会有Object.class这个文件的。
总结
如果不想打破双亲委派模型,就重写ClassLoader类中的findClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。而如果想打破双亲委派模型则需要重写loadClass()方法(当然其中的坑也不会少)。典型的打破双亲委派模型的框架和中间件有tomcat与osgi,如果相对java的类加载过程有更深入的了解学习这两个框架的源码会是不错的选择。