java知识之类加载

一. 类加载介绍

类加载是在运行期,JVM将java的编译文件.class读取至内存中,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的java类型的过程。

具体流程包括:加载、验证、准备、解析、初始化、使用和卸载,其中加载、验证、准备、初始化和卸载这五个步骤的顺序是确定的。而解析阶段则不一定遵守这种顺序,他在某些情况下可以在初始化阶段之后在开始解析,这是为了支持java语言的运行时绑定(也成为动态绑定),这些阶段通常都是交叉的混合式进行的,通常会在一个阶段执行的时候去激活另外一个阶段。

JVM规范中对类加载过程中的第一个阶段并没有进行强制约束,这点可以交给虚拟机的具体实现来把控,但是对于初始化阶段,JVM规范则是严格规定了有且只有5种情况下,必须对类进行初始化。

a. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要触发初始化操作。生成这4条指令最常见的场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final关键字修饰已在编译器期把结果放入常量池的静态字段除外)的时候,以及调用类的静态方法的时候。

b. 使用java.lang.reflect包方法对类进行发射调用的时候,如果类没有进行初始化,则需要触发初始化操作。

c. 当初始化一个类的时候,如果发现父类没有初始化的时候,则需要先触发父类的初始化操作。

d. 当虚拟机启动时,用户指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。

e. 当使用动态语言支持时,如果一个java.lang.invoke.MethodHandler实例最后的解析结果是REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发初始化操作。

JVM 规范中规定有且只有这五种场景会触发类的初始化,这5种场景称为对一个类进行主动引用。除此之外,所有的引用类的方式都不会触发初始化操作,称为被动引用。

二. 各阶段详解

1. 加载

在加载阶段,虚拟机需要完成下面3件事:

a. 通过一个类的全名来获取此类的二进制流。

b. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据。

c. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类数据访问入口。

加载阶段是通过类加载器(ClassLoader)来完成的,加载器主要有4种:

a. JVM启动类加载器(Bootstrap Class Loader),负责JAVA_HOME/lib下面的类库中的类加载,这个加载器程序无法引用。

b. 扩展类加载器(Extension Class Loader),由sun.misc.Launcher$ExtClassLoader实现,可在java程序中使用,负责JAVA_HOME/lib/ext目录和java.ext.dir目录中库的类的加载。

c. 应用系统类加载器(Application System Class Loader),由sun.misc.Lanucher$AppClassLoader实现,负责加载用户类路径中库的类加载,如果没有使用自定义类加载器,则默认使用该类加载器。

d. 用户自定义类加载器,需要继承抽象类ClassLoader,实现findClass方法,该方法会在loadClass时被调用。findClass根据类名查找类对象;loadClass根据类名进行双亲委托模型进行类加载并返回类对象;defineClass表示根据类的字节码转换成类对象。

双亲委托模型,约定类加载器的加载机制

                                     

当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委托给它的父类加载器去执行,每一层的类都采用相同的方式,直至委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行加载。

双亲委托模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。
使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。

使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委托给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种加载器环境中都是同一个类。相反,如果没有使用双亲委托模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。


双亲委托模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委托的代码都集中在java.lang.ClassLoader的loadClass()方法中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载。

 2. 链接

链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。

1)验证

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

2)准备 

为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内) 被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值

3)解析

将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。 可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

autored

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值