首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java虚拟机--类加载机制

Java虚拟机--类加载机制

作者头像
贾博岩
发布2018-05-11 15:49:13
6080
发布2018-05-11 15:49:13
举报
文章被收录于专栏:博岩Java大讲堂博岩Java大讲堂

类加载

对于虚拟机来说,一个对象的创建十分复杂,包含了很多步骤。首先,我们要从类加载说起。

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载这7个阶段,类加载包含了前五个,具体如图:

1522982931(1).png

一个.java文件在编译后会形成一个或多个class文件(若有内部类,则编译后会产生多个.class文件),但这些class文件中的信息,只有被加载到虚拟机中才能被运行和使用。

虚拟机把类的数据从class文件加载到内存,并对class文件中的数据进行校验、转换、解析、初始化等操作后,最终形成可以被虚拟机识别并使用的过程就叫做“虚拟机的类加载”。

接下来,笔者就对于其中的一些步骤进行详细的说明。

加载

“加载”是类加载中的一个过程,并且是通过类加载器来完成的。但是,什么时候会进行“加载”,虚拟机规范中并没有强制约束,而是交给具体的虚拟机实现来完成。

在此阶段中,虚拟机需要完成以下步骤:

通过全限定类名来获取类的二进制字节流,获取的方式可以通过jar包、war包、网络、JSP文件等方式获取,通过都是通过.class文件中获取的。

获取到字节流后,会将字节流中的信息转化为方法区中的运行时数据结构。

在内存中,生成代表该类的Class对象,作为访问该类的数据入口。在HotSpot中,Class对象并不存在于JVM虚拟机堆中,而是存在于方法区中。

在虚拟机中,当程序主动使用某个类时,如果该类还未被加载到内存中,JVM虚拟机会进行加载操作,直至初始化完成。

关于类加载器的话题,笔者在后面会有单独一章进行阐述。

验证

在完成加载阶段后,JVM虚拟机开始验证阶段,此阶段的目的很简单,很纯粹,就是为了保证class文件中的内容符合虚拟机的规范要求,在实际运行时不会威胁到虚拟机自身的安全。

你可能会问,在javac编译时,不是已经对java程序进行了校验了吗,我写的代码有问题也不会通过编译阶段啊!

你说的没错,在javac编译期是进行了一次代码校验,例如类型转换、代码语法等常见错误。如果你写的代码有错误,在编译期就会暴露出来,不修改是无法通过编译,更别说后续的运行了。

但是,在编译期完成后,我们是可以随意修改生成的class文件,如果此时将class文件修改,等到实际代码执行时候,程序就有可能抛出异常,威胁到整个系统的可用性。在类加载时验证就十分有必要,可以理解为对代码的二次校验。

此外,class文件并一定是本地的Java源码编译得来的,可以是网络中下载而来,可以是jsp页面解析得来,也甚至是你自己用16进制编写而来,这些都会跳过javac编译阶段,所以Java虚拟机如果不进行校验的话,很有可能因为载入了有问题的字节流而导致系统崩溃,无法保障系统的健壮性。

在实际的验证过程中,如果验证到输入的字节流不符合class文件的格式约束,那么虚拟机就会抛出异常,拒绝继续执行。

但是,关于class文件格式如何验证,直到2011年才有了具体的规范,在Java虚拟机规范(Java SE 7版)中,大致规定了4类验证:文件格式验证、元数据验证、字节码验证、符号引用验证。

文件格式验证:

文件格式验证,就是验证class文件字节流是否符合class文件格式规范。

其中,包含:字节流文件是否以魔数0xCAFEBABE开头;主、次版本号是否在虚拟机处理范围之内;class常量池中是否有不支持的常量类型....等等。

该阶段验证的主要目的是为了保障输入的class文件字节流能正确地解析,格式上符合要求。在通过该阶段验证后,class文件字节流会进入内存方法区中存储,后续的三种验证都是基于方法区的。

元数据验证:

接下来,进行元数据验证,对字节码描述的信息进行语义化分析,保证其描述的内容符合java语言的语法规范。验证点如下:这个类是否有父类;这个类的父类是否继承了不允许被继承的类(final修饰的类);这个类是否实现了其父类或者接口中所要求实现的所有方法(抽象类可不实现);类中的字段、方法是否与父类产生矛盾(不符合规则的重载、覆盖了父类中的final字段等)。

该阶段的验证,类似于我们编译期间的校验,对于java语法上的检查。

字节码验证:

通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

符号引用验证:

主要来验证一些引用的真实性与可行性,比如代码里面引用了其他类,这时就需要去检测一下类是否存在;或者说代码中访问了其他类的一些属性,就对那些属性是否可以访问进行检验。

如果无法通过符号引用验证,那么就会抛出异常,我们在编写程序时,经常会遇到java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError,这就是符号引用验证所产生的。

对于,虚拟机类加载阶段来说,符号引用验证可以省略,如果你所运行的代码都已经被反复使用和验证过,那么你可以考虑省略此步骤,虚拟机也提供了一个参数供我们修改,使用-Xverify:none参数可关闭验证措施,以缩短类加载阶段耗时。

准备

验证阶段完成后,就到了准备阶段。准备阶段,说直白点,就是对类变量设置初始值。这里所说的类变量,是被static修饰的变量,而不是实例变量,这一点要搞清楚。

而准备阶段的初始值,是数据类型的零值。即

public static int value = 123;

对于变量value来说,变量value的初始值是0,并不是123,123在初始化阶段才会被赋值给value。

在Java中,有两大的数据类型,一种是基本类型、另一种是引用类型。对于这两种类型的初始值如下:

image

值得注意的是,初始值也会存在一些特殊情况,如果类的静态变量被final所修饰,那么在准备阶段,该值会直接赋予变量中,而不再需要零值了。

解析

解析阶段主要是将常量池内的符号引用替换为直接引用的过程。

Java在编译阶段,会将.java文件编译成.class文件,在生成的.class文件中,static修饰的静态变量就是我们常说的符号引用,但是在编译阶段该符号引用并不知道引用类的实际内存地址(虚拟机还没运行)。直到解析阶段,虚拟机加载了该类才能真正解析到具体的内存地址。

例如:在com.jiaboyan.Person类中引用了com.jiaboyan.Animal类,在编译阶段,Person类并不知道Animal的实际内存地址,因此只能用com.jiaboyan.Animal来代表Animal真实的内存地址。在解析阶段,JVM可以通过解析该符号引用,来确定com.jiaboyan.Animal类的真实内存地址(如果该类未被加载过,则先加载)。

初始化

初始化,是类加载阶段的最后一步。在这步中,才真正开始执行类中定义的Java代码。

在Java虚拟机规范中,如果有以下几种情况时必须立即对类进行“初始化”操作:

(1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候;读取或设置一个类的静态字段(被final修饰除外)的时候;调用一个类的静态方法的时候。

(2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

(3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

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

在之前的准备阶段,类中定义的static静态变量已经被赋过一次零值。而在初始化阶段,则会调用类构造器<clinit>来完成初始化操作,为静态变量赋原始值。

此处,需要注意的是类构造器<clinit>和类构造方法<init>区别。

类构造方法<init>就不用多说了,每创建一次对象,都会自动调用一次;例如:

public class Student{
    public Student(){}
}

类构造器<clinit>类似于一个无参的构造函数,只不过该函数是静态修饰,只会初始化静态所修饰的代码。

public class Student{

    public static int x = 10;

    public String zz = "jiaboyan";

    static{
        System.out.println("12345");
    }
}

类构造器<clinit>是由编译器自动收集类中的所有静态变量的赋值动作和静态语句块static{}中的代码合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。综上所述,对于上面的例子来说,类构造器<clinit>为:

public class Student{
    
    <clinit>{
        public static int x = 10;
        System.out.println("12345");
    }
    
}

image

类构造器<clinit>和类构造方法<init>不同的是,类构造器不需要显式地调用父类类构造器,虚拟机会保证子类类构造器执行之前,父类类构造器已经执行完毕。

image

此外,虚拟机会保证类构造器<clinit>在多线程环境中被正确执行,如果有多个线程同时去初始化一个类,那么只会有一个线程去执行该类的类构造器<clinit>,其他线程都需要阻塞的等待。并且,类构造器<clinit>只会执行一次。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.04.04 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 类加载
    • 加载
      • 验证
        • 准备
          • 解析
            • 初始化
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档