[jvm解析系列][九]类的加载过程和类的初始化。你的类该怎么执行?为什么需要ClassLoader?
Posted 胖子程序员
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[jvm解析系列][九]类的加载过程和类的初始化。你的类该怎么执行?为什么需要ClassLoader?相关的知识,希望对你有一定的参考价值。
通过前面好几章的或详细或不详细的介绍,我们终于把字节码的结构分析的差不多了。现在我们面临这样一个问题,如何运行一个字节码文件呢?
首先,java语言不同于其他的编译时需要进行链接工作的语言不通,java语言有一个很明显的特性,那就是动态加载,一个字节码的加载往往都是在程序运行的时候加载进来的,很多时候这种方式给我们带来了便利。虽然从某种意义上来说他可能消耗了一定的资源降低了性能。
类的生命周期?
没错,一个类的生命周期,在很多人眼里可能类天生都摆在那里了,随着程序生,随着程序死。但是事实情况并不是这样,java语言的动态加载要求了一个类肯定有他的生命周期,一个类的生命周期有7个阶段,如下图所示:
这个图里第二行我特意放了三个并排就是因为第二行可以统称为链接,这中间遵循了严格的开始顺序,但是解析是有可能在初始化之后开始,这也是为了java语言的动态绑定而做的改变。并且这中间只有开始的先后关系,没有结束的先后关系。
首先我们说一说第一个情况加载:什么时候开始一个类的加载过程,jvm规范没有规定,不同的虚拟机有不同的实现。
加载这个过程主要是通过一个类的权限定名获取类的二进制字节流
然后把字节流转化为一种方法区运行时的数据结构
最后在内存中形成一个Class对象
图示:
这里面我们可操作性比较强的也就是加载字节码这个过程了,我们都很熟悉一个方法叫做loadClass,没错他就是用来加载字节码的,我们可以自定义一个类加载器重写loadClass方法去控制字节流的加载方式,于是我们可以从网络上获取,从数据库获取,从文件中获取,还有一种动态代理技术是通过计算生成的。
加载完成后就是验证了。
验证主要是确保Class文件里没有坑爹的东西,不会损害虚拟机自身。大概就是分为4个验证流程。
1、文件格式验证:我们之前讲过的魔数和大小version就是在这个位置验证的,常量池中是不是所有类型都支持,Constant_utf8_info是不是都是UTF8编码等等。这一阶段的目的主要是验证输入的字节流能够正确的解析,保证格式上的正确,如果通过了这个验证就把字节流加入到了方法区中,下面的验证都是对方法区的验证。(给几个名词链接魔数,大小版本号,方法区)
2、元数据验证:这一个阶段主要是看看元数据是否符合语义,像父类是否继承了不被允许的类(final类),是不是实现了父类和接口中所有要实现的方法等。
3、字节码验证;这个阶段的验证,主要是看看逻辑上有没有错误,比如有一个跳转指令跳到了方法的外部这种。
4、符号引用验证:这个大家应该很常见,比较常见的就是是不是能访问到某个类(不是private和protected),通过字符串描述的权限定名能不能找到对应的类。
在验证之后,我们就会开始准备。
在准备阶段里,会为类变量分配内存并且设置类变量(static修饰的变量)的初始值,而且诸如static int a = 1;这种情况,在准备阶段是不会赋值1的。而是赋值最基本的初始值0,因为1需要时初始化的时候在类的构造器中调用。
但是,如果字段变量被final修饰,这个字段表就会存在一个ConstantValue属性(详见ConstantValue),在这个时候这个变量就会被赋值,如static final int a = 1;这时候a就会被赋值为1;
在准备之后,jvm会开始解析过程。
解析在通俗意义上讲就是把常量池里的符号引用替换成直接引用。首先我们来解释一下这两个名词的意思
1、符号引用:符号引用就是指使用一组符号来进行引用,被引用的目标不一定加载到内存中
2、直接引用:直接引用是指直接指向目标的指针,相对偏移量或是句柄。
对于解析,jvm并没有具体规定什么时候执行,只要在操作符号引用之前进行解析就可以了。所以具体的实现还要看jvm是怎么设计的(在加载时解析还是在使用前解析)
接下来就是初始化了
我们之前说过,一个类的加载时间是要依靠具体的虚拟机而定,但是遇到主动引用时加载验证准备工作必须完结
有五种情况,我们把这五种情况叫做主动引用。(这之前已经进行了加载验证准备,遇到下面情况直接初始化即可)
1、遇到new、getstatic、putstatic和invokestatic这4条字节码时,对应到java语言就是new了一个类,调用了一个类的静态字段(被final修饰的已经在常量池了,不算)
2、使用java.lang.reflect包对类进行反射条用的时候。
3、初始化一个类,他的父类没有进行过初始化的时候,需要先初始化他的父类。
4、虚拟机启动的那个执行类(也就是带有public void main(String[] args){}方法的那个类)
5、jdk1.7之后新增特性动态语言支持,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个类没有初始化的时候。
当然除了这五个主动引用以外,其余所有方法都不会出发初始化,我们叫做被动引用,下面举几个例子。
1、通过子类引用父类的静态字段,不会触发子类的初始化。
2、通过数组定义来引用类,不会触发类的初始化,他把原来的类翻译成了另外一个类,这个类带有一些数组元素的访问方法。
3、常量不会触发类的初始化。
初始化的过程:
在初始化的时候就是真正开始执行java代码的时候了,这个时候会执行一个叫做类构造器的东西(<client>())他负责收集类中所有的static{}然后合并,顺序和原顺序一样,在static{}中只能访问定义在静态语句块之前的变量,对于后面的变量只能赋值不能访问。
我们之前就讲过在初始化一个类的时候,如果他的父类没有进行过初始化,则需要先初始化他的父类。于是父类的<client>()方法肯定优于子类执行,也就是说父类的静态代码块优于子类的静态代码块执行。但是接口的<client>()方法并不一定先于子类方法执行,因为父接口的<client>()方法是在调用时才执行的。
于是我们可以看出来,在整个加载过程中,程序员可以操作的部分仅仅只有ClassLoader(加载字节码)和初始化静态代码块部分。所以我们接下来就会讲ClassLoader
以上是关于[jvm解析系列][九]类的加载过程和类的初始化。你的类该怎么执行?为什么需要ClassLoader?的主要内容,如果未能解决你的问题,请参考以下文章