Java类加载整理总结

1.概述

一个Java文件从编写到最后地执行其实总结起来就是两步:1.编译;2.运行。编译,即把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件。运行,则是把编译生成的.class文件交给Java虚拟机(JVM)执行。而虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行验证,准备,解析,初始化,最终生成虚拟机可以直接使用的java类型,这个过程就叫做类加载。

2.加载机制

类加载过程如下图所示:

1.加载  

               1.1 加载流程 

在加载阶段,虚拟机需要完成以下三件事情:

    1、通过一个类的全限定名来获取其定义的二进制字节流。
    2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。


        注意:这里第1条中的二进制字节流并不只是单纯地从Class文件中获取,比如它还可以从Jar包中获取、从网络中获取(最典型的应用便是Applet)、由其他文件生成(JSP应用)等。
        相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
    加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。


        1.2 类加载器介绍

   1.2.1 类加载器可以大致划分为以下三类:

  • 启动类加载器:负责加载$JAVA_HOME中jre/lib/里所有的 class(JDK 代表 JDK 的安装目录,下同),或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如 rt.jar,所有的java.*开头的类均被 Bootstrap ClassLoader 加载)。启动类加载器由 C++ 实现,不是 ClassLoader 子类。无法被 Java 程序直接引用的。
  • 扩展类加载器:该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器:该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

  1.2.2 类加载器总结:

  • 启动类加载器:Bootstrap ClassLoader 它使用 C++ 实现(这里仅限于 Hotspot,也就是 JDK1.5 之后默认的虚拟机,有很多其他的虚拟机是用 Java 语言实现的),是虚拟机自身的一部分。 
  • 所有其他的类加载器:Extension ClassLoader、Application ClassLoader、自定义类加载器等这些类加载器都由 Java 语言实现,独立于虚拟机之外,并且全部继承自抽象类 java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。 

       应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
 1)在执行非置信代码之前,自动验证数字签名。
 2)动态地创建符合用户特定需要的定制化构建类。
 3)从特定的场所取得java class,例如数据库中和网络中。
事实上当使用Applet的时候,就用到了特定的ClassLoader,因为这时需要从网络上加载java class,并且要检查相关的安全信息,应用服务器也大都使用了自定义的ClassLoader技术。

1.3 类加载器的加载机制

    1.3.1双亲委派模型

     java一个类的加载一般是按照双亲委派机制来走:自下而上寻找校验加载的一个过程。大致模型如下图所示 :

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。 

使用这种方式的好处:能够有效确保一个类的全局唯一性,当程序中出现多个全限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。(可以防止一些恶意的第三方类的注入替换基础类)

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的Class-Path中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。
          双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
         双亲委派机制只是Java虚拟机规范建议采用的加载机制,实际在tomcat中,类加载器所采用的加载机制与传统的双亲委派模型有一定的区别,当缺省的类加载器接收到一个类的加载任务时,首先会去由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行。

1.4 自定义类加载器

程序中如果没有显式指定类加载器的话,默认是AppClassLoader来加载,它负责加载ClassPath目录中的所有类型,如果被加载的类型并没有在ClassPath目录中时,抛出java.lang.ClassNotFoundException异常。
           一般是继承ClassLoader,如果要符合双亲委派规范,则重写findClass方法;要破坏的话,重写loadClass方法。
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2发布之前。由于双亲委派模型在JDK 1.2之后才被引入,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的load-Class()。
双亲委派的具体逻辑就实现在loadClass()方法之中,JDK1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。

2.验证

这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
主要包括验证内容涵盖了类数据信息的格式验证、语义分析、操作验证:

  • 格式验证:验证是否符合class文件规范
  • 语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
  • 操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)

注意:

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

3.准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:

public static int v = 8080;

          实际上变量v在准备阶段过后的初始值为0而不是8080,将v赋值为8080的putstatic指令是程序被编译后,存放于类构造器<client>方法之中。
但是注意如果声明为:

在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080。原因:可以参考一下ConstantValue属性。

public static final int v = 8080;


     4.解析

将常量池中所有的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法)。这个阶段可以在初始化之后再执行。(在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080)
符号引用就是class文件中的:
CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info
等类型的常量。
下面我们解释一下符号引用直接引用的概念:

符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

      5.初始化

将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。
所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法。该方法的作用就是初始化一个中的变量,使用用户指定的值覆盖之前在准备阶段里设定的初始值。任何invoke之类的字节码都无法调用<clinit>方法,因为该方法只能在类加载的过程中由JVM调用。
如果父类还没有被初始化,那么优先对父类初始化,但在<clinit>方法内部不会显示调用父类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的父类<clinit>方法已经被执行。
          JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。
p.s: 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>()方法。

5.1 类初始化的6种时机

(1)为一个类型创建一个新的对象实例时(比如new、反射、序列化)

(2)调用一个类型的静态方法时(即在字节码中执行invokestatic指令)

(3)调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式

(4)调用JavaAPI中的反射方法时(比如调用java.lang.Class中的方法,或者java.lang.reflect包中其他类的方法)

(5)初始化一个类的派生类时(Java虚拟机规范明确要求初始化一个类时,它的超类必须提前完成初始化操作,接口例外)

(6)JVM启动包含main方法的启动类时。

*注意以下几种情况不会执行类初始化:

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

6.使用

                     略

7.卸载

                     略

参考文章:

   https://blog.csdn.net/zhangjg_blog/article/details/21486985

   https://blog.csdn.net/qq_39327985/article/details/81504646

   http://www.importnew.com/25295.html

   https://segmentfault.com/a/1190000004597758

   https://blog.csdn.net/u012585964/article/details/52011138/

猜你喜欢

转载自blog.csdn.net/weixin_40151613/article/details/87937552