深入理解JVM之 ==> 类加载机制

一、类加载的生命周期

  类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载总共 7 个阶段。其中,验证、准备、解析 3 个阶段统称为连接。 

  上图中,加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的运行时邦栋。

二、类加载的时机

  什么情况下需要开始类加载过程的第一个阶段:加载?Java 虚拟机规范种并没有进行强制约束,但是对于初始化阶段,虚拟机规范则严格规定了有且只有 5 种情况必须立即对类进行“初始化”,而加载、验证、准备自然需要在初始化之前开始 。

  • 遇到 new(用new实例对象),getStatic(读取一个静态字段),putstatic(设置一个静态字段),invokeStatic(调用一个类的静态方法)这四条指令字节码命令时,类没有进行过初始化,则需要先触发其初始化(被 final 修饰、已在编译期把结果放入常量池的静态字段除外);
  • 使用Java.lang.reflect包的方法对类进行反射调用时,如果此时类没有进行过初始化,则需要先触发其初始化;
  • 当初始化一个类时,如果其父类没有进行初始化,则需要先触发其父类的初始化;
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
  • 当使用JDK1.7的动态语言支持的时候,如果一个 java.lang.invoke.MethodHandler 实例后的解析结果是 REF-getStatic/REF_putstatic/REF_invokeStatic 的方法句柄,并且这个方法句柄对应的类没初始化,则需要先触发其初始化;

三、类加载的过程

1、加载

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

  • 通过一个类的全限定名来获取定义此类的二进制字节流;
  • 将这个字节流所代表的金泰存储结构转化为方法区的运行时数据结构;
  • 在内存种生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口;

加载方式:

  • 本地直接加载
  • JAR 包
  • 网络

  加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区当中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范为规定此区域的具体数据机构。然后在内存中实例化一个 java.lang.Class 类的对象(并没有明确规定是在 Java 堆中,对于HotSpot虚拟机而言,Class 对象比较特殊,它虽然是对象,但是存放在方法区中),这个对象将作为程序访问方法曲中的这些类型数据的外部接口。

2、验证

  验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  验证阶段是非常重要的,这个阶段是否严谨,直接决定了 Java 虚拟机是否能承受恶意代码的攻击,从执行性能的角度来讲,验证阶段的工作量在虚拟机的类加载子系统中占了相当大的一部分。 从整体上看,验证阶段大致上会完成下面 4 个阶段的检验动作:

文件格式验证

第一阶段要验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。这一阶段可能包括下面这些验证点:

  • 是否以魔数 0xCAFEBABE 开头。
  • 主、次版本号是否在当前虚拟机处理范围之内。
  • 常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)。
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据。
  • Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息。

元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求,这个阶段可能包括的验证点如下:

  • 这个类是否有父类(除了 java.lang.Object 之外,所有类都应当有父类)。
  • 这个类是否继承了不允许被继承的类(被 final 修饰的类)。
  • 如果这个类不是抽象类,是否实现了其父类或接口之中所要求实现的所有方法。
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等等)。 

字节码验证

第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的事件,例如:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
  • 保证跳转指令不会跳转到方法体以外的字节码指令上。
  • 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险不合法的。

符号引用验证

最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候。符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容:

  • 符号引用中通过字符串描述的全限定名是否能够找到对应的类。
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
  • 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。

3、准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

  • JVM为类的静态变量分配内存,并设置默认初始值;
  • 例::private static int a = 4; // 为静态变量 a 分配 4 个字节的内存空间,并赋予默认值 0;
  • 如果类的静态变量被 final 修饰,则在准备阶段虚拟机就会将静态变量设置为最终值;
  • 例:private static final int a = 4; // 为静态变量 a 分配 4 个字节的内存空间,并赋予默认值 4;

4、解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。

  • 类或接口的解析
  • 字段解析
  • 类方法解析
  • 接口方法解析
  • 方法类型解析
  • 方法句柄解析
  • 调用点限定符解析

5、初始化

  类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的 Java 程序代码(或者说是字节码)。在准备阶段,类的静态变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序指定的主观计划去初始化类变量和其他资源。从另外一个角度来表达:初始化阶段是执行类构造器<clinit>方法的过程。

初始化的结果

  • 为类的静态变量赋予正确的初始值
静态变量的初始化的途径
  • 静态变量声明处进行初始化
  • 在静态代码块中初始化
变量的初始化顺序
  • JVM会按照初始化语句在类文件中的先后顺序来依次执行他们
  • 从上到下
类的初始化步骤
  • 如这个类还没被加载和连接,则先进行加和连接
  • 如类存在直接父类,且此父类还没有被初始化,则先初始化直接父类
  • 如类中存在初始化语句,则依次执行这些初始化语句
类的初始化前提
  • JVM初始化一个类时,要求所有父类都已经被初始化,但此规则不适用于接口
  • 初始化一个类,并不会先初始化它所实现的接口
  • 初始化一个接口时,并不会先初始化它的父接口

类的初始化时机

  • 创建类的实例
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射,Class.forName("xxx.xxx")
  • 初始化一个类的子类
  • Java虚拟机启动时标明为启动的类,如 Java MainTest 

四、类加载器

1、类与类加载器

  对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性。换句话说就是,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。
  这里所指的“相等”,包括Class对象的equals()方法,isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。
类加载器的类型
  • 启动类加载器(Bootstrap ClassLoader)
    • 这个类加载器负责将存放在<JAVA_HOME>\lib目录中,或者被-Xbootclasspath虚拟机参数指定的路径中,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名称不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中;
    • 主要加载JVM自身工作需要的类;
    • 完全是由JVM自己控制;
    • 外界无法访问此类;
    • 此加载器不遵循上级加载机制,即此加载器没有父加载器,也没有子加载器;
  • 扩展类加载器(Extension ClassLoader)
    • 这个类加载器由 sun.misc.Launcher&ExtClassLoader 实现,它负责加载 <JAVA_HOME>\lib\ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器;
  • 应用程序类加载器(AppClassLoader)
    • 这个类加载器由 sun.misc.Launcher$ApplicationClassLoader 实现。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    • 父类是 ExtClassLoader
    • 目录 System.getProperty("java.class.path") 下的类都可以被这个类加载器加载,即常用的classpath

2、双亲委派模型

  我们的应用程序都是由这三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。我们的应用程序都是由这三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如下图所示:

类加载器之间的层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应该有自己的父类加载器。注意,这里类加载器之间的父子关系一般不会以继承的关系实现,而是使用组合关系来复用父加载器的代码。 

双亲委派模型的工作过程

  如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该首先传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲委派模型的实现

  双亲委派模型对于保证 Java 程序的稳定运作非常重要,但它的实现非常简单,实现双亲委派的代码都集中在 java.lang.ClassLoader 类的 loadClass() 方法中。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {//对加载类的过程进行同步
            // 首先,检查请求的类是否已经被加载过了
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);//委派请求给父加载器
                    } else {
                        //父加载器为null,说明this为扩展类加载器的实例,父加载器为启动类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果父加载器抛出ClassNotFoundException
                    // 说明父加载器无法完成加载请求
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
 
                if (c == null) {
                    // 如果父加载器无法加载
                    // 调用本身的findClass方法来进行类加载
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
 
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
java.lang.ClassLoader.loadClass()

Java 虚拟机加载 class 文件到内存的方式

  • 隐式加载
    • 不通过在代码里调用ClassLoader来加载需要的类,而是通过JVM来自动加载需要的类到内存方式
    • 例:再类中继承或引用某个类时,JVM在解析当前这个类时发现引用的类不再内存中,那么就会自动将这些类加载到内存中
  • 显示加载
    • 再代码中通过调用ClassLoader类来加载一个类
    • loader.getClass().getClassLoader().loadClass("class path")
    • Class.forName("class path")
    • 自定义ClassLoader调用findClass("class path")方法

五、类加载完成后所存储的数据

  • 当前类的信息
  • 类的静态变量
  • 类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)
  • 实例变量定义
  • 实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法
  • 还有父类的类信息引用

posted on 2020-04-21 23:22  破解孤独  阅读(361)  评论(0编辑  收藏  举报

导航