1、什么是类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在java堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class 对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
2、 类的加载过程
JVM 将类的加载过程分为三个大的步骤:加载(loading),链接(link),初始化(initialize)。其中链接又分为三个步骤:验证,准备,解析。
(1) 加载:查找并加载类的二进制数据
加载是类加载过程中的第一个阶段,加载过程虚拟机需要完成以下三件事情:
1) 通过一个类的全限定名来获取其定义的二进制字节流;
2) 将这个字节流所代表的静态存储结构转为方法区的运行时数据结构;
3) 在Java 堆中生成一个代表这个类的java.lang.Class 对象,作为方法区中这些数据的访问入口。
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据
(2) 链接:
① 验证:确保被加载类的正确性;
主要是为了安全考虑,为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
② 准备:为类的静态变量分配内存,并将其初始化为默认值;
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1)、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2)、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个类变量的定义为:public static int value = 3;
那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
③ 解析:把类中的符号引用转换为直接引用;
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
(3) 初始化:为类的静态变量赋予正确的初始值
为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
① 声明类变量是指定初始值;
② 使用静态代码块为类变量指定初始值;
③ JVM初始化步骤
1)、假如这个类还没有被加载和连接,则程序先加载并连接该类
2)、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3)、假如类中有初始化语句,则系统依次执行这些初始化语句
④ 类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
– 创建类的实例,也就是new的方式
– 访问某个类或接口的静态变量,或者对该静态变量赋值
– 调用类的静态方法
– 反射(如Class.forName(“com.shengsiyuan.Test”))
– 初始化某个类的子类,则其父类也会被初始化
– Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
(4) 结束生命周期
在如下几种情况下,Java虚拟机将结束生命周期
– 执行了System.exit()方法
– 程序正常执行结束
– 程序在执行过程中遇到了异常或错误而异常终止
– 由于操作系统出现错误而导致Java虚拟机进程终止
3、类加载器
JVM 类加载器作用,将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。
类加载器是通过ClassLoader 及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
(1) Bootstrap ClassLoader 引导类加载器
负责加载Java核心库$JAVA_HOME中的jre/lib/rt.jar 里所有的class,由c++实现,不是ClassLoader子类。
(2) Extension ClassLoader 扩展类加载器
负责加载Java 平台中扩展功能的一些jar包,包括$JAVA_HOME中的jre/lib/ext/*.jar 或 -D java.ext.dirs指定目录下的jar包。
(3) App ClassLoader
负责加载classpath 中指定的jar包及目录中class
(4) Custom ClassLoader
应用程序根据自身需要自定义的ClassLoader,如tomcat,jboss 都会根据j2ee规范自行实现ClassLoader,加载过程中会先检查是否已被加载,检查顺序是自底向上,从Custom ClassLoader 到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类在所有ClassLoader 只加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
4、JVM 三种预定义加载器
JVM预定义有三种类加载器,当一个 JVM启动的时候,Java 默认开始使用如下三种类加载器:
(1) 引导类加载器(Bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。它负责将/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
(2) 扩展类加载器(Extensions class loader):该类加载器在此目录里面查找并加载 Java 类。扩展类加载器是由Sun的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dirs指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
(3) 系统类加载器(System class loader):系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
5、类加载器 "双亲委派" 机制
(1) 双亲委派机制介绍
在这里需要着重说明,JVM在加载类时默认采用的是双亲委派机制。所谓的双亲委派机制,就是某个特定的类加载器在接到类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和标准扩展类加载器为例作简单分析。
双亲委派机制是为了保证Java核心库的类型安全。这种机制能保证不会出现用户自己能定义java.lang.Object类的情况,因为即使定义了,也加载不了。
图一 标准扩展类加载器继承层次图 图二 系统类加载器继承层次图
图一与图二可以看出,类加载器均是继承自java.lang.ClassLoader 抽象类。我们来看看java.lang.ClassLoader 中几个最重要的方法:
//加载指定名称(包括包名)的二进制类型,供用户调用的接口
public Class> loadClass(String name) throws ClassNotFoundException{//…}//加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是,这里的resolve参数不一定真正能达到解析的效果~_~),供继承用
protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException{//…}//findClass方法一般被loadClass方法调用去加载指定名称类,供继承用
protected Class> findClass(String name) throws ClassNotFoundException {//…}//定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承(说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以 无需覆写,直接调用就可以了)
protected final Class> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{//…}
通过进一步分析标准扩展类加载器(sun.misc.Launcher$ExtClassLoader) 和系统类加载器(sun.misc.Launcher$AppClassLoader)的代码以及其公共父类(java.net.URLClassLoader和 java.security.SecureClassLoader) 的代码可以看出,都没有覆写java.lang.ClassLoader中默认的加载委派规则 loadClass() 方法。既然这样我们可以通过分析java.lang.ClassLoader中的 loadClass(String name) 方法代码看到虚拟机默认采用的双亲委派机制到底是什么模样:
1 public Class> loadClass(String name) throwsClassNotFoundException {2 return loadClass(name, false);3 }4
5 protected synchronized Class> loadClass(String name, boolean resolve) throwsClassNotFoundException {6 //首先判断该类型是否已经被加载
7 Class c =findLoadedClass(name);8 if (c == null) {9 //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
10 try{11 if (parent != null) {12 //如果存在父类加载器,就委派给父类加载器加载
13 c = parent.loadClass(name, false);14 } else{15 //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
16 c =findBootstrapClass0(name);17 }18 }catch(ClassNotFoundException e) {19 //如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
20 c =findClass(name);21 }22 }23 if(resolve) {24 resolveClass(c);25 }26 returnc;27 }
通过上面的代码分析,我们可以对JVM采用的双亲委派类加载机制有了更感性的认识。
我们就接着分析一下启动类加载器、标准扩展类加载器和系统类加载器三者之间的关系。可能大家已经从各种资料上面看到了如下类似的一幅图片:
图三 类加载器默认委派关系图
上面图片给人的直观印象是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面我们就用代码具体测试一下:
public static voidmain(String[] args) {System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}/*输出结果:
sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
null*/
通过以上的代码输出,我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,就是说标准扩展类加载器本身强制设定父类加载器为null。我们借助于代码分析一下:
我们首先看一下java.lang.ClassLoader抽象类中默认实现的两个构造函数:protectedClassLoader() {
SecurityManager security=System.getSecurityManager();if (security != null) {
security.checkCreateClassLoader();
}//默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器
this.parent =getSystemClassLoader();
initialized= true;
}protectedClassLoader(ClassLoader parent) {
SecurityManager security=System.getSecurityManager();if (security != null) {
security.checkCreateClassLoader();
}//强制设置父类加载器
this.parent =parent;
initialized= true;
}
我们再看一下ClassLoader抽象类中parent成员的声明://The parent class loader for delegation
private ClassLoader parent;
声明为私有变量的同时并没有对外提供可供派生类访问的public或者protected设置器接口(对应的setter方法),结合前面的测试代码的输出,我们可以推断出:
(1) 系统类加载器(AppClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)
(2) 扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)
现在我们可能会有这样的疑问:扩展类加载器(ExtClassLoader)的父类加载器被强制设置为null了,那么扩展类加载器为什么还能将加载任务委派给启动类加载器呢?
图四 标准扩展类加载器和系统类加载器成员大纲视图
图五 扩展类加载器和系统类加载器公共父类成员大纲视图
通过以上两图可以看出,标准扩展类加载器和系统类加载器及其父类(java.net.URLClassLoader和java.security.SecureClassLoader) 都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass()方法。有关java.lang.ClassLoader中默认的加载委派规则前面已经分析过,如果父加载器为null,则会调用本地方法进行启动类加载尝试。所以,图三中,启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系事实上是仍就成立的。
(2) 类加载双亲委派示例
以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合JDK代码对JVM默认使用的双亲委派类加载机制做了分析。下面我们就来看一个综合的例子。首先在eclipse中建立一个简单的java应用工程,然后写一个简单的JavaBean如下:
packagecom.latiny.bean;public classTestBean {publicTestBean()
{}
}
在现有当前工程中另外建立一测试类(JVMTest1.java)内容如下:
packagecom.latiny.reflect;public classJVMTest1 {public static voidmain(String[] args) {try{//查看当前系统类路径中包含的路径条目
System.out.println(System.getProperty("java.class.path"));//调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean
Class typeLoaded = Class.forName("com.latiny.bean.TestBean");//查看被加载的TestBean类型是被那个类加载器加载的s
System.out.println(typeLoaded.getClassLoader());
}catch(ClassNotFoundException e) {//TODO Auto-generated catch block
e.printStackTrace();
}
}
}
输出结果:
C:\Work\Project\Java\Eclipse\ReflectProject\bin
sun.misc.Launcher$AppClassLoader@73d16e93
测试二:
将当前工程输出目录下的C:\Work\Project\Java\Eclipse\ReflectProject\bin\com\latiny\bean打包进test.jar剪贴到< Java_Runtime_Home >/lib/ext目录下(现在工程输出目录下和JRE扩展目录下都有待加载类型的class文件)。再运行测试一测试代码,结果如下:
C:\Work\Project\Java\Eclipse\ReflectProject\bin
sun.misc.Launcher$AppClassLoader@2a139a55 78
测试三和测试二输出结果一致。那就是说,放置到< Java_Runtime_Home >/lib目录下的TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载< Java_Runtime_Home >/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除< Java_Runtime_Home >/lib/ext目录下和工程输出目录下的TestBean对应的class文件,然后再运行测试代码,则将会有ClassNotFoundException异常抛出。有关这个问题,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中设置相应断点运行测试三进行调试,会发现findBootstrapClass0()会抛出异常,然后在下面的findClass方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过JDT中变量视图查看验证。
6 java程序动态扩展方式
Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以装载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。
运行时动态扩展java应用程序有如下两个途径:
(1) 调用java.lang.Class.forName(…)
这个方法其实在前面已经讨论过,在后面的问题2解答中说明了该方法调用会触发哪个类加载器开始加载任务。这里需要说明的是多参数版本的forName(…)方法:
public static Class> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException
这里的initialize参数是很重要的,可以决定类被加载同时是否完成初始化的工作(说明:单参数版本的forName方法默认是不完成初始化的).有些场景下,需要将initialize设置为true来强制加载同时完成初始化,例如典型的就是利用DriverManager进行JDBC驱动程序类注册的问题,因为每一个JDBC驱动程序类的静态初始化方法都用DriverManager注册驱动程序,这样才能被应用程序使用,这就要求驱动程序类必须被初始化,而不单单被加载。
(2) 用户自定义类加载器
通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader 抽象类中的几个重要的方法做了介绍,这里就简要叙述一下一般用户自定义类加载器的工作流程吧(可以结合后面问题解答一起看):
① 首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2;
② 委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,整个虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3;
③ 调用本类加载器的findClass() 方法,试图获取对应的字节码,如果获取的到,则调用defineClass() 导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(), loadClass() 转抛异常,终止加载过程(注意:这里的异常种类不止一种)。
(说明:这里说的自定义类加载器是指JDK 1.2以后版本的写法,即不覆写改变java.lang.loadClass() 已有委派逻辑情况下)