前言
我们上一章节讨论了字节码的一些分布,具体的请看深入JVM字节码(1) Java Class 详解 ,字节码是对象生成的一个模板,本节将会从个源码分析来剖析一下,JVM是怎样加载类到内存的。
ClassLoader
JAVA类加载流程
Java语言系统自带有三个类加载器:
Bootstrap ClassLoader 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap
ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。
Extention ClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
Appclass Loader 也称为SystemAppClass 加载当前应用的classpath的所有类。
JAVA 类加载顺序
Bootstrap CLassloder
Extention ClassLoader
AppClassLoader
Java类都是通过java.lang.ClassLoader的某个实例来加载的,那么到底是谁来加载java.lang.ClassLoader呢,答案就是Bootstrap Class Loader。
Bootstrap ClassLoader用于加载JDK内部类,如rt.jar和其他JRE中lib目录的Java类库中的类。Bootstrap ClassLoader充当所有其他ClassLoader实例的父级。
Bootstrap ClassLoader是JVM核心的一部分,由原生代码编写,不同的平台可能会有不同的实现
AppClassLoader 和 Extention ClassLoader
系统类加载器,是指 Sun公司实现的sun.misc.Launcher$AppClassLoader
。它负责加载系统类路径java -classpath
或-D java.class.path
指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()
方法可以获取到该类加载器。
我们来看 ClassLoader.getSystemClassLoader
的代码。
这是返回系统加载器的方法,该方法会在JVM启动早期被调用,主要是用来初始化 AppClassLoader
加载器。
并将系统类加载器放到当前线程的上下文当中。 返回是是scl
1 2 3 4 5 6 7 8 9 10 11 12 13 @CallerSensitive public static ClassLoader getSystemClassLoader () { initSystemClassLoader(); if (scl == null ) { return null ; } SecurityManager sm = System.getSecurityManager(); if (sm != null ) { checkClassLoaderPermission(scl, Reflection.getCallerClass()); } return scl; }
接下来我们看 scl 的初始化是在initSystemClassLoader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private static synchronized void initSystemClassLoader () { sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); if (l != null ) { Throwable oops = null ; scl = l.getClassLoader(); try { scl = AccessController.doPrivileged( new SystemClassLoaderAction(scl)); } catch (PrivilegedActionException pae) { oops = pae.getCause(); if (oops instanceof InvocationTargetException) { oops = oops.getCause(); } } } }
scl 的生成过程我们要看 SystemClassLoaderAction
的run 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public ClassLoader run () throws Exception { String cls = System.getProperty("java.system.class.loader" ); if (cls == null ) { return parent; } Constructor<?> ctor = Class.forName(cls, true , parent) .getDeclaredConstructor(new Class<?>[] { ClassLoader.class }) ; ClassLoader sys = (ClassLoader) ctor.newInstance( new Object[] { parent }); Thread.currentThread().setContextClassLoader(sys); return sys; }
这里会不会有个疑问,我们所说的 AppClassLoader在哪里出现的呢?别着急,我们接下来来看 Launcher
,因为ClassLoader 就是从这里出现的。
通过上面的分析,我们知道, 系统类的加载器是从 Launcher 中拿到的,接下来我们来看sun.misc.Launcher
,它是一个java虚拟机的入口应用。 但是在我们OpenJDK home 目录下 src.zip 源码包中是不全的,因此我们需要看完整的JDK实现。
这里推荐几个扩展阅读:
我们接下来主要看一下 sun.misc.Launcher
的实现。原本笔者是想打断开来看一下加载过程的,但是断点失败了,所有去查了一下相关资料:
OpenJDK 对于 Launcher 类的描述:This class is used by the system to launch the main application.system 。
于是我又翻了翻 IBM 关于 Java 中 Debug 实现原理的介绍,文章地址如下:https://www.ibm.com/developerworks/cn/java/j-lo-jpda1/
文章中说到:JDI(Java Debug Interface)是三个模块中最高层的接口,在多数的 JDK 中,它是由 Java 语言实现的。参考 Oracle 的官方文档:https://docs.oracle.com/javase/9/docs/api/jdk.jdi-summary.html
可以知道 jdi 是一个位于 tools.jar 包下的子包,而 tools.jar 也是由 BootStrap 类加载器负责加载的。
所以现在我们可以知道了,为 Java 提供 Debug 支持的类加载和 Launcher 的类加载都是由 Bootstrap 类加载器负责的,只是后者先发生,所以 debug 功能实现的时候,Launcher 的构造器早已运行结束了。
Launcher
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class Launcher { private static Launcher launcher = new Launcher(); private static String bootClassPath = System.getProperty("sun.boot.class.path" ); public static Launcher getLauncher () { return launcher; } private ClassLoader loader; public Launcher () { ClassLoader extcl; extcl = ExtClassLoader.getExtClassLoader(); loader = AppClassLoader.getAppClassLoader(extcl); Thread.currentThread().setContextClassLoader(loader); String s = System.getProperty("java.security.manager" ); } public ClassLoader getClassLoader () { return loader; }
Launcher 是整个应用程序的入口启动类,主要是初始化了ExtClassLoader和AppClassLoader。
首先我们来看 ExtClassLoader 的源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static ExtClassLoader getExtClassLoader () throws IOException { final File[] dirs = getExtDirs(); return AccessController.doPrivileged( new PrivilegedExceptionAction<ExtClassLoader>() { public ExtClassLoader run () throws IOException { int len = dirs.length; for (int i = 0 ; i < len; i++) { MetaIndex.registerDirectory(dirs[i]); } return new ExtClassLoader(dirs); } }); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static ClassLoader getAppClassLoader (final ClassLoader extcl) throws IOException { final String s = System.getProperty("java.class.path" ); final File[] path = (s == null ) ? new File[0 ] : getClassPath(s); return AccessController.doPrivileged( new PrivilegedAction<AppClassLoader>() { public AppClassLoader run () { URL[] urls = (s == null ) ? new URL[0 ] : pathToURLs(path); return new AppClassLoader(urls, extcl); } }); }
总结
上文提到的 Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,
JVM启动时通过 Bootstrap 类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。
然后呢,我们前面已经分析了,JVM初始化sun.misc.Launcher并创建Extension ClassLoader
和AppClassLoader
实例。并将ExtClassLoader
设置为AppClassLoader
的父加载器。Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。比如ExtClassLoader。
双亲委托机制
一个类加载器查找class和resource时,是通过委托模式
进行的,它首先判断这个class
是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader
,如果Bootstrap classloader
找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。
这种双亲委派模式的好处,一个可以避免类的重复加载,另外也避免了java的核心API被篡改。
一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
递归,重复第1部的操作。
如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。
我们还需要了解几个个重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。
类加载过程机器双亲委托实现原理
我们先看这几个方法的层级
loadClass(String)
AppClassLoader 的loadClass 直接调用的是 ClassLoader 的loadClass 方法
该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { 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) { } if (c == null ) { c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected final Class<?> findLoadedClass(String name) { if (!checkName(name)) return null ; return findLoadedClass0(name); } private boolean checkName (String name) { if ((name == null ) || (name.length() == 0 )) return true ; if ((name.indexOf('/' ) != -1 ) || (!VM.allowArraySyntax() && (name.charAt(0 ) == '[' ))) return false ; return true ; } private native final Class findLoadedClass0 (String name) ;
我们发现findLoadedClass0是一个native方法。查了下openjdk源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 JNIEXPORT jclass JNICALL Java_java_lang_ClassLoader_findLoadedClass0(JNIEnv *env, jobject loader, jstring name) { if (name == NULL) { return 0 ; } else { return JVM_FindLoadedClass(env, loader, name); } } JVM_ENTRY(jclass, JVM_FindLoadedClass(JNIEnv *env, jobject loader, jstring name)) JVMWrapper("JVM_FindLoadedClass" ); ResourceMark rm (THREAD) ; Handle h_name (THREAD, JNIHandles::resolve_non_null(name) ) ; Handle string = java_lang_String::internalize_classname(h_name, CHECK_NULL); const char * str = java_lang_String::as_utf8_string(string()); if (str == NULL) return NULL; const int str_len = (int )strlen(str); if (str_len > Symbol::max_length()) { return NULL; } TempNewSymbol klass_name = SymbolTable::new_symbol(str, str_len, CHECK_NULL); Handle h_loader (THREAD, JNIHandles::resolve(loader) ) ; if (UsePerfData) { is_lock_held_by_thread(h_loader, ClassLoader::sync_JVMFindLoadedClassLockFreeCounter(), THREAD); } klassOop k = SystemDictionary::find_instance_or_array_klass(klass_name, h_loader, Handle(), CHECK_NULL); return (k == NULL) ? NULL : (jclass) JNIHandles::make_local(env, Klass::cast(k)->java_mirror()); JVM_END }
这段看不懂没关系,我会在JVM的启动的时候,再次分析,这里只需要知道是JVM负责查找Class就好了。
接着的逻辑就是类的双亲加载机制的实现。把类加载的任务交给父类加载器执行,直到父类加载器为空,此时会返回通过JDK提供的系统启动类加载器加载的类。
我们发现 findBootstrapClassOrNull 也是个native方法
1 2 3 4 5 6 7 8 9 private Class<?> findBootstrapClassOrNull(String name){ if (!checkName(name)) return null ; return findBootstrapClass(name); } private native Class<?> findBootstrapClass(String name);
由于篇幅有限,我们这里就不在介绍 findBootstrapClass 的jdk 源码了。
findClass
该方法和它的名字一样,就是根据类的名字ClassLoader不提供该方法的具体实现,要求我们根据自己的需要来覆写该方法。
所以我们可以看一看URLClassLoader对findClass方法的实现,类加载的工作又被代理给了defineClass方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 protected Class<?> findClass(final String name) throws ClassNotFoundException { final Class<?> result; try { result = AccessController.doPrivileged( new PrivilegedExceptionAction<Class<?>>() { public Class<?> run() throws ClassNotFoundException { String path = name.replace('.' , '/' ).concat(".class" ); Resource res = ucp.getResource(path, false ); if (res != null ) { try { return defineClass(name, res); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } else { return null ; } } }, acc); } catch (java.security.PrivilegedActionException pae) { throw (ClassNotFoundException) pae.getException(); } return result; }
defineClass
defineClass方法主要是把字节数组转化为类的实例。同时definClass方法为final的,故不可以覆写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private Class<?> defineClass(String name, Resource res) throws IOException { long t0 = System.nanoTime(); int i = name.lastIndexOf('.' ); URL url = res.getCodeSourceURL(); if (i != -1 ) { String pkgname = name.substring(0 , i); Manifest man = res.getManifest(); definePackageInternal(pkgname, man, url); } java.nio.ByteBuffer bb = res.getByteBuffer(); if (bb != null ) { CodeSigner[] signers = res.getCodeSigners(); CodeSource cs = new CodeSource(url, signers); sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0); return defineClass(name, bb, cs); } else { byte [] b = res.getBytes(); CodeSigner[] signers = res.getCodeSigners(); CodeSource cs = new CodeSource(url, signers); sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0); return defineClass(name, b, 0 , b.length, cs); } }
最后几个defineClass 会落在几个Natice方法上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private native Class<?> defineClass0( String name, byte [] b, int off, int len, ProtectionDomain pd); private native Class<?> defineClass1( String name, byte [] b, int off, int len, ProtectionDomain pd, String source); private native Class<?> defineClass2( String name, java.nio.ByteBuffer b, int off, int len, ProtectionDomain pd, String source);
这里不做分析了。
总结
到此我们就分析完了整个ClassLoader的加载过程,其中有很多细节我们了解,我们下一章再去分析,下一章节,我们主要针对于JVM的源码分析一下JVM启动过程。
参考