武汉java培训
达内武汉中心

15271940953

热门课程

详解 Java 类的加载过程

  • 时间:2018-01-13 11:05
  • 发布:武汉Java培训机构
  • 来源:互联网

    1.1 类的生命周期
    一个类从加载进内存到卸载出内存为止,一共经历 7 个阶段:
    加载--> 验证--> 准备--> 解析--> 初始化--> 使用--> 卸载
    其中,类加载包括 5 个阶段:
    加载--> 验证--> 准备--> 解析--> 初始化
    在类加载的过程中,以下 3 个过程称为连接:
    验证--> 准备--> 解析
    因此,JVM 的类加载过程也可以概括为 3 个过程:
    加载--> 连接--> 初始化
    C/C++ 在运行前需要完成预处理、编译、汇编、链接;而在 Java 中,类加载 (加载、连接、初始化) 是在程序运行期间完成的.
    在程序运行期间进行类加载会稍微增加程序的开销,但随之会带来更大的好处--提高程序的灵活性.

    Java 语言的灵活性体现在它可以在运行期间动态扩展,所谓动态扩展就是在运行期间动态加载和动态连接.

武汉Java培训机构

    1.2 类加载的时机
    1.2.1 类加载过程中每个步骤的顺序
    我们已经知道,类加载的过程包括:加载、连接、初始化,连接又分为:验证、准备、解析,所以说类加载一共分为 5 步:加载、验证、准备、解析、初始化.武汉Java培训机构
    其中加载、验证、准备、初始化的开始顺序是依次进行的,这些步骤开始之后的过程可能会有重叠.而解析过程会发生在初始化过程中.
    1.2.2 类加载过程中 "初始化" 开始的时机
    JVM 规范中只定义了类加载过程中初始化过程开始的时机,加载、连接过程都应该在初始化之前开始 (解析除外),这些过程具体在何时开始,JVM 规范并没有定义,不同的虚拟机可以根据具体的需求自定义.
    初始化开始的时机:
    在运行过程中遇到如下字节码指令时,如果类尚未初始化,那就要进行初始化:new、getstatic、putstatic、invokestatic.
    这四个指令对应的 Java 代码场景是:
    通过 new 创建对象;
    读取、设置一个类的静态成员变量 (不包括 final 修饰的静态变量);
    调用一个类的静态成员函数.
    使用 java.lang.reflect 进行反射调用的时候,如果类没有初始化,那就需要初始化.
    当初始化一个类的时候,若其父类尚未初始化,那就先要让其父类初始化,然后再初始化本类;当虚拟机启动时,虚拟机会首先初始化带有 main 方法的类,即主类.
    1.2.3 主动引用 与 被动引用
    JVM 规范中要求在程序运行过程中,"当且仅当" 出现上述 4 个条件之一的情况才会初始化一个类.如果间接满足上述初始化条件是不会初始化类的.
    其中,直接满足上述初始化条件的情况叫做主动引用;间接满足上述初始化过程的情况叫做被动引用.
    那么,只有当程序在运行过程中满足主动引用的时候才会初始化一个类,若满足被动引用就不会初始化一个类.
    1.2.4 被动引用的场景示例
    示例一
    public class Fu{ public static String name = " 柴毛毛 "; static{ System.out.println(" 父类被初始化!"); }}public class Zi{ static{ System.out.println(" 子类被初始化!"); }}public static void main(String[] args){ System.out.println(Zi.name);}
    输出结果:父类被初始化!柴毛毛.武汉Java培训机构
    原因分析:
    本示例看似满足初始化时机的第一条:当要获取某一个类的静态成员变量的时候如果该类尚未初始化,则对该类进行初始化.
    但由于这个静态成员变量属于 Fu 类,Zi 类只是间接调用 Fu 类中的静态成员变量,因此 Zi 类调用 name 属性属于间接引用,而 Fu 类调用 name 属性属于直接引用,由于 JVM 只初始化直接引用的类,因此只有 Fu 类被初始化.
    示例二
    public class A{ public static void main(String[] args){ Fu[] arr = new Fu[10]; }}
    输出结果:并没有输出 "父类被初始化!"
    原因分析:
    这个过程看似满足初始化时机的第一条:遇到 new 创建对象时若类没被初始化,则初始化该类.
    但现在通过 new 要创建的是一个数组对象,而非 Fu 类对象,因此也属于间接引用,不会初始化 Fu 类.
    示例三
    public class Fu{ public static final String name = " 柴毛毛 "; static{ System.out.println(" 父类被初始化!"); }}public class A{ public static void main(String[] args){ System.out.println(Fu.name); }}
    输出结果:柴毛毛.
    原因分析:
    本示例看似满足类初始化时机的第一个条件:获取一个类静态成员变量的时候若类尚未初始化则初始化类.
    但是,Fu 类的静态成员变量被 final 修饰,它已经是一个常量.被 final 修饰的常量在 Java 代码编译的过程中就会被放入它被引用的 class 文件的常量池中 (这里是 A 的常量池).
    所以程序在运行期间如果需要调用这个常量,直接去当前类的常量池中取,而不需要初始化这个类.
    1.2.5 接口的初始化
    接口和类都需要初始化,接口和类的初始化过程基本一样.
    不同点在于:类初始化时,如果发现父类尚未被初始化,则先要初始化父类,然后再初始化自己;
    但接口初始化时,并不要求父接口已经全部初始化,只有程序在运行过程中用到当父接口中的东西时才初始化父接口.
    1.3 类加载的过程.武汉Java培训机构
    通过之前的介绍可知,类加载过程共有 5 个步骤,分别是:加载、验证、准备、解析、初始化.其中,验证、准备、解析称为连接.
    下面详细介绍这 5 个过程 JVM 所做的工作.
    1.3.1 加载
    注意:"加载" 是 "类加载" 过程的第一步,千万不要混淆.
    加载的过程
    在加载过程中,JVM 主要做 3 件事情:
    通过一个类的全限定名来获取这个类的二进制字节流,即 class 文件:
    在程序运行过程中,当要访问一个类时,若发现这个类尚未被加载,并满足类初始化时机的条件时,就根据要被初始化的这个类的全限定名找到该类的二进制字节流,开始加载过程.
    将二进制字节流的存储结构转化为特定的数据结构,存储在方法区中;
    在内存中创建一个 java.lang.Class 类型的对象.
    接下来程序在运行过程中所有对该类的访问都通过这个类对象,也就是这个 Class 类型的类对象是提供给外界访问该类的接口.
    从哪里加载?
    JVM 规范对于加载过程给予了较大的宽松度.一般二进制字节流都从已经编译好的本地 class 文件中读取,此外还可以从以下地方读取:
    从压缩包中读取,如 Jar、War、Ear 等.
    从其它文件中动态生成,如:从 JSP 文件中生成 Class 类.
    从数据库中读取,将二进制字节流存储至数据库中,然后在加载时从数据库中读取.有些中间件会这么做,用来实现代码在集群间分发.武汉Java培训机构
    从网络中获取,从网络中获取二进制字节流.典型就是 Applet.
    类和数组加载过程的区别?
    数组也有类型,称为 "数组类型".如:
    String[] str = new String[10];
    这个数组的数组类型是 Ljava.lang.String,而 String 只是这个数组中元素的类型.
    当程序在运行过程中遇到 new 关键字创建一个数组时,由 JVM 直接创建数组类,再由类加载器创建数组中的元素类.
    而普通类的加载由类加载器完成.既可以使用系统提供的引导类加载器,也可以使用用户自定义的类加载器.
    加载过程的注意点
    JVM 规范并未给出类在方法区中存放的数据结构;
    类完成加载后,二进制字节流就以特定的数据结构存储在方法区中,但存储的数据结构是由虚拟机自己定义的,JVM 规范并没有指定.
    JVM 规范并没有指定 Class 对象存放的位置;
    在二进制字节流以特定格式存储在方法区后,JVM 会创建一个 java.lang.Class 类型的对象,作为本类的外部接口.
    既然是对象就应该存放在堆内存中,不过 JVM 规范并没有给出限制,不同的虚拟机根据自己的需求存放这个对象.HotSpot 将 Class 对象存放在方法区.
    加载阶段和连接阶段是交叉的;
    通过之前的介绍可知,类加载过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制.也就是说,类加载过程中,必须按照如下顺序开始:
    加载、连接、初始化,但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉.
    1.3.2 验证
    验证阶段比较耗时,它非常重要但不一定必要,如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none 参数关闭,以缩短类加载时间.武汉Java培训机构
    验证的目的是什么?
    验证是为了保证二进制字节流中的信息符合虚拟机规范,并没有安全问题.
    为什么需要验证?
    虽然 Java 语言是一门安全的语言,它能确保程序猿无法访问数组边界以外的内存、避免让一个对象转换成任意类型、避免跳转到不存在的代码行,如果出现这些情况,编译无法通过.
    也就是说,Java 语言的安全性是通过编译器来保证的.
    但是我们知道,编译器和虚拟机是两个独立的东西,虚拟机只认二进制字节流,它不会管所获得的二进制字节流是哪来的.
    当然,如果是编译器给它的,那么就相对安全,但如果是从其它途径获得的,那么无法确保该二进制字节流是安全的.
    通过上文可知,虚拟机规范中没有限制二进制字节流的来源,那么任意来源的二进制字节流虚拟机都能接受,为了防止字节流中有安全问题,因此需要验证!
    验证的过程
    文件格式验证;
    这个阶段主要验证输入的二进制字节流是否符合 class 文件结构的规范.二进制字节流只有通过了本阶段的验证,才会被允许存入到方法区中.
    本验证阶段是基于二进制字节流的,而后面的三个验证阶段都是在方法区中进行,并基于类特定的数据结构的.
    通过上文可知,加载开始前,二进制字节流还没进方法区,而加载完成后,二进制字节流已经存入方法区.而在文件格式验证前,二进制字节流尚未进入方法区,文件格式验证通过之后才进入方法区.
    也就是说,加载开始后,立即启动了文件格式验证,本阶段验证通过后,二进制字节流被转换成特定数据结构存储至方法区中,继而开始下阶段的验证和创建 Class 对象等操作.武汉Java培训机构
    这个过程印证了:加载和验证是交叉进行的.
    元数据验证;
    本阶段对方法区中的字节码描述信息进行语义分析,确保其符合 Java 语法规范.
    字节码验证;
    本阶段是验证过程的最复杂的一个阶段.
    本阶段对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件.
    符号引用验证;
    本阶段验证发生在解析阶段,确保解析能正常执行.
    1.3.3 准备
    准备阶段完成两件事情:
    为已经在方法区中的类中的静态成员变量分配内存,类的静态成员变量也存储在方法区中;
    为静态成员变量设置初始值,初始值为 0、false、null 等.
    示例 1:
    public static String name = " 柴毛毛 ";
    在准备阶段,JVM 会在方法区中为 name 分配内存空间,并赋上初始值 null.
    给 name 赋上 " 柴毛毛 " 是在初始化阶段完成的.
    示例 2:
    public static final String name = " 柴毛毛 ";
    被 final 修饰的常量如果有初始值,那么在编译阶段就会将初始值存入 constantValue 属性中,在准备阶段就将 constantValue 的值赋给该字段.
    1.3.3 解析
    解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程.
    1.3.4 初始化
    初始化阶段就是执行类构造器 clinit() 的过程.
    clinit() 方法由编译器自动产生,收集类中 static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句.
    在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit() 方法对静态成员变量进行显示初始化.
    初始化过程的注意点:
    clinit() 方法中静态成员变量的赋值顺序是根据 Java 代码中成员变量的出现的顺序决定的;
    静态代码块能访问出现在静态代码块之前的静态成员变量,无法访问出现在静态代码块之后的成员变量;
    静态代码块能给出现在静态代码块之后的静态成员变量赋值;
    构造函数 init() 需要显示调用父类构造函数,而类的构造函数 clinit() 不需要调用父类的类构造函数,因为虚拟机会确保子类的 clinit() 方法执行前已经执行了父类的 clinit() 方法;武汉Java培训机构
    如果一个类 / 接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会生成 clinit() 方法;
    接口也需要通过 clinit() 方法为接口中定义的静态成员变量显示初始化;
    接口中不能使用静态代码块;
    接口在执行 clinit() 方法前,虚拟机不会确保其父接口的 clinit() 方法被执行,只有当父接口中的静态成员变量被使用到时才会执行父接口的 clinit() 方法;
    虚拟机会给 clinit() 方法加锁,因此当多条线程同时执行某一个类的 clinit() 方法时,只有一个方法会被执行,其它的方法都被阻塞.并且,只要有一个 clinit() 方法执行完,其它的 clinit() 方法就不会再被执行.因此,在同一个类加载器下,同一个类只会被初始化一次.
    1.4 类加载器
    1.4.1 类与类加载器
    类加载器的作用:将 Class 文件加载进 JVM 的方法区,并在方法区中创建一个 java.lang.Class 对象作为外界访问这个类的接口;
    类与类加载器的关系:比较两个类是否相等,只有当这两个类由同一个加载器加载才有意义;否则,即使同一个 class 文件被不同的类加载器加载,那这两个类必定不同,即通过类的 Class 对象的 equals 执行的结果必为 false.
    1.4.2 类加载器种类
    JVM 提供如下三种类加载器:
    启动类加载器:负责加载 Java_Homelib 中的 class 文件;
    扩展类加载器:负责加载 Java_Homelibext 目录下的 class 文件;
    应用程序类加载器:负责加载用户 classpath 下的 class 文件.
    1.4.3 双亲委派模型
    工作过程:如果一个类加载器收到了加载类的请求,它首先将请求交由父类加载器加载;若父类加载器加载失败,当前类加载器才会自己加载类.
    作用:像 java.lang.Object 这些存放在 rt.jar 中的类,无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载,从而使得不同加载器加载的 Object 类都是同一个.
    原理:双亲委派模型的代码在 java.lang.ClassLoader 类中的 loadClass 函数中实现,其逻辑如下:
    首先检查类是否被加载;
    若未加载,则调用父类加载器的 loadClass 方法;
    若该方法抛出 ClassNotFoundException 异常,则表示父类加载器无法加载,则当前类加载器调用 findClass 加载类;

    若父类加载器可以加载,则直接返回 Class 对象.

   本篇文章是有武汉Java为您呈现,希望给您带来更多更好的文章

更多武汉Java培训机构相关资讯,请扫描下方二维码

武汉Java培训机构

上一篇:class 文件结构详解
下一篇:Java 虚拟机的锁优化策略

有关java静态方法的代码示例详解

string中equals与==有关的代码示例

简单对比类与对象的区别

实现java中map接口的方法

选择城市和中心
贵州省

广西省

海南省