ClassLoader

JAVA类装载方式

  1. 隐式装载 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中
  2. 显式装载 通过class.forname()等方法,显式加载需要的类

类加载的动态性体现

一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,因为java最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制,而用到时再加载这也是java动态性的一种体现

类装载器

  1. Bootstrp loader
    Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。

  2. ExtClassLoader
    加载Java的扩展API,也就是/lib/ext中的类。

  3. AppClassLoader
    用来加载用户机器上CLASSPATH设置目录中的Class的,通常在没有指定ClassLoader的情况下,程序员自定义的类就由AppClassLoader进行加载。

综上所述,它们之间的关系可以通过下图形象的描述:

类加载器之间是如何协调工作的

前面说了,java中有三个类加载器,问题就来了,碰到一个类需要加载时,它们之间是如何协调工作的,即java是如何区分一个类该由哪个类加载器来完成呢。 在这里java采用了委托模型机制,这个机制简单来讲,就是“类装载器有载入类的需求时,会先请示其Parent使用其搜索路径帮忙载入,如果Parent 找不到,那么才由自己依照自己的搜索路径搜索类”

举例:

1
2
3
4
5
6
7
8
9
10
11
Public class Test{
Public static void main(String[] arg){

ClassLoader c = Test.class.getClassLoader(); //获取Test类的类加载器
System.out.println(c);
ClassLoader c1 = c.getParent(); //获取c这个类加载器的父类加载器
System.out.println(c1);
ClassLoader c2 = c1.getParent();//获取c1这个类加载器的父类加载器
System.out.println(c2);
}
}

运行结果:

1
2
3
4
5
……AppClassLoader……

……ExtClassLoader……

Null

可以看出Test是由AppClassLoader加载器加载的,AppClassLoader的Parent 加载器是 ExtClassLoader,但是ExtClassLoader的Parent为 null 是怎么回事呵,朋友们留意的话,前面有提到Bootstrap Loader是用C++语言写的,依java的观点来看,逻辑上并不存在Bootstrap Loader的类实体,所以在java程序代码里试图打印出其内容时,我们就会看到输出为null。

ClassLoader加载流程

当运行一个程序时,jvm(java虚拟机启动),运行bootstrap classloader(启动类加载器),该加载器加载java核心API。注意此时Extclassloader和AppClassloader也在此时被加载。然后调用ExtClassloader加载扩展API,最后AppClassloader加载CLASSPATH目录下定义的Class,这就是一个程序最基本的加载流程。

下面来看一下ClassLoader中的一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
//首先检查该name指定的class是否有被加载 Class c = findLoadedClass(name);
If (c == null) {
try {
if (parent != null) {
//如果parent不为null,则调用parent的loadClass进行加载 c = parent.loadClass(name, false);
} else {
//parent 为 null,则调用 BootstrapClassLoader 进行加载 c = findBootstrapClassO(name);
}
} catch (ClassNotFoundException e) {
//如果仍然无法加载成功,则调用自身的findClass进行加载 c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

从上面一段代码中也可以看出,一个类加载的过程使用了一种父类委托模式。 为什么要使用这种父类委托模式呢?
第1个原因就是这样可以避免重复加载,当父类已经加载了该类的时候, 就没有必要子ClassLoader再加载一次,就保证了使用不同的类加载器最终得到的都是同样一个Object对象
第2个原因就是考虑到安全因素,如果不使用这种委托模式,那么可以随 时使用自定义的String来动态替代Java核心API中定义的类型,这样会存在非 常大的安全隐患,而父类委托的方式就可以避免这种情况,因为String已经在 启动时被加载,所以,用户自定义类是无法加载一个自定义的ClassLoader。

JVM 类加载机制

JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。

(1)加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。
(2)验证
这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
(3)准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:

1
public static int v = 8080;

在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080。
(4)解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中的:

  • CONSTANT_Class_info
  • CONSTANT_Field_info
  • CONSTANT_Method_info

等类型的常量。
下面我们解释一下符号引用和直接引用的概念:

  • 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
  • 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

(5)初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。
初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。p.s: 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。
注意以下几种情况不会执行类初始化:

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化
  • 定义对象数组,不会触发该类的初始化。
  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  • 通过类名获取Class对象,不会触发类的初始化。
  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

forName和loadClass的区别

Class类中有一个静态方法forName,这个方法和ClassLoader中的loadClass方法 的目的一样,都是用来加载class的,但是两者在作用上却有所区別。

1
2
3
4
5
6
Class clazz = Class. forName ("something");

或者

ClassLoadercl = Thread.currentThread().getContextClassLoader();
Class clazz = cl.loadClass("something");

Class.forName()调用 Class.forName(name,initialize, loader);也就是 Class. forName(“something”);等同于 Class.forName (“something”,true, CALLCLASS. class.getClassLoader());

第二个参数“true”是用于设置加载类的时候是否连接该类,true就连接, 否则就不连接。
在Java API文档中,loadClass方法的定义是protected,也就是说,该方法 是被保护的,而用户使用的方法是一个参数,一个参数的loadClass方法实际上 就是调用了两个参数,第二个参数默认为false。因此,在这里可以看出通过 loadClass加载类实际上就是加载的时候并不对该类进行解释,因此不会初始化 该类,而Class类的forName方法则相反,使用forName加载的时候就会将Class 进行解释和初始化。

参考文档:
【1】Java程序员面试宝典
【2】http://www.importnew.com/25295.html
【3】https://www.cnblogs.com/doit8791/p/5820037.html