code学习

JAVA 类加载 深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?

java虚拟机将编译后的class文件加载到内存中,进行校验、转换、解析和初始化,到最终的使用。这就是java类加载机制;

下面就开始今天的内容:

1、类加载的生命周期:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)等阶段,其中验证、准备、解析3阶段也可以称为连接(Lingking),如下图:

JAVA 类加载 深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?

类的生命周期

2、类加载的时机

在类加载的生命周期中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,其加载过程一定是按照这个顺序执行的。而解析阶段有点特殊,在某些特定的情况下,它是在初始化之后开始的。

那什么情况下需要开始类加载的第一个阶段呢?对此,Java虚拟机规范中并没有进行强制约束,并且交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况(类没有初始化)必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):1)、遇到new、getstatic、putstatic或invokestaic这四条字节码指令时。

2)、使用java.lang.reflect包的方法对类进行反射调用的时候。

3)、当初始化一个类的时候,如果发现父类还没有初始化,则需要先触发其父类的初始化。

4)、当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。

5)、当使用JDK1.7的动态语言支持的时候,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个句柄所对应的类没有进行过初始化,则需要先触发器初始化。

3、类加载过程

接下来详细介绍类加载的几个重要阶段:加载、验证、准备、解析和初始化

3.1、加载

在加载阶段,虚拟机主要执行以下三个操作

1)、通过类的全限定名来获取定义这个类的二进制字节流。

2)、将这个字节流所代表的静态存储结构转化成方法区的运行时数据结构。

3)、在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。

这个阶段相比其他阶段来说,是开发人员可控性最强的阶段。因为这个阶段既能使用系统提供的加载器(这个加载器后面会进行介绍)加载,又能通过开发人员自定义的加载器进行加载。

在加载这个阶段还有一个需要注意的地方,在执行第一个操作时,需要知道可以从哪里获取class文件,例如:

1)、从压缩文件中读取(JAR,WAR等)

2)、从本地磁盘中获取

3)、从网络上获取(Applet)

4)、运行过程中动态生成(动态代理)

5)、其他文件生成(jsp生成对应的class文件)

6)、从数据库中读取

3.2、验证

验证阶段主要有4个阶段的验证:文件格式验证、元数据验证、字节码验证和符号验证

3.2.1、文件格式验证

这一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,主要包括魔数、版本号、常量池等验证。

3.2.2、元数据验证

这个阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求。主要包括是否有父类,类中的字段、方法是否与父类冲突,如果不是抽象类,是否实现了其父类或接口中要求实现的所有方法等;

3.2.3、字节码验证

这个阶段是在元数据验证之后,对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机的安全事件,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。也是验证过程最复杂的一个阶段。

3.2.4、符号引用验证

这个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候。是对类自身以外的信息进行匹配性校验。主要目的是确保解析动作能正常执行。

3.3、准备

准备阶段是为类变量分配内存并设置类变量初始值的阶段,分配这些内存是在方法区里面进行的,这个阶段有两点需要重点介绍以下的:

1)、只有类变量(被static修饰的变量)会分配内存,不包括实例变量,实例变量是在对象实例化的时候在堆中分配内存的。

2)、设置类变量的初始值是数量类型对应的默认值,而不是代码中设置的默认值。例如public static int number=111,这类变量number在准备阶段之后的初始值是0而不是111。而给number赋值为111是在初始化阶段。

基本数据类型默认值如下:

JAVA 类加载 深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?

基本数据类型的默认值

3.4、解析

解析阶段是虚拟机将常量池里内的符号引用转换为直接引用。这里注意2个概念:

1)、符号引用:以一组符号来描述所有引用的目标,符号可以是任何形式的字面量,只要使用时能正确定义到目标即可。

2)、直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等7类符号引用进行的。

3.5、初始化

这个阶段是类加载过程的最后一步,是代码真正开始执行的时候,在这个阶段,开发人员可以根据自己的需求去给类变量初始化赋值。简单来说就是执行类构造器<clinit>()方法的过程。

4、类加载器

接下来看看是什么是类加载器:

虚拟机设计团队将加载动作放到了Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称之为“类加载器”。

4.1、系统提供的3种类加载器

1)、启动类加载器(Bootstrap ClassLoader):负责将存放在<JAVA_HOME>\lib目录中,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。(注:仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)

2)、扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录中的,或被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

3)、应用程序类加载器(Application ClassLoader):负责加载用户路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,一般情况下该类加载是程序中默认的类加载器。

这三种加载器的加载顺序如下:

JAVA 类加载 深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?

系统提供的类加载器的执行顺序

4.2、双亲委派模型

JAVA 类加载 深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?

双亲委派模型

如上图展示的类加载器之间的这种层次关系就是双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应有自己的父类加载器。

双亲委派原则的好处:

1)、避免重复加载同一个类;

2)、防止用户任意修改java中的类;

双亲委派:如果一个类加载器收到类加载的请求,他首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每一层次的类加载器都是这样,因此所有的加载请求最终都应该传送到底层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试去加载。

如下图所示:

JAVA 类加载 深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?

双亲委派原则的加载过程

4.3、自定义类加载器

上面讲述的是系统提供的类加载器以及它们之间的关系,还有很多情况需要我们自定义类加载器。那该如何定义呢?有以下两种方式

1、如果我们自定义的加载器不想破坏双亲委派,继承 java.lang.ClassLoader 类并重写 findClass 方法。

2、如果使用我们自定义的加载器破坏双亲委派,继承 java.lang.ClassLoader 类并重写loadClass(java.lang.String) 方法。

深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?