类加载过程
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接。
加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始,而解析阶段不一定;它在某些情况下可以在初始化之后再开始,这是为了运行时动态绑定特性。值得注意的是:这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。
加载
获得类的二进制字节流
将这个字节流所代表的静态存储结构转为方法区的运行时数据结构
在Java堆中生成对应的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证
验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
1.文件格式验证
是否以魔术0xCAFEBABE开头
主、次版本号是否在当前虚拟机处理范围之内
2.元数据验证
保证起字节码描述的信息符合java语言规范要求。
是否有父类
是否继承了不允许被继承的类(被final修饰的)
非抽象类实现了所有的抽象方法
3.字节码验证
栈数据类型和操作码数据参数吻合(栈里放的是int类型,使用时却按long类型来加载入本地变量表)
跳转指令跳转到合理的位置
方法体中的类型转换有效,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但不能把一个父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系的、完全不想干的一个数据类型。
4.符号引用验证
确保解析动作能正常执行
常量池中描述类是否存在
访问的方法和字段是否存在且有足够的权限(private,protected,public,default)
准备
为类变量分配内存(方法区中分 配),并为类变量设置初始值。
public static int value = 1;
在准备阶段中, v会被设置为0
在初始化的< clinit >()中才会被设置为1
对于static final类型,在准备阶段就会被赋上正确的值
public static final int value = 1
解析
将常量池中的符号引用替换为直接引用
- 符号引用:字符串,引用对象不一定被加载
- 直接引用:指针或者地址偏移量,引用对象一定在内存
初始化
- 执行类构造器< clinit >方法由下面两部分合成:
– static变量的赋值语句
– static{}块
//静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中只能赋值不能访问
public class Test{
static{
i = 0; //给变量赋值可以正常编译通过
System.out.println(i); //提示“非法向前引用”
}
static int i = 1;
}
- 子类的< clinit >调用前保证父类的< clinit >方法已经执行完毕
– 虚拟机中第一个被执行< clinit >方法的类是java.lang.Object - 一个类的< clinit >方法是线程安全的
类加载器
ClassLoader是一个抽象类
ClassLoader的实例将读入Java字节码将类装载到JVM中
ClassLoader可以定制,满足不同的字节码流获取方式
ClassLoader负责类装载过程中的加载阶段
package com.kelly.classloader;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(filename);
if (is == null)
return super.loadClass(name);
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.kelly.classloader.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.kelly.classloader.ClassLoaderTest);
}
}
上面代码输出结果;
class com.kelly.classloader.ClassLoaderTest
false
因为虚拟机中存在两个ClassLoaderTest类,一个由系统应用程序类加载器加载的,另外一个是我们自定义的类加载器加载的。
双亲委派模型
类加载器分类
启动类加载器(Bootstrap ClassLoader)
这个类加载器负责将< JAVA_HOME >\lib目录中的,或者被 -Xbootclasspath参数所指定的路径中,并且被虚拟机所识别的类库加载到虚拟机内存中。开发者不能直接使用启动类加载器。
扩展类加载器(Extension ClassLoader)
负责加载< JAVA_HOME >\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。开发者可以直接使用类加载器。
应用程序类加载器
负责加载用户路径上(ClassPath)上所指定的类库。默认的类加载器。
图2. 类加载器双亲委派模型
双亲委派模型的代码都集中在java.lang.ClassLoader的loadClass()方法中,如下代码
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) {
// 如果父类加载抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
//如果父类加载器无法完成加载
//再调用自身的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;
}
}
破坏双亲委派模型
优点:对于通用类,各个类加载器去加载都会是同一个类。(比如Object类)。
缺点:顶层的ClassLoader,无法加载底层ClassLoader的类。
思考一个问题:java框架(rt.jar类)如何加载应用的类? javax.xml.parsers包中定义了xml解析的类接口Service Provider Interface (SPI) 位于rt.jar 即接口在启动ClassLoader中。而SPI的实现类,在AppLoader。但启动类记载其不可能认识这些实现类的代码,怎么办呢?
解决:线程上下文加载器(Thread Context ClassLoader)。通过Thread.setContextClassLoader()方法进行设置。它的基本思想是在顶层ClassLoader中,传入底层ClassLoader的实例。
//设置类加载器contextClassLoader
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
代码来自javax.xml.parsers.FactoryFinder展示如何在启动类加载器中加载AppLoader的类
static private Class<?> getProviderClass(String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
try {
if (cl == null) {
if (useBSClsLoader) {//使用bootstrap classLoader加载这个类
return Class.forName(className, false, FactoryFinder.class.getClassLoader());
} else {
cl = ss.getContextClassLoader();
if (cl == null) {
throw new ClassNotFoundException();
}
else {
return Class.forName(className, false, cl);//使用上下文加载器去加载这个类
}
}
}
else {//使用当前提供的类加载器去加载这个类
return Class.forName(className, false, cl);
}
}
catch (ClassNotFoundException e1) {
if (doFallback) {
// Use current class loader - should always be bootstrap CL
return Class.forName(className, false, FactoryFinder.class.getClassLoader());
}
else {
throw e1;
}
}
}
双亲委派模型是默认的模式,并不是必须要这么做。Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent。OSGi的ClassLoader形成网状结构,根据需要自由加载Class。