java类加载-ClassLoader双亲委派机制
“类加载体系”及ClassLoader双亲委派机制。java程序中的 .java文件编译完会生成 .class文件,而 .class文件就是通过被称为类加载器的ClassLoader加载的,而ClassLoder在加载过程中会使用“双亲委派机制”来加载 .class文件,先上图:
看着图从上往下介绍:
-
BootStrapClassLoader:启动类加载器,该ClassLoader是jvm在启动时创建的,用于加载 $JAVA_HOME/jre/lib下面的类库(或者通过参数-Xbootclasspath指定)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不能直接通过引用进行操作。
-
ExtClassLoader:扩展类加载器,该ClassLoader是在sun.misc.Launcher里作为一个内部类ExtClassLoader定义的(即 sun.misc.Launcher$ExtClassLoader),ExtClassLoader会加载 $JAVA_HOME/jre/lib/ext下的类库(或者通过参数-Djava.ext.dirs指定)。
-
AppClassLoader:应用程序类加载器,该ClassLoader同样是在sun.misc.Launcher里作为一个内部类AppClassLoader定义的(即 sun.misc.Launcher$AppClassLoader),AppClassLoader会加载java环境变量CLASSPATH所指定的路径下的类库,而CLASSPATH所指定的路径可以通过System.getProperty("java.class.path")获取;当然,该变量也可以覆盖,可以使用参数-cp,例如:java -cp 路径 (可以指定要执行的class目录)。
-
CustomClassLoader:自定义类加载器,该ClassLoader是指我们自定义的ClassLoader,比如tomcat的StandardClassLoader属于这一类;当然,大部分情况下使用AppClassLoader就足够了。
ClassLoader初始化源码
下面贴下jdk关于类加载的源码,上述四种类加载器中CustomClassLoader是用户自定义的,BootStrapClassLoader是jvm创建的,就不展示了;这里展示下AppClassLoader和ExtClassLoader的启动过程,前面介绍过,AppClassLoader和ExtClassLoader都是在sun.misc.Launcher里定义的,而我的sun.misc.Launcher没有源码,大家将就看看反编译的代码吧。如果想看sun.*包下的类源码,大家可以下载openjdk来查看。
1 public Launcher(){ 2 ExtClassLoader extclassloader; 3 try{ 4 extclassloader = ExtClassLoader.getExtClassLoader(); 5 } 6 catch(IOException ioexception) { 7 throw new InternalError("Could not create extension class loader"); 8 } 9 try{ 10 loader = AppClassLoader.getAppClassLoader(extclassloader); 11 } 12 catch(IOException ioexception1){ 13 throw new InternalError("Could not create application class loader"); 14 } 15 Thread.currentThread().setContextClassLoader(loader); 16 String s = System.getProperty("java.security.manager"); 17 if(s != null){ 18 SecurityManager securitymanager = null; 19 if("".equals(s) || "default".equals(s)) 20 securitymanager = new SecurityManager(); 21 else 22 try{ 23 securitymanager = (SecurityManager)loader.loadClass(s).newInstance(); 24 } 25 catch(IllegalAccessException illegalaccessexception) { } 26 catch(InstantiationException instantiationexception) { } 27 catch(ClassNotFoundException classnotfoundexception) { } 28 catch(ClassCastException classcastexception) { } 29 if(securitymanager != null) 30 System.setSecurityManager(securitymanager); 31 else 32 throw new InternalError((new StringBuilder()).append("Could not create SecurityManager: ").append(s).toString()); 33 } 34 }
可以看到在Launcher构造函数的执行过程如下:
-
通过ExtClassLoader.getExtClassLoader()创建了ExtClassLoader;
-
通过AppClassLoader.getAppClassLoader(ExtClassLoader)创建了AppClassLoader,并将ExtClassLoader设为AppClassLoader的parent ClassLoader;
-
通过Thread.currentThread().setContextClassLoader(loader)把AppClassLoader设为线程的上下文 ClassLoader;
-
根据jvm参数-Djava.security.manager创建安全管理器(安全管理器的相关内容会在后续博客安全管理器及Java API中介绍),此时jvm会设置系统属性"java.security.manager"为空字符串""。
再贴下ExtClassLoader源码:
1 static class ExtClassLoader extends URLClassLoader { 2 private File[] dirs; 3 4 public static ExtClassLoader getExtClassLoader() throws IOException { 5 // 用调getExtDirs()方法取获配置的扩展类路径 6 final File[] dirs = getExtDirs(); 7 try { 8 // 应用getExtDirs()方法返回的路径生成一个新的ClassLoader实例 9 return (ExtClassLoader) AccessController.doPrivileged(new PrivilegedExceptionAction() { 10 public Object run() throws IOException { 11 int len = dirs.length; 12 for (int i = 0; i < len; i++) { 13 MetaIndex.registerDirectory(dirs[i]); 14 } 15 return new ExtClassLoader(dirs); 16 } 17 }); 18 } catch (java.security.PrivilegedActionException e) { 19 throw (IOException) e.getException(); 20 } 21 } 22 23 24 // 再看这个方法 25 private static File[] getExtDirs() { 26 // 取获配置的扩展类路径 27 String s = System.getProperty("java.ext.dirs"); 28 File[] dirs; 29 if (s != null) { 30 StringTokenizer st = new StringTokenizer(s, File.pathSeparator); 31 int count = st.countTokens(); 32 dirs = new File[count]; 33 for (int i = 0; i < count; i++) { 34 dirs[i] = new File(st.nextToken()); 35 } 36 } else { 37 dirs = new File[0]; 38 } 39 return dirs; 40 } 41 42 // 其他码代略 43 ... 44 }
反编译的源码,大家将就看下;这里大家关注下getExtDirs()这个方法,它会获取属性"java.ext.dirs"所对应的值,然后通过系统分隔符分割,然后加载分割后的字符串对应的目录作为ClassLoader的类加载库。
下面看看AppClassLoader源码:
1 public static ClassLoader getAppClassLoader(ClassLoader classloader) throws IOException{ 2 String s = System.getProperty("java.class.path"); 3 File afile[] = s != null ? Launcher.getClassPath(s) : new File[0]; 4 return (AppClassLoader)AccessController.doPrivileged(new PrivilegedAction(s, afile, classloader) { 5 public Object run() { 6 URL aurl[] = s != null ? Launcher.pathToURLs(path) : new URL[0]; 7 return new AppClassLoader(aurl, extcl); 8 } 9 10 final String val$s; 11 final File val$path[]; 12 final ClassLoader val$extcl; 13 14 { 15 s = s1; 16 path = afile; 17 extcl = classloader; 18 super(); 19 } 20 }); 21 }
首先获取"java.class.path"对应的属性,并转换为URL[]并设置为ClassLoader的类加载库,注意这里的方法入参classloader就是ExtClassLoader,在创AppClassLoader会传入ExtClassLoader作为parent ClassLoader。
上面就是ClassLoader的启动和初始化过程,后面会把loader作为应用程序的默认ClassLoader使用,看下面的测试用例:
1 public static void main(String... args) { 2 ClassLoader loader = Test.class.getClassLoader(); 3 System.err.println(loader); 4 while (loader != null) { 5 loader = loader.getParent(); 6 System.err.println(loader); 7 } 8 }
可以看到ClassLoader的层次结构,输出结果为:
ClassLoader双亲委派机制源码
前面谈到了ClassLoader的几类加载器,而ClassLoader使用双亲委派机制来加载class文件的。
ClassLoader的双亲委派机制是这样的(这里先忽略掉自定义类加载器CustomClassLoader):
-
当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
-
当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
-
如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
-
若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
下面贴下ClassLoader的loadClass(String name, boolean resolve)源码:
1 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 2 // First, check if the class has already been loaded 3 Class c = findLoadedClass(name); 4 if (c == null) { 5 try { 6 if (parent != null) { 7 c = parent.loadClass(name, false); 8 } else { 9 c = findBootstrapClassOrNull(name); 10 } 11 } catch (ClassNotFoundException e) { 12 // ClassNotFoundException thrown if class not found 13 // from the non-null parent class loader 14 } 15 if (c == null) { 16 // If still not found, then invoke findClass in order 17 // to find the class. 18 c = findClass(name); 19 } 20 } 21 if (resolve) { 22 resolveClass(c); 23 } 24 return c; 25 }
代码很明朗:首先找缓存(findLoadedClass),没有的话就判断有没有parent,有的话就用parent来递归的loadClass,然而ExtClassLoader并没有设置parent,则会通过findBootstrapClassOrNull来加载class,而findBootstrapClassOrNull则会通过JNI方法”private native Class findBootstrapClass(String name)“来使用BootStrapClassLoader来加载class。
然后如果parent未找到class,则会调用findClass来加载class,findClass是一个protected的空方法,可以覆盖它以便自定义class加载过程。
另外,虽然ClassLoader加载类是使用loadClass方法,但是鼓励用 ClassLoader 的子类重写 findClass(String),而不是重写loadClass,这样就不会覆盖了类加载默认的双亲委派机制。
双亲委派机制为什么安全
前面谈到双亲委派机制是为了安全而设计的,但是为什么就安全了呢?举个例子,ClassLoader加载的class文件来源很多,比如编译器编译生成的class、或者网络下载的字节码。而一些来源的class文件是不可靠的,比如我可以自定义一个java.lang.Integer类来覆盖jdk中默认的Integer类,例如下面这样:
1 package java.lang; 2 3 /** 4 * hack 5 */ 6 public class Integer { 7 public Integer(int value) { 8 System.exit(0); 9 } 10 }
初始化这个Integer的构造器是会退出JVM,破坏应用程序的正常进行,如果使用双亲委派机制的话该Integer类永远不会被调用,以为委托BootStrapClassLoader加载后会加载JDK中的Integer类而不会加载自定义的这个,可以看下下面这测试个用例:
1 public static void main(String... args) { 2 Integer i = new Integer(1); 3 System.err.println(i); 4 }
执行时JVM并未在new Integer(1)时退出,说明未使用自定义的Integer,于是就保证了安全性。