类的加载指的是将类的.class文件
中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,而后在堆区建立一个java.lang.Class对象
,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,而且向Java程序员提供了访问方法区内的数据结构的接口。java
类加载器并不须要等到某个类被“首次主动使用”时再加载它,JVM规范容许类加载器在预料某个类将要被使用时就预先加载它,若是在预先加载的过程当中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)若是这个类一直没有被程序主动使用,那么类加载器就不会报告错误。程序员
加载.class文件的方式:web
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是肯定的,而解析阶段则不必定,它在某些状况下能够在初始化阶段以后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,由于这些阶段一般都是互相交叉地混合进行的,一般在一个阶段执行的过程当中调用或激活另外一个阶段。数据库
查找并加载类的二进制数据加载时类加载过程的第一个阶段,在加载阶段,虚拟机须要完成如下三件事情:数组
相对于类加载的其余阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动做)是可控性最强的阶段,由于开发人员既可使用系统提供的类加载器来完成加载,也能够自定义本身的类加载器来完成加载。缓存
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,并且在Java堆中也建立一个java.lang.Class类的对象,这样即可以经过该对象访问方法区中的这些数据。tomcat
验证:确保被加载的类的正确性安全
验证是链接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。验证阶段大体会完成4个阶段的检验动做:服务器
验证阶段是很是重要的,但不是必须的,它对程序运行期没有影响,若是所引用的类通过反复验证,那么能够考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。网络
为类的静态变量分配内存,并将其初始化为默认值
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有如下几点须要注意:
假设一个类变量的定义为:public static int value = 3;
那么变量value在准备阶段事后的初始值为0,而不是3,由于这时候还没有开始执行任何Java方法,而把value赋值为3的public static指令是在程序编译后,存放于类构造器<clinit>()方法之中的,因此把value赋值为3的动做将在初始化阶段才会执行。
这里还须要注意以下几点:
假设上面的类变量value被定义为: public static final int value = 3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。咱们能够理解为static final常量在编译期就将其结果放入了调用它的类的常量池中
把类中的符号引用转换为直接引用。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,能够是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
JVM初始化步骤:
类初始化时机:只有当对类的主动使用的时候才会致使类的初始化,类的主动使用包括如下六种:
在以下几种状况下,Java虚拟机将结束生命周期
JVM类加载机制的特色:
全盘负责
,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其余Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入父类委托
,先让父类加载器试图加载该类,只有在父类加载器没法加载该类时才尝试从本身的类路径中加载该类缓存机制
,缓存机制将会保证全部加载过的Class都会被缓存,当程序中须要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为何修改了Class后,必须重启JVM,程序的修改才会生效双亲委派机制
一般类加载器能够大体划分为如下三类,它们遵循双亲委派规则:
启动类加载器:Bootstrap ClassLoader
,负责加载存放在JDKjrelib(JDK表明JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,而且能被虚拟机识别的类库(如rt.jar,全部的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是没法被Java程序直接引用的。扩展类加载器:Extension ClassLoader
,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDKjrelibext目录中,或者由java.ext.dirs系统变量指定的路径中的全部类库(如javax.开头的类),开发者能够直接使用扩展类加载器。应用程序类加载器:System ClassLoader
,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者能够直接使用该类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。双亲委派模型的工做流程是:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,所以,全部的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即没法完成该加载,子加载器才会尝试本身去加载该类。
双亲委派优势
委托模式主要为了确保Java核心库的组件老是正确地被加载,避免重复加载。优先使用“引导类载入器”,而后是“扩展类载入器”,为了在出现和JAVA核心库同名资源的时候,加载的老是正确的系统组件。好比说就算我在本身的CLASSPATH下写了一个恶意的java.lang.Object类,也不会被载入。JVM载入的永远是系统核心库中的正确的java.lang.Object类。
双亲委派可见性
子类加载器能够看到父类加载器加载的类,而反之则不行。
正由于这个前提,当“启动类加载器”加载了Java核心库,“系统类加载器”后续加载的库才能够访问Java核心库。反过来,若是当“系统类加载器”加载了某些自开发的类,“启动类加载器”中是没法直接访问的。
双亲委派模型不是一个强制性的约束模型,双亲委派模型也有不太适用的时候,这时根据具体的状况咱们就要破坏这种机制,下面介绍两种破坏双亲委派的状况:
Thread.currentThread().getContextClassLoader();
从方法名字来看,应该是获取当前上下文的类加载器。
Java 提供了不少服务提供者接口(Service Provider Interface,SPI),容许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是做为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码常常须要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是没法找到 SPI 的实现类的,由于依照双亲委派模型,BootstrapClassloader没法委派SystemClassLoader来加载类。
ClassLoader A -> System class loader -> Extension class loader -> Bootstrap class loader
那么委派链左边的ClassLoader就能够很天然的使用右边的ClassLoader所加载的类。但若是状况要反过来,是右边的ClassLoader所加载的代码须要反过来去找委派链靠左边的ClassLoader去加载东西怎么办呢?没辙,双亲委派是单向的,没办法反过来从右边找左边。
因而,Thread就把当前的类加载器给保存下来了,其余加载器须要的时候,就经过当前线程的加载器获取到。每个Thread都有一个相关联的Context ClassLoader(由native方法创建的除外),能够经过Thread.setContextClassLoader()方法设置。若是你没有主动设置,Thread默认集成Parent Thread的 Context ClassLoader(注意,是parent Thread 不是父类)。若是你整个应用中都没有对此做任何处理,那么 全部的Thread都会以System ClassLoader做为Context ClassLoader。知道这一点很重要,由于从web服务器,java企业服务器使用一些复杂并且精巧的ClassLoader结构去实现诸如JNDI、线程池和热部署等功能以来,这种简单的状况愈加的少见了,通常都会使用特定的classloader来设置thread context classLoader。
破坏委派双亲模型就是因为用户追求动态性致使的,“动态性”就是指代码热替换、模块热部署等,就是但愿程序不须要重启就能够更新class文件,最典型的例子就是SpringBoot的热部署和OSGi。这里拿OSGi举例,OSGi实现模块化热部署的关键就是它自定义类加载机制的实现,每个程序模块(OSGi中称为Bundle)都有本身的类加载器,当须要更换一个Bundle时,就把Bundle连同类加载器一块儿换掉实现热部署。因此,在OSGi环境下,类加载器再也不是层次模型,而是网状模型。
当OSGi收到一个类加载的时候会按照如下的顺序进行搜索:
java.*
开头的类委派给父类加载器加载前文咱们了解了Java中类加载器的运行方式;但主流的Web服务器都会有本身的一套类加载器,为何呢?由于对于服务器来讲他要本身解决一些问题:
显然,若是Tomcat使用默认的类加载机制是没法知足上述要求的:
Tomcat的类加载流程如上图:
目录结构大体为:
所以就解决了上面的四个问题: