定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机直接使用的java类型。类加载和链接的过程都是在运行期间完成的。 java
1. 类加载的生命周期:加载(Loading)-->验证(Verification)-->准备(Preparation)-->解析(Resolution)-->初始化(Initialization)-->使用(Using)-->卸载(Unloading) 程序员
2. 加载:这有虚拟机自行决定。 web
3. 初始化阶段: bootstrap
a) 遇到new、getstatic、putstatic、invokestatic这4个字节码指令时,若是类没有进行过初始化,出发初始化操做。 api
b) 使用java.lang.reflect包的方法对类进行反射调用时。 tomcat
c) 当初始化一个类的时候,若是发现其父类尚未执行初始化则进行初始化。 安全
d) 虚拟机启动时用户须要指定一个须要执行的主类,虚拟机首先初始化这个主类。 服务器
注意:接口与类的初始化规则在第三点不一样,接口不要气全部的父接口都进行初始化。 网络
a) 加载阶段的工做 数据结构
i. 经过一个类的全限定名来获取定义此类的二进制字节流。
ii. 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
iii. 在java堆中生成一个表明这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
b) 加载阶段完成以后二进制字节流就按照虚拟机所需的格式存储在方区去中。
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。
a) 文件格式验证:验证字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机处理。
b) 元数据验证:对字节码描述的信息进行语义分析,以确保其描述的信息符合java语言规范的要求。
c) 字节码验证:这个阶段的主要工做是进行数据流和控制流的分析。任务是确保被验证类的方法在运行时不会作出危害虚拟机安全的行为。
d) 符号引用验证:这一阶段发生在虚拟机将符号引用转换为直接引用的时候(解析阶段),主要是对类自身之外的信息进行匹配性的校验。目的是确保解析动做可以正常执行。
准备阶段是正式为变量分配内存并设置初始值,这些内存都将在方法区中进行分配,这里的变量仅包括类标量不包括实例变量。
解析是虚拟机将常量池的符号引用替换为直接引用的过程。
a) 符号引用:符号引用以一组符号来描述所引用的目标,符号能够是任意形式的字面量,只要使用时能无歧义地定位到目标便可。符号引用与虚拟机实现的内存布局无关,引用的目标并不必定已经加载到内存中。
b) 直接引用:直接引用能够是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接饮用是与内存布局相关的。
c) 类或接口的解析
d) 字段的解析
e) 类方法解析
f) 接口方法解析
是根据程序员制定的主观计划区初始化变量和其余资源,或者能够从另一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。
当一个 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中几个最重要的方法:
经过进一步分析标准扩展类加载器(sun.misc.Launcher$ExtClassLoader)和系统类加载器(sun.misc.Launcher$AppClassLoader)的代码以及其公共父类(java.net.URLClassLoader和java.security.SecureClassLoader)的代码能够看出,都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法。既然这样,咱们就能够经过分析java.lang.ClassLoader中的loadClass(String name)方法的代码就能够分析出虚拟机默认采用的双亲委派机制究竟是什么模样:
经过上面的代码分析,咱们能够对JVM采用的双亲委派类加载机制有了更感性的认识,下面咱们就接着分析一下启动类加载器、标准扩展类加载器和系统类加载器三者之间的关系。可能你们已经从各类资料上面看到了以下相似的一幅图片:
图三 类加载器默认委派关系图
上面图片给人的直观印象是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面咱们就用代码具体测试一下:
示例代码:
说明:经过java.lang.ClassLoader.getSystemClassLoader()能够直接获取到系统类加载器。
经过以上的代码输出,咱们能够断定系统类加载器的父加载器是标准扩展类加载器,可是咱们试图获取标准扩展类加载器的父类加载器时确获得了null,就是说标准扩展类加载器自己强制设定父类加载器为null。咱们仍是借助于代码分析一下:
咱们首先看一下java.lang.ClassLoader抽象类中默认实现的两个构造函数:
咱们再看一下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,则会调用本地方法进行启动类加载尝试。因此,图三中,启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系事实上是仍就成立的。(在后面的用户自定义类加载器部分,还会作更深刻的分析)。
以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合JDK代码对JVM默认使用的双亲委派类加载机制作了分析。下面咱们就来看一个综合的例子。首先在eclipse中创建一个简单的java应用工程,而后写一个简单的JavaBean以下:
在现有当前工程中另外创建一测试类(ClassLoaderTest.java)内容以下:
测试一:
对应的输出以下:
(说明:当前类路径默认的含有的一个条目就是工程的输出目录)
测试二:
将当前工程输出目录下的…/classloader/test/bean/TestBean.class打包进test.jar剪贴到< Java_Runtime_Home >/lib/ext目录下(如今工程输出目录下和JRE扩展目录下都有待加载类型的class文件)。再运行测试一测试代码,结果以下:
对比测试一和测试二,咱们明显能够验证前面说的双亲委派机制,系统类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。
测试三:
将test.jar拷贝一份到< Java_Runtime_Home >/lib下,运行测试代码,输出以下:
测试三和测试二输出结果一致。那就是说,放置到< 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中变量视图查看验证。
Java的链接模型容许用户运行时扩展引用程序,既能够经过当前虚拟机中预约义的加载器加载编译时已知的类或者接口,又容许用户自行定义类装载器,在运行时动态扩展用户的程序。经过用户自定义的类装载器,你的程序能够装载在编译时并不知道或者还没有存在的类或者接口,并动态链接它们并进行有选择的解析。
运行时动态扩展java应用程序有以下两个途径:
这个方法其实在前面已经讨论过,在后面的问题2解答中说明了该方法调用会触发那个类加载器开始加载任务。这里须要说明的是多参数版本的forName(…)方法:
这里的initialize参数是很重要的,能够以为被加载同时是否完成初始化的工做(说明: 单参数版本的forName方法默认是不完成初始化的).有些场景下,须要将initialize设置为true来强制加载同时完成初始化,例如典型的就是利用DriverManager进行JDBC驱动程序类注册的问题,由于每个JDBC驱动程序类的静态初始化方法都用DriverManager注册驱动程序,这样才能被应用程序使用,这就要求驱动程序类必须被初始化,而不仅仅被加载.
经过前面的分析,咱们能够看出,除了和本地实现密切相关的启动类加载器以外,包括标准扩展类加载器和系统类加载器在内的全部其余类加载器咱们均可以当作自定义类加载器来对待,惟一区别是是否被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法作了介绍,这里就简要叙述一下通常用户自定义类加载器的工做流程吧(能够结合后面问题解答一块儿看):
一、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,若是已经装载,直接返回;不然转入步骤2
二、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真个虚拟机中各类类加载器最终会呈现树状结构),若是父类加载器可以完成,则返回父类加载器加载的Class实例;不然转入步骤3
三、调用本类加载器的findClass(…)方法,试图获取对应的字节码,若是获取的到,则调用defineClass(…)导入类型到方法区;若是获取不到对应的字节码或者其余缘由失败,返回异常给loadClass(…), loadClass(…)转抛异常,终止加载过程(注意:这里的异常种类不止一种)。
(说明:这里说的自定义类加载器是指JDK 1.2之后版本的写法,即不覆写改变java.lang.loadClass(…)已有委派逻辑状况下)
在Java中,一个类用其彻底匹配类名(fully qualified class name)做为标识,这里指的彻底匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例做为惟一标识,不一样类加载器加载的类将被置于不一样的命名空间.咱们能够用两个自定义类加载器去加载某自定义类型(注意,不要将自定义类型的字节码放置到系统路径或者扩展路径中,不然会被系统类加载器或扩展类加载器抢先加载),而后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会获得不相等的结果。这个你们能够写两个自定义的类加载器去加载相同的自定义类型,而后作个判断;同时,能够测试加载java.*类型,而后再对比测试一下测试结果。
Class.forName(String name)默认会使用调用类的类加载器来进行类加载。咱们直接来分析一下对应的jdk的代码:
前面讲过,在不指定父类加载器的状况下,默认采用系统类加载器。可能有人以为不明白,如今咱们来看一下JDK对应的代码实现。众所周知,咱们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现以下:
咱们再来看一下对应的getSystemClassLoader()方法的实现:
咱们能够写简单的测试代码来测试一下:
本机对应输出以下:
因此,咱们如今能够相信当自定义类加载器没有指定父类加载器的状况下,默认的父类加载器即为系统类加载器。同时,咱们能够得出以下结论:
即时用户自定义类加载器不指定父类加载器,那么,一样能够加载以下三个地方的类:
1. <Java_Runtime_Home>/lib下的类
2. < Java_Runtime_Home >/lib/ext下或者由系统变量java.ext.dir指定位置中的类
3. 当前工程类路径下或者由系统变量java.class.path指定位置中的类
JVM规范中规定若是用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,咱们能够得出以下结论:
即时用户自定义类加载器不指定父类加载器,那么,一样能够加载到<Java_Runtime_Home>/lib下的类,但此时就不可以加载<Java_Runtime_Home>/lib/ext目录下的类了。
说明:问题3和问题4的推断结论是基于用户自定义的类加载器自己延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,若是用户对这一默认委派逻辑进行了改变,以上推断结论就不必定成立了,详见问题5。
通常在JDK 1.2以前的版本才这样作,并且事实证实,这样作极有可能引发系统默认的类加载器不能正常工做。在JVM规范和JDK文档中(1.2或者之后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:
经过前面的分析咱们已经知道,用户自定义类加载器(WrongClassLoader)的默
认的类加载器是系统类加载器,可是如今问题4种的结论就不成立了。你们能够简
单测试一下,如今<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工
程类路径上的类都加载不上了。
(说明:D:"classes"beans"Account.class物理存在的)
输出结果:
这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的因为覆写loadClass(…)引发的逻辑错误明显是比较简单的,实际引发的逻辑错误可能复杂的多。
将自定义类加载器代码WrongClassLoader.Java作以上修改后,再运行测试代码,输出结果以下:
这说明,beans.Account加载成功,且是由自定义类加载器WrongClassLoader加载。
这其中的缘由分析,我想这里就没必要解释了,你们应该能够分析的出来了。
经过上面问题4和问题5的分析咱们应该已经理解,我的以为这是自定义用户类加载器时最重要的一点,但经常被忽略或者轻易带过。有了前面JDK代码的分析做为基础,我想如今你们均可以随便举出例子了。
事先尽可能准确理解待定义的类加载器要完成的加载任务,确保最大程度上可以获取到对应的字节码内容。
一是能够直接调用ClassLoader.getSystemClassLoader()或者其余方式获取到系统类加载器(系统类加载器和扩展类加载器自己都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法能够获取到;
二是能够直接经过获取系统属性java.class.path 来查看当前类路径上的条目信息 , System.getProperty("java.class.path")
方法之一:
本机对应输出以下:
1, 每一个ClassLoader都维护了一份本身的名称空间, 同一个名称空间里不能出现两个同名的类。
2, 为了实现java安全沙箱模型顶层的类加载器安全机制, java默认采用了 ” 双亲委派的加载链 ” 结构.
以下图:
Class Diagram:
类图中, BootstrapClassLoader是一个单独的java类, 其实在这里, 不该该叫他是一个java类。
由于, 它已经彻底不用java实现了。
它是在jvm启动时, 就被构造起来的, 负责java平台核心库。(具体上面已经有介绍)
启动类加载实现 (其实咱们不用关心这块, 可是有兴趣的, 能够研究一下 ):
bootstrap classLoader 类加载原理探索
ClassLoader 类加载逻辑分析, 如下逻辑是除 BootstrapClassLoader 外的类加载器加载流程:
即在通常状况下, 保证同一个类中所关联的其余类都是由当前类的类加载器所加载的.
上图中 ClassLoader.getCallerClassLoader 就是获得调用当前forName方法的类的类加载器
以上代码摘自sun.misc.Launch的无参构造函数Launch()。
使用线程上下文类加载器, 能够在执行线程中, 抛弃双亲委派加载链模式, 使用线程上下文里的类加载器加载类.
典型的例子有, 经过线程上下文来加载第三方库jndi实现, 而不依赖于双亲委派.
大部分java app服务器(jboss, tomcat..)也是采用contextClassLoader来处理web服务。
还有一些采用 hotswap 特性的框架, 也使用了线程上下文类加载器, 好比 seasar (full stack framework in japenese).
线程上下文从根本解决了通常应用不能违背双亲委派模式的问题.
使java类加载体系显得更灵活.
随着多核时代的来临, 相信多线程开发将会愈来愈多地进入程序员的实际编码过程当中. 所以,
在编写基础设施时, 经过使用线程上下文来加载类, 应该是一个很好的选择.
固然, 好东西都有利弊. 使用线程上下文加载类, 也要注意, 保证多根须要通讯的线程间的类加载器应该是同一个,
防止由于不一样的类加载器, 致使类型转换异常(ClassCastException).
使用该接口, 能够动态的加载class文件.
例如,
在jdk中, URLClassLoader是配合findClass方法来使用defineClass, 能够从网络或硬盘上加载class.
而使用类加载接口, 并加上本身的实现逻辑, 还能够定制出更多的高级特性.
好比,
一个简单的hot swap 类加载器实现:
这个类的做用是能够从新载入同名的类, 可是, 为了实现hotswap, 老的对象状态
须要经过其余方式拷贝到重载过的类生成的全新实例中来。(A类中的b实例)
而新实例所依赖的B类若是与老对象不是同一个类加载器加载的, 将会抛出类型转换异常(ClassCastException).
为了解决这种问题, HotSwapClassLoader自定义了load方法. 即当前类是由自身classLoader加载的, 而内部依赖的类
仍是老对象的classLoader加载的.
输出