.java 文件经过Java编译器编译后生成.class文件,.class文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的.class 文件,并创建对应的Class对象,将Class文件加载到虚拟机的内存,这个过程称为类加载。
类加载器ClassLoader
类加载器根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class 对象实例,虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)
类初始化
类加载的初始化阶段,在虚拟机规范严格规定了有且只有5种场景必须对类进行初始化:
- 使用new关键字实例化对象时、读取或者设置一个类的静态字段(不包含编译期常量)以及调用静态方法的时候,必须触发类加载的初始化过程(类加载过程最终阶段)。
- 使用反射包(java.lang.reflect)的方法对类进行反射调用时,如果类还没有被初始化,则需先进行初始化,这点对反射很重要。
- 当初始化一个类的时候,如果其父类还没进行初始化则需先触发其父类的初始化。
- 当Java虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类
- 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应类没有初始化时,必须触发其初始化(这点看不懂就算了,这是1.7的新增的动态语言支持,其关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的。
加载方式
类加载有三种方式
- 命令行 启动应用时候由JVM初始化加载
- 通过 Class.forName() 方法动态加载
- 通过 ClassLoader#loadClass() 方法动态加载
启动(Bootstrap)类加载器
主要加载JVM自身需要的类,该类使用C++实现,负责将JAVA_HOME/lib下的核心类库加载到内存中,它按照文件名识别jar包,如rt.jar,这个类加载使用C++语言实现的,是虚拟机自身的一部分,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。
它虽然叫做加载器,但它并不是ClassLoader的子类,这点和后两种类加载器不同;
Jvm启动过程,就是通过ClassLoader加载class文件到内存的过程,class文件通过类加载器被加载到jvm中,那么类加载器本身是如何被加载的呢?实际上类加载器是由引导类加载器来加载的。
引导类加载器 在jvm启动时(亦即执行java这个命令时),负责加载Java核心的API以满足Java程序的最基本的需求,通过载入并初始化一个叫sun.misc.Launcher.class的类,这个是类加载中涉及到的最初始的类,jvm通过它去调用其他类加载器,加载其他的类。在rt.jar包下,可以找到该类,在其源码定义中,有这样一个私有静态变量:
|
|
sun.boot.calss.path 指定了引导类加载器应该去哪些路径底下搜索并加载类;(在外部可以使用
sun.misc.Launcher.getBootstrapClassPath()
获取所有的路径);
获取到的路径列表如下:
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
当尝试去获取启动类加载器加载的类的ClassLoader时,将获取到空对象;
|
|
当jvm将以上lib包都载入之后,jvm继续通过Launcher,加载 扩展类加载器 和 系统(也称为应用)类加载器。
扩展(Extension)类加载器
sun.misc.Launcher~ExtClassLoader,负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的)中JAR的类包。
这为引入除Java核心类以外的新功能提供了一个标准机制。因为默认的扩展目录对所有从同一个JRE中启动的JVM都是通用的,所以放入这个目录的JAR类包对所有的JVM和系统类加载器都是可见的。
/Users/cj/Library/Java/Extensions
/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
/Network/Library/Java/Extensions
/System/Library/Java/Extensions
/usr/lib/java
系统(System)类加载器
sun.misc.Launcher~AppClassLoader,负责在JVM被启动时,加载来自在命令java中附带的-classpath或者java.class.path属性指定的路径下的jar或者.class文件;可以通过ClassLoader.getSystemClassLoader()
方法获取该类加载器。
以下是通过获取java.class.path属性获取到的部分路径,实际上就是项目的bin目录和libs目录下的jar文件:
/Users/cj/Documents/workspace/Change/bin
/Users/cj/Documents/workspace/Change/libs/jsoup-1.8.3.jar
...
Summary
当执行java命令的时候,JVM会先使用引导类加载器载入并初始化一个Launcher,Launcher构造方法中会初始化ExtClassLoader和AppClassLoader,并载入所有的需要载入的Class,最后执行java命令指定的带有静态的main方法的Class。ExtClassLoader和AppClassLoader都是java.net.URLClassLoader的子类。
ExtClassLoader和AppClassLoader在JVM启动后,会在JVM中保存一份,并且在程序运行中无法改变其搜索路径。如果想在运行时从其他搜索路径加载类,就要使用新的类加载器,我们可以自定义类加载器来实现;Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式。
双亲委派模式
双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:
- 启动类加载器,由C++实现,没有父类。
- 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
- 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
- 自定义类加载器,父类加载器为AppClassLoader。
|
|
为什么要使用双亲委派模式?
- Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
- 其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
|
|
ExtClassLoader 和 AppClassLoader 都继承自 URLClassLoader。
加载过程方法
loadClass
当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载。
|
|
findClass
findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。
findClass()方法在ClassLoader中并没有进行实现,需要由子类去实现;
defineClass
defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够将本地class文件实例化成class对象,也可以通过比如从网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用。
需要注意的是,如果直接调用defineClass()方法生成类的Class对象,这个类的Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。
resolveClass
使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。
URLClassPath
URLClassPath类负责找到要加载的字节码,再读取成字节流,最后通过defineClass()方法创建类的Class对象。
其构造方法都有一个必须传递的参数URL[],该参数的元素是代表字节码文件的路径,在创建URLClassLoader对象时必须要指定这个类加载器到哪个目录下去找class文件。
在JVM中表示两个class对象是否为同一个类对象存在两个必要条件
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
也就是说,在JVM中,即使这个两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的,这是因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间,所以加载的class对象也会存在不同的类名空间中,loadClass()方法第一步会通过Class<?> c = findLoadedClass(name);
从缓存查找,类名完整名称相同则不会再次被加载。
显示加载与隐式加载
class文件的显示加载与隐式加载的方式是指JVM加载class文件到内存的方式,显示加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。
而隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
自定义类加载器
实现自定义类加载器需要继承ClassLoader或者URLClassLoader,继承ClassLoader则需要自己重写findClass()方法并编写加载逻辑,如果继承URLClassLoader则可以省去编写findClass()方法以及class文件加载转换成字节码流的代码。那么编写自定义类加载器的意义何在呢?
- 当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象。
- 当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。
- 当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。
继承ClassLoader
|
|
以下是一个例子:两个类AB和BB,AB中引用了BB,在AB初始化的时候,会实例化BB对象;
|
|
可以看到,不同的ClassLoader,加载同一个.class文件,所提到的Class对象,确实是不同的对象,它们的hashCode不相同;
继承URLClassLoader
|
|
|
|
Class.forName和ClassLoader#loadClass区别?
- Class.forName
|
|
Class.forName有个重载方法,这两个方法都最终都调用了forName0(name, initialize, loader, caller);
;第二个参数表示是对类进行初始化;如果传true,则会对Class的静态变量进行初始化并赋值,并且也会执行static代码块;
- ClassLoader#loadClass
ClassLoader#loadClass方法,仅仅就是将.class文件加载到jvm中生成对应的Class对象,而不执行static代码块,不对static变量进行赋值;