java--类加载

转:https://blog.csdn.net/xiaohai_chen/article/details/79538897 

一、类的生命周期

    一个类的生命周期包括了加载、验证、准备、解析、初始化、使用、卸载这七个阶段,一般我们只研究前五个阶段,这五个阶段又可以分为加载、连接(准备,验证,解析)、初始化
    加载、验证、准备、初始化这几个阶段都是按顺序开始的,而解析阶段在某些情况下可以在初始化后开始,这几个阶段一般都是交叉混合进行的,通常会在一个阶段执行时激活、调用另一个阶段。同时要注意的是,这里说的都是开始,并不代表他们会按顺序完成

   加载:加载类的二进制数据
      首先会根据各种途径(比如网络下载、数据库提取、从jar,zip中读取等)获取类的二进制数据,也就是class文件,把获取   到的二进制数据读入内存,存储在运行时数据区的方法区,这些二进制数据所代表的存储结构会被转化为方法区中运行时的数据结构,接着会在方法区中创建相应的class对象(这里的class对象与平时所说的对象是不一样的,当使用new创建实例对象时,就会通过class对象在堆中创建实例对象)用来封装保存在方法区内的数据结构。

     连接:把已经读入内存的二进制数据合并到java虚拟机的运行环境
            验证:正常情况下都是通过javac进行编译的,所以都是正确的,但是如果用户自己手动生成一个java字节  码文件,为了保证class文件的字节流中包含的信息是正确的,不会危害虚拟机自身安全,就需要对这个class文件进行验证。大致分为4个验证阶段。

              文件格式验证:验证字节流是否符合class文件的规范,确保输入的字节流能正确解析并存储到方法区                                

                            例如: 是否以0xCAFEBABE开头(所验证的文件是否是一个虚拟机所能接受的文件)。
                                       当前 jdk的版本是否能与class文件版本兼容,只能向上兼容,不能向下兼容等。

                   元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合规范要求。

                             例如:该类是否有父类。
                                       是否继承了final类,这是不允许的。
                                       如果该类非接口,是否实现了父类或接口中要求实现的方法等。

                  字节码验证:这个阶段是比较复杂的,通过数据流和控制流分析,对类的方法体进行校验,确保程序的合法性
                             例如: 确保操作数栈的数据类型与指令序列能够配合,例如防止出现操作栈数据类型与指令序列类型不同的情况
                                        确保方法体中类型转换的有效性。

              符号引用验证:这里的符号引用不单单指类的,也包括方法。发生在符号引用转为直接引用的时候,也就是解析阶段, 对常量池中各种符号引用的信息进行匹配性校验,确保解析动作正确执行
                           例如:常量池中符号引用所描述的类是否存在
                                     访问的方法或字段是否存在足够的权限

         准备:分配内存,并为类设置初始值(在方法区中),只为类变量(static修饰的变量)分配;

                   例如一个声明 为int型的static属性,在准备阶段会被设置为0

                   实例变量的分配会在类初始化时在堆中分配;
                   对于用static final修饰的变量,也就是常量,会在该阶段就直接赋值  

    
         解析:解析的内容有类或接口,字段,类方法,接口方法
                   符号引用替换为直接引用
                   字符串引用对象不一定被加载
                   直接引用就是指针或地址偏移量,引用对象一定在内存中

   初始化:给已分配空间的属性赋值
     在准备阶段中,类变量已经被赋上了默认值,而在该阶段中,会初始化类变量和其他资源,可以看成是执行类构造器<clint>的过程,下面先说说什么是<clinit>方法
     <clinit>()方法:是编译器自动收集类中的所有类变量和静态语句块(static{})中的语句合并而成的。

                               知道这一点很重要,而<clinit>()方法里面语句的顺序由源程序代码决定。

                              多个线程去初始化一个类时,只能有一个线程去执行这个类的clinit方法,其他都会阻塞,

                              这也是为什么被static修饰的代码只会被执行一次的的原因

    在虚拟机规范中,只有如下情况才会对类进行初始化,自然的,类的加载,连接阶段在此前就已经开始了

               1)通过new关键字实例化一个对象,或者调用类的static属性,static方法

               2)通过反射进行操作调用类进行调用,例如Class.forName("com.hai.Demo")

               3)当初始化一个类时,如果发现其父类没有初始化过,则先对其初始化,接口例外,接口不要求其父类接口一定要被初始化过,只有当真正使用到父接口时,才会初始化

               4)当虚拟机启动时,会初始化包含main方法的类

二、类加载器

    

    这张图是可以清楚的看出加载器间层次的关系,下面详细说说这些加载器

启动类加载器:Bootstrap ClassLoader,负责加载存放在JDKjrelib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DKjrelibext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。


应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

注意:每个类加载器都会有一个类加载器来作为它的父类加载器,除了启动类加载器之外

    每次请求加载类时,会先请求父类,自下向上的发送加载请求,最终都会到达启动类加载器,如果父类能够完成加载请求,就交给父类完成,如果完成不了子类加载器才会尝试自己去加载,这种机制称为双亲委派模型

工作流程:

1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;


4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

一般应用都是由这3个加载器互相配合进行加载的,如果有必要,可以加入自定义的加载器。ClassLoader是一个抽象类,它的实例具有将java字节码装载到JVM的功能,可以通过自定义,满足不同的字节码流获取方式

要实现自定义类加载器只需要实现ClassLoader的loadClass方法即可,它是一个抽象方法。

原文地址:https://www.cnblogs.com/jvStarBlog/p/11248130.html