【JVM系列2】Java虚拟机类加载机制及双亲委派模式分析

前言

上一篇咱们粗略的介绍了一下Java虚拟机的运行时数据区,并对运行时数据区内的划分进行了解释,今天咱们就会从类加载开始分析并会深刻去看看数据是具体以什么格式存储到运行时数据区的。java

编译

一个.java文件通过编译以后,变成了了.class文件,主要通过留下步骤:
.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> .class文件 。
具体的过程不作分析,涉及到编译原理比较复杂,咱们须要分析的是.class文件究竟是一个什么样的文件?数据库

Class文件

在Java中,每一个类文件包含单个类或接口,每一个类文件由一个8位字节流组成。全部16位、32位和64位的量都是经过分别读取2个、4个和8个连续的8位字节来构建的。数组

Java虚拟机规范中规定,Class文件格式使用一种相似于C语言的伪结构来存储数据,class文件中只有两种数据类型,无符号数。注意,class文件中没有任何对齐和填充的说法,全部数据都按照特定的顺序紧凑的排列在Class文件中安全

  • 无符号数
    属于数据的基本类型,以u1,u2,u4,u8来表示1个字节,2个儿字节,4个字节,8个字节(在Java SE平台中,这些类型能够经过readUnsignedByte、readUnsignedShort和接口java.io.DataInput中的的readInt方法进行读取)。网络


  • 由0个或多个大小可变的项组成,用于多个类文件结构中,也就是说一个类其实就至关因而一个表。数据结构

Class文件结构

一个Class文件大体由以下结构组成:jvm

ClassFile {
    u4             magic;//魔数
    u2             minor_version;//次版本号
    u2             major_version;//主版本号
    u2             constant_pool_count;//常量池数量
    cp_info        constant_pool[constant_pool_count-1];//常量池信息
    u2             access_flags;//访问标志
    u2             this_class;//类索引
    u2             super_class;//父类索引
    u2             interfaces_count;//接口数(2位,因此一个类最多65535个接口)
    u2             interfaces[interfaces_count];//接口索引 
    u2             fields_count;//字段数
    field_info     fields[fields_count];//字段表集合 
    u2             methods_count;//方法数
    method_info    methods[methods_count];//方法集合
    u2             attributes_count;//属性数
    attribute_info attributes[attributes_count];//属性表集合
}

这个结构在本篇文章里不会一一去解释,若是一一去解释的话一来显得很枯燥,二来可能会占据大量篇幅,这些东西脑子里面有个总体的概念,须要的时候再查下资料就行了,后面的内容中,若是遇到一些很是经常使用的类结构含义会进行说明,如魔数等仍是有必要了解一下的。ide

Class文件示例

咱们先任意写一个示例TestClassFormat.java文件:函数

package com.zwx.jvm;

public class TestClassFormat {

    public static void main(String[] args) {
        System.out.println("Hello JVM");
    }
}

而后进行编译,获得TestClassFormat.class,利用16进制打开:
学习

在这里插入图片描述
由于Java虚拟机只认Class文件,因此必然会对Class文件的格式有严格的安全性校验。


魔数

每一个Class文件中都会以一个4字节的魔数(magic)开头(u4),即上图中的CA FE BA BE(咖啡宝贝)用来标记一个文件是否是一个Class文件。

主次版本号

魔数以后的2个字节(u2)就是minor_version(次版本号),再日后2个字节(u2)记录的是major_version(次版本号),这个仍是很是有必要了解的,下面这个异常我想可能不少人都曾经遇到过:

java.lang.UnsupportedClassVersionError: com/zwx/demo : Unsupported major.minor version 52.0
 at java.lang.ClassLoader.defineClass1(Native Method)
 at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)

这个异常就是提示主版本号不对。
Java中的版本号是从45开始的,也就是JDK1.0对应到Class文件的主版本号就是45,而JDK8对应到的主版本就是52。
上图中类文件的主版本号(第7和第8位)00 34 ,转成10进制就是52,也就是这个类就用JDK1.8来编译的,而后由于我用的是JDK1.6来运行,就会报上面的错了,由于高版本的JDK能向下兼容低版本的Class文件,可是不能向上兼容更高版本的Class文件,因此就会出现上面的异常。

其余

其余还有不少校验,好比说常量池的一些信息和计数,访问权限(public等)及其余一些规定,都是按照Class文件规定好的顺序日后紧凑的排在一块儿。

类加载机制

.java文件通过编译以后,就须要将class文件加载到内存了了,并将数据按照分类存储在运行时数据区的不一样区域。

一个类从被加载到内存,再到使用完毕以后卸载,总共会通过5大步骤(7个阶段):
加载(Loading),链接(Linking),初始化(Initialization),使用(Using),卸载(Unloading) ,其中链接(Linking)又分为:验证(Verification),准备(Preparation),解析(Resolution)。

在这里插入图片描述


加载(Loading)

加载指的是经过一个完整的类或接口名称来得到其二进制流的形式并将其按照Java虚拟机规范将数据存储到运行时数据区。

类的加载主要是要作如下三件事:

  • 一、经过一个类的全限定名获取定义此类的二进制字节流。

  • 二、将这个二进制字节流所表明的静态存储结构转化为方法区的运行时数据结构。

  • 三、在Java堆中生成一个表明这个类的java.lang.Class对象,做为对方法区中这些数据的访问入口。

上面的第1步在虚拟机规范中并无说明Class来源于哪里,也没有说明怎么获取,因此就会产生了很是多的不一样实现方式,下面就是一些经常使用的实现方式:

  • 一、最正常的方式,读取本地通过编译后的.class文件。

  • 二、从压缩包,如:zip,jar,war等文件中读取。

  • 三、从网络中获取。

  • 四、经过动态代理动态生成.class文件。

  • 五、从数据库中读取。

执行Class(类或者接口)的加载操做须要一个类加载器,而一个良好的,合格的类加载器须要具备如下两个属性:

  • 一、对于同一个Class名称,任什么时候候都应该返回相同的类对象

  • 二、若是类加载器L1委派另外一个类加载器L2来加载一个Class对象C,那么如下场景出现的任何类型T,两个类加载器L1和L2应返回相同的Class对象:
    (1) C的直接父类或者父接口类型;
    (2) C中的字段类型;
    (3) C中方法或构造函数的中的参数类型;
    (4) C中方法的返回类型

在Java中的类加载器不止一种,而对于同一个类,用不一样的类加载器加载出来的对象是不相等的,那么Java是如何保证上面的两点的呢?
这就是双亲委派模式,Java中经过双亲委派模式来防止恶意加载,双亲委派模式也确保了Java的安全性。

双亲委派模式

双亲委派模式的工做流程很简单,当一个类加载器收到加载请求时,本身不去加载,而是交给它的父加载器去加载,以此类推,直到传递到最顶层类加载器,而只有当父加载器反馈说本身没法加载这个类,子加载器才会尝试去加载这个类。

在这里插入图片描述
上图中就是双亲委派模型,细心的人可能注意到,顶层加载器我使用了虚线来表示,由于顶层加载器是一个特殊的存在,没有父加载器,并且从实现上来讲,也没有子加载器,是一个独立的加载器,由于扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)两个加载器从继承关系来看,是有父子关系的,均继承了URLClassLoader。可是虽然从类的继承关系来讲启动类加载器(Bootstrap ClassLoader)没有子加载器,可是逻辑上扩展类加载器(Extension ClassLoader)仍是会将收到的请求优先交给启动类加载器(Bootstrap ClassLoader)来进行优先加载。


  • 启动类加载器(Bootstrap ClassLoader),负责加载$JAVA_HOME\lib下的类或者被参数-Xbootclasspath指定的能被虚拟机识别的类(经过jar名字识别,如:rt.jar),启动类加载器由Java虚拟机直接控制,开发者不能直接使用启动类加载器。

  • 扩展类加载器(Extension ClassLoader),负责加载$JAVA_HOME\lib\ext下的类或者被java.ext.dirs系统变量指定的路径中全部类库(System.getProperty(“java.ext.dirs”)),开发者能够直接使用这个类加载器。

  • 应用程序类加载器(Application ClassLoader),负责加载$CLASS_PATH中指定的类库。开发者能直接使用这个类加载器,正常状况下若是在咱们的应用程序中没有自定义类加载器,通常用的就是这个类加载器。

  • 自定义类加载器。若是须要,能够经过java.lang.ClassLoader的子类来定义本身的类加载器,通常咱们都选择继承URLClassLoader来进行适当的改写就能够了。

破坏双亲委派模式

双亲委派模式并非一个强制性的约束模型,只是一种推荐的加载模型,虽然你们大都遵照了这个规则,可是也有不遵照双亲委派模型的,好比:JNDI,JDBC等相关的SPI动做并无彻底遵照双亲委派模式

破坏双亲委派模式的一个最简单的方式就是:继承ClassLoader类,而后重写其中的loadClass方法(由于双亲委派的逻辑就写在了loadClass()方法中)。

常见异常

若是加载过程当中发生异常,那么可能抛出如下异常(均为LinkageError的子类):

  • ClassCircularityError:extends或者implements了本身的类或接口

  • ClassFormatError:类或者接口的二进制格式不正确

  • NoClassDefFoundError:根据提供的全限定类名找不到对应的类或者接口

ClassNotFoundException和NoClassDefFoundError

还有一个异常ClassNotFoundException可能也会常常遇到,这个看起来和NoClassDefFoundError很类似,但其实看名字就知道ClassNotFoundException是继承自Exception,而NoClassDefFoundError是继承自Error。

  • ClassNotFoundException
    当JVM要加载指定文件的字节码到内存时,发现这个文件并不存在,就会抛出这个异常。这个异常通常出如今显式加载中,主要有如下三种场景:
    (1)调用Class.forName() 方法
    (2)调用ClassLoader中的findSystemClass() 方法
    (3)调用ClassLoader中的loadClass() 方法
    解决方法:通常须要检查classpath目录下是否存在指定文件。

  • NoClassDefFoundError
    这个异常通常出如今隐式加载中,出现的状况是可能使用了new关键字,或者是属性引用了某个类,或者是继承了某个类或者接口,或者是方法中的某个参数中引用了某个类,这时候就会触发JVM隐式加载,而在加载时发现类并不存在,则会抛出这个异常。
    解决方法:确保每一个引用的类都在当前classpath下

链接(Linking)

连接是获取类或接口类型的二进制形式并将其结合到Java虚拟机的运行时状态以便执行的过程。链包含三个步骤:验证,准备和解析。

注意:由于连接涉及到新数据结构的分配,因此它可能会抛出异常OutOfMemoryError。

验证(Verification)

这个步骤很好理解,类加载进来了确定是须要对格式作一个校验,要否则什么东西都直接放到内存里面,Java的安全性就彻底没法获得保障。
主要验证如下几个方面:

  • 一、文件格式的验证:好比说是否是以魔数开头,jdk版本号的正确性等等。

  • 二、元数据验证:好比说类中的字段是否合法,是否有父类,父类是否合法等等

  • 三、字节码验证:主要是肯定程序的语义和控制流是否符合逻辑

若是验证失败,会抛出一个异常VerifyError(继承自LinkageError)。

准备(Preparation)

准备工做是正式开始分配内存地址的一个阶段,主要为类或接口建立静态字段(类变量和常量),并将这些字段初始化为默认值。
如下是一些经常使用的初始值:

数据类型 默认值
int 0
long 0L
short (short)0
float 0.0f
double 0.0d
char ‘\u0000’
byte (byte)0
boolean false
引用类型 null

须要注意的是,假设某些字段的在常量池中已经存在了,则会直接在春被阶段就会将其赋值。
如:

static final int i = 100;

这种被final修饰的会直接被赋初始值,而不会赋默认值。

解析(Resolution)

解析阶段就是将常量池中符号引用替换为直接引用的过程。在使用符号引用以前,它必须通过解析,解析过程当中符号引用会对符号引用的正确性进行检查。

注意:由于Java是支持动态绑定的,有些引用须要等到具体使用的时候才会知道具体须要指向的对象,因此解析这个步骤是能够在初始化以后才进行的。

常见异常

解析过程当中可能会发生如下异常:

  • IllegalAccessError:权限异常,好比一个方法或者属性被声明为private,可是又被调用了,就会抛出这个异常。

  • InstantiationError:实例化错误。在解析符号引用时,发现指向了一个接口或者抽象类而致使对象不能被实例化,就会抛出这个异常。

  • NoSuchFieldError:遇到了引用特定类或接口的特定字段的符号引用,可是类或接口不包含该名称的字段。

  • NoSuchMethodError:遇到了引用特定类或接口的特定方法的符号引用,但该类或接口不包含该签名的方法。

符号引用

符号引用以一组符号来描述锁引用的目标,其中的符号能够是任何形式的字面量,只要根据符号惟一的定位到目标便可。好比说:String s = xx,xx就是一个符号,只要根据这个符号能定位到这个xx就是变量s的值就行。

直接引用

直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。对于同一个符号引用通过不一样虚拟机转换而获得的直接饮用通常是不相同的。当有了直接引用以后,那么引用的目标必然已经存在于内存,因此这一步要在准备以后,由于准备阶段会分配内存,而这一步实际上也就是一个地址的配对的过程。

初始化(Initialization)

这个阶段就是执行真正的赋值,会将以前赋的默认值替换为真正的初始值,在这一步,会执行构造器方法。

那么一个类何时须要初始化?父类和子类的初始化顺序又是如何?

初始化顺序

在Java虚拟机规范中规定了5种状况必须当即对类进行初始化,主动触发初始化的行为也被称之为主动引用(除了如下5种状况以外,其他不会触发初始化的引用都称之为被动引用)。

  • 一、虚拟机启动时候,会先初始化咱们指定的须要执行的主类(即main方法所在类)。

  • 二、使用new关键字实例化对象时候,读取或者设置一个类的静态字段(final修饰除外),以及调用一个类的静态方法时。

  • 三、初始化一个类的时候,若是其父类没被初始化,则会先触发父类的初始化。

  • 四、使用反射调用类的时候。

  • 五、JDK1.7开始提供的动态语言支持时,若是一个
    java.lang.invoke.MethodHandle实例解析的结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄对应的类没有被初始化,须要触发其初始化。

注意:接口的初始化在第3点会有点不同,就是当一个接口在初始化的时候,并不要求其父接口所有都初始化,只有在真正使用到父接口的时候(如调用接口中定义的常量)才会初始化

初始化实战举例

下面咱们来看一些初始化的例子:

package com.zwx.jvm;

public class TestInit1 {
    public static void main(String[] args) {
        System.out.println(new SubClass());//A-先初始化父类,后初始化子类
//        System.out.println(SubClass.value);//B-只初始化父类,由于对于static字段,只会初始化字段所在类
//        System.out.println(SubClass.finalValue);//C-不会初始化任何类,final修饰的数据初始化以前就会放到常量池
//        System.out.println(SubClass.s1);//D-不会初始化任何类,final修饰的数据初始化以前就会放到常量池
//        SubClass[] arr = new SubClass[5];//E-数组不会触发初始化
    }
}

class SuperClass{
  static {
        System.out.println("Init SuperClass");
    }
    static int value = 100;

    final static int finalValue = 200;

    final static String s1 = "Hello JVM";
}

class SubClass extends SuperClass{
    static {
        System.out.println("Init SubClass");
    }
}
  • 一、语句A输出结果为:

    Init SuperClass Init SubClass com.zwx.jvm.SubClass@xxxxxx

由于new关键字会触发SubClass的初始化(主动引用状况2),而其父类没有被初始化会先触发父类的初始化(主动引用状况3)

  • 二、语句B输出结果为:

    Init SuperClass 100

调用了类的静态常量(主动引用状况2),虽然是经过子类调用的,可是静态常量却定义在父类,因此只会触发其父类初始化,由于静态属性的调用只会触发属性所在类

  • 三、语句C和语句D输出结果为:

    200

由于被final修饰的静态常量存在于常量池中,在链接的准备阶段就会将属性直接进行赋值了,不须要初始化类

  • 四、语句E不会输出任何结果
    由于构造数组对象和直接构造对象是经过不一样的字节码指令来实现的,建立数组是经过一个单独的newarray指令来实现的,并不会初始化对象。

使用(Using)

通过上面五个步骤以后,一个完整的对象已经加载到内存中了,接下来在咱们的代码中就能够直接使用啦。

卸载(Unloading)

当一个对象再也不被使用以后,会被垃圾回收掉,垃圾回收会在JVM系列后续文章中进行介绍。

总结

本文主要介绍了Java虚拟机的类加载机制,相信看完这篇再结合上一篇对运行时数据区的讲解,你们对Java虚拟机的类加载机制的工做原理有了一个总体的认知,那么下一篇,咱们会从更深层次的字节码上来更具体更深刻的分析Java虚拟机的方法调用流程及方法重载和方法重写的原理分析。

**请关注我,一块儿学习进步**

相关文章
相关标签/搜索