JVM类加载器
JVM系列之类加载、类加载器、双亲委派机制
一.概述
定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。类加载和连接的过程都是在运行期间完成的。
二. 类的加载方式
1):本地编译好的class中直接加载
2):网络加载:java.net.URLClassLoader可以加载url指定的类
3):从jar、zip等等压缩文件加载类,自动解析jar文件找到class文件去加载util类
4):从java源代码文件动态编译成为class文件
三.类加载的时机
1.类加载的生命周期:加载(Loading)–>验证(Verification)–>准备(Preparation)–>解析(Resolution)–>初始化(Initialization)–>使用(Using)–>卸载(Unloading)
2.加载:这有虚拟机自行决定。
3.初始化阶段:
a)遇到new、getstatic、putstatic、invokestatic这4个字节码指令时,如果类没有进行过初始化,出发初始化操作。
b)使用java.lang.reflect包的方法对类进行反射调用时。
c)当初始化一个类的时候,如果发现其父类还没有执行初始化则进行初始化。
d)虚拟机启动时用户需要指定一个需要执行的主类,虚拟机首先初始化这个主类。
注意:接口与类的初始化规则在第三点不同,接口不要气所有的父接口都进行初始化。
四.类加载的过程
4.1.加载
a)加载阶段的工作
i.通过一个类的全限定名来获取定义此类的二进制字节流。
ii.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
iii.在java堆中生成一个代表这个类的java.lang.Class对象,做为方法区这些数据的访问入口。
b)加载阶段完成之后二进制字节流就按照虚拟机所需的格式存储在方区去中。
4.2.验证
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。
a)文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
b)元数据验证:对字节码描述的信息进行语义分析,以确保其描述的信息符合java语言规范的要求。
c)字节码验证:这个阶段的主要工作是进行数据流和控制流的分析。任务是确保被验证类的方法在运行时不会做出危害虚拟机安全的行为。
d)符号引用验证:这一阶段发生在虚拟机将符号引用转换为直接引用的时候(解析阶段),主要是对类自身以外的信息进行匹配性的校验。目的是确保解析动作能够正常执行。
4.3.准备
准备阶段是正式为变量分配内存并设置初始值,这些内存都将在方法区中进行分配,这里的变量仅包括类标量不包括实例变量。
4.4.解析
解析是虚拟机将常量池的符号引用替换为直接引用的过程。
a)符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
b)直接引用:直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接饮用是与内存布局相关的。
c)类或接口的解析
d)字段的解析
e)类方法解析
f)接口方法解析
4.5.初始化
是根据程序员制定的主观计划区初始化变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器()方法的过程。
五.JVM三种预定义类型类加载器
当一个 JVM 启动的时候,Java 缺省开始使用如下三种类型类装入器:
启动(Bootstrap)类加载器:引导类装入器是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib 下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
标准扩展(Extension)类加载器:扩展类加载器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 实现的。它负责将
< Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
系统(System)类加载器:系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。
除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载器,这个将在后面单独介绍。
a. Bootstrap ClassLoader/启动类加载器
主要负责jdk_home/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入工作.
b. Extension ClassLoader/扩展类加载器
主要负责jdk_home/lib/ext目录下的jar包或 -Djava.ext.dirs 指定目录下的jar包装入工作
c. System ClassLoader/系统类加载器
主要负责java -classpath/-Djava.class.path所指的目录下的类与jar包装入工作.
d. User Custom ClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类)
在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性.
六. 类加载双亲委派机制介绍和分析
在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和标准扩展类加载器为例作简单分析。
类加载器均是继承自java.lang.ClassLoader抽象类。我们下面我们就看简要介绍一下java.lang.ClassLoader中的loadClass()方法会有个直观的认识:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查该类是否已经被加载,如果从JVM缓存中找到该类,则直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//遵循双亲委派机制,首先会从父类加载器开始找,
//直到父类是BootstrapClassLoader为止
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果还找不到,尝试通过findClass方法去寻找
// findClass是留给开发者实现的,自定义加载器时,重写此方法
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
七、破坏双亲委派机制的应用
双亲委派模型不能解决所有的类加载器问题,比如,Java提供了很多服务提供者接口(ServiceProviderInterface,SPI),允许第三方为这些接口提供实现。常见的SPI有JDBC、JNDI等,这些SPI接口是由核心类库提供,确由第三方实现,这样就存在一个问题SPI接口是核心类库的一部分,是由BootstrapClassLoader加载的,SPI实现的java类一般是AppClassLoader来加载的。BootstrapClassLoader是无法找到SPI的实现类的,因为它只加载Java的核心库。它也不能代理给AppClassLoader,因为它是最顶层的类加载器,此时双亲委派模型就不能解决这个问题。
线程上下文类加载器(ContextClassLoader)正好可以解决这个问题。这不是一个新的类加载器,实际上,它仅仅是Thread类的一个变量而已,通过getContextClassLoader()与setContextClassLoader(ClassLoader c1)分别用来获取和设置类加载器。如果不做任何设置,java应用的线程上下文加载器默认是AppClassLoader。在核心类库使用API接口时,传递的类加载器使用线程上下文加载器,就可以加载到SPI的实现类。
java.sql.DriverManager中的loadInitialDrivers()方法加载Driver时直接使用的就是AppClassLoader。
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}