为什么会有ClassLoader

Oracle有一篇好文诠释ClassLoader机制。

动机

在静态编译语言中,像C和C++,在编译的时候会“连接”本地,机器指令,然后生成“可执行”程序。但是动态编译语言,像Java,编译器只会生成.class文件,并没有进行“连接”操作,此任务由java虚拟机来执行。因为需要将class加载至虚拟机内。

当java程序启动,第一个运行的class文件是申明为public static void main方法。这个类通常会引用其他类,并且通过classloader尝试加载引用类。
可以尝试创建一个名为HelloApp.java文件,写一个简单的main方法,编译,然后运行。


public class HelloApp {
   public static void main(String argv[]) {
      System.out.println("Aloha! Hello and Bye");
   }
}

执行“java -verbose:class HelloApp -> text.txt”,查看text.txt,会发现,加载HelloApp类之前会加载JAVA_HOME\lib下的jar包,这个应该是bootstrap类加载器加载的。当然类加载对于一般java使用者是透明的或者说简化的。bootstrap类加载器只加载classes found in the boot classpath,“校验”过程不会执行。JVM还有一个扩展类加载器,用于加载标准扩展的API,最后还有一个系统类加载器,用于加载创建的应用程序相关的classes。

类的生命周期

当启动JVM,执行Java程序时,需要加载类,作为程序的载体。它的生命周期有以下几个过程:

  • 加载
  • 连接(验证->准备->解析)
  • 初始化
  • 使用
  • 卸载

由于java具有“运行时绑定”的特性,不一定都会按部就班。何时加载,虚拟机并没有特别规定。
初始化有且仅有5种情况必须对类进行初始化:

  • 遇到new,getstatic,putstatic或invokestatic
  • 使用java.lang.reflect包的方法对类进行反射调用
  • 当初始化一个类时,父类未初始化,则触发父类初始化
  • 虚拟机启动,需要指定一个要执行的主类(包含main方法的那个类)
  • 使用JDK1.7的动态语言支持时,如果java.lang.invoke.MethodHandle最后的解析结果为REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄,并且句柄对应的类没有进行初始化,则触发。

阶段:加载

此过程主要是来获取类的二进制字节流,通过class文件,jar,war,网络IO,以及动态生成。然后将字节流所代表的静态存储结构转化为方法区的运行时数据结构,最终在内存中生成代表这个类的java.lang.Class对象。

阶段:验证

各种验证,文件格式验证,如果魔数OXCAFEBABE,主次版本等。元数据验证,进行语义分析。字节码验证,符号引用验证。

阶段:准备

此过程为类变量分配内存并设置初始化(所谓的”零值”)

注意,final static属性,不会进行初始化零值

阶段:解析

将常量池内的符号引用替换

  • 类或接口的解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

阶段:初始化

真正执行类中定义的程序代码(字节码),编译器会自动收集类中的所有变量的赋值动作和静态语句块中的语句合并产生类的方法,在多线程环境中,虚拟机能够保证类加载被正确执行,同步。

类加载器

类加载器,用于实现类的加载动作。其实现是在虚拟机掌控之外,也就是可以灵活控制。Java类加载器采用双亲委派模型,除了顶层的启动类加载器外,其他类加载器都应有父类。类加载器收到类加载请求时,先委派给父类,因此最终会传送到顶层的启动类加载器器,当父类加载器反馈无法完成加载时,子加载器才会尝试。

不同的类加载器创建的同一个类的Class对象,是没有可比性的。
Class对象,有一个ClassLoader对象引用,指向加载该类的加载器。如果当前类的加载器为bootstrap,则classloader引用为null。如果非null,通过SecurityManager检查权限,允许则返回,否则抛出异常。(AccessControlException)

Bootstrap ClassLoader

启动类加载器(Bootstrap ClassLoader),负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数指定路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。

非Java实现

Extension ClassLoader

扩展类加载器(Extension ClassLoader),加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量指定路径中的所有类库,开发者可以直接使用扩展类加载器。

Application ClassLoader

应用程序类加载器(Application ClassLoader),这个类加载器由sun.misc.Launcher$AppClassLoader实现。这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,是系统的类加载器。如果没有自己定义的类加载器,则就是程序默认的类加载器。

类加载器源码分析

ClassLoader主要提供两个方法,getResourceAsStream和loadClass。也是经常用到的,所以看了下源码,能力有限,native方法的源码看不懂。这两个方法都是采用委托模式,先父类找,然后在当前类加载器找。

ClassLoader

ClassLoader是一个抽象类,在应用程序中可以被继承使用,根据需求,用于动态加载。创建自己的classloader时,可以指定其parent classloader。否则默认为系统类加载器。
开发自己的classloader时,要注意潜在的危险,java api提供SecureClassLoader子类,提供基于系统默认的安全,权限校验,URLClassLoader是SecureClassLoader的子类。可以轻易的基于URL来加载classes和resources。

loadClass方法,先根据native方法查找是否有这个Class对象,不存在时,利用委托模式,在父类查找,顶层会调用native方法,去bootstrap找class。没有找到,则调用当前类的findClass方法。ClassLoader中直接抛出ClassNotFoundException异常。

Launcher

下载openjdk8源码,下载后解压,并查看“[openjdkhome]\jdk\src\share\classes\sun\misc\Launcher.java”

声明Launcher静态变量,直接new,并暴露get方法。
声明ClassLoader引用对象,暴露get方法。这个引用对象指向APP ClassLoader。
构造函数,调用ExtClassLoader的getExtClassLoader静态方法获取ext classloader。调用AppClassLoader的getAppClassLoader静态方法,并传入ext classloader,设置为父加载器。设置SecurityManager。
ExtClassLoader静态内部类,继承URLClassLoader,获取ext的folders(System.getProperty(“java.ext.dirs”)),在我本地windows系统输出是“C:\Program Files\Java\jre1.8.0_60\lib\ext;C:\Windows\Sun\Java\lib\ext”,然后将Files转换成URL数组,交由URLClassLoader初始化。父类为null。
AppClassLoader静态内部类,继承URLClassLoader,获取app的folders(System.getProperty(“java.class.path”)),转换成URL数组,交由URLClassLoader初始化,ext classloader为其父类。

URLClassLoader

getResourceAsStream(String name),最终获取InputStream,先获取URL,这个是委托模式,先从父加载器尝试获取,顶层为sun.misc.Launcher.getBootstrapClassPath()。获取System.getProperty(“sun.boot.class.path”)所有的目录以及jar,转成URL,放入URLClassPath对象,遍历判断是否能找到resource,返回URL。如果父类都找不到,则调用findResource(String name)方法,ClassLoader是默认返回null,URLClassLoader的ucp(URLClassPath)查找resource,返回URL。如果URL不为null,则openConnection,获取InputSream。

findClass(),当父加载器找不到类时,会调用当前类加载器的这个方法,获取Resource,然后define class,最终会调用native方法。