Java类加载机制

基于JVM的语言,如java,kotlin,groovy等语言,在各自编译器编译完成之后,都会编译为.class文件,用JVM加载。而class文件只有被确的加载到JVM正中才能运行和使用。虚拟机是如何在家这些文件呢?本文将详细讲解。

类的生命周期

一个类从被加载到虚拟机到最后被卸载,生命周期包括:加载验证准备解析初始化使用,卸载7个阶段。其中验证,准备,解析3个部分称为连接阶段。

类的生命周期

这7个阶段在实际JVM中并不是按照图中所示的顺序来开始运行的,里面存在时间上的交叉进行。但是其中加载,验证,准备,初始化,卸载5个结算的顺序是确定的。

加载

这是类生命周期的第一个阶段,那么加载的是什么呢?加载的应该是一个字节码文件的二进制字节流。那么此二进制流如何得来呢?java虚拟机规范并没有强制要求,我们可以灵活运用这一特性实现很多的加载源:

  • 最常见的,从压缩包获取,比如jar,EAR,WAR等
  • 从网络中获取,比如早期嵌入在浏览器中的Applet程序
  • 在运行是生成字节码,动态代理技术。如著名的GCLib字节码类库,再如现在Android中常用的网络请求库retrofit中所使用的动态代理Proxy.newProxyInstance()中,最终会调用的sun.misc.ProxyGenerator.generateProxyClass()方法,该方法在运行时动态产生了一组字节码流(标识为$Proxy的代理类)。
  • 由其他文件生成,比如由JSP文件生成class类
  • 等等等等

在此阶段,开发人员可以使用系统的类加载器进行加载,也可以使用自己定义的类加载器来自定义获取字节码流的方式(重写来加载器的loadClass方法)。

加载字节码文件结束后,虚拟机将字节流存储在方法区中,同时在内存中(Hot Spot中实在方法区中)实例化一个Class对象,外部可以同过此实例访问该类对象。

在此阶段运行中,验证阶段就已开始,交叉进行。只有通过通过了验证阶段,只有通过了验证阶段,字节流才会进入内存的方法区中进行存储。

验证

验证阶段的主要任务是:确保字节码流中包含的信息符合当前版本虚拟机的要求,并不会有危害虚拟机自身安全的行为。

如:将一个对象强转为一个未声明实现的类型,执行一个虚方法,执行一个并不存在的方法。在我们平时编码的经验中,虽然以上这些错误会在编译时报出,无法通过编译;但是,我们上面提到过,class文件是由多种方式得来,对于直接生成.class文件、无需编译的方式,验证这一阶段对于虚拟机的保护就显得尤其重要。

简要的概述,虚拟机对类的验证阶段分为以下4个方面,这四个方面层层深入:

文件格式的验证

针对类文件(字节码流)的验证

验证字节码流是否符合java虚拟机规范中规定的class文件格式,如:

  • 魔数是否为CAFEBABY
  • 当前虚拟机持否可以处理文件声明的主,次版本号
  • 常量池中是否有不被支持的常量类型
  • 检查指向常量的索引是否指向了不存在的常量
  • CONSTANT_Utf8_info型的常量是否符合Utf8编码
  • Class文件中的各个部分是否被删除(class文件是否完整)
  • 等等

通过了验证阶段,字节流会进入内存的方法区中进行存储。以后的验证和其他操作都针对于内存方法区中的数据进行操作,而不针对字节码流。

元数据验证

针对数据类型的验证

该阶段是进行语义分析验证,以保证其信息符合Java语言规范的要求,比如:

  • 检查这个类是否有父类(除了Object之外都应有父类)
  • 本类的父类是否继承了不允许被继承的类(被final修饰)
  • 如果本类不是抽象类,是否实现了父类中的全部虚方法或接口
  • 等等

字节码验证

针对方法体的验证

此阶段通过数据流和控制流分析,检查程序的语义是合法的,符合逻辑的。保证程序逻辑的正确运行,检验的内容如:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。(如:不能出现这样的状况:操作栈中放了一个int类型的数据,使用却按照long或者引用类型加载)
  • 保证跳转指令不会跳转到方法体以外的字节码指令上
  • 类型转换是有效的(如多态)
  • 等等

符号引用的验证

针对常量池匹配的验证

此阶段检查是为了:确保在后续的解析阶段,虚拟机可以顺利的将符号引用转化为直接引用。(关于符号引用与直接引用的概念,祥见下文解析过程)见下图,讲解一下验证内容:

常量池
  • 符号引用中通过字符串的描述能够找对应的类(如:上图中常量池中有一个指向类型为class的常量#4 = Class #17 // java/lang/Object,应该确保有一个类与之对应,此处为String类)
  • 符号引用中通过字符串的描述的能够找到相应的方法(如:上图中#2 = Methodref #3.#15 // VinctorTest.test:()V描述的,需要在VinctorTest类中有一个test方法与之对应)
  • 符号引用中的用到的类,方法,字段的访问性(public private等)确保可以被当前类访问到
  • 等等

准备

针对类变量(static)

经过验证阶段,虚拟机从文件,数据类型,方法逻辑,符号引用等各个方面对类进行了验证,已确保代码的正确性。接下来开始为代码的运行做准备,进入准备阶段。

准备阶段是为正式类变量(注意,不是实例变量)分配内存并设置类变量初始值的阶段,注意,此初始值并不是我们java代码中所写的初始值(如 int a=123;),而是java虚拟机规范中规定的初始值,
java体系中各种类型的初始值如下:

各类型的初始值

如果一个变量声明为static int a=123,则在此阶段,声明a的值为0;

注意:如果类变量被final修饰,如

final static int a=123;

这种情况下,javac编译阶段,将为此变量生成ConstantValue属性,在此准备阶段直接将其赋值为123;

解析

针对常量池

解析阶段是将常量池中符号引用转化成直接引用的过程。主要针对常量池中的类或接口,字段,类方法,接口方法,方法类型,方法句柄,调用限定符

  • 符号引用:见上文中class文件中常量池的图片,我们可以知道常量池中有描述类,方法,字段等常量,这些常量通过一组符号(比如UTF8字符串)描述所引用的目标。虽然在验证阶段已经对此进行了验证,但是这些毕竟只是一些字符串,并不能拿来直接为虚拟机使用,并不指向任何真实的内存地址。
  • 直接引用:直接引用则是指向这些目标的指针,偏移量或者句柄。

直接引用指向的目标必须真实存在于内存之中的。在代码运行过程中,会不断产生新对象,故而解析这一过程并不是一次就完成的,其发生的时机不固定。

java虚拟机规范中规定了只有执行了以下字节码指令前才会将所用到的符号引用转化为直接引用:

  • anewarray 创建一个引用类型的数组
  • checkcast 检查对象是否是给定类型
  • getfield putfield 从对象获取某一个字段 设置对象的字段
  • getstatic putstatic 从类中获取某一静态变量 设置静态变量
  • instanceof 确定对象是否是给定类型
  • invokedynamic invokeinterface invokestatic invokevirtual 调用动态方法,接口方法,静态方法,虚方法
  • invokespecial 调用实例化方法,私有方法,父类中的方法
  • ldc idc_w 把常量池中的项压入栈
  • multianewarray 创建多为引用类型性数组
  • new 实例化对象

在解析过程中,如果需要解析类或接口的的字段,方法,则先查找该字段,方法所属的类或接口是否被解析,如果没有,则先解析类或接口,然后在查找当前的类或接口中是否有该字段或方法,如果没有,则递归向上到父类或父接口中寻找该字段或接口。

初始化

至此,程序终于开始执行我们开发人员写的代码了(等了好久)。此阶段是为类设置类变量的值和一些其他初始化操作的阶段(如执行static{ }静态代码块)。

在类编译过充中,编译器为每一个方法生成了一个<clinit>()类初始化方法,初始化阶段也是此方法的执行阶段。

注意<clinit>()并不是默认构造方法,前者是类的初始化方法,后者是实例的初始化方法。我们此文讨论的是类的生命周期,而不是实例的生命周期。

<clinit>()是如何生成的呢?其中又包含什么呢?

<clinit>()方法是在编译阶段,编译器收集整个类中的类变量的赋值以及静态代码块而形成的。顺序是按照赋值以及静态代码在源文件中出现的顺序生成的。同时,如果一个类有父类,则虚拟机会保证父类的初始化先于子类的初始化执行。

使用

至此 一个类已经具备我们使用的条件了,我们可以对这个类进行实例化和其他操作了。

github上的地址:DevelopBlog

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

推荐阅读更多精彩内容

  • 1.虚拟机如何加载这些Class文件?(类加载的过程)2.Class文件中的信息进入到虚拟机后会发生什么变化? J...
    wangcanfeng阅读 229评论 0 0
  • Java的核心是 JVM ,了解并熟悉JVM对于我们理解Java语言非常重要。 一、类加载机制 当程序主动使用某个...
    年少懵懂丶流年梦阅读 1,075评论 2 15
  • 概述 虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行验证,准备,解析,初始化的一个过程,最终是可以...
    Wen_Q_M阅读 274评论 0 1
  • 虚拟机类加载机制 1. 类加载的时机1.1 类从被加载到虚拟机内存开始,到卸载出内存为止,他的整个生命周期包括: ...
    天空在微笑阅读 194评论 0 0
  • 类加载器简单来说是用来加载 Java 类到 Java 虚拟机中的。Java 虚拟机使用 Java 类的方式如下:J...
    爱情小傻蛋阅读 728评论 2 11