我们知道,我们写的java代码保存的格式是 .java, java文件被编译后会转换为字节码,字节码可以在任何平台通过java虚拟机来运行,这也是java能够跨平台的原因。
那jvm是如何来让我们写的java文件运行的呢? 这个问题通常的问法好像是:类是如何被加载的。
记得第一次遇见这个问题的时候,同学给我的回答是:
1.虚拟机会加载jdk里类的核心包
2.虚拟机会加载jdk里类的扩展包
3.虚拟机会加载jdk里类的系统包
4.虚拟机再会加载我们写好的java类。
初学的时候,大家都这么说,好像也没发现什么错。 最近在浏览一些博客时看到一些更为详细的讲解,如java类加载全过程,该博文有一万多的点击,但感觉还是讲得不够详细,说了类的加载过程有哪些,但没有详细的展开,说了一些类初始化的细节。 在翻读《深入理解java虚拟机》209-235页后,总结了其内容,谈谈自己对该部分的理解吧。
希望大家看了之后更能理解jvm的工作原理和java类的生产过程(类加载的过程);
类从被加载到虚拟机类存中开始,到被卸载出内存为止,它的整个生命周期包括
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载 7个部分、
下面我就来详细的说说每个部分的详细过程,再补充一下双亲委派模型。
再次之前我想补充一个名词解释,类加载器:虚拟机把 实现 类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流” 这个过程的代码称为类加载器
1. 加载
加载只是类加载过程的一个阶段而已,但往往被大家弄成了这就是类的加载过程,所以才有了博文开头时同学给我的那个回答;
希望大家不要混淆出这个很相似的名词,从而对类加载有所误读。
1.jdk在执行程序运行命令时会去jre目录中找到jvm.dll , 并初始化jvm
这时会产生一个bootstrap loader(启动类加载器)
2.bootstrap loader 自动加载 extended loader(标准扩展类加载器)
3.bootstrap loader 自动加载 appclass loader(系统类加载器)
4.最后由 appclass loader 加载 我们指定(想要运行)的 java 类
这里可以提一下双亲委派模型加载类的方式:
实现双亲委派的代码都集中在java.lang.classloader的 loadclass()方法中, 源码我就不贴出来了;
其源码大概意思如下:
1.先检查此类是否被加载过,若没有加载则调用父加载器的loadclass()方法,
2.若父加载器为空,则默认使用启动类加载器作为父加载器,
3.若父类加载失败,会抛出一个异常,然后再调用自己的findclass()方法来进行加载;
结合第一步加载可以这么理解,
1.首先要启动→ 启动类加载器,这时会调用启动类加载器的父加载器,但由于启动类加载器时所有类的父加载器,
所以其父加载器为空(相当于object是所有类的父类,这种感脚~),然后它就会调用自己的findclass方法来自启动加载 ;
2.标准扩展类加载器启动时就会借助其父类 启动类加载器 作为父加载器 来启动了;
3.系统类加载器启动时就会借助其父类 标准扩展类加载器 作为父加载器 来启动了;
4.最后我们编写的普通类就会借助其父类 系统类加载器 作为父加载器 来启动了;
2.验证
验证主要分为以下几个步骤:文件格式验证->元数据验证->字节码验证->符号引用验证
1.文件格式验证:主要是检查字节码的字节流是否符合class文件格式的规范,验证该文件是否能被当前的 jvm 所处理,
如果没问题,字节里就可以进入方法区进行保存了;
2.元数据验证:对字节码描述的信息进行语义分析,保证其描述的内容符合java语言的语法规范,能被java虚拟机识别;
3.字节码验证:该部分最为复杂,对方法体内的内容进行验证,保证代码在运行时不会做出什么危害虚拟机安全的事件;
4.符号引用验证:来验证一些引用的真实性与可行性,比如代码里面引了其他类(符号中通过字符串描述的全限定名是否能找到对应的类),这里就要去检测一下那些来究竟是否存在;或者说代码中访问了其他类的一些属性,这里就对那些属性的可以访问行进行了检验。(这一步将为后面的解析工作打下基础)
多说两句。。。 我觉得这个验证就是看class文件符不符合 jvm 的胃口 , 如果不符合 jvm 的胃口的话,无法完成加载,说明你写的代码 有毒.... 偷笑偷笑
3.准备
准备阶段会为类变量(指的是静态变量,这就是我们常说的,静态变量/方法 在类加载的时候就执行了,通过类名.静态**来调用)分配内存并设置类的初始值; 值得一提的是 如果有以下语句:
public static int i = 123 ;
在准备阶段的初始值是 0 ,而不是 123 , 是因为此时 只是分配内存空间而已, 并没有对 i 进行初始化, 真正的对 i 赋值是在 初始化 阶段;
4.解析
1.类或接口的解析;
2.字段解析;
3.类方法解析;
4.接口方法解析;
此部分内容涉及 invokedynamic指令,静态、动态语音调用 不做展开
如果解析到代码内容有问题,解析不通过将会抛出异常!
5.初始化
类初始化阶段是类加载过程中的最后一步,这才是执行类中定义的java程序代码(也可以说是字节码)。
在准备阶段,已经为变量赋过一次系统要求的初始值,到了初始化阶段会根据程序员的要求出初始化变量赋值。
java虚拟机没有严格约束什么时候开始类加载过程的第一阶段,但严格规定了有且只有5钟情况必须立即马上光速对类进行 初始化
当然加载、验证、准备需要在次之前,(解析也可以在初始化以后再开始~)
1.遇到new,get static,put static,invoke static这4条字节码指令时,假如类还没进行初始化,则马上对其进行初始化工作。
也就是三种情况:用new实例化一个对象时、读取或设置一个雷的静态字段时、执行静态方法时;
2.使用java.lang.reflect.*的方法对类进行反射调用时,如果类还没有进行过初始化,立即马上光速对其进行初始化!!!
3.初始化一个类的时候,如果其父类还没有被初始化,那么会先去初始化其父类;
4.当 jvm 启动时,用户需要指定一个要执行的主类(包含static void main(string 【】args)的那个类),则jvm会先去初始化这个类;
5.当使用jdk1.7 的动态语言支持时,如果一个java.lang.invoke.methodhandle实力最后的解析结果为 get static,put static,invoke static 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先初始化;
小结:
介绍了类加载过程的 加载、验证、准备、解析、初始化、等5个阶段,以及虚拟机进行了哪些动作,简单叙述了类加载器的工作原理,如果有说得不妥当的地方,还以请大家批评指正,多多交流。
原文链接:https://blog.csdn.net/world6/article/details/52041857