类加载是将类的.class文件读入内存,通常是创建一个字节数组读入class文件内容。然后生成对应的class对象,加载类完成,不过这时候的类还不能使用,之后还要经过验证、准备(为静态变量分配内存并设置变量的初始值)、解析(将符号引用替换为直接引用)。最后JVM对类进行初始化,如果当前类有父类,则先去初始化父类。如果当前类中有初始化语句,则去依次执行这些初始化语句。具体过程:
JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化。
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。
这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:
1 |
|
实际上变量v在准备阶段过后的初始值为0而不是8080,将v赋值为8080的putstatic指令是程序被编译后,存放于类构造器
但是注意如果声明为:
1 |
|
在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080。
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中的:
等类型的常量。
下面我们解释一下符号引用和直接引用的概念:
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。
初始化阶段是执行类构造器
虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了3种类加载器:
1、Bootstrap ClassLoader:启动类加载器,也叫根类加载器,它负责加载Java的核心类库,加载如(%JAVA_HOME%/lib)目录下的rt.jar(包含System、String这样的核心类)这样的核心类库。根类加载器非常特殊,它不是java.lang.ClassLoader的子类,它是JVM自身内部由C/C++实现的,并不是Java实现的。
2、Extension ClassLoader:扩展类加载器,它负责加载扩展目录(%JAVA_HOME%/jre/lib/ext)下的jar包,用户可以把自己开发的类打包成jar包放在这个目录下即可扩展核心类以外的新功能。
3、System ClassLoader\APP ClassLoader:系统类加载器或称为应用程序类加载器,父类是Extension ClassLoader,是加载CLASSPATH环境变量所指定的jar包与类路径。
4.用户自定义的类:一般来说,用户自定义的类就是由APP ClassLoader加载的。
JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器
。
各种类加载器间关系:以组合关系复用父类加载器的父子关系,注意,这里的父子关系并不是以继承关系实现的。
//验证类加载器与类加载器间的父子关系
public static void main(String[] args) throws Exception{
//获取系统/应用类加载器
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统/应用类加载器:" + appClassLoader);
//获取系统/应用类加载器的父类加载器,得到扩展类加载器
ClassLoader extcClassLoader = appClassLoader.getParent();
System.out.println("扩展类加载器" + extcClassLoader);
System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
//获取扩展类加载器的父加载器,但因根类加载器并不是用Java实现的所以不能获取
System.out.println("扩展类的父类加载器:" + extcClassLoader.getParent());
}
}
输出结果为:
类加载器的双亲委派加载机制(重点):当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
这个过程如下图标号过程所示:
双亲委派模型的源码实现:
主要体现在ClassLoader的loadClass()方法中,思路很简单:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,调用自己的findClass()方法进行加载。
public Class> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
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) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
代码演示双亲委派:
//输出ClassLoaderText的类加载器名称
System.out.println("ClassLoaderText类的加载器的名称:"+test.class.getClassLoader().getClass().getName());
System.out.println("System类的加载器的名称:"+System.class.getClassLoader());
System.out.println("List类的加载器的名称:"+List.class.getClassLoader());
ClassLoader cl = test.class.getClassLoader();
while(cl != null){
System.out.print(cl.getClass().getName()+"->");
cl = cl.getParent();
}
System.out.println(cl);
输出结果为:
1、ClassLoaderTest类是用户定义的类,位于CLASSPATH下,由系统/应用程序类加载器加载。
2、System类与List类都属于Java核心类,由祖先类启动类加载器加载,而启动类加载器是在JVM内部通过C/C++实现的,并不是Java,自然也就不能继承ClassLoader类,自然就不能输出其名称。
3、而箭头项代表的就是类加载的流程,层级委托,从祖先类加载器开始,直到系统/应用程序类加载器处才被加载。
委托机制的意义
主要是防止内存中出现多份同样的字节码
比如两个类A和类B都要加载System类:
如果不用委托而是自己加载自己的,那么类A就会加载一份System字节码,然后类B又会加载一份System字节码,这样内存中就出现了两份System字节码。
如果使用委托机制,会递归的向父类查找,也就是首选用Bootstrap尝试加载,如果找不到再向下。这里的System就能在Bootstrap中找到然后加载,如果此时类B也要加载System,也从Bootstrap开始,此时Bootstrap发现已经加载过了System那么直接返回内存中的System即可而不需要重新加载,这样内存中就只有一份System的字节码了。