网上讲解java虚拟机的类加载机制的资料浩如烟海,本人搜索了几篇稍稍靠谱的博客,做了下总结,加了自己的理解。同时参考了java虚拟机规范(Java SE7版)这本书,在此做个记录。大概是别人整理过的东西比较好理解吧,相比于网上的论坛博客而言,java虚拟机规范这本书,倾向于形式化的定义,严谨的逻辑术语,比较难懂。
首先声明,类加载有三种方式,本文只是涉及第一种方式:
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
java虚拟机的类加载机制主要是通过类加载器实现的,也就是大部分博客所说的"ClassLoader",本文我仅把这个单词视为类加载机制的代名词,虽然在java.lang包中确实有这么一个抽象类ClassLoader,并且除了引导类加载器之外的其他类加载器都是它的子类。
Jvm启动过程,就是通过classloader加载class文件到内存的过程,class文件通过类加载器被加载到jvm中,那么,类加载器本身是如何被加载的呢?这里就要讲到刚刚提到的引导类加载器,它是用非java语言实现的(有说用C语言写的,有说用C++写的,没深究),jvm虚拟机的一部分。引导类加载器(又称为原始类加载器,启动加载器,bootstrap classLoader,中文名真不少,基本上都是一个意思,后文统称 bootstrap classloader,简称BC )在jvm启动时,负责加载java核心的API以满足java程序的最基本的需求。BC在jvm启动时(亦即执行java这个命令时),通过载入并初始化一个叫sun.misc.Launcher.class的类,这个是类加载中涉及到的最初始的类,jvm通过它去调用其他类加载器,加载其他的类。在rt.jar包下,可以找到该类,在其源码定义中,有这样一个私有静态变量,
private static String bootClassPath = System.getProperty("sun.boot.class.path");
sun.boot.calss.path指定了BC应该去哪些路径底下搜索并加载类,打印出这个路径(也可以是这个方法sun.misc.Launcher.getBootstrapClassPath()),你可以看见,它指向的是你本机上的javahome底下的java运行环境的lib。三个lib中的一个(三个lib在lesson1中有提到,这里不再赘述)。
D:\Program Files\Java\jdk1.8.0_60\jre\lib\resources.jar;
D:\Program Files\Java\jdk1.8.0_60\jre\lib\rt.jar;
D:\Program Files\Java\jdk1.8.0_60\jre\lib\sunrsasign.jar;
D:\Program Files\Java\jdk1.8.0_60\jre\lib\jsse.jar;
D:\Program Files\Java\jdk1.8.0_60\jre\lib\jce.jar;
D:\Program Files\Java\jdk1.8.0_60\jre\lib\charsets.jar;
D:\Program Files\Java\jdk1.8.0_60\jre\lib\jfr.jar;
D:\Program Files\Java\jdk1.8.0_60\jre\classes
这里我们可以看见,比较熟悉的一些jar包,以前经常在系统环境变量中的CLASSPATH中配置,其实不必多此一举,我们只需要指定javahome即可,jvm启动时会自动寻找javahome目录下的java运行环境lib包,加载其中的java运行所需的基础类。实际上,这些基础类都有BC去加载,当你试图去get这些基础类的Classloader时,返回的都是null,因为BC本身不是一个java类,而是用其他语言写的jvm实现的一部分。例如,执行如下程序
System.out.println(System.class.getClassLoader()); System.out.println(sun.misc.Launcher.getLauncher().getClass().getClassLoader());
你得到的结果都是null。
当jvm将以上lib包都载入之后,jvm继续通过Launcher,加载extension classloader -扩展类加载器,以及system classloader -系统(也称为应用)类加载器。以下是类加载器的执行流程图
这里需要提一句的是,JVM在加载类时默认采用的是双亲委派 机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
下面说下在Launcher中初始化的其他两类类加载器。
extension classloader-扩展类加载器,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的)中JAR的类包。这为引入除Java核心类以外的新功能提供了一个标准机制。因为默认的扩展目录对所有从同一个JRE中启动的JVM都是通用的,所以放入这个目录的 JAR类包对所有的JVM和system classloader都是可见的。在这个实例上调用方法getParent()总是返回空值null,因为引导加载器bootstrap classloader不是一个真正的ClassLoader实例。所以当大家执行以下代码时:
System.out.println(System.getProperty("java.ext.dirs")); // System.out.println(System.getProperty("java.class.path")); System.out.println(ClassLoader.getSystemClassLoader().getParent()); System.out.println("the parent of extension classloader : "+ClassLoader.getSystemClassLoader().getParent().getParent());
结果为:
D:\Program Files\Java\jdk1.8.0_60\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
sun.misc.Launcher$ExtClassLoader@74a14482
the parent of extension classloader : null
extension classloader是system classloader的parent,而bootstrap classloader是extension classloader的parent,但它不是一个实际的java类,使用其他语言实现的jvm的一部分,所以为null。
system classloader -系统(也称为应用)类加载器,它负责在JVM被启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或者 CLASSPATH操作系统属性所指定的JAR类包和类路径。总能通过静态方法ClassLoader.getSystemClassLoader()找到该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。执行以下代码即可获得:System.out.println(System.getProperty("java.class.path"));输出结果则为用户在系统属性里面设置的CLASSPATH(执行了下,好像不是,这个有待商榷)。
Bootstrap ClassLoader、Extension ClassLoader、App ClassLoader三者的关系如下:
Bootstrap ClassLoader是Extension ClassLoader的parent,Extension ClassLoader是App ClassLoader的parent。但是这并不是继承关系,只是语义上的定义,基本上,每一个ClassLoader实现,都有一个Parent ClassLoader。可以通过ClassLoader的getParent方法得到当前ClassLoader的parent。Bootstrap ClassLoader比较特殊,因为它不是java class所以Extension ClassLoader的getParent方法返回的是null。
文章开头提到,当你执行java命令的时候,JVM会先使用BC载入并初始化一个Launcher,Launcher 会根据系统和命令设定初始化好class loader结构,JVM就用它来获得extension classloader和system classloader,并载入所有的需要载入的Class,最后执行java命令指定的带有静态的main方法的Class。extension classloader实际上是sun.misc.Launcher$ExtClassLoader类的一个实例,system classloader实际上是sun.misc.Launcher$AppClassLoader类的一个实例。并且都是 java.net.URLClassLoader的子类。
ExtClassLoader和AppClassLoader在JVM启动后,会在JVM中保存一份,并且在程序运行中无法改变其搜索路径。如果想在运行时从其他搜索路径加载类,就要产生新的类加载器。
下面是Launcher的部分代码,直接复制Launcher.class
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory(null); private static Launcher launcher = new Launcher(); private static String bootClassPath = System.getProperty("sun.boot.class.path"); private ClassLoader loader; private static URLStreamHandler fileHandler; public static Launcher getLauncher() {
return launcher; }
public Launcher() {
Launcher.ExtClassLoader var1; try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();// 初始化extension classloader } catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10); }
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);// 初始化system classloader,注意参数var1 } catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9); }
Thread.currentThread().setContextClassLoader(this.loader);//将system classloader设置为当前的context classloader String var2 = System.getProperty("java.security.manager"); if(var2 != null) {
SecurityManager var3 = null; if(!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); } catch (IllegalAccessException var5) {
; } catch (InstantiationException var6) {
; } catch (ClassNotFoundException var7) {
; } catch (ClassCastException var8) {
; }
} else {
var3 = new SecurityManager(); }
if(var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2); }
System.setSecurityManager(var3); }
}
public ClassLoader getClassLoader() {
return this.loader; }
public static URLClassPath getBootstrapClassPath() {
return Launcher.BootClassPathHolder.bcp; }
private static URL[] pathToURLs(File[] var0) {
URL[] var1 = new URL[var0.length]; for(int var2 = 0; var2 < var0.length; ++var2) {
var1[var2] = getFileURL(var0[var2]); }
return var1; }
更多的详细代码,可以自行查看sun.misc.Launcher。
根据代码,我们可以得出以下结论:
extension classloader是使用系统属性“java.ext.dirs”设置类搜索路径的,并且没有parent。
system classloader是使用系统属性“java.class.path”设置类搜索路径的,并且有一个parent classloader。
Launcher初始化extension classloader,system classloader,并将system classloader设置成为context classloader,但是仅仅返回system classloader给JVM。
这里怎么又出来一个context classloader呢?它有什么用呢?我们在建立一个线程Thread的时候,可以为这个线程通过setContextClassLoader方法来指定一个合适的classloader作为这个线程的context classloader,当此线程运行的时候,我们可以通过getContextClassLoader方法来获得此context classloader,就可以用它来载入我们所需要的Class。默认的是system classloader。
利用这个特性,我们可以“打破”classloader委托机制了,父classloader可以获得当前线程的context classloader,而这个context classloader可以是它的子classloader或者其他的classloader,那么父classloader就可以从其获得所需的 Class,这就打破了只能向父classloader请求的限制了。这个机制可以满足当我们的classpath是在运行时才确定,并由定制的 classloader加载的时候,由system classloader(即在jvm classpath中)加载的class可以通过context classloader获得定制的classloader并加载入特定的class(通常是抽象类和接口,定制的classloader中是其实现),例如web应用中的servlet就是用这种机制加载的。
到此为止,JVM类加载器的结构和工作原理,我们应该有一个大致的了解了。后续会说明类的动态加载。