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>只会执行一次。

image
可伸缩服务架构-框架与中间件

京东购买链接:可伸缩服务架构-框架与中间件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容